sinon icon indicating copy to clipboard operation
sinon copied to clipboard

Proposal: Fix async functions not executed in time when ticking a fake clock forward

Open Zomono opened this issue 3 years ago • 1 comments

Is your feature request related to a problem? Please describe.

I had some trouble with async function calls submitted to setTimeout/setImmediate/setInterval when using faked timers, and it seems that I am not the only one. The problem is that methods like tick and even tickAsync do sometimes return even before the scheduled callbacks have finished. So it is not possible to have a reproducible state after e.g. tickAsync has returned.

Describe the solution you'd like

I have noticed that there are some plans to mock or stub promises completely, but for now I propose to implement a short workaround that may cover most of the use cases I can imagine:

It should be quite easy for users to make all of their callbacks from the production code returning a promise that is resolved or rejected exactly when the purpose of the callback has been done. As it is a best practice to not have unhandled promises, this may already been the case in clean implementations, even if the original APIs for setTimeout and friends do not expect promises as return types.

So assuming that, you may implement a queue of promises, that is filled with the promises of all callbacks (or their normal return values wrapped as a promise).

    const promiseQueue = new Set() // global or one instance per InstalledClock
    const wrappedCallback = (...args: any[]) => {
        const promise = Promise.resolve(callback(...args))
        promiseQueue.add(promise)
        promise.finally(() => promiseQueue.delete(promise))
    }
  // call internal setTimeout/setImmediate/setInterval with the wrapped callback

The methods tickAsync, runToLastAsync, runAllAsync and nextAsync would than wait for all promises of that queue when called and clear the queue afterwards.

Depending on the used tick-Function (e.g. runAllAsync) you may need to check'n'wait for the promise queue multiple times.

Things to consider

A promise from a user's callback may depend on the execution of other callbacks submitted by e.g. setImmediate. So while waiting for such promises to resolve/reject, the execution of callbacks from setImmediate, setTimeout(0), etc. should not be blocked.

If the user's callback returns a promise that depends on future executions like setTimeout(100) and the user awaits the result of e.g. tickAsync at the same time, waiting infinite long should be the expected behaviour.

Benefits

Using this workaround all the async tick functions won't return to early anymore, unless there is a bug in the users promise chain, which is something they probably want to fix anyway.

Zomono avatar Apr 19 '22 11:04 Zomono

Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.

stale[bot] avatar Dec 27 '23 10:12 stale[bot]