Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions lib/src/promise/itemProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,27 @@
* 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";

export type PromisePendingProcessor = (pending: PromisePendingFn[]) => void;
export type PromisePendingFn = () => void;
export type PromiseCreatorFn = <T, TResult2 = never>(newExecutor: PromiseExecutor<T>, ...extraArgs: any) => IPromise<T | TResult2>;

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
Expand Down Expand Up @@ -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);
}
}
}

Expand All @@ -69,4 +102,4 @@ export function idleItemProcessor(timeout?: number): (pending: PromisePendingFn[
syncItemProcessor(pending);
}, options);
};
}
}
30 changes: 30 additions & 0 deletions lib/test/src/promise/async.microtask.promise.test.ts
Original file line number Diff line number Diff line change
@@ -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<void>((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");
});
});