From d642ffe636abe20916f5021f8fb919848dabbece Mon Sep 17 00:00:00 2001 From: Nev Date: Fri, 22 May 2026 09:11:59 -0700 Subject: [PATCH] feat(timer): add cancellable microtask scheduling with cross-runtime fallback support Add a new microtask scheduling API that extends standard microtasks with cancellable handlers while preserving runtime parity across Node.js, browsers, and web workers. - Add timer microtask API surface: - scheduleMicrotask - hasQueueMicrotask - getQueueMicrotask - setMicroTaskFallbackOptions - ScheduleMicrotaskFn and MicroTaskOptions types - Implement fallback chain: - native queueMicrotask when available - Promise.resolve().then fallback - timer-backed microtask queue fallback - Return ITimerHandler from scheduled microtasks to support cancellation and refresh semantics. - Ensure thrown microtask callback errors are surfaced via task rethrow handling. - Integrate timer-backed microtask flushing with timeout scheduling behavior for correct ordering. - Export new public APIs from index entrypoints. - Add comprehensive common-runtime tests for ordering, cancellation, fallback behavior, nested microtasks, batching across ticks, and throw surfacing. - Update documentation and release notes: - README and CHANGELOG now describe runtime parity and that helpers extend standard microtasks as cancellable. - Update package metadata: - add microtask-related keywords to both package manifests. - Refresh size/check expectations and related tests for new API additions. --- .size-limit.json | 28 +- CHANGELOG.md | 14 +- README.md | 5 +- lib/package.json | 3 + lib/src/index.ts | 6 +- lib/src/timer/microtask.ts | 227 +++++++++ lib/src/timer/microtasks/promiseMicrotask.ts | 54 +++ lib/src/timer/microtasks/runMicrotask.ts | 30 ++ lib/src/timer/microtasks/timerMicrotask.ts | 93 ++++ lib/src/timer/timeout.ts | 25 + lib/test/bundle-size-check.js | 8 +- .../src/common/object/for_each_key.test.ts | 4 +- lib/test/src/common/timer/microtask.test.ts | 452 ++++++++++++++++++ package.json | 3 + 14 files changed, 936 insertions(+), 16 deletions(-) create mode 100644 lib/src/timer/microtask.ts create mode 100644 lib/src/timer/microtasks/promiseMicrotask.ts create mode 100644 lib/src/timer/microtasks/runMicrotask.ts create mode 100644 lib/src/timer/microtasks/timerMicrotask.ts create mode 100644 lib/test/src/common/timer/microtask.test.ts diff --git a/.size-limit.json b/.size-limit.json index 81319fe2..c8131b57 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,21 +2,21 @@ { "name": "es5-full", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "32.5 kb", + "limit": "33.5 kb", "brotli": false, "running": false }, { "name": "es6-full", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "31.5 kb", + "limit": "32 kb", "brotli": false, "running": false }, { "name": "es5-full-brotli", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "11.5 kb", + "limit": "12 kb", "brotli": true, "running": false }, @@ -30,14 +30,14 @@ { "name": "es5-zip", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "12.5 Kb", + "limit": "13 Kb", "gzip": true, "running": false }, { "name": "es6-zip", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "12.5 Kb", + "limit": "13 Kb", "gzip": true, "running": false }, @@ -91,9 +91,25 @@ { "name": "es5-simple-string", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "1kb", + "limit": "1 kb", "import": "{ getWindow, strEndsWith }", "gzip": true, "running": false + }, + { + "name": "es5-scheduleTimeout", + "path": "lib/dist/es5/mod/ts-utils.js", + "limit": "1 kb", + "import": "{ scheduleTimeout }", + "gzip": true, + "running": false + }, + { + "name": "es5-scheduleMicrotask", + "path": "lib/dist/es5/mod/ts-utils.js", + "limit": "1.5 kb", + "import": "{ scheduleMicrotask }", + "gzip": true, + "running": false } ] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cb8a3733..30705886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# Unreleased + +## Changelog + +### Features + +- Add microtask scheduling helpers with native `queueMicrotask`, Promise, and timer-backed fallbacks + - New functions: `scheduleMicrotask`, `hasQueueMicrotask`, `getQueueMicrotask`, `setMicroTaskFallbackOptions` + - New public types: `ScheduleMicrotaskFn`, `MicroTaskOptions` + - Extends microtask support by providing cancellable microtasks via `ITimerHandler`, plus fallback ordering to run microtasks before queued timers when using the timer-backed implementation + - Provides runtime parity across all supported environments by using native `queueMicrotask` when present, Promise-backed scheduling when available, and a timer-backed microtask queue otherwise + # v0.14.0 May 18th, 2026 ## Changelog @@ -5,7 +17,7 @@ ### Features - [#525](https://github.com/nevware21/ts-utils/pull/525) feat(array): add new array helpers and array-like detection - - New helpers: `isArrayLike`, `arrSlice`, and other array utility improvements + - New helpers: `isArrayLike`, `arrUnique`, `arrCompact`, `arrFlatten`, `arrGroupBy`, `arrChunk` and export previously missed `isArrayLike` - [#527](https://github.com/nevware21/ts-utils/pull/527) feat(string): add `strReplace` and `strReplaceAll` helpers with refactored internal replacements - [#528](https://github.com/nevware21/ts-utils/pull/528) feat(string): add `strCapitalizeWords` helper - [#529](https://github.com/nevware21/ts-utils/pull/529) / [#530](https://github.com/nevware21/ts-utils/pull/530) feat(string): add `strTruncate`, `strCount`, `strAt`, and `strMatchAll` helpers with shared literal regex helper diff --git a/README.md b/README.md index 92cc4bbf..d3c5899c 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ npm install @nevware21/ts-utils --save - Consistent timing APIs across environments - Performance measurement utilities - Scheduling helpers with automatic polyfills +- Microtask scheduling helpers that extend standard microtasks with cancellable handlers, with cross-runtime parity for Node.js, browsers, and web workers - [Customizable timeout handling](https://nevware21.github.io/ts-utils/timeout-overrides.html) via package-level and global overrides For advanced timeout customization options, including global overrides, see our [Timeout Overrides Guide](https://nevware21.github.io/ts-utils/timeout-overrides.html). @@ -114,7 +115,7 @@ Below is a categorized list of all available utilities with direct links to thei | Type | Functions / Helpers / Aliases / Polyfills |----------------------------|--------------------------------------------------- -| Runtime Environment Checks | [getCancelIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/getCancelIdleCallback.html)(); [getDocument](https://nevware21.github.io/ts-utils/typedoc/functions/getDocument.html)(); [getGlobal](https://nevware21.github.io/ts-utils/typedoc/functions/getGlobal.html)(); [getHistory](https://nevware21.github.io/ts-utils/typedoc/functions/getHistory.html)(); [getIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/getIdleCallback.html)(); [getInst](https://nevware21.github.io/ts-utils/typedoc/functions/getInst.html)(); [getNavigator](https://nevware21.github.io/ts-utils/typedoc/functions/getNavigator.html)(); [getPerformance](https://nevware21.github.io/ts-utils/typedoc/functions/getPerformance.html)(); [getWindow](https://nevware21.github.io/ts-utils/typedoc/functions/getWindow.html)(); [hasDocument](https://nevware21.github.io/ts-utils/typedoc/functions/hasDocument.html)(); [hasHistory](https://nevware21.github.io/ts-utils/typedoc/functions/hasHistory.html)(); [hasNavigator](https://nevware21.github.io/ts-utils/typedoc/functions/hasNavigator.html)(); [hasPerformance](https://nevware21.github.io/ts-utils/typedoc/functions/hasPerformance.html)(); [hasWindow](https://nevware21.github.io/ts-utils/typedoc/functions/hasWindow.html)(); [isNode](https://nevware21.github.io/ts-utils/typedoc/functions/isNode.html)(); [isWebWorker](https://nevware21.github.io/ts-utils/typedoc/functions/isWebWorker.html)(); [hasIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/hasIdleCallback.html)(); [lazySafeGetInst](https://nevware21.github.io/ts-utils/typedoc/functions/lazySafeGetInst.html)(); +| Runtime Environment Checks | [getCancelIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/getCancelIdleCallback.html)(); [getDocument](https://nevware21.github.io/ts-utils/typedoc/functions/getDocument.html)(); [getGlobal](https://nevware21.github.io/ts-utils/typedoc/functions/getGlobal.html)(); [getHistory](https://nevware21.github.io/ts-utils/typedoc/functions/getHistory.html)(); [getIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/getIdleCallback.html)(); [getInst](https://nevware21.github.io/ts-utils/typedoc/functions/getInst.html)(); [getNavigator](https://nevware21.github.io/ts-utils/typedoc/functions/getNavigator.html)(); [getPerformance](https://nevware21.github.io/ts-utils/typedoc/functions/getPerformance.html)(); [getQueueMicrotask](https://nevware21.github.io/ts-utils/typedoc/functions/getQueueMicrotask.html)(); [getWindow](https://nevware21.github.io/ts-utils/typedoc/functions/getWindow.html)(); [hasDocument](https://nevware21.github.io/ts-utils/typedoc/functions/hasDocument.html)(); [hasHistory](https://nevware21.github.io/ts-utils/typedoc/functions/hasHistory.html)(); [hasNavigator](https://nevware21.github.io/ts-utils/typedoc/functions/hasNavigator.html)(); [hasPerformance](https://nevware21.github.io/ts-utils/typedoc/functions/hasPerformance.html)(); [hasQueueMicrotask](https://nevware21.github.io/ts-utils/typedoc/functions/hasQueueMicrotask.html)(); [hasWindow](https://nevware21.github.io/ts-utils/typedoc/functions/hasWindow.html)(); [isNode](https://nevware21.github.io/ts-utils/typedoc/functions/isNode.html)(); [isWebWorker](https://nevware21.github.io/ts-utils/typedoc/functions/isWebWorker.html)(); [hasIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/hasIdleCallback.html)(); [lazySafeGetInst](https://nevware21.github.io/ts-utils/typedoc/functions/lazySafeGetInst.html)(); | Type Identity | [isArray](https://nevware21.github.io/ts-utils/typedoc/functions/isArray.html)(); [isArrayLike](https://nevware21.github.io/ts-utils/typedoc/functions/isArrayLike.html)(); [isArrayBuffer](https://nevware21.github.io/ts-utils/typedoc/functions/isArrayBuffer.html)(); [isAsyncFunction](https://nevware21.github.io/ts-utils/typedoc/functions/isAsyncFunction.html)(); [isAsyncGenerator](https://nevware21.github.io/ts-utils/typedoc/functions/isAsyncGenerator.html)(); [isAsyncIterable](https://nevware21.github.io/ts-utils/typedoc/functions/isAsyncIterable.html)(); [isBigInt](https://nevware21.github.io/ts-utils/typedoc/functions/isBigInt.html)(); [isBlob](https://nevware21.github.io/ts-utils/typedoc/functions/isBlob.html)(); [isBoolean](https://nevware21.github.io/ts-utils/typedoc/functions/isBoolean.html)(); [isDate](https://nevware21.github.io/ts-utils/typedoc/functions/isDate.html)(); [isElement](https://nevware21.github.io/ts-utils/typedoc/functions/isElement.html)(); [isElementLike](https://nevware21.github.io/ts-utils/typedoc/functions/isElementLike.html)(); [isError](https://nevware21.github.io/ts-utils/typedoc/functions/isError.html)(); [isFile](https://nevware21.github.io/ts-utils/typedoc/functions/isFile.html)(); [isFiniteNumber](https://nevware21.github.io/ts-utils/typedoc/functions/isFiniteNumber.html)(); [isFormData](https://nevware21.github.io/ts-utils/typedoc/functions/isFormData.html)(); [isFunction](https://nevware21.github.io/ts-utils/typedoc/functions/isFunction.html)(); [isGenerator](https://nevware21.github.io/ts-utils/typedoc/functions/isGenerator.html)(); [isInteger](https://nevware21.github.io/ts-utils/typedoc/functions/isInteger.html)(); [isIntegerInRange](https://nevware21.github.io/ts-utils/typedoc/functions/isIntegerInRange.html)(); [isIterable](https://nevware21.github.io/ts-utils/typedoc/functions/isIterable.html)(); [isIterator](https://nevware21.github.io/ts-utils/typedoc/functions/isIterator.html)(); [isMap](https://nevware21.github.io/ts-utils/typedoc/functions/isMap.html)(); [isMapLike](https://nevware21.github.io/ts-utils/typedoc/functions/isMapLike.html)(); [isNullOrUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isNullOrUndefined.html)(); [isNumber](https://nevware21.github.io/ts-utils/typedoc/functions/isNumber.html)(); [isObject](https://nevware21.github.io/ts-utils/typedoc/functions/isObject.html)(); [isPlainObject](https://nevware21.github.io/ts-utils/typedoc/functions/isPlainObject.html)(); [isPrimitive](https://nevware21.github.io/ts-utils/typedoc/functions/isPrimitive.html)(); [isPrimitiveType](https://nevware21.github.io/ts-utils/typedoc/functions/isPrimitiveType.html)(); [isPromise](https://nevware21.github.io/ts-utils/typedoc/functions/isPromise.html)(); [isPromiseLike](https://nevware21.github.io/ts-utils/typedoc/functions/isPromiseLike.html)(); [isRegExp](https://nevware21.github.io/ts-utils/typedoc/functions/isRegExp.html)(); [isSet](https://nevware21.github.io/ts-utils/typedoc/functions/isSet.html)(); [isSetLike](https://nevware21.github.io/ts-utils/typedoc/functions/isSetLike.html)(); [isStrictNullOrUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isStrictNullOrUndefined.html)(); [isStrictUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isStrictUndefined.html)(); [isString](https://nevware21.github.io/ts-utils/typedoc/functions/isString.html)(); [isThenable](https://nevware21.github.io/ts-utils/typedoc/functions/isThenable.html)(); [isTypeof](https://nevware21.github.io/ts-utils/typedoc/functions/isTypeof.html)(); [isUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isUndefined.html)(); [isWeakMap](https://nevware21.github.io/ts-utils/typedoc/functions/isWeakMap.html)(); [isWeakSet](https://nevware21.github.io/ts-utils/typedoc/functions/isWeakSet.html)(); | Value Check | [hasValue](https://nevware21.github.io/ts-utils/typedoc/functions/hasValue.html)(); [isDefined](https://nevware21.github.io/ts-utils/typedoc/functions/isDefined.html)(); [isEmpty](https://nevware21.github.io/ts-utils/typedoc/functions/isEmpty.html)(); [isNotTruthy](https://nevware21.github.io/ts-utils/typedoc/functions/isNotTruthy.html)(); [isNullOrUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isNullOrUndefined.html)(); [isStrictNullOrUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isStrictNullOrUndefined.html)(); [isStrictUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isStrictUndefined.html)(); [isTruthy](https://nevware21.github.io/ts-utils/typedoc/functions/isTruthy.html)(); [isUndefined](https://nevware21.github.io/ts-utils/typedoc/functions/isUndefined.html)(); | Value | [getValueByKey](https://nevware21.github.io/ts-utils/typedoc/functions/getValueByKey.html)(); [setValueByKey](https://nevware21.github.io/ts-utils/typedoc/functions/setValueByKey.html)(); [getValueByIter](https://nevware21.github.io/ts-utils/typedoc/functions/getValueByIter.html)(); [setValueByIter](https://nevware21.github.io/ts-utils/typedoc/functions/setValueByIter.html)(); [encodeAsJson](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsJson.html)(); [encodeAsHtml](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHtml.html)(); [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getIntValue](https://nevware21.github.io/ts-utils/typedoc/functions/getIntValue.html)(); [normalizeJsName](https://nevware21.github.io/ts-utils/typedoc/functions/normalizeJsName.html)(); @@ -132,7 +133,7 @@ Below is a categorized list of all available utilities with direct links to thei | Object | [deepExtend](https://nevware21.github.io/ts-utils/typedoc/functions/deepExtend.html)(); [forEachOwnKey](https://nevware21.github.io/ts-utils/typedoc/functions/forEachOwnKey.html)(); [forEachOwnKeySafe](https://nevware21.github.io/ts-utils/typedoc/functions/forEachOwnKeySafe.html)(); [isObject](https://nevware21.github.io/ts-utils/typedoc/functions/isObject.html)(); [isUnsafePropKey](https://nevware21.github.io/ts-utils/typedoc/functions/isUnsafePropKey.html)(); [isUnsafeTarget](https://nevware21.github.io/ts-utils/typedoc/functions/isUnsafeTarget.html)(); [objAssign](https://nevware21.github.io/ts-utils/typedoc/functions/objAssign.html)(); [objCopyProps](https://nevware21.github.io/ts-utils/typedoc/functions/objCopyProps.html)(); [objCreate](https://nevware21.github.io/ts-utils/typedoc/functions/objCreate.html)(); [objDeepCopy](https://nevware21.github.io/ts-utils/typedoc/functions/objDeepCopy.html)(); [objDeepFreeze](https://nevware21.github.io/ts-utils/typedoc/functions/objDeepFreeze.html)(); [objDefaults](https://nevware21.github.io/ts-utils/typedoc/functions/objDefaults.html)(); [objDefine](https://nevware21.github.io/ts-utils/typedoc/functions/objDefine.html)(); [objDefineAccessors](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineAccessors.html)(); [objDefineGet](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineGet.html)(); [objDefineProp](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProp.html)(); [objDefineProps](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProps.html)(); [objDefineProperties](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProperties.html)(); [objDiff](https://nevware21.github.io/ts-utils/typedoc/functions/objDiff.html)(); [objEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objEntries.html)(); [objExtend](https://nevware21.github.io/ts-utils/typedoc/functions/objExtend.html)(); [objForEachKey](https://nevware21.github.io/ts-utils/typedoc/functions/objForEachKey.html)(); [objForEachKeySafe](https://nevware21.github.io/ts-utils/typedoc/functions/objForEachKeySafe.html)(); [objFreeze](https://nevware21.github.io/ts-utils/typedoc/functions/objFreeze.html)(); [objFromEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objFromEntries.html)(); [objGetOwnPropertyDescriptor](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyDescriptor.html)(); [objGetOwnPropertyDescriptors](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyDescriptors.html)(); [objGetOwnPropertyNames](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyNames.html)(); [objGetOwnPropertySymbols](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertySymbols.html)(); [objHasOwn](https://nevware21.github.io/ts-utils/typedoc/functions/objHasOwn.html)(); [objHasOwnProperty](https://nevware21.github.io/ts-utils/typedoc/functions/objHasOwnProperty.html)(); [objIs](https://nevware21.github.io/ts-utils/typedoc/functions/objIs.html)(); [objIsExtensible](https://nevware21.github.io/ts-utils/typedoc/functions/objIsExtensible.html)(); [objIsFrozen](https://nevware21.github.io/ts-utils/typedoc/functions/objIsFrozen.html)(); [objIsSealed](https://nevware21.github.io/ts-utils/typedoc/functions/objIsSealed.html)(); [objKeys](https://nevware21.github.io/ts-utils/typedoc/functions/objKeys.html)(); [objMapValues](https://nevware21.github.io/ts-utils/typedoc/functions/objMapValues.html)(); [objMergeIf](https://nevware21.github.io/ts-utils/typedoc/functions/objMergeIf.html)(); [objOmit](https://nevware21.github.io/ts-utils/typedoc/functions/objOmit.html)(); [objOmitBy](https://nevware21.github.io/ts-utils/typedoc/functions/objOmitBy.html)(); [objPick](https://nevware21.github.io/ts-utils/typedoc/functions/objPick.html)(); [objPickBy](https://nevware21.github.io/ts-utils/typedoc/functions/objPickBy.html)(); [objPreventExtensions](https://nevware21.github.io/ts-utils/typedoc/functions/objPreventExtensions.html)(); [objPropertyIsEnumerable](https://nevware21.github.io/ts-utils/typedoc/functions/objPropertyIsEnumerable.html)(); [objSeal](https://nevware21.github.io/ts-utils/typedoc/functions/objSeal.html)(); [objGetPrototypeOf](https://nevware21.github.io/ts-utils/typedoc/functions/objGetPrototypeOf.html)(); [objSetPrototypeOf](https://nevware21.github.io/ts-utils/typedoc/functions/objSetPrototypeOf.html)(); [objToString](https://nevware21.github.io/ts-utils/typedoc/functions/objToString.html)(); [objValues](https://nevware21.github.io/ts-utils/typedoc/functions/objValues.html)();
[polyObjEntries](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjEntries.html)(); [polyObjIs](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjIs.html)(); [polyObjKeys](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjKeys.html)();
| String | [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [isString](https://nevware21.github.io/ts-utils/typedoc/functions/isString.html)(); [strCount](https://nevware21.github.io/ts-utils/typedoc/functions/strCount.html)(); [strEndsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strEndsWith.html)(); [strIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strIndexOf.html)(); [strIsNullOrEmpty](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrEmpty.html)(); [strIsNullOrWhiteSpace](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrWhiteSpace.html)(); [strLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strLastIndexOf.html)(); [strLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strLeft.html)(); [strPadEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strPadEnd.html)(); [strPadStart](https://nevware21.github.io/ts-utils/typedoc/functions/strPadStart.html)(); [strRepeat](https://nevware21.github.io/ts-utils/typedoc/functions/strRepeat.html)(); [strReplace](https://nevware21.github.io/ts-utils/typedoc/functions/strReplace.html)(); [strReplaceAll](https://nevware21.github.io/ts-utils/typedoc/functions/strReplaceAll.html)(); [strRight](https://nevware21.github.io/ts-utils/typedoc/functions/strRight.html)(); [strSlice](https://nevware21.github.io/ts-utils/typedoc/functions/strSlice.html)(); [strSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSplit.html)(); [strStartsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strStartsWith.html)(); [strSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstr.html)(); [strSubstring](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstring.html)(); [strSymSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSymSplit.html)(); [strTruncate](https://nevware21.github.io/ts-utils/typedoc/functions/strTruncate.html)(); [strTrim](https://nevware21.github.io/ts-utils/typedoc/functions/strTrim.html)(); [strTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimEnd.html)(); [strTrimLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimLeft.html)(); [strTrimRight](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimRight.html)(); [strTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimStart.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCapitalizeWords](https://nevware21.github.io/ts-utils/typedoc/functions/strCapitalizeWords.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)(); [strContains](https://nevware21.github.io/ts-utils/typedoc/functions/strContains.html)(); [strIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/strIncludes.html)();
[polyStrSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrSubstr.html)(); [polyStrTrim](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrim.html)(); [polyStrTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimEnd.html)(); [polyStrTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimStart.html)(); [polyStrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrIncludes.html)();
| Symbol | [WellKnownSymbols](https://nevware21.github.io/ts-utils/typedoc/enums/WellKnownSymbols.html) (const enum);
[getKnownSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/getKnownSymbol.html)(); [getSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/getSymbol.html)(); [hasSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/hasSymbol.html)(); [isSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/isSymbol.html)(); [newSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/newSymbol.html)(); [symbolFor](https://nevware21.github.io/ts-utils/typedoc/functions/symbolFor.html)(); [symbolKeyFor](https://nevware21.github.io/ts-utils/typedoc/functions/symbolKeyFor.html)();
[polyGetKnownSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/polyGetKnownSymbol.html)(); [polyNewSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/polyNewSymbol.html)(); [polySymbolFor](https://nevware21.github.io/ts-utils/typedoc/functions/polySymbolFor.html)(); [polySymbolKeyFor](https://nevware21.github.io/ts-utils/typedoc/functions/polySymbolKeyFor.html)();

Polyfills are used to automatically backfill runtimes that do not support `Symbol`, not all of the Symbol functionality is provided. -| Timer | [createTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeout.html)(); [createTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeoutWith.html)(); [elapsedTime](https://nevware21.github.io/ts-utils/typedoc/functions/elapsedTime.html)(); [perfNow](https://nevware21.github.io/ts-utils/typedoc/functions/perfNow.html)(); [setGlobalTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setGlobalTimeoutOverrides.html)(); [setTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setTimeoutOverrides.html)(); [utcNow](https://nevware21.github.io/ts-utils/typedoc/functions/utcNow.html)(); [scheduleIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleIdleCallback.html)(); [scheduleInterval](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleInterval.html)(); [scheduleTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeout.html)(); [scheduleTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeoutWith.html)(); [hasIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/hasIdleCallback.html)();
For runtimes that don't support `requestIdleCallback` normal setTimeout() is used with the values from [`setDefaultIdleTimeout`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultIdleTimeout.html)() and [`setDefaultMaxExecutionTime`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultMaxExecutionTime.html)();
[polyUtcNow](https://nevware21.github.io/ts-utils/typedoc/functions/polyUtcNow.html)(); +| Timer | [createTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeout.html)(); [createTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeoutWith.html)(); [elapsedTime](https://nevware21.github.io/ts-utils/typedoc/functions/elapsedTime.html)(); [perfNow](https://nevware21.github.io/ts-utils/typedoc/functions/perfNow.html)(); [setGlobalTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setGlobalTimeoutOverrides.html)(); [setMicroTaskFallbackOptions](https://nevware21.github.io/ts-utils/typedoc/functions/setMicroTaskFallbackOptions.html)(); [setTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setTimeoutOverrides.html)(); [utcNow](https://nevware21.github.io/ts-utils/typedoc/functions/utcNow.html)(); [scheduleIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleIdleCallback.html)(); [scheduleInterval](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleInterval.html)(); [scheduleMicrotask](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleMicrotask.html)(); [scheduleTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeout.html)(); [scheduleTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeoutWith.html)(); [hasIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/hasIdleCallback.html)();
Microtask helpers extend standard microtasks with cancellable handlers, and provide parity across all supported runtimes by using native `queueMicrotask` when present, Promise-backed scheduling when available, and a timer-backed queue otherwise.
For runtimes that don't support `requestIdleCallback` normal setTimeout() is used with the values from [`setDefaultIdleTimeout`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultIdleTimeout.html)() and [`setDefaultMaxExecutionTime`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultMaxExecutionTime.html)();
[polyUtcNow](https://nevware21.github.io/ts-utils/typedoc/functions/polyUtcNow.html)(); | Conversion & Encoding | [encodeAsJson](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsJson.html)(); [encodeAsHtml](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHtml.html)(); [encodeAsBase64](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsBase64.html)(); [decodeBase64](https://nevware21.github.io/ts-utils/typedoc/functions/decodeBase64.html)(); [encodeAsBase64Url](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsBase64Url.html)(); [decodeBase64Url](https://nevware21.github.io/ts-utils/typedoc/functions/decodeBase64Url.html)(); [encodeAsHex](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHex.html)(); [decodeHex](https://nevware21.github.io/ts-utils/typedoc/functions/decodeHex.html)(); [encodeAsUri](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsUri.html)(); [decodeUri](https://nevware21.github.io/ts-utils/typedoc/functions/decodeUri.html)(); [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getIntValue](https://nevware21.github.io/ts-utils/typedoc/functions/getIntValue.html)(); [normalizeJsName](https://nevware21.github.io/ts-utils/typedoc/functions/normalizeJsName.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCapitalizeWords](https://nevware21.github.io/ts-utils/typedoc/functions/strCapitalizeWords.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)(); | Cache | [createCachedValue](https://nevware21.github.io/ts-utils/typedoc/functions/createCachedValue.html)(); [createDeferredCachedValue](https://nevware21.github.io/ts-utils/typedoc/functions/createDeferredCachedValue.html)(); [getDeferred](https://nevware21.github.io/ts-utils/typedoc/functions/getDeferred.html)(); [getWritableDeferred](https://nevware21.github.io/ts-utils/typedoc/functions/getWritableDeferred.html)(); | Lazy | [getLazy](https://nevware21.github.io/ts-utils/typedoc/functions/getLazy.html)(); [getWritableLazy](https://nevware21.github.io/ts-utils/typedoc/functions/getWritableLazy.html)(); [lazySafeGetInst](https://nevware21.github.io/ts-utils/typedoc/functions/lazySafeGetInst.html)(); [safeGetLazy](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetLazy.html)(); [safeGetLazy](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetLazy.html)(); [setBypassLazyCache](https://nevware21.github.io/ts-utils/typedoc/functions/setBypassLazyCache.html)(); diff --git a/lib/package.json b/lib/package.json index 3ca314dd..e9864043 100644 --- a/lib/package.json +++ b/lib/package.json @@ -105,6 +105,9 @@ "idle timer", "timer", "interval", + "microtask", + "queueMicrotask", + "cancellable microtask", "includes", "string contains", "html encode", diff --git a/lib/src/index.ts b/lib/src/index.ts index 1b256813..064df429 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -76,7 +76,7 @@ export { export { deepExtend, objExtend } from "./helpers/extend"; export { getValueByKey, setValueByKey, getValueByIter, setValueByIter } from "./helpers/get_set_value"; export { ILazyValue, getLazy, setBypassLazyCache, getWritableLazy } from "./helpers/lazy"; -export { IGetLength as GetLengthImpl, getLength } from "./helpers/length"; +export { IGetLength, IGetLength as GetLengthImpl, getLength } from "./helpers/length"; export { getIntValue, isInteger, isIntegerInRange, isFiniteNumber } from "./helpers/number"; export { getPerformance, hasPerformance, elapsedTime, perfNow } from "./helpers/perf"; export { createFilenameRegex, createLiteralRegex, createWildcardRegex, makeGlobRegex } from "./helpers/regexp"; @@ -166,6 +166,10 @@ export { getIdleCallback, getCancelIdleCallback, RequestIdleCallback, CancelIdleCallback } from "./timer/idle"; export { scheduleInterval } from "./timer/interval"; +export { + hasQueueMicrotask, scheduleMicrotask, getQueueMicrotask, + ScheduleMicrotaskFn, MicroTaskOptions, setMicroTaskFallbackOptions +} from "./timer/microtask"; export { TimeoutOverrideFn, ClearTimeoutOverrideFn, TimeoutOverrideFuncs, scheduleTimeout, scheduleTimeoutWith, createTimeout, createTimeoutWith, setTimeoutOverrides, setGlobalTimeoutOverrides diff --git a/lib/src/timer/microtask.ts b/lib/src/timer/microtask.ts new file mode 100644 index 00000000..00bb04dc --- /dev/null +++ b/lib/src/timer/microtask.ts @@ -0,0 +1,227 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { isStrictUndefined } from "../helpers/base"; +import { _getGlobalInstFn, getInst } from "../helpers/environment"; +import { ITimerHandler, _TimerHandler, _createTimerHandler } from "./handler"; +import { _getPromiseMicrotaskFn } from "./microtasks/promiseMicrotask"; +import { _addMicrotaskToQueue } from "./microtasks/timerMicrotask"; + +let _defaultOptions: MicroTaskOptions | undefined; + +/** + * Type alias for a microtask callback function, which is a function that is scheduled to run in the microtask + * queue after the current execution context completes. + * @since 0.15.0 + * @group Timer + * @group Environment + */ +export type MicrotaskFn = () => void; + +/** + * Type alias for a function that is used to schedule a microtask, which is a function + * that takes a callback and schedules it to run + * + * @since 0.15.0 + * @group Timer + * @group Environment + * @param callback - The microtask callback function to schedule. + */ +export type ScheduleMicrotaskFn = (callback: MicrotaskFn) => void; + +/** + * Controls how `scheduleMicrotask` chooses fallback behavior when native + * `queueMicrotask` is not available. + * + * @since 0.15.0 + * @group Timer + */ +export interface MicroTaskOptions { + /** + * Provide a custom scheduling function to use when native `queueMicrotask` is unavailable. + * When specified, this takes precedence over both the Promise fallback and the timer-backed + * queue fallback, regardless of whether Promise support is available. + */ + scheduleFn?: ScheduleMicrotaskFn; + + /** + * When `true`, skips the Promise fallback and uses `scheduleTimeout(..., 0)` instead. When + * `scheduleFn` is not provided, this option controls whether to use `scheduleTimeout(..., 0)` + * as the fallback instead of `Promise.resolve().then(...)`. + * A per-call value of `false` explicitly opts back in to the Promise fallback even when the global + * default set via {@link setMicroTaskFallbackOptions} has `useTimeout: true`. + */ + useTimeout?: boolean; +} + +/** + * Returns the global `queueMicrotask` function if available, or `null` when unavailable. + * + * @function + * @since 0.15.0 + * @group Timer + * @group Environment + * @example + * ```ts + * const queueFn = getQueueMicrotask(); + * if (queueFn) { + * queueFn(() => { + * console.log("microtask"); + * }); + * } + * ``` + */ +export const getQueueMicrotask = (/*#__PURE__*/_getGlobalInstFn(getInst as any, ["queueMicrotask"])); + +/** + * Identifies if the runtime supports the `queueMicrotask` API. + * + * @since 0.15.0 + * @group Timer + * @group Environment + * @returns True if the runtime supports `queueMicrotask` otherwise false. + * @example + * ```ts + * if (hasQueueMicrotask()) { + * console.log("Native queueMicrotask support is available"); + * } + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function hasQueueMicrotask(): boolean { + return !!( /*#__PURE__*/getQueueMicrotask()); +} + +/** + * Sets the default fallback behavior for {@link scheduleMicrotask} when + * `queueMicrotask` is not available. + * + * @since 0.15.0 + * @group Timer + * + * @param options - The fallback options to apply. Passing `undefined` resets + * options to defaults. + * @example + * ```ts + * setMicroTaskFallbackOptions({ + * useTimeout: true + * }); + * + * scheduleMicrotask(() => { + * console.log("timer-backed microtask fallback"); + * }); + * + * setMicroTaskFallbackOptions(); + * ``` + */ +export function setMicroTaskFallbackOptions(options?: MicroTaskOptions): void { + _defaultOptions = options; +} + +/** + * Schedules a callback to run in the microtask queue. + * + * It uses the native `queueMicrotask` when available, otherwise falls back to + * `Promise.resolve().then(...)`, and if `Promise` is unavailable it falls back to + * `scheduleTimeout(..., 0)`. + * Unlike standard microtasks, this helper returns a cancellable `ITimerHandler` + * so scheduled callbacks can be canceled before execution. + * This provides consistent microtask scheduling behavior across all supported runtimes, + * including Node.js, browsers, and web workers. + * + * @since 0.15.0 + * @group Timer + * + * @param callback - The callback to execute. + * @param options - Optional per-call fallback options when `queueMicrotask` is unavailable. + * @returns A handler that can be used to cancel or refresh the scheduled callback. + * @example + * ```ts + * let order: string[] = []; + * order.push("sync"); + * + * const handler = scheduleMicrotask(() => { + * order.push("microtask"); + * }); + * + * scheduleTimeout(() => { + * order.push("timeout"); + * }, 0); + * + * // order becomes ["sync", "microtask", "timeout"] + * handler.enabled; // true until the microtask executes + * ``` + * @example + * ```ts + * scheduleMicrotask(() => { + * console.log("custom fallback scheduler"); + * }, { + * scheduleFn: (cb) => { + * setTimeout(cb, 0); + * } + * }); + * ``` + */ +export function scheduleMicrotask(callback: () => void, options?: MicroTaskOptions): ITimerHandler { + let scheduleFn: ScheduleMicrotaskFn | undefined; + let queueMicrotaskFn = getQueueMicrotask(); + + if (!queueMicrotaskFn) { + // Do we have a custom schedule function to use + scheduleFn = (options && options.scheduleFn) || (_defaultOptions && _defaultOptions.scheduleFn); + if (!scheduleFn) { + // Per-call options.useTimeout takes full precedence over the global default when it is + // explicitly provided (even as false), so only fall back to _defaultOptions when the per-call + // value is undefined/absent. usePromise is the inverse: true unless useTimeout is explicitly set. + let usePromise = !((options && !isStrictUndefined(options.useTimeout)) + ? options.useTimeout + : (_defaultOptions && _defaultOptions.useTimeout)); + if (usePromise) { + // Use the Promise based fallback if available, otherwise use setTimeout + queueMicrotaskFn = _getPromiseMicrotaskFn(); + } + } + } + + return _createCancellableMicroTask(callback, scheduleFn || queueMicrotaskFn || _addMicrotaskToQueue); +} + +/** + * @internal + * @since 0.15.0 + */ +function _createCancellableMicroTask(callback: () => void, queueFn: ScheduleMicrotaskFn): ITimerHandler { + let handler: _TimerHandler; + // Used to track the currently scheduled task, incremented to cancel pending tasks when needed + let currentTask = 0; + + function _scheduleTask() { + let taskId = ++currentTask; + queueFn(() => { + if (taskId === currentTask) { + handler.dn(); + callback(); + } + }); + + return taskId; + } + + function _cancelTask(taskId: number) { + if (taskId === currentTask) { + currentTask++; + } + } + + handler = _createTimerHandler(false, _scheduleTask, _cancelTask); + // Start the timer only after handler is fully assigned so that any synchronous + // queueFn implementation that fires the callback immediately can safely access handler.dn(). + handler.h.refresh(); + + return handler.h; +} diff --git a/lib/src/timer/microtasks/promiseMicrotask.ts b/lib/src/timer/microtasks/promiseMicrotask.ts new file mode 100644 index 00000000..348f61ae --- /dev/null +++ b/lib/src/timer/microtasks/promiseMicrotask.ts @@ -0,0 +1,54 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { isFunction } from "../../helpers/base"; +import { createCachedValue, ICachedValue } from "../../helpers/cache"; +import { getInst } from "../../helpers/environment"; +import { _globalLazyTestHooks, _initTestHooks } from "../../helpers/lazy"; +import { UNDEF_VALUE } from "../../internal/constants"; +import { ScheduleMicrotaskFn } from "../microtask"; +import { _runMicroTask } from "./runMicrotask"; + +let _promiseFn: ICachedValue; + +/** + * @internal + * @since 0.15.0 + */ +function _promiseScheduleFn(promiseCls: PromiseConstructor): ScheduleMicrotaskFn { + return function(callback: () => void): void { + promiseCls.resolve().then(() => { + _runMicroTask(callback); + }); + }; +} + +/** + * @internal + * Resolves the Promise-based scheduler from the current global state without caching. + * @since 0.15.0 + */ +function _resolvePromiseFn(): ScheduleMicrotaskFn | undefined { + let promiseCls = getInst("Promise"); + return (promiseCls && isFunction(promiseCls.resolve)) ? _promiseScheduleFn(promiseCls) : UNDEF_VALUE as any; +} + +/** + * @internal + * Returns the Promise-based microtask scheduler, re-resolving the global Promise when the lazy + * bypass flag is active (e.g. during tests) so that changes to the global Promise are reflected. + * @since 0.15.0 + */ +export function _getPromiseMicrotaskFn(): ScheduleMicrotaskFn | null { + !_globalLazyTestHooks && _initTestHooks(); + if (!_promiseFn || _globalLazyTestHooks.lzy) { + _promiseFn = createCachedValue(_resolvePromiseFn()); + } + + return _promiseFn.v || null; +} \ No newline at end of file diff --git a/lib/src/timer/microtasks/runMicrotask.ts b/lib/src/timer/microtasks/runMicrotask.ts new file mode 100644 index 00000000..5f8d7ea6 --- /dev/null +++ b/lib/src/timer/microtasks/runMicrotask.ts @@ -0,0 +1,30 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { MicrotaskFn } from "../microtask"; +import { scheduleTimeout } from "../timeout"; + +/** + * @internal + * Runs a microtask callback and ensures that any exceptions thrown are re-thrown in a new task to avoid swallowing errors. + * This is necessary because when using the fallback for microtasks, any exceptions thrown in the callback will be caught + * and not re-thrown, which can lead to silent failures. By catching exceptions and re-throwing them in a new task + * (using `scheduleTimeout`), we ensure that errors are properly surfaced even when using the Promise or timeout fallback. + * @since 0.15.0 + * @param callback - The callback to execute. + */ +export function _runMicroTask(callback: MicrotaskFn): void { + try { + callback(); + } catch (e) { + scheduleTimeout(() => { + throw e; + }, 0); + } +} + diff --git a/lib/src/timer/microtasks/timerMicrotask.ts b/lib/src/timer/microtasks/timerMicrotask.ts new file mode 100644 index 00000000..33fb7240 --- /dev/null +++ b/lib/src/timer/microtasks/timerMicrotask.ts @@ -0,0 +1,93 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { UNDEF_VALUE } from "../../internal/constants"; +import { ITimerHandler } from "../handler"; +import { MicrotaskFn } from "../microtask"; +import { _setMicrotaskCallback, scheduleTimeout } from "../timeout"; +import { _runMicroTask } from "./runMicrotask"; + +let _microtaskQueue: MicrotaskFn[] | undefined; +let _microtaskTimer: ITimerHandler | undefined; + +/** + * @internal + * @since 0.15.0 + */ +function _flushMicrotaskQueue(): void { + if (_microtaskTimer) { + // Cancel the timeout used to trigger the microtask queue flush, if it exists. If this function + // is being called as part of the timeout callback and the flush may have been called before this + // timeout callback was executed, so we need to check if the timer is still active before canceling it. + _microtaskTimer.cancel(); + } + + // Run all microtasks in the queue, if any are added while the flushing is being executed, they will be + // appended to the end of the queue and will be flushed in this loop as well before the function exits, + // this ensures that all microtasks are executed in the correct order even if new microtasks are scheduled + // while flushing the queue. + let queueIdx = 0; + while (_microtaskQueue && _microtaskQueue.length > queueIdx) { + _runMicroTask(_microtaskQueue[queueIdx++]); + } + + // Now clear the queue to ensure that any new tasks scheduled after this point will be added to a new queue + // and not executed in the current flush loop. + _microtaskQueue = UNDEF_VALUE; +} + +/** + * @internal + * @since 0.15.0 + */ +export function _addMicrotaskToQueue(callback: () => void): void { + if (!_microtaskQueue) { + _microtaskQueue = []; + } + + // Add the microtask callback to the queue, if the queue is currently being flushed and the callback is added + // after the current index, it will be executed as part of the current flush loop, otherwise it will be executed + // in the next flush loop when the queue is flushed again. + _microtaskQueue.push(callback); + + if (!_microtaskTimer || !_microtaskTimer.enabled) { + // Hook into the scheduleTimeout callback to flush the microtask queue, this is used as a + // fallback when native queueMicrotask is not available. + _setMicrotaskCallback(_flushMicrotaskQueue); + + // As there may not be any existing timers to flush the microtask queue, we need to schedule + // a timeout to ensure the queue is flushed even if the user does not schedule any timeouts + // themselves. If there is an existing timeout scheduled before the microtask callback is + // flushed, the microtask queue will be flushed before this timeout callback is executed + // as part of the timeout scheduling logic. + if (!_microtaskTimer) { + _microtaskTimer = scheduleTimeout(_flushMicrotaskQueue, 0); + } else { + // If there is already a timer scheduled, we can just ensure it is enabled to trigger the flush of the microtask queue + // If we called refresh() and the timer was already active, it will reset (and reorder) the timer to trigger the flush + // later than any existing timers, but this is necessary to ensure that the microtask queue is flushed in a future turn + // of the event loop and not immediately. + _microtaskTimer.enabled = true; + } + } +} + +/** + * @internal + * Reset the timer-backed microtask queue state. Intended for tests that need a clean queue/timer instance. + * @since 0.15.0 + */ +export function _resetMicrotaskQueue(): void { + if (_microtaskTimer) { + _microtaskTimer.cancel(); + _microtaskTimer = UNDEF_VALUE; + } + + _microtaskQueue = UNDEF_VALUE; +} + diff --git a/lib/src/timer/timeout.ts b/lib/src/timer/timeout.ts index 747ba879..e56822eb 100644 --- a/lib/src/timer/timeout.ts +++ b/lib/src/timer/timeout.ts @@ -15,6 +15,7 @@ import { ITimerHandler, _createTimerHandler } from "./handler"; // Package instance timeout override functions let _setTimeoutFn: TimeoutOverrideFn | undefined; let _clearTimeoutFn: ClearTimeoutOverrideFn | undefined; +let _microtaskCallback: (() => void) | undefined; function _resolveTimeoutFn(timeoutFn: TimeoutOverrideFn): TimeoutOverrideFn { let result = isFunction(timeoutFn) ? timeoutFn : _setTimeoutFn; @@ -53,6 +54,16 @@ function _createTimeoutWith(startTimer: boolean, overrideFn: TimeoutOverrideFn | let timerFn = theArgs[0]; theArgs[0] = function () { + // Microtask fallback hook for environments without native queueMicrotask support, this allows us to + // simulate the running of microtasks before standard timeouts. + let microTasksFn = _microtaskCallback; + if (microTasksFn) { + // Run any pending microtasks before running the timeout callback to allow any microtasks + // scheduled within the callback to run before the next timeout. + _microtaskCallback = UNDEF_VALUE; + microTasksFn(); + } + handler.dn(); fnApply(timerFn, UNDEF_VALUE, ArrSlice[CALL](arguments)); }; @@ -75,6 +86,20 @@ function _createTimeoutWith(startTimer: boolean, overrideFn: TimeoutOverrideFn | return handler.h; } +/** + * @internal + * Internal function to set the microtask callback used by the microtask scheduling functions, this is used as + * a hook to allow the timeout implementation to run any pending microtasks before running a timeout callback + * @since 0.15.0 + * @param queueFn - The function to be called to flush the microtask queue, this will be set by the microtask + * scheduling functions when they need to schedule a flush of the microtask queue + */ +export function _setMicrotaskCallback(queueFn: (() => void) | undefined): void { + if (!_microtaskCallback && isFunction(queueFn)) { + _microtaskCallback = queueFn; + } +} + /** * Sets the setTimeout and clearTimeout override functions for this package/closure instance to be used by all timeout operations * when no specific override functions are provided. If called with no parameters or undefined, diff --git a/lib/test/bundle-size-check.js b/lib/test/bundle-size-check.js index 61699374..4f71e372 100644 --- a/lib/test/bundle-size-check.js +++ b/lib/test/bundle-size-check.js @@ -7,25 +7,25 @@ const configs = [ { name: "es5-min-full", path: "../bundle/es5/umd/ts-utils.min.js", - limit: 36 * 1024, // 36 kb in bytes + limit: 37 * 1024, // 37 kb in bytes compress: false }, { name: "es6-min-full", path: "../bundle/es6/umd/ts-utils.min.js", - limit: 35.5 * 1024, // 35.5 kb in bytes + limit: 36 * 1024, // 36 kb in bytes compress: false }, { name: "es5-min-zip", path: "../bundle/es5/umd/ts-utils.min.js", - limit: 14 * 1024, // 14 kb in bytes + limit: 14.5 * 1024, // 14.5 kb in bytes compress: true }, { name: "es6-min-zip", path: "../bundle/es6/umd/ts-utils.min.js", - limit: 14 * 1024, // 14 kb in bytes + limit: 14.5 * 1024, // 14.5 kb in bytes compress: true }, { diff --git a/lib/test/src/common/object/for_each_key.test.ts b/lib/test/src/common/object/for_each_key.test.ts index ac7afa0c..43eb19c0 100644 --- a/lib/test/src/common/object/for_each_key.test.ts +++ b/lib/test/src/common/object/for_each_key.test.ts @@ -258,9 +258,9 @@ describe("object for_each_key tests", () => { "constructor": "attack", "prototype": "attack", "name": "Dave" - } as { [key: string]: string }; + } as { [key: string]: string }; - Object.defineProperty(obj, "__proto__", { + Object.defineProperty(obj, "__proto__", { configurable: true, enumerable: true, value: "attack", diff --git a/lib/test/src/common/timer/microtask.test.ts b/lib/test/src/common/timer/microtask.test.ts new file mode 100644 index 00000000..f70d43b2 --- /dev/null +++ b/lib/test/src/common/timer/microtask.test.ts @@ -0,0 +1,452 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import * as sinon from "sinon"; +import { assert } from "@nevware21/tripwire-chai"; +import { getGlobal } from "../../../../src/helpers/environment"; +import { setBypassLazyCache } from "../../../../src/helpers/lazy"; +import { getQueueMicrotask, hasQueueMicrotask, scheduleMicrotask, setMicroTaskFallbackOptions } from "../../../../src/timer/microtask"; +import { _addMicrotaskToQueue, _resetMicrotaskQueue } from "../../../../src/timer/microtasks/timerMicrotask"; +import { _runMicroTask } from "../../../../src/timer/microtasks/runMicrotask"; +import { scheduleTimeout, setTimeoutOverrides } from "../../../../src/timer/timeout"; + +describe("microtask tests", () => { + let orgPromise: any; + let orgQueueMicrotask: any; + let orgSetTimeout: any; + + before(() => { + orgPromise = (getGlobal()).Promise; + orgQueueMicrotask = (getGlobal()).queueMicrotask; + orgSetTimeout = setTimeout; + }); + + beforeEach(() => { + setBypassLazyCache(true); + setMicroTaskFallbackOptions(); + (getGlobal()).Promise = orgPromise; + (getGlobal()).queueMicrotask = orgQueueMicrotask; + }); + + afterEach(() => { + (getGlobal()).Promise = orgPromise; + (getGlobal()).queueMicrotask = orgQueueMicrotask; + _resetMicrotaskQueue(); + setTimeoutOverrides(); + setMicroTaskFallbackOptions(); + setBypassLazyCache(false); + }); + + it("hasQueueMicrotask", () => { + assert.equal(hasQueueMicrotask(), !!orgQueueMicrotask, "Check if we have queueMicrotask support"); + assert.equal(!!getQueueMicrotask(), !!orgQueueMicrotask, "Check direct queueMicrotask getter"); + }); + + it("surfaces exceptions thrown by a microtask", (done) => { + let thrownError: Error | undefined; + let expectedError = new Error("microtask failure"); + let testTimeoutOverride = ((callback: () => void) => { + orgSetTimeout(() => { + try { + callback(); + } catch (e) { + thrownError = e as Error; + } + }, 0); + + return 0; + }) as any; + + setTimeoutOverrides(testTimeoutOverride); + + _runMicroTask(() => { + throw expectedError; + }); + + orgSetTimeout(() => { + assert.equal(thrownError, expectedError, "Expected the microtask error to be re-thrown on a new task"); + done(); + }, 10); + }); + + it("scheduleMicroTask with available microtask queue", (done) => { + let events: string[] = []; + let handler = scheduleMicrotask(() => { + events.push("micro"); + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + events.push("sync"); + + orgSetTimeout(() => { + events.push("timeout"); + assert.deepEqual(events, ["sync", "micro", "timeout"], "Expected microtask ordering"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 0); + }); + + it("cancel prevents callback with available microtask queue", (done) => { + let called = 0; + let handler = scheduleMicrotask(() => { + called++; + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + handler.cancel(); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + + orgSetTimeout(() => { + assert.equal(called, 0, "Expected callback to not run after cancel"); + done(); + }, 0); + }); + + it("refresh reschedules callback with available microtask queue", (done) => { + let called = 0; + let handler = scheduleMicrotask(() => { + called++; + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + handler.refresh(); + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(called, 1, "Expected callback to run once after refresh"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 0); + }); + + it("enabled=false then enabled=true re-schedules with available microtask queue", (done) => { + let called = 0; + let handler = scheduleMicrotask(() => { + called++; + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + handler.enabled = false; + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + handler.enabled = true; + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(called, 1, "Expected callback to run once after enabled toggle"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 0); + }); + + describe("without queueMicrotask", () => { + beforeEach(() => { + (getGlobal()).queueMicrotask = null; + }); + + afterEach(() => { + setMicroTaskFallbackOptions(); + }); + + it("refresh reschedules Promise fallback callback", (done) => { + let called = 0; + let handler = scheduleMicrotask(() => { + called++; + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + handler.refresh(); + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(called, 1, "Expected callback to run once after refresh"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 0); + }); + + it("enabled=false then enabled=true re-schedules Promise fallback callback", (done) => { + let called = 0; + let handler = scheduleMicrotask(() => { + called++; + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + handler.enabled = false; + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + handler.enabled = true; + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(called, 1, "Expected callback to run once after enabled toggle"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 0); + }); + + it("uses Promise fallback", (done) => { + let events: string[] = []; + let handler = scheduleMicrotask(() => { + events.push("micro"); + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + events.push("sync"); + + orgSetTimeout(() => { + events.push("timeout"); + assert.deepEqual(events, ["sync", "micro", "timeout"], "Expected Promise microtask ordering"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 0); + }); + + it("cancel prevents Promise fallback callback", (done) => { + let called = false; + let handler = scheduleMicrotask(() => { + called = true; + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + handler.cancel(); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + + orgSetTimeout(() => { + assert.equal(called, false, "Expected callback to not run after cancel"); + done(); + }, 0); + }); + + it("uses timeout fallback when Promise fallback is disabled", () => { + let clock = sinon.useFakeTimers(); + try { + setMicroTaskFallbackOptions({ useTimeout: true }); + let called = 0; + let handler = scheduleMicrotask(() => { + called++; + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + assert.equal(called, 0, "Callback should not be called yet"); + clock.tick(0); + assert.equal(called, 1, "Callback should be called once via timeout fallback"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + } finally { + clock.restore(); + } + }); + + it("supports per-call custom fallback scheduling function", (done) => { + let scheduleCalls = 0; + let called = 0; + let handler = scheduleMicrotask(() => { + called++; + }, { + scheduleFn: (cb) => { + scheduleCalls++; + orgSetTimeout(cb, 0); + } + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + assert.equal(scheduleCalls, 1, "Custom fallback scheduler should be used"); + assert.equal(called, 0, "Callback should not be called yet"); + orgSetTimeout(() => { + assert.equal(called, 1, "Callback should be called once via custom fallback scheduler"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("supports global custom fallback scheduling function", (done) => { + let scheduleCalls = 0; + setMicroTaskFallbackOptions({ + scheduleFn: (cb) => { + scheduleCalls++; + orgSetTimeout(cb, 0); + } + }); + + let called = 0; + let handler = scheduleMicrotask(() => { + called++; + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + assert.equal(scheduleCalls, 1, "Global custom fallback scheduler should be used"); + assert.equal(called, 0, "Callback should not be called yet"); + orgSetTimeout(() => { + assert.equal(called, 1, "Callback should be called once via global custom fallback scheduler"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("executes microtasks before previously scheduled timers", () => { + let clock = sinon.useFakeTimers(); + try { + let events: string[] = []; + + scheduleTimeout(() => { + events.push("timer"); + }, 0); + + scheduleMicrotask(() => { + events.push("microtask"); + }, { + useTimeout: true + }); + + clock.tick(0); + + assert.deepEqual(events, ["microtask", "timer"], "Expected the microtask to run before the existing timer"); + } finally { + clock.restore(); + } + }); + + it("executes nested microtasks before previously scheduled timers", () => { + let clock = sinon.useFakeTimers(); + try { + let events: string[] = []; + + scheduleTimeout(() => { + events.push("timer"); + }, 0); + + scheduleMicrotask(() => { + events.push("microtask-1"); + scheduleMicrotask(() => { + events.push("microtask-2"); + }, { + useTimeout: true + }); + }, { + useTimeout: true + }); + + clock.tick(0); + + assert.deepEqual(events, ["microtask-1", "microtask-2", "timer"], "Expected nested microtasks to complete before the existing timer"); + } finally { + clock.restore(); + } + }); + }); + + describe("without queueMicrotask and Promise", () => { + beforeEach(() => { + setMicroTaskFallbackOptions(); + (getGlobal()).queueMicrotask = null; + (getGlobal()).Promise = null; + _resetMicrotaskQueue(); + }); + + it("uses scheduleTimeout fallback", (done) => { + let called = 0; + let handler = scheduleMicrotask(() => { + called++; + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + assert.equal(called, 0, "Callback should not be called yet"); + orgSetTimeout(() => { + assert.equal(called, 1, "Callback should be called once"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("cancel prevents scheduleTimeout fallback callback", (done) => { + let called = 0; + let handler = scheduleMicrotask(() => { + called++; + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + handler.cancel(); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + orgSetTimeout(() => { + assert.equal(called, 0, "Callback should not be called after cancel"); + done(); + }, 10); + }); + + it("executes multiple microtask batches on different ticks", (done) => { + let flushedBatches: string[] = []; + let enabled = false; + let pendingCallback: (() => void) | undefined; + let fakeHandler: any; + + function _fail(err: Error) { + done(err); + } + + fakeHandler = { + cancel: () => { + enabled = false; + }, + refresh: () => { + enabled = true; + orgSetTimeout(() => { + pendingCallback && pendingCallback(); + }, 0); + return fakeHandler; + }, + ref: () => { + return fakeHandler; + }, + unref: () => { + return fakeHandler; + }, + hasRef: () => { + return true; + } + }; + + Object.defineProperty(fakeHandler, "enabled", { + get: () => enabled, + set: (value: boolean) => { + enabled = value; + } + }); + + setTimeoutOverrides([ + ((callback: () => void) => { + pendingCallback = callback; + enabled = true; + orgSetTimeout(() => { + callback(); + }, 0); + return fakeHandler; + }) as any, + () => { + enabled = false; + } + ]); + + _addMicrotaskToQueue(() => { + flushedBatches.push("batch-1"); + }); + + orgSetTimeout(() => { + _addMicrotaskToQueue(() => { + flushedBatches.push("batch-2"); + }); + + orgSetTimeout(() => { + try { + assert.deepEqual(flushedBatches, ["batch-1", "batch-2"], "Expected both microtask batches to flush in order"); + done(); + } catch (e) { + _fail(e as Error); + } + }, 10); + }, 10); + }); + }); +}); diff --git a/package.json b/package.json index d9c99d7b..8f8c6bb5 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,9 @@ "idle timer", "timer", "interval", + "microtask", + "queueMicrotask", + "cancellable microtask", "includes", "string contains", "html encode",