Skip to content
Merged
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
1,031 changes: 539 additions & 492 deletions common/config/rush/npm-shrinkwrap.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"preproc": "ts-preproc -C ../preproc.json -R .."
},
"dependencies": {
"@nevware21/ts-utils": ">= 0.12.6 < 2.x"
"@nevware21/ts-utils": ">= 0.15.0 < 2.x"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
Expand Down
41 changes: 41 additions & 0 deletions lib/src/internal/timeout_helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* @nevware21/ts-async
* https://github.com/nevware21/ts-async
*
* Copyright (c) 2026 NevWare21 Solutions LLC
* Licensed under the MIT license.
*/

import { isArray, isNumber, isUndefined } from "@nevware21/ts-utils";

/**
* @internal
* @ignore
* Normalizes timeout values that may be passed either directly or inside an extra-args array.
* @param timeout - The timeout value or argument array.
* @param defaultTimeout - The fallback timeout when no explicit timeout is provided.
* @returns The normalized timeout value.
*/
/*#__NO_SIDE_EFFECTS__*/
export function _normalizeTimeoutValue(timeout?: number | any, defaultTimeout?: number): number | undefined {
let result = defaultTimeout;
if (!isUndefined(timeout)) {
if (isNumber(timeout)) {
result = timeout;
} else {

// Promise creation can re-wrap additional args for chained promises (for example [[10]]).
// Unwrap nested array values so explicit timeouts keep flowing through then/catch/finally chains.
while(!isUndefined(timeout) && isArray(timeout) && timeout.length > 0) {
timeout = timeout[0];

if (isNumber(timeout)) {
result = timeout;
break;
}
}
}
}

return result;
}
24 changes: 19 additions & 5 deletions lib/src/promise/idlePromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
* Licensed under the MIT license.
*/

import { ICachedValue, isUndefined } from "@nevware21/ts-utils";
import { ICachedValue, isNumber, scheduleIdleCallback } from "@nevware21/ts-utils";
import { _createAllPromise, _createAllSettledPromise, _createAnyPromise, _createPromise, _createRacePromise, _createRejectedPromise, _createResolvedPromise } from "./base";
import { IPromise } from "../interfaces/IPromise";
import { idleItemProcessor } from "./itemProcessor";
import { PromiseExecutor } from "../interfaces/types";
import { IPromiseResult } from "../interfaces/IPromiseResult";
import { _pureAssign } from "../internal/treeshake_helpers";
import { _normalizeTimeoutValue } from "../internal/timeout_helpers";
import { PromisePendingFn, syncItemProcessor } from "./itemProcessor";

let _defaultIdleTimeout: number | undefined;

Expand Down Expand Up @@ -56,8 +57,21 @@ export const setDefaultIdleTimeout = (/*#__PURE__*/_pureAssign(setDefaultIdlePro
* positive value or it is ignored.
*/
export function createIdlePromise<T>(executor: PromiseExecutor<T>, timeout?: number): IPromise<T> {
let theTimeout = isUndefined(timeout) ? _defaultIdleTimeout : timeout;
return _createPromise(createIdlePromise, idleItemProcessor(theTimeout), executor, theTimeout);
let options: any;
let theTimeout = _normalizeTimeoutValue(timeout, _defaultIdleTimeout);
if (isNumber(theTimeout) && theTimeout >= 0) {
options = {
timeout: theTimeout
};
}

let processor = (pending: PromisePendingFn[]) => {
scheduleIdleCallback((deadline: IdleDeadline) => {
syncItemProcessor(pending);
}, options);
};

return _createPromise(createIdlePromise, processor, executor, theTimeout);
}

/**
Expand Down Expand Up @@ -290,4 +304,4 @@ export function createIdleAnyPromise<T>(values: Iterable<T | PromiseLike<T>>, ti
export function createIdleAnyPromise<T extends readonly unknown[] | []>(values: T, timeout?: number): IPromise<Awaited<T[number]>> {
!_anyIdleCreator && (_anyIdleCreator = _createAnyPromise(createIdlePromise));
return _anyIdleCreator.v(values, timeout);
}
}
51 changes: 24 additions & 27 deletions lib/src/promise/itemProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@
* Licensed under the MIT license.
*/

import { arrForEach, isNumber, scheduleIdleCallback, scheduleTimeout } from "@nevware21/ts-utils";
import { arrForEach, isNumber, scheduleMicrotask, scheduleTimeout } from "@nevware21/ts-utils";
import { IPromise } from "../interfaces/IPromise";
import { PromiseExecutor } from "../interfaces/types";
import { _normalizeTimeoutValue } from "../internal/timeout_helpers";

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

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 All @@ -39,34 +47,23 @@ export function syncItemProcessor(pending: PromisePendingFn[]): void {
* @return An item processor
*/
export function timeoutItemProcessor(timeout?: number): (pending: PromisePendingFn[]) => void {
let callbackTimeout = isNumber(timeout) ? timeout : 0;
let timeoutValue = _normalizeTimeoutValue(timeout);
let hasTimeout = isNumber(timeoutValue);
let callbackTimeout = hasTimeout ? (timeoutValue as number) : 0;

return (pending: PromisePendingFn[]) => {
scheduleTimeout(() => {
function _processPending() {
syncItemProcessor(pending);
}, callbackTimeout);
}
}
}

/**
* @internal
* @ignore
* Return an item processor that processes all of the pending items using an idle callback (if available) or based on
* a timeout (when `requestIdenCallback` is not supported) using the optional timeout.
* @param timeout - Optional timeout to wait before processing the items, defaults to zero.
* @return An item processor
*/
export function idleItemProcessor(timeout?: number): (pending: PromisePendingFn[]) => void {
let options: any;
if (timeout >= 0) {
options = {
timeout: +timeout
};
if (hasTimeout && callbackTimeout > 0) {
scheduleTimeout(_processPending, 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, 0);
} else {
scheduleMicrotask(_processPending);
}
}

return (pending: PromisePendingFn[]) => {
scheduleIdleCallback((deadline: IdleDeadline) => {
syncItemProcessor(pending);
}, options);
};
}
}
10 changes: 5 additions & 5 deletions lib/test/bundle-size-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,31 @@ const configs = [
{
name: "es5-min-full",
path: "../bundle/es5/umd/ts-async.min.js",
limit: 17.5 * 1024, // 17.5 kb in bytes
limit: 18.75 * 1024, // 18.75 kb in bytes
compress: false
},
{
name: "es6-min-full",
path: "../bundle/es6/umd/ts-async.min.js",
limit: 17 * 1024, // 17 kb in bytes
limit: 18.5 * 1024, // 18.5 kb in bytes
compress: false
},
{
name: "es5-min-zip",
path: "../bundle/es5/umd/ts-async.min.js",
limit: 7.5 * 1024, // 7.5 kb in bytes
limit: 8 * 1024, // 8 kb in bytes
compress: true
},
{
name: "es6-min-zip",
path: "../bundle/es6/umd/ts-async.min.js",
limit: 7.5 * 1024, // 7.5 kb in bytes
limit: 8 * 1024, // 8 kb in bytes
compress: true
},
{
name: "es5-min-poly",
path: "../bundle/es5/ts-polyfills-async.min.js",
limit: 10 * 1024, // 10 kb in bytes
limit: 11.5 * 1024, // 11.5 kb in bytes
compress: false
}
];
Expand Down
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");
});
});
17 changes: 8 additions & 9 deletions lib/test/src/promise/use.doAwait.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1323,8 +1323,8 @@ function batchTests(testKey: string, definition: TestDefinition) {
it("should resolve with the value of the first resolved promise", (done) => {
const promises = [
createResolvedPromise("sync"),
createNewPromise((resolve) => setTimeout(() => resolve("fast"), 50)),
createNewPromise((resolve) => setTimeout(() => resolve("slowest"), 150))
createNewPromise((resolve) => setTimeout(() => resolve("fast"), 150)),
createNewPromise((resolve) => setTimeout(() => resolve("slowest"), 250))
];

const promise = createRacePromise(promises);
Expand All @@ -1333,31 +1333,30 @@ function batchTests(testKey: string, definition: TestDefinition) {
assert.equal(value, "sync");

// Wait for the slowest promise to resolve so that it doesn't cause an unhandled rejection later
doAwait(createTimeoutPromise(200, true), () => {
doAwaitResponse(createTimeoutPromise(250, true), () => {
done();
});
}, (reason) => {
assert.fail("Should not have been rejected");
// assert.fail("Should not have been rejected");
done(reason);
});
});

it("should reject with the reason of the first rejected promise", (done) => {
const promises = [
createRejectedPromise("sync"),
createNewPromise((_, reject) => setTimeout(() => reject("fast"), 50)),
createNewPromise((_, reject) => setTimeout(() => reject("slowest"), 150))
createNewPromise((_, reject) => setTimeout(() => reject("fast"), 150)),
createNewPromise((_, reject) => setTimeout(() => reject("slowest"), 250))
];

const promise = createRacePromise(promises);

doAwait(promise, () => {
assert.fail("Should not have been resolved");
done();
done(new Error("Expected promise to be rejected"));
}, (reason) => {
assert.equal(reason, "sync");
// Wait for the slowest promise to resolve so that it doesn't cause an unhandled rejection later
doAwait(createTimeoutPromise(200, true), () => {
doAwaitResponse(createTimeoutPromise(250, true), () => {
done();
});
});
Expand Down
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"node": ">= 0.8.0"
},
"dependencies": {
"@nevware21/ts-utils": ">= 0.12.6 < 2.x"
"@nevware21/ts-utils": ">= 0.15.0 < 2.x"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
Expand Down Expand Up @@ -128,7 +128,7 @@
{
"name": "es5-full",
"path": "lib/dist/es5/mod/ts-async.js",
"limit": "18 kb",
"limit": "20 kb",
"brotli": false,
"ignore": [
"lib/dist/es5/mod/polyfills.js",
Expand All @@ -138,7 +138,7 @@
{
"name": "es6-full",
"path": "lib/dist/es6/mod/ts-async.js",
"limit": "17.5 kb",
"limit": "19.5 kb",
"brotli": false,
"ignore": [
"lib/dist/es6/mod/polyfills.js",
Expand All @@ -148,7 +148,7 @@
{
"name": "es5-zip",
"path": "lib/dist/es5/mod/ts-async.js",
"limit": "7.5 Kb",
"limit": "8.5 Kb",
"gzip": true,
"ignore": [
"lib/dist/es5/mod/polyfills.js",
Expand All @@ -158,7 +158,7 @@
{
"name": "es6-zip",
"path": "lib/dist/es6/mod/ts-async.js",
"limit": "7.5 Kb",
"limit": "8.5 Kb",
"gzip": true,
"ignore": [
"lib/dist/es6/mod/polyfills.js",
Expand All @@ -168,7 +168,7 @@
{
"name": "es5-promise",
"path": "lib/dist/es5/mod/ts-async.js",
"limit": "9 kb",
"limit": "11 kb",
"import": "{ createAsyncPromise }",
"brotli": false,
"ignore": [
Expand All @@ -179,7 +179,7 @@
{
"name": "es5-any",
"path": "lib/dist/es5/mod/ts-async.js",
"limit": "11 kb",
"limit": "12.75 kb",
"import": "{ createAnyPromise }",
"brotli": false,
"ignore": [
Expand All @@ -190,7 +190,7 @@
{
"name": "es5-race",
"path": "lib/dist/es5/mod/ts-async.js",
"limit": "10 kb",
"limit": "12 kb",
"import": "{ createRacePromise }",
"brotli": false,
"ignore": [
Expand All @@ -201,14 +201,14 @@
{
"name": "es5-polypromise",
"path": "lib/dist/es5/mod/ts-async.js",
"limit": "12 kb",
"limit": "14 kb",
"import": "{ PolyPromise }",
"brotli": false
},
{
"name": "es5-polyfill",
"path": "lib/build/es5/mod/polyfills.js",
"limit": "12.5 kb",
"limit": "14.5 kb",
"brotli": false
}
]
Expand Down