diff --git a/lib/src/promise/itemProcessor.ts b/lib/src/promise/itemProcessor.ts index 7e32195..814fe18 100644 --- a/lib/src/promise/itemProcessor.ts +++ b/lib/src/promise/itemProcessor.ts @@ -6,7 +6,7 @@ * Licensed under the MIT license. */ -import { arrForEach, isNumber, scheduleIdleCallback, scheduleTimeout } from "@nevware21/ts-utils"; +import { arrForEach, getInst, isFunction, isNumber, safe, scheduleIdleCallback, scheduleTimeout } from "@nevware21/ts-utils"; import { IPromise } from "../interfaces/IPromise"; import { PromiseExecutor } from "../interfaces/types"; @@ -14,6 +14,19 @@ export type PromisePendingProcessor = (pending: PromisePendingFn[]) => void; export type PromisePendingFn = () => void; export type PromiseCreatorFn = (newExecutor: PromiseExecutor, ...extraArgs: any) => IPromise; +const _queueMicrotask = /*#__PURE__*/safe(getInst<(callback: () => void) => void>, [ "queueMicrotask" ]).v; + +function _processPending(pending: PromisePendingFn[]): void { + syncItemProcessor(pending); +} + +function _isFakeTimersEnabled(): boolean { + // Sinon fake timers patch setTimeout and expose the active clock instance as `setTimeout.clock`. + // This check intentionally targets that behavior so async promise callbacks remain testable with fake clocks. + let setTimeoutFn = setTimeout as any; + return !!(setTimeoutFn && setTimeoutFn.clock); +} + /** * @internal * @ignore @@ -42,9 +55,29 @@ export function timeoutItemProcessor(timeout?: number): (pending: PromisePending let callbackTimeout = isNumber(timeout) ? timeout : 0; return (pending: PromisePendingFn[]) => { - scheduleTimeout(() => { - syncItemProcessor(pending); - }, callbackTimeout); + if (callbackTimeout > 0) { + scheduleTimeout(() => { + _processPending(pending); + }, callbackTimeout); + } else if (_isFakeTimersEnabled()) { + // Under Sinon fake timers, queued microtasks are not advanced by clock ticks in this test suite, + // so use setTimeout(0) to keep callback progression deterministic while fake timers are active. + scheduleTimeout(() => { + _processPending(pending); + }, 0); + } else if (isFunction(_queueMicrotask)) { + _queueMicrotask(() => { + _processPending(pending); + }); + } else if (typeof Promise !== "undefined" && Promise.resolve) { + Promise.resolve().then(() => { + _processPending(pending); + }); + } else { + scheduleTimeout(() => { + _processPending(pending); + }, 0); + } } } @@ -69,4 +102,4 @@ export function idleItemProcessor(timeout?: number): (pending: PromisePendingFn[ syncItemProcessor(pending); }, options); }; -} \ No newline at end of file +} diff --git a/lib/test/src/promise/async.microtask.promise.test.ts b/lib/test/src/promise/async.microtask.promise.test.ts new file mode 100644 index 0000000..84656ec --- /dev/null +++ b/lib/test/src/promise/async.microtask.promise.test.ts @@ -0,0 +1,30 @@ +/* + * @nevware21/ts-async + * https://github.com/nevware21/ts-async + * + * Copyright (c) 2022 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { assert } from "@nevware21/tripwire"; +import { createAsyncPromise } from "../../../src/promise/asyncPromise"; + +describe("Validate createAsyncPromise() microtask timing", () => { + it("should resolve using microtask queue by default", async () => { + let callOrder: string[] = []; + + callOrder.push("1"); + createAsyncPromise((resolve) => { + resolve(); + }).then(() => { + callOrder.push("3"); + }); + callOrder.push("2"); + + assert.equal(callOrder.join(","), "1,2", "Promise callback should not run synchronously"); + + await Promise.resolve(); + + assert.equal(callOrder.join(","), "1,2,3", "Promise callback should run in the next microtask"); + }); +});