From 5e239fbbdf7c747507b32a20bc61535b7fab8045 Mon Sep 17 00:00:00 2001 From: Nev Date: Tue, 26 May 2026 21:53:55 -0700 Subject: [PATCH] feat: add nextTick support, add arrConcat and fnBindArgs helpers - Add a separate scheduleNextTick API with Node-native resolution and browser/worker fallback behavior. - Preserve existing scheduleMicrotask semantics while sharing cancellable queue and timer-backed queue logic. - Hook nextTick ordering into the timeout path so nextTick runs before microtasks and normal timers. - update scheduleMicrotask and scheduleNextTick to support callback arguments consistently - add arrConcat as a new array helper with tests - add fnBindArgs as a dedicated helper for binding pre-supplied arguments - improve fnBind typing with BoundFunction so bound argument prefixes narrow the returned signature --- .size-limit.json | 30 +- CHANGELOG.md | 8 +- README.md | 10 +- lib/package.json | 4 + lib/src/array/concat.ts | 45 ++ lib/src/funcs/fnBindArgs.ts | 44 ++ lib/src/funcs/funcs.ts | 35 +- lib/src/funcs/types.ts | 32 + lib/src/helpers/environment.ts | 5 +- lib/src/index.ts | 5 +- lib/src/timer/microtask.ts | 99 +-- lib/src/timer/microtasks/cancellableTask.ts | 47 ++ lib/src/timer/microtasks/processNextTick.ts | 57 ++ lib/src/timer/microtasks/promiseMicrotask.ts | 54 -- lib/src/timer/microtasks/promiseQueue.ts | 91 +++ lib/src/timer/microtasks/resolveScheduleFn.ts | 46 ++ lib/src/timer/microtasks/runMicrotask.ts | 4 +- lib/src/timer/microtasks/taskQueue.ts | 83 ++ lib/src/timer/microtasks/timerMicrotask.ts | 93 --- lib/src/timer/microtasks/timerQueue.ts | 85 ++ lib/src/timer/nextTick.ts | 209 +++++ lib/test/bundle-size-check.js | 8 +- lib/test/src/common/array/concat.test.ts | 41 + lib/test/src/common/funcs/funcs.test.ts | 37 + lib/test/src/common/helpers/array.test.ts | 8 + lib/test/src/common/timer/microtask.test.ts | 125 ++- lib/test/src/common/timer/nextTick.test.ts | 730 ++++++++++++++++++ lib/test/src/common/timer/timeout.test.ts | 13 +- package.json | 4 + 29 files changed, 1816 insertions(+), 236 deletions(-) create mode 100644 lib/src/array/concat.ts create mode 100644 lib/src/funcs/fnBindArgs.ts create mode 100644 lib/src/timer/microtasks/cancellableTask.ts create mode 100644 lib/src/timer/microtasks/processNextTick.ts delete mode 100644 lib/src/timer/microtasks/promiseMicrotask.ts create mode 100644 lib/src/timer/microtasks/promiseQueue.ts create mode 100644 lib/src/timer/microtasks/resolveScheduleFn.ts create mode 100644 lib/src/timer/microtasks/taskQueue.ts delete mode 100644 lib/src/timer/microtasks/timerMicrotask.ts create mode 100644 lib/src/timer/microtasks/timerQueue.ts create mode 100644 lib/src/timer/nextTick.ts create mode 100644 lib/test/src/common/array/concat.test.ts create mode 100644 lib/test/src/common/timer/nextTick.test.ts diff --git a/.size-limit.json b/.size-limit.json index c8131b57..2b4cbd53 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,42 +2,42 @@ { "name": "es5-full", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "33.5 kb", + "limit": "35 kb", "brotli": false, "running": false }, { "name": "es6-full", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "32 kb", + "limit": "33.5 kb", "brotli": false, "running": false }, { "name": "es5-full-brotli", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "12 kb", + "limit": "12.5 kb", "brotli": true, "running": false }, { "name": "es6-full-brotli", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "11.5 kb", + "limit": "12 kb", "brotli": true, "running": false }, { "name": "es5-zip", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "13 Kb", + "limit": "13.5 Kb", "gzip": true, "running": false }, { "name": "es6-zip", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "13 Kb", + "limit": "13.5 Kb", "gzip": true, "running": false }, @@ -107,9 +107,25 @@ { "name": "es5-scheduleMicrotask", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "1.5 kb", + "limit": "1.75 kb", "import": "{ scheduleMicrotask }", "gzip": true, "running": false + }, + { + "name": "es5-scheduleNextTick", + "path": "lib/dist/es5/mod/ts-utils.js", + "limit": "2 kb", + "import": "{ scheduleNextTick }", + "gzip": true, + "running": false + }, + { + "name": "es5-scheduleNextTickAndMicrotask", + "path": "lib/dist/es5/mod/ts-utils.js", + "limit": "2 kb", + "import": "{ scheduleNextTick, scheduleMicrotask }", + "gzip": true, + "running": false } ] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 30705886..359ccfce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,17 @@ ### Features -- Add microtask scheduling helpers with native `queueMicrotask`, Promise, and timer-backed fallbacks +- [#570](https://github.com/nevware21/ts-utils/pull/570) 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 +- [#573](https://github.com/nevware21/ts-utils/pull/573) Add nextTick scheduling support and new helper exports + - New timer/runtime functions: `scheduleNextTick`, `getProcessNextTick`, `hasProcessNextTick`, `setNextTickFallbackOptions` + - Uses native `process.nextTick` in Node runtimes when available, while providing equivalent nextTick behavior in browser and worker runtimes via Promise and timer-backed fallbacks + - Adds shared queue logic and callback-argument support across microtask and nextTick scheduling + - Adds `arrConcat` as a dedicated array helper and `fnBindArgs` as a dedicated function helper + - Improves function binding typing with exported `BoundFunction` and updated signatures for `fnBind` / `fnBindArgs` # v0.14.0 May 18th, 2026 diff --git a/README.md b/README.md index d3c5899c..03f8b8d8 100644 --- a/README.md +++ b/README.md @@ -115,17 +115,17 @@ 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)(); [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)(); +| 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)(); [getProcessNextTick](https://nevware21.github.io/ts-utils/typedoc/functions/getProcessNextTick.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)(); [hasProcessNextTick](https://nevware21.github.io/ts-utils/typedoc/functions/hasProcessNextTick.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)(); |   |   -| Array | [arrAppend](https://nevware21.github.io/ts-utils/typedoc/functions/arrAppend.html)(); [arrAt](https://nevware21.github.io/ts-utils/typedoc/functions/arrAt.html)(); [arrChunk](https://nevware21.github.io/ts-utils/typedoc/functions/arrChunk.html)(); [arrCompact](https://nevware21.github.io/ts-utils/typedoc/functions/arrCompact.html)(); [arrContains](https://nevware21.github.io/ts-utils/typedoc/functions/arrContains.html)(); [arrDifference](https://nevware21.github.io/ts-utils/typedoc/functions/arrDifference.html)(); [arrDrop](https://nevware21.github.io/ts-utils/typedoc/functions/arrDrop.html)(); [arrDropWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrDropWhile.html)(); [arrEvery](https://nevware21.github.io/ts-utils/typedoc/functions/arrEvery.html)(); [arrFilter](https://nevware21.github.io/ts-utils/typedoc/functions/arrFilter.html)(); [arrFill](https://nevware21.github.io/ts-utils/typedoc/functions/arrFill.html)(); [arrFind](https://nevware21.github.io/ts-utils/typedoc/functions/arrFind.html)(); [arrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindIndex.html)(); [arrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLast.html)(); [arrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLastIndex.html)(); [arrFlatten](https://nevware21.github.io/ts-utils/typedoc/functions/arrFlatten.html)(); [arrForEach](https://nevware21.github.io/ts-utils/typedoc/functions/arrForEach.html)(); [arrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/arrFrom.html)(); [arrGroupBy](https://nevware21.github.io/ts-utils/typedoc/functions/arrGroupBy.html)(); [arrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/arrIncludes.html)(); [arrIndexKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexKeys.html)(); [arrIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexOf.html)(); [arrIntersection](https://nevware21.github.io/ts-utils/typedoc/functions/arrIntersection.html)(); [arrKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrKeys.html)(); [arrLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrLastIndexOf.html)(); [arrMap](https://nevware21.github.io/ts-utils/typedoc/functions/arrMap.html)(); [arrPartition](https://nevware21.github.io/ts-utils/typedoc/functions/arrPartition.html)(); [arrReduce](https://nevware21.github.io/ts-utils/typedoc/functions/arrReduce.html)(); [arrReverse](https://nevware21.github.io/ts-utils/typedoc/functions/arrReverse.html)(); [arrRotate](https://nevware21.github.io/ts-utils/typedoc/functions/arrRotate.html)(); [arrSample](https://nevware21.github.io/ts-utils/typedoc/functions/arrSample.html)(); [arrShuffle](https://nevware21.github.io/ts-utils/typedoc/functions/arrShuffle.html)(); [arrSlice](https://nevware21.github.io/ts-utils/typedoc/functions/arrSlice.html)(); [arrSome](https://nevware21.github.io/ts-utils/typedoc/functions/arrSome.html)(); [arrTake](https://nevware21.github.io/ts-utils/typedoc/functions/arrTake.html)(); [arrTakeWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrTakeWhile.html)(); [arrUnion](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnion.html)(); [arrUnique](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnique.html)(); [arrUnzip](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnzip.html)(); [arrWith](https://nevware21.github.io/ts-utils/typedoc/functions/arrWith.html)(); [arrZip](https://nevware21.github.io/ts-utils/typedoc/functions/arrZip.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [isArray](https://nevware21.github.io/ts-utils/typedoc/functions/isArray.html)();
[polyIsArray](https://nevware21.github.io/ts-utils/typedoc/functions/polyIsArray.html)(); [polyArrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrIncludes.html)(); [polyArrFind](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFind.html)(); [polyArrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindIndex.html)(); [polyArrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindLast.html)(); [polyArrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindLastIndex.html)(); [polyArrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFrom.html)();
+| Array | [arrAppend](https://nevware21.github.io/ts-utils/typedoc/functions/arrAppend.html)(); [arrConcat](https://nevware21.github.io/ts-utils/typedoc/functions/arrConcat.html)(); [arrAt](https://nevware21.github.io/ts-utils/typedoc/functions/arrAt.html)(); [arrChunk](https://nevware21.github.io/ts-utils/typedoc/functions/arrChunk.html)(); [arrCompact](https://nevware21.github.io/ts-utils/typedoc/functions/arrCompact.html)(); [arrContains](https://nevware21.github.io/ts-utils/typedoc/functions/arrContains.html)(); [arrDifference](https://nevware21.github.io/ts-utils/typedoc/functions/arrDifference.html)(); [arrDrop](https://nevware21.github.io/ts-utils/typedoc/functions/arrDrop.html)(); [arrDropWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrDropWhile.html)(); [arrEvery](https://nevware21.github.io/ts-utils/typedoc/functions/arrEvery.html)(); [arrFilter](https://nevware21.github.io/ts-utils/typedoc/functions/arrFilter.html)(); [arrFill](https://nevware21.github.io/ts-utils/typedoc/functions/arrFill.html)(); [arrFind](https://nevware21.github.io/ts-utils/typedoc/functions/arrFind.html)(); [arrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindIndex.html)(); [arrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLast.html)(); [arrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLastIndex.html)(); [arrFlatten](https://nevware21.github.io/ts-utils/typedoc/functions/arrFlatten.html)(); [arrForEach](https://nevware21.github.io/ts-utils/typedoc/functions/arrForEach.html)(); [arrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/arrFrom.html)(); [arrGroupBy](https://nevware21.github.io/ts-utils/typedoc/functions/arrGroupBy.html)(); [arrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/arrIncludes.html)(); [arrIndexKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexKeys.html)(); [arrIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexOf.html)(); [arrIntersection](https://nevware21.github.io/ts-utils/typedoc/functions/arrIntersection.html)(); [arrKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrKeys.html)(); [arrLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrLastIndexOf.html)(); [arrMap](https://nevware21.github.io/ts-utils/typedoc/functions/arrMap.html)(); [arrPartition](https://nevware21.github.io/ts-utils/typedoc/functions/arrPartition.html)(); [arrReduce](https://nevware21.github.io/ts-utils/typedoc/functions/arrReduce.html)(); [arrReverse](https://nevware21.github.io/ts-utils/typedoc/functions/arrReverse.html)(); [arrRotate](https://nevware21.github.io/ts-utils/typedoc/functions/arrRotate.html)(); [arrSample](https://nevware21.github.io/ts-utils/typedoc/functions/arrSample.html)(); [arrShuffle](https://nevware21.github.io/ts-utils/typedoc/functions/arrShuffle.html)(); [arrSlice](https://nevware21.github.io/ts-utils/typedoc/functions/arrSlice.html)(); [arrSome](https://nevware21.github.io/ts-utils/typedoc/functions/arrSome.html)(); [arrTake](https://nevware21.github.io/ts-utils/typedoc/functions/arrTake.html)(); [arrTakeWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrTakeWhile.html)(); [arrUnion](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnion.html)(); [arrUnique](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnique.html)(); [arrUnzip](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnzip.html)(); [arrWith](https://nevware21.github.io/ts-utils/typedoc/functions/arrWith.html)(); [arrZip](https://nevware21.github.io/ts-utils/typedoc/functions/arrZip.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [isArray](https://nevware21.github.io/ts-utils/typedoc/functions/isArray.html)();
[polyIsArray](https://nevware21.github.io/ts-utils/typedoc/functions/polyIsArray.html)(); [polyArrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrIncludes.html)(); [polyArrFind](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFind.html)(); [polyArrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindIndex.html)(); [polyArrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindLast.html)(); [polyArrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFindLastIndex.html)(); [polyArrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/polyArrFrom.html)();
| ArrayLike | [arrAt](https://nevware21.github.io/ts-utils/typedoc/functions/arrAt.html)(); [arrChunk](https://nevware21.github.io/ts-utils/typedoc/functions/arrChunk.html)(); [arrCompact](https://nevware21.github.io/ts-utils/typedoc/functions/arrCompact.html)(); [arrContains](https://nevware21.github.io/ts-utils/typedoc/functions/arrContains.html)(); [arrDifference](https://nevware21.github.io/ts-utils/typedoc/functions/arrDifference.html)(); [arrDrop](https://nevware21.github.io/ts-utils/typedoc/functions/arrDrop.html)(); [arrDropWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrDropWhile.html)(); [arrEvery](https://nevware21.github.io/ts-utils/typedoc/functions/arrEvery.html)(); [arrFilter](https://nevware21.github.io/ts-utils/typedoc/functions/arrFilter.html)(); [arrFill](https://nevware21.github.io/ts-utils/typedoc/functions/arrFill.html)(); [arrFind](https://nevware21.github.io/ts-utils/typedoc/functions/arrFind.html)(); [arrFindIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindIndex.html)(); [arrFindLast](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLast.html)(); [arrFindLastIndex](https://nevware21.github.io/ts-utils/typedoc/functions/arrFindLastIndex.html)(); [arrFlatten](https://nevware21.github.io/ts-utils/typedoc/functions/arrFlatten.html)(); [arrForEach](https://nevware21.github.io/ts-utils/typedoc/functions/arrForEach.html)(); [arrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/arrFrom.html)(); [arrGroupBy](https://nevware21.github.io/ts-utils/typedoc/functions/arrGroupBy.html)(); [arrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/arrIncludes.html)(); [arrIndexKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexKeys.html)(); [arrIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrIndexOf.html)(); [arrIntersection](https://nevware21.github.io/ts-utils/typedoc/functions/arrIntersection.html)(); [arrKeys](https://nevware21.github.io/ts-utils/typedoc/functions/arrKeys.html)(); [arrLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/arrLastIndexOf.html)(); [arrMap](https://nevware21.github.io/ts-utils/typedoc/functions/arrMap.html)(); [arrPartition](https://nevware21.github.io/ts-utils/typedoc/functions/arrPartition.html)(); [arrReduce](https://nevware21.github.io/ts-utils/typedoc/functions/arrReduce.html)(); [arrReverse](https://nevware21.github.io/ts-utils/typedoc/functions/arrReverse.html)(); [arrRotate](https://nevware21.github.io/ts-utils/typedoc/functions/arrRotate.html)(); [arrSample](https://nevware21.github.io/ts-utils/typedoc/functions/arrSample.html)(); [arrShuffle](https://nevware21.github.io/ts-utils/typedoc/functions/arrShuffle.html)(); [arrSlice](https://nevware21.github.io/ts-utils/typedoc/functions/arrSlice.html)(); [arrSome](https://nevware21.github.io/ts-utils/typedoc/functions/arrSome.html)(); [arrTake](https://nevware21.github.io/ts-utils/typedoc/functions/arrTake.html)(); [arrTakeWhile](https://nevware21.github.io/ts-utils/typedoc/functions/arrTakeWhile.html)(); [arrUnion](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnion.html)(); [arrUnique](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnique.html)(); [arrUnzip](https://nevware21.github.io/ts-utils/typedoc/functions/arrUnzip.html)(); [arrWith](https://nevware21.github.io/ts-utils/typedoc/functions/arrWith.html)(); [arrZip](https://nevware21.github.io/ts-utils/typedoc/functions/arrZip.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [objEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objEntries.html)(); [objValues](https://nevware21.github.io/ts-utils/typedoc/functions/objValues.html)(); | DOM | [isElement](https://nevware21.github.io/ts-utils/typedoc/functions/isElement.html)(); [isElementLike](https://nevware21.github.io/ts-utils/typedoc/functions/isElementLike.html)(); | Enum | [createEnum](https://nevware21.github.io/ts-utils/typedoc/functions/createEnum.html)(); [createEnumKeyMap](https://nevware21.github.io/ts-utils/typedoc/functions/createEnumKeyMap.html)(); [createEnumValueMap](https://nevware21.github.io/ts-utils/typedoc/functions/createEnumValueMap.html)(); [createSimpleMap](https://nevware21.github.io/ts-utils/typedoc/functions/createSimpleMap.html)(); [createTypeMap](https://nevware21.github.io/ts-utils/typedoc/functions/createTypeMap.html)(); | Error | [createCustomError](https://nevware21.github.io/ts-utils/typedoc/functions/createCustomError.html)(); [isError](https://nevware21.github.io/ts-utils/typedoc/functions/isError.html)(); [throwError](https://nevware21.github.io/ts-utils/typedoc/functions/throwError.html)(); [throwRangeError](https://nevware21.github.io/ts-utils/typedoc/functions/throwRangeError.html)(); [throwTypeError](https://nevware21.github.io/ts-utils/typedoc/functions/throwTypeError.html)(); [throwUnsupported](https://nevware21.github.io/ts-utils/typedoc/functions/throwUnsupported.html)(); -| Function | [fnApply](https://nevware21.github.io/ts-utils/typedoc/functions/fnApply.html)(); [fnBind](https://nevware21.github.io/ts-utils/typedoc/functions/fnBind.html)(); [fnCall](https://nevware21.github.io/ts-utils/typedoc/functions/fnCall.html)(); [createFnDeferredProxy](https://nevware21.github.io/ts-utils/typedoc/functions/createFnDeferredProxy.html)(); [createProxyFuncs](https://nevware21.github.io/ts-utils/typedoc/functions/createProxyFuncs.html)(); [readArgs](https://nevware21.github.io/ts-utils/typedoc/functions/readArgs.html)(); +| Function | [fnApply](https://nevware21.github.io/ts-utils/typedoc/functions/fnApply.html)(); [fnBind](https://nevware21.github.io/ts-utils/typedoc/functions/fnBind.html)(); [fnBindArgs](https://nevware21.github.io/ts-utils/typedoc/functions/fnBindArgs.html)(); [fnCall](https://nevware21.github.io/ts-utils/typedoc/functions/fnCall.html)(); [createFnDeferredProxy](https://nevware21.github.io/ts-utils/typedoc/functions/createFnDeferredProxy.html)(); [createProxyFuncs](https://nevware21.github.io/ts-utils/typedoc/functions/createProxyFuncs.html)(); [readArgs](https://nevware21.github.io/ts-utils/typedoc/functions/readArgs.html)(); | Idle | [getCancelIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/getCancelIdleCallback.html)(); [getIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/getIdleCallback.html)(); [hasIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/hasIdleCallback.html)(); [setDefaultIdleTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultIdleTimeout.html)(); [setDefaultMaxExecutionTime](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultMaxExecutionTime.html)(); | Iterator | [createArrayIterator](https://nevware21.github.io/ts-utils/typedoc/functions/createArrayIterator.html)(); [createIterator](https://nevware21.github.io/ts-utils/typedoc/functions/createIterator.html)(); [createIterable](https://nevware21.github.io/ts-utils/typedoc/functions/createIterable.html)(); [createRangeIterator](https://nevware21.github.io/ts-utils/typedoc/functions/createRangeIterator.html)(); [iterForOf](https://nevware21.github.io/ts-utils/typedoc/functions/iterForOf.html)(); [isAsyncIterable](https://nevware21.github.io/ts-utils/typedoc/functions/isAsyncIterable.html)(); [isIterable](https://nevware21.github.io/ts-utils/typedoc/functions/isIterable.html)(); [isIterator](https://nevware21.github.io/ts-utils/typedoc/functions/isIterator.html)(); [makeIterable](https://nevware21.github.io/ts-utils/typedoc/functions/makeIterable.html)(); [arrAppend](https://nevware21.github.io/ts-utils/typedoc/functions/arrAppend.html)(); [arrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/arrFrom.html)(); | Number | [getIntValue](https://nevware21.github.io/ts-utils/typedoc/functions/getIntValue.html)(); [isInteger](https://nevware21.github.io/ts-utils/typedoc/functions/isInteger.html)(); [isIntegerInRange](https://nevware21.github.io/ts-utils/typedoc/functions/isIntegerInRange.html)(); [isFiniteNumber](https://nevware21.github.io/ts-utils/typedoc/functions/isFiniteNumber.html)(); [isNumber](https://nevware21.github.io/ts-utils/typedoc/functions/isNumber.html)(); @@ -133,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)(); [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)(); +| 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)(); [setNextTickFallbackOptions](https://nevware21.github.io/ts-utils/typedoc/functions/setNextTickFallbackOptions.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)(); [scheduleNextTick](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleNextTick.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)(); @@ -309,7 +309,7 @@ Some additional polyfills are provided for simple backward compatibility to enab ### TypeScript Support -Built with TypeScript v5.2.2, with minimal requirements of TypeScript v2.8+ for the type definitions. +Built with TypeScript v5.2.2, while currently limiting exported types to those supported by TypeScript v3.4+. ## Size Analysis and Bundle Optimization diff --git a/lib/package.json b/lib/package.json index e9864043..1543cef6 100644 --- a/lib/package.json +++ b/lib/package.json @@ -107,7 +107,10 @@ "interval", "microtask", "queueMicrotask", + "nextTick", + "process-nexttick", "cancellable microtask", + "function-bind", "includes", "string contains", "html encode", @@ -147,6 +150,7 @@ "array-reduce", "array-filter", "array-find", + "array-concat", "object-copy", "object-keys", "lightweight", diff --git a/lib/src/array/concat.ts b/lib/src/array/concat.ts new file mode 100644 index 00000000..c1127069 --- /dev/null +++ b/lib/src/array/concat.ts @@ -0,0 +1,45 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { ArrProto } from "../internal/constants"; +import { _unwrapFunction } from "../internal/unwrapFunction"; + +/** + * Alias of `Array.prototype.concat()` for consumers that prefer the `arr*` helper naming. + * + * Unlike {@link arrAppend}, this helper does NOT mutate the input arrays and returns a new array. + * @since 0.15.0 + * @function + * @group Array + * @param theArray - The array to concatenate into a new array. + * @param items - Additional arrays and/or items to concatenate. + * @returns A new array containing values from `theArray` followed by each concatenated item. + * @example + * ```ts + * let values = arrConcat([1], [2, 3]); + * // values is [1, 2, 3] + * + * let next = arrConcat(values, ["a", "b"]); + * // next is [1, 2, 3, "a", "b"] + * // values is still [1, 2, 3] + * ``` + * @example + * ```ts + * const left = [1, 2]; + * const right = [3, 4]; + * const merged = arrConcat(left, right, 5); + * // merged is [1, 2, 3, 4, 5] + * // left is still [1, 2] + * // right is still [3, 4] + * ``` + */ +export const arrConcat: { + (theArray: T[]): T[]; + (theArray: T[], ...items: ConcatArray[]): T[]; + (theArray: T[], ...items: (T | ConcatArray)[]): T[]; +} = (/*#__PURE__*/_unwrapFunction("concat", ArrProto)); diff --git a/lib/src/funcs/fnBindArgs.ts b/lib/src/funcs/fnBindArgs.ts new file mode 100644 index 00000000..7832c687 --- /dev/null +++ b/lib/src/funcs/fnBindArgs.ts @@ -0,0 +1,44 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { BoundFunction } from "./types"; + +/** + * Creates a new bound function using `fn.bind()` where the pre-bound arguments are supplied as an + * array or tuple value, similar to how {@link fnApply} supplies arguments to invocation. + * + * This is useful when the argument list is already available as an array or tuple value, + * but you want to return a reusable bound function rather than invoke immediately. + * @param fn - The function instance to bind. + * @param thisArg - The value to be used as the `this` when calling `fn`. + * @param argArray - Optional array or tuple value containing arguments to pre-bind to `fn`. + * @returns A new bound function. + * @since 0.15.0 + * @group Function + * @example + * ```ts + * const module1 = { + * prefix: "Hello", + * log(value: string, punctuation: string) { + * return this.prefix + " " + value + punctuation; + * } + * }; + * + * const module2 = { + * prefix: "Hi" + * }; + * + * const bound = fnBindArgs(module1.log, module2, ["friend"]); + * bound("!"); // "Hi friend!" + * ``` + */ +export function fnBindArgs any, T, TArgs extends any[]>(fn: F, thisArg: T, argArray: TArgs): BoundFunction; +export function fnBindArgs any, T>(fn: F, thisArg: T): F; +export function fnBindArgs any, T>(fn: F, thisArg: T, argArray?: any[]): any { + return fn.bind.apply(fn, (argArray ? [ thisArg ].concat(argArray) : [ thisArg ]) as any); +} diff --git a/lib/src/funcs/funcs.ts b/lib/src/funcs/funcs.ts index f6428630..2ea4a666 100644 --- a/lib/src/funcs/funcs.ts +++ b/lib/src/funcs/funcs.ts @@ -7,6 +7,7 @@ */ import { ArrSlice, CALL } from "../internal/constants"; +import { BoundFunction } from "./types"; /** * The `fnApply` function calls the specified `fn` function with the given `thisArg` as the `this` value, @@ -213,7 +214,7 @@ export function fnCall any, T>(fn: F, thisArg: T): R * module2.getX(); // 21 * ``` */ -export function fnBind(fn: F, thisArg: T, ...argArray: any[]): F; +export function fnBind any, T>(fn: F, thisArg: T): F; /** * Creates a new function that when called will set the value of `thisArg` as the `this` keyword @@ -249,6 +250,34 @@ export function fnBind(fn: F, thisArg: T, ...argArray: an * module2.getX(); // 21 * ``` */ -export function fnBind(fn: F, thisArg: T): F { - return fn.bind.apply(fn, ArrSlice[CALL](arguments, 1)); +export function fnBind any, T, TArgs extends any[]>(fn: F, thisArg: T, ...argArray: TArgs): BoundFunction; + +/** + * Fallback overload for less-specific `Function`-typed call sites where the concrete parameter + * list is not available, such as dynamic proxy wiring. + * @param fn - The function instance to be called. + * @param thisArg - The value to be used as the `this` when calling the `fn`. + * @param argArray - Zero or more arguments to pre-bind to the function. + * @returns The original function type with the bound `this` value and any provided arguments. + * @since 0.9.8 + * @group Function + * @example + * ```ts + * const host = { + * prefix: "Hello", + * log(value: string) { + * return this.prefix + " " + value; + * } + * }; + * + * const dynamicLog: Function = host.log; + * const bound = fnBind(dynamicLog, host); + * + * bound("friend"); // "Hello friend" + * ``` + */ +export function fnBind(fn: F, thisArg: T, ...argArray: any[]): F; + +export function fnBind any, T>(fn: F, thisArg: T): any { + return fn.bind.apply(fn, ArrSlice[CALL](arguments, 1) as any); } diff --git a/lib/src/funcs/types.ts b/lib/src/funcs/types.ts index 24906102..b9defd83 100644 --- a/lib/src/funcs/types.ts +++ b/lib/src/funcs/types.ts @@ -66,3 +66,35 @@ export type ProxyFunctionDef = { */ rp?: boolean } + +/** + * Produces the return type for a function after a prefix of arguments has already been bound. + * + * This implementation is a TypeScript 3.4+ compatible middle ground between: + * 1. A fully simplified fallback (`(...args: any[]) => ReturnType`) + * 2. A TS 4.0+ variadic tuple implementation (shown below as comments) + * + * It preserves remaining argument inference for up to 5 pre-bound arguments and then + * falls back to a generic callable return type. + * @since 0.15.0 + * @group Funcs + * @typeParam TFn - The function being bound. + * @typeParam TArgs - The tuple of arguments that will be pre-bound. + */ +export type BoundFunction any, TArgs extends any[]> = + TArgs extends [] ? TFn : + TArgs extends [any] ? (TFn extends (a0: any, ...args: infer R) => infer RT ? (...args: R) => RT : (...args: any[]) => ReturnType) : + TArgs extends [any, any] ? (TFn extends (a0: any, a1: any, ...args: infer R) => infer RT ? (...args: R) => RT : (...args: any[]) => ReturnType) : + TArgs extends [any, any, any] ? (TFn extends (a0: any, a1: any, a2: any, ...args: infer R) => infer RT ? (...args: R) => RT : (...args: any[]) => ReturnType) : + TArgs extends [any, any, any, any] ? (TFn extends (a0: any, a1: any, a2: any, a3: any, ...args: infer R) => infer RT ? (...args: R) => RT : (...args: any[]) => ReturnType) : + TArgs extends [any, any, any, any, any] ? (TFn extends (a0: any, a1: any, a2: any, a3: any, a4: any, ...args: infer R) => infer RT ? (...args: R) => RT : (...args: any[]) => ReturnType) : + (...args: any[]) => ReturnType; +// Requires TypeScript 4.0+ for variadic tuple types (this is a better alternative for the fnBind and fnBindArgs return types) +// /** +// * Produces the return type for a function after a prefix of arguments has already been bound. +// * @since 0.15.0 +// * @group Funcs +// * @typeParam TFn - The function being bound. +// * @typeParam TArgs - The tuple of arguments that will be pre-bound. +// */ +// export type BoundFunction any, TArgs extends any[]> = TFn extends (...args: [...TArgs, ...infer TRemaining]) => infer TReturn ? (...args: TRemaining) => TReturn : never; diff --git a/lib/src/helpers/environment.ts b/lib/src/helpers/environment.ts index f56d04cd..5a9154ca 100644 --- a/lib/src/helpers/environment.ts +++ b/lib/src/helpers/environment.ts @@ -223,7 +223,10 @@ export const getHistory = (/*#__PURE__*/_getGlobalInstFn(getInst, ["his * @returns True if you are */ export const isNode = (/*#__PURE__*/_getGlobalInstFn(() => { - return !!safe(() => (process && (process.versions||{}).node)).v; + return !!safe(() => { + let processInst = getInst("process"); + return processInst && (processInst.versions || {}).node; + }).v; })); /** diff --git a/lib/src/index.ts b/lib/src/index.ts index 064df429..50aa0c7b 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -9,6 +9,7 @@ /// export { arrAppend } from "./array/append"; +export { arrConcat } from "./array/concat"; export { ArrPredicateCallbackFn, ArrPredicateCallbackFn2, ArrMapCallbackFn, ArrFlatMapCallbackFn, ArrFromMapFn } from "./array/callbacks"; export { arrAt } from "./array/at"; export { arrChunk } from "./array/chunk"; @@ -46,9 +47,10 @@ export { arrUnzip } from "./array/unzip"; export { arrWith } from "./array/with"; export { arrZip } from "./array/zip"; export { fnApply, fnBind, fnCall } from "./funcs/funcs"; +export { fnBindArgs } from "./funcs/fnBindArgs"; export { createFnDeferredProxy, createProxyFuncs } from "./funcs/fnProxy"; export { readArgs } from "./funcs/readArgs"; -export { ProxyFunctionDef, TypeFuncNames } from "./funcs/types"; +export { BoundFunction, ProxyFunctionDef, TypeFuncNames } from "./funcs/types"; export { WritableArrayLike } from "./helpers/arrayLike"; export { isTypeof, isUndefined, isNullOrUndefined, isDefined, isString, isFunction, isObject, isArray, isArrayLike, isDate, isNumber, isBoolean, @@ -170,6 +172,7 @@ export { hasQueueMicrotask, scheduleMicrotask, getQueueMicrotask, ScheduleMicrotaskFn, MicroTaskOptions, setMicroTaskFallbackOptions } from "./timer/microtask"; +export { hasProcessNextTick, scheduleNextTick, getProcessNextTick, NextTickOptions, ProcessNextTickFn, setNextTickFallbackOptions } from "./timer/nextTick"; export { TimeoutOverrideFn, ClearTimeoutOverrideFn, TimeoutOverrideFuncs, scheduleTimeout, scheduleTimeoutWith, createTimeout, createTimeoutWith, setTimeoutOverrides, setGlobalTimeoutOverrides diff --git a/lib/src/timer/microtask.ts b/lib/src/timer/microtask.ts index 00bb04dc..d4f10c55 100644 --- a/lib/src/timer/microtask.ts +++ b/lib/src/timer/microtask.ts @@ -6,11 +6,15 @@ * 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"; +import { ITimerHandler } from "./handler"; +import { _createCancellableTask } from "./microtasks/cancellableTask"; +import { isArray } from "../helpers/base"; +import { _resolveScheduleFn } from "./microtasks/resolveScheduleFn"; +import { _eTaskQueueType } from "./microtasks/taskQueue"; +import { _addMicrotaskQueue } from "./microtasks/timerQueue"; +import { fnBindArgs } from "../funcs/fnBindArgs"; +import { UNDEF_VALUE } from "../internal/constants"; let _defaultOptions: MicroTaskOptions | undefined; @@ -31,8 +35,10 @@ export type MicrotaskFn = () => void; * @group Timer * @group Environment * @param callback - The microtask callback function to schedule. + * @param maxQueuedTasks - Optional, the maximum number of queued tasks allowed before the scheduler + * starts dropping tasks or throwing errors, depending on the implementation. */ -export type ScheduleMicrotaskFn = (callback: MicrotaskFn) => void; +export type ScheduleMicrotaskFn = (callback: MicrotaskFn, maxQueuedTasks?: number) => void | boolean; /** * Controls how `scheduleMicrotask` chooses fallback behavior when native @@ -138,6 +144,7 @@ export function setMicroTaskFallbackOptions(options?: MicroTaskOptions): void { * @group Timer * * @param callback - The callback to execute. + * @param args - Optional callback arguments to pass when the callback executes. * @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 @@ -166,62 +173,40 @@ export function setMicroTaskFallbackOptions(options?: MicroTaskOptions): void { * } * }); * ``` + * @example + * ```ts + * // Args only + * scheduleMicrotask((name: string, count: number) => { + * console.log(name, count); + * }, ["task", 1]); + * + * // Args + options + * scheduleMicrotask((name: string, count: number) => { + * console.log(name, count); + * }, ["task", 2], { + * useTimeout: true + * }); + * ``` */ -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(); - } - } +export function scheduleMicrotask(callback: () => void, options?: MicroTaskOptions): ITimerHandler; +export function scheduleMicrotask(callback: (...args: TArgs) => void, args: TArgs, options?: MicroTaskOptions): ITimerHandler; +export function scheduleMicrotask(callback: (...args: TArgs) => void, argsOrOptions?: TArgs | MicroTaskOptions, options?: MicroTaskOptions): ITimerHandler { + let callbackArgs: TArgs | undefined; + let theOptions: MicroTaskOptions | undefined; + + if (arguments.length > 1 && argsOrOptions && isArray(argsOrOptions)) { + callbackArgs = argsOrOptions as TArgs; + theOptions = options; + } else { + theOptions = argsOrOptions as MicroTaskOptions; } - 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; - } + let taskCallback = callbackArgs ? fnBindArgs(callback, UNDEF_VALUE, callbackArgs) : (callback as MicrotaskFn); + let queueMicrotaskFn = getQueueMicrotask(); - function _cancelTask(taskId: number) { - if (taskId === currentTask) { - currentTask++; - } + if (!queueMicrotaskFn) { + queueMicrotaskFn = _resolveScheduleFn(theOptions, _defaultOptions, _eTaskQueueType.microtask); } - 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; + return _createCancellableTask(taskCallback, queueMicrotaskFn || _addMicrotaskQueue); } diff --git a/lib/src/timer/microtasks/cancellableTask.ts b/lib/src/timer/microtasks/cancellableTask.ts new file mode 100644 index 00000000..a645cc4c --- /dev/null +++ b/lib/src/timer/microtasks/cancellableTask.ts @@ -0,0 +1,47 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { ITimerHandler, _TimerHandler, _createTimerHandler } from "../handler"; +import { ScheduleMicrotaskFn } from "../microtask"; + +/** + * @internal + * @since 0.15.0 + */ +export function _createCancellableTask(callback: () => void, queueFn: ScheduleMicrotaskFn, maxQueuedTasks?: number): ITimerHandler { + let handler: _TimerHandler; + let currentTask = 0; + + function _scheduleTask() { + let taskId = ++currentTask; + let queued = queueFn(() => { + if (taskId === currentTask) { + handler.dn(); + callback(); + } + }, maxQueuedTasks); + + if (queued === false) { + handler.dn(); + } + + return taskId; + } + + function _cancelTask(taskId: number) { + if (taskId === currentTask) { + currentTask++; + } + } + + handler = _createTimerHandler(false, _scheduleTask, _cancelTask); + // Start after handler assignment in case queueFn executes synchronously. + handler.h.refresh(); + + return handler.h; +} diff --git a/lib/src/timer/microtasks/processNextTick.ts b/lib/src/timer/microtasks/processNextTick.ts new file mode 100644 index 00000000..90e30246 --- /dev/null +++ b/lib/src/timer/microtasks/processNextTick.ts @@ -0,0 +1,57 @@ +/* + * @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, isNode } from "../../helpers/environment"; +import { _globalLazyTestHooks, _initTestHooks } from "../../helpers/lazy"; +import { UNDEF_VALUE } from "../../internal/constants"; +import { ScheduleMicrotaskFn } from "../microtask"; +import { _runMicroTask } from "./runMicrotask"; + +interface IProcessLike { + nextTick?: (callback: () => void) => void; +} + +let _nextTickFn: ICachedValue; + +/** + * @internal + * @since 0.15.0 + */ +function _nextTickScheduleFn(processInst: IProcessLike): ScheduleMicrotaskFn { + return function(callback: () => void): void { + processInst.nextTick(() => { + _runMicroTask(callback); + }); + }; +} + +/** + * @internal + * Resolves the process.nextTick-based scheduler from the current global state without caching. + * @since 0.15.0 + */ +function _resolveProcessNextTickFn(): ScheduleMicrotaskFn | undefined { + let processInst = getInst("process"); + return (isNode() && processInst && isFunction(processInst.nextTick)) ? _nextTickScheduleFn(processInst) : UNDEF_VALUE as any; +} + +/** + * @internal + * Returns the process.nextTick-based scheduler, re-resolving globals when lazy bypass is active (e.g. tests). + * @since 0.15.0 + */ +export function _getProcessNextTickFn(): ScheduleMicrotaskFn | undefined { + !_globalLazyTestHooks && _initTestHooks(); + if (!_nextTickFn || _globalLazyTestHooks.lzy) { + _nextTickFn = createCachedValue(_resolveProcessNextTickFn()); + } + + return _nextTickFn.v; +} diff --git a/lib/src/timer/microtasks/promiseMicrotask.ts b/lib/src/timer/microtasks/promiseMicrotask.ts deleted file mode 100644 index 348f61ae..00000000 --- a/lib/src/timer/microtasks/promiseMicrotask.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * @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/promiseQueue.ts b/lib/src/timer/microtasks/promiseQueue.ts new file mode 100644 index 00000000..9da39afc --- /dev/null +++ b/lib/src/timer/microtasks/promiseQueue.ts @@ -0,0 +1,91 @@ +/* + * @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 { ScheduleMicrotaskFn } from "../microtask"; +import { _addToQueue, _eTaskQueueType, _flushQueues } from "./taskQueue"; + +let _promiseCls: ICachedValue; +let _promisePending = false; +let _pendingPromiseCls: PromiseConstructor | undefined; +let _promiseQueueFns: { [key in _eTaskQueueType]?: ScheduleMicrotaskFn } | undefined; + +function _ensurePromiseTask(promiseCls: PromiseConstructor): void { + // If the Promise implementation has changed (e.g. lazy bypass tests), clear stale pending state + // so a fresh Promise batch can be scheduled with the current implementation. + if (_promisePending && _pendingPromiseCls !== promiseCls) { + _promisePending = false; + } + + if (!_promisePending) { + _promisePending = true; + _pendingPromiseCls = promiseCls; + try { + promiseCls.resolve().then(() => { + _promisePending = false; + _pendingPromiseCls = undefined; + _flushQueues(); + }); + } catch (e) { + _promisePending = false; + _pendingPromiseCls = undefined; + _flushQueues(); + } + } +} + +function _promiseScheduleFn(promiseCls: PromiseConstructor, queueType: _eTaskQueueType): ScheduleMicrotaskFn { + return function(callback: () => void, maxQueuedTasks?: number): boolean { + let added = _addToQueue(queueType, callback, maxQueuedTasks); + if (added) { + _ensurePromiseTask(promiseCls); + } + + return added; + }; +} + +/** + * @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 _getPromiseQueueFn(queueType: _eTaskQueueType): ScheduleMicrotaskFn | undefined { + !_globalLazyTestHooks && _initTestHooks(); + if (!_promiseCls || _globalLazyTestHooks.lzy) { + let promClass = getInst("Promise"); + if (promClass && isFunction(promClass.resolve)) { + _promiseCls = createCachedValue(promClass); + _promiseQueueFns = undefined; + } else { + // Clear stale Promise cache when Promise is unavailable or invalid so callers can + // reliably fall back to timer-backed scheduling. + _promiseCls = undefined; + _promiseQueueFns = undefined; + _promisePending = false; + _pendingPromiseCls = undefined; + } + } + + let result: ScheduleMicrotaskFn | undefined; + if (_promiseCls && _promiseCls.v) { + !_promiseQueueFns && (_promiseQueueFns = {}); + result = _promiseQueueFns[queueType]; + if (!result) { + result = _promiseScheduleFn(_promiseCls.v, queueType); + _promiseQueueFns[queueType] = result; + } + } + + return result; +} + diff --git a/lib/src/timer/microtasks/resolveScheduleFn.ts b/lib/src/timer/microtasks/resolveScheduleFn.ts new file mode 100644 index 00000000..c75f4c82 --- /dev/null +++ b/lib/src/timer/microtasks/resolveScheduleFn.ts @@ -0,0 +1,46 @@ +/* + * @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 { ScheduleMicrotaskFn } from "../microtask"; +import { _getPromiseQueueFn } from "./promiseQueue"; +import { _eTaskQueueType } from "./taskQueue"; + +/** + * @internal + * Shared fallback scheduler options shape used by timer helpers. + * @since 0.15.0 + */ +export interface _IFallbackScheduleOptions { + scheduleFn?: ScheduleMicrotaskFn; + useTimeout?: boolean; +} + +/** + * @internal + * Resolve a fallback schedule function from per-call and default options. + * The per-call options take precedence over default options. + * @since 0.15.0 + */ +export function _resolveScheduleFn(options: _IFallbackScheduleOptions | undefined, defaultOptions: _IFallbackScheduleOptions | undefined, taskQueueType: _eTaskQueueType): ScheduleMicrotaskFn | undefined { + let resolvedFn = (options && options.scheduleFn) || (defaultOptions && defaultOptions.scheduleFn); + if (!resolvedFn) { + // 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 + resolvedFn = _getPromiseQueueFn(taskQueueType); + } + } + + return resolvedFn; +} diff --git a/lib/src/timer/microtasks/runMicrotask.ts b/lib/src/timer/microtasks/runMicrotask.ts index 5f8d7ea6..737e844b 100644 --- a/lib/src/timer/microtasks/runMicrotask.ts +++ b/lib/src/timer/microtasks/runMicrotask.ts @@ -20,7 +20,9 @@ import { scheduleTimeout } from "../timeout"; */ export function _runMicroTask(callback: MicrotaskFn): void { try { - callback(); + if (callback) { + callback(); + } } catch (e) { scheduleTimeout(() => { throw e; diff --git a/lib/src/timer/microtasks/taskQueue.ts b/lib/src/timer/microtasks/taskQueue.ts new file mode 100644 index 00000000..cfe7c590 --- /dev/null +++ b/lib/src/timer/microtasks/taskQueue.ts @@ -0,0 +1,83 @@ +/* + * @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 { throwRangeError } from "../../helpers/throw"; +import { UNDEF_VALUE } from "../../internal/constants"; +import { _runMicroTask } from "./runMicrotask"; + +export const enum _eTaskQueueType { + microtask = 0, + nextTick = 1 +} + +let _taskQueues: { [key in _eTaskQueueType]?: (() => void)[] } | undefined; + +export function _flushQueues(): void { + if (_taskQueues) { + _taskQueues[_eTaskQueueType.nextTick] && _flushTaskQueue(_eTaskQueueType.nextTick); + _taskQueues[_eTaskQueueType.microtask] && _flushTaskQueue(_eTaskQueueType.microtask); + } +} + +export function _flushTaskQueue(queueType: _eTaskQueueType): void { + if (_taskQueues) { + let queue = _taskQueues[queueType]; + let queueIdx = 0; + while (queue && queue.length > queueIdx) { + _runMicroTask(queue[queueIdx++]); + + // Special case for microtask queue to ensure nextTick queue is flushed between each microtask callback + if (queueType === _eTaskQueueType.microtask && _taskQueues[_eTaskQueueType.nextTick]) { + _flushTaskQueue(_eTaskQueueType.nextTick); + } + } + + _taskQueues[queueType] = UNDEF_VALUE; + } +} + + +export function _addToQueue(queueType: _eTaskQueueType, callback: () => void, maxQueuedTasks?: number): boolean { + let result = false; + + if (!_taskQueues) { + _taskQueues = {}; + } + + let queue = _taskQueues[queueType]; + if (!queue) { + queue = []; + _taskQueues[queueType] = queue; + } + + if (isStrictUndefined(maxQueuedTasks) || maxQueuedTasks === 0 || (queue.length < maxQueuedTasks)) { + queue.push(callback); + result = true; + } else { + throwRangeError( + "schedule" + (queueType === _eTaskQueueType.microtask ? "Microtask" : "NextTick") + "() queue depth exceeded [" + maxQueuedTasks + "]" + ); + } + + return result; +} + + +/** + * @internal + * Reset helper for tests. + * @since 0.15.0 + */ +export function _clearTaskQueues(): void { + if (_taskQueues) { + _taskQueues[_eTaskQueueType.nextTick] = UNDEF_VALUE; + _taskQueues[_eTaskQueueType.microtask] = UNDEF_VALUE; + _taskQueues = UNDEF_VALUE; + } +} \ No newline at end of file diff --git a/lib/src/timer/microtasks/timerMicrotask.ts b/lib/src/timer/microtasks/timerMicrotask.ts deleted file mode 100644 index 33fb7240..00000000 --- a/lib/src/timer/microtasks/timerMicrotask.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * @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/microtasks/timerQueue.ts b/lib/src/timer/microtasks/timerQueue.ts new file mode 100644 index 00000000..c7186a63 --- /dev/null +++ b/lib/src/timer/microtasks/timerQueue.ts @@ -0,0 +1,85 @@ +/* + * @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 { _returnNothing } from "../../internal/stubs"; +import { ITimerHandler } from "../handler"; +import { _setMicrotaskCallback, scheduleTimeout } from "../timeout"; +import { _addToQueue, _eTaskQueueType, _flushQueues } from "./taskQueue"; + +let _sharedTimer: ITimerHandler | undefined; +let _isFlushing = false; + +function _doFlush() { + try { + _isFlushing = true; + _flushQueues(); + } finally { + _isFlushing = false; + } +} + +/** + * @internal + * Internal function to ensure the shared timer is set. If the timer is already set and not enabled it will be refreshed + * to ensure it will pick up the new queue items when it flushes. + */ +function _ensureSharedTimer(): void { + if (_isFlushing) { + return; + } + + _setMicrotaskCallback(_doFlush); + + let handler = _sharedTimer; + if (!handler) { + _sharedTimer = scheduleTimeout(_returnNothing, 0); + } else if (!handler.enabled) { + // Restart the timer if it's not already running, otherwise just let the current timer run + // and it will pick up the new queue items when it flushes + handler.refresh(); + } +} + +/** + * @internal + * @since 0.15.0 + */ +export function _addNextTickToQueue(callback: () => void, maxQueuedTasks?: number): boolean { + return _addToQueueWithTimer(_eTaskQueueType.nextTick, callback, maxQueuedTasks); +} + +/** + * @internal + * @since 0.15.0 + */ +export function _addMicrotaskQueue(callback: () => void, maxQueuedTasks?: number): boolean { + return _addToQueueWithTimer(_eTaskQueueType.microtask, callback, maxQueuedTasks); +} + +function _addToQueueWithTimer(queueType: _eTaskQueueType, callback: () => void, maxQueuedTasks?: number): boolean { + let result = _addToQueue(queueType, callback, maxQueuedTasks); + if (result) { + _ensureSharedTimer(); + } + + return result; +} + + +/** + * @internal + * Resets the internal shared timer. This is primarily intended for testing purposes to ensure a clean state between tests. + * @since 0.15.0 + */ +export function _resetSharedTimer(): void { + if (_sharedTimer) { + _sharedTimer.cancel(); + _sharedTimer = UNDEF_VALUE; + } +} diff --git a/lib/src/timer/nextTick.ts b/lib/src/timer/nextTick.ts new file mode 100644 index 00000000..494d8d4e --- /dev/null +++ b/lib/src/timer/nextTick.ts @@ -0,0 +1,209 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { isArray, isFunction, isStrictUndefined } from "../helpers/base"; +import { _getGlobalInstFn, getInst, isNode } from "../helpers/environment"; +import { ITimerHandler } from "./handler"; +import { _createCancellableTask } from "./microtasks/cancellableTask"; +import { MicrotaskFn, ScheduleMicrotaskFn } from "./microtask"; +import { _getProcessNextTickFn } from "./microtasks/processNextTick"; +import { _resolveScheduleFn } from "./microtasks/resolveScheduleFn"; +import { _addNextTickToQueue } from "./microtasks/timerQueue"; +import { _eTaskQueueType } from "./microtasks/taskQueue"; +import { fnBindArgs } from "../funcs/fnBindArgs"; +import { UNDEF_VALUE } from "../internal/constants"; + +const _defaultMaxQueuedTasks = 1000; + +let _defaultOptions: NextTickOptions | undefined; + +/** + * Controls how `scheduleNextTick` chooses fallback behavior when native `process.nextTick` is not available. + * + * @since 0.15.0 + * @group Timer + */ +export interface NextTickOptions { + /** + * Provide a custom scheduling function to use when native `process.nextTick` is unavailable. + * When specified, this takes precedence over both the Promise fallback and the timer-backed + * queue fallback. + */ + scheduleFn?: ScheduleMicrotaskFn; + + /** + * When `true`, skips the Promise fallback and uses the timer-backed queue instead. + * When omitted or `false`, `scheduleNextTick` prefers the Promise fallback before the timer fallback. + */ + useTimeout?: boolean; + + /** + * The maximum number of queued nextTick callbacks that may be pending at once when using the fallback queues. + * When the limit is reached, scheduling throws a RangeError. + */ + maxQueuedTasks?: number; +} + +/** + * Node-compatible `process.nextTick` function signature. + * + * Unlike {@link ScheduleMicrotaskFn}, this supports variadic callback arguments and always returns `void`. + * @since 0.15.0 + * @group Timer + * @group Environment + */ +export type ProcessNextTickFn = (callback: (...args: any[]) => void, ...args: any[]) => void; + +/** + * Sets the default fallback behavior for {@link scheduleNextTick} when + * `process.nextTick` is not available. + * + * @since 0.15.0 + * @group Timer + * + * @param options - The fallback options to apply. Passing `undefined` resets + * options to defaults. + * @example + * ```ts + * setNextTickFallbackOptions({ + * useTimeout: true + * }); + * + * scheduleNextTick(() => { + * console.log("timer-backed nextTick fallback"); + * }); + * + * setNextTickFallbackOptions(); + * ``` + */ +export function setNextTickFallbackOptions(options?: NextTickOptions): void { + _defaultOptions = options; +} + +/** + * Returns the global `process.nextTick` function if available, otherwise `null`. + * + * The returned function matches the Node.js signature and supports variadic callback arguments. + * + * @function + * @since 0.15.0 + * @group Timer + * @group Environment + * @example + * ```ts + * const nextTickFn = getProcessNextTick(); + * if (nextTickFn) { + * nextTickFn((value: string) => { + * console.log("process.nextTick", value); + * }, "works-with-args"); + * } + * ``` + */ +export const getProcessNextTick = (/*#__PURE__*/_getGlobalInstFn(() => { + let processInst = getInst("process"); + return isNode() && processInst && isFunction(processInst.nextTick) ? processInst.nextTick as ProcessNextTickFn : null; +})); + +/** + * Identifies if the runtime supports the `process.nextTick` API. + * + * @since 0.15.0 + * @group Timer + * @group Environment + * @returns True if the runtime supports `process.nextTick` otherwise false. + * @example + * ```ts + * if (hasProcessNextTick()) { + * console.log("process.nextTick is available"); + * } + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function hasProcessNextTick(): boolean { + return !!getProcessNextTick(); +} + +/** + * Schedules a callback to run using `process.nextTick` when available and otherwise uses an + * Promise-based fallback before using the timer-backed queue. + * + * When `scheduleFn` is provided, it is used before the Promise and timer-backed fallbacks. + * When `useTimeout` is `true`, the timer-backed queue is used instead of the Promise fallback. + * `maxQueuedTasks` controls the fallback queue depth limit; when omitted the Promise/timer fallback queues + * use a Node-compatible default of 1000. Set `maxQueuedTasks` to `0` to disable the limit (for fallbacks only, + * the native `process.nextTick` is not affected by this limit). + * + * @since 0.15.0 + * @group Timer + * + * @param callback - The callback to execute. + * @param args - Optional callback arguments to pass when the callback executes. + * @param options - Optional fallback behavior when native `process.nextTick` is not available. + * @returns A handler that can be used to cancel or refresh the scheduled callback. + * @example + * ```ts + * scheduleNextTick(() => { + * console.log("nextTick callback"); + * }); + * + * scheduleNextTick(() => { + * console.log("timer-backed nextTick"); + * }, { + * useTimeout: true + * }); + * + * scheduleNextTick(() => { + * console.log("custom nextTick fallback"); + * }, { + * scheduleFn: (cb) => { + * setTimeout(cb, 0); + * } + * }); + * ``` + * @example + * ```ts + * // Args only + * scheduleNextTick((name: string, count: number) => { + * console.log(name, count); + * }, ["task", 1]); + * + * // Args + options + * scheduleNextTick((name: string, count: number) => { + * console.log(name, count); + * }, ["task", 2], { + * useTimeout: true + * }); + * ``` + */ +export function scheduleNextTick(callback: MicrotaskFn, options?: NextTickOptions): ITimerHandler; +export function scheduleNextTick(callback: (...args: TArgs) => void, args: TArgs, options?: NextTickOptions): ITimerHandler; +export function scheduleNextTick(callback: (...args: TArgs) => void, argsOrOptions?: TArgs | NextTickOptions, options?: NextTickOptions): ITimerHandler { + let callbackArgs: TArgs | undefined; + let theOptions: NextTickOptions | undefined; + + if (arguments.length > 1 && argsOrOptions && isArray(argsOrOptions)) { + callbackArgs = argsOrOptions as TArgs; + theOptions = options; + } else { + theOptions = argsOrOptions as NextTickOptions; + } + + let taskCallback = callbackArgs ? fnBindArgs(callback, UNDEF_VALUE, callbackArgs) : (callback as MicrotaskFn); + let nextTickFn = _getProcessNextTickFn(); + let maxQueuedTasks = (theOptions && !isStrictUndefined(theOptions.maxQueuedTasks)) + ? theOptions.maxQueuedTasks + : ((_defaultOptions && !isStrictUndefined(_defaultOptions.maxQueuedTasks)) + ? _defaultOptions.maxQueuedTasks + : _defaultMaxQueuedTasks); + + if (!nextTickFn) { + nextTickFn = _resolveScheduleFn(theOptions, _defaultOptions, _eTaskQueueType.nextTick); + } + + return _createCancellableTask(taskCallback, nextTickFn || _addNextTickToQueue, maxQueuedTasks); +} diff --git a/lib/test/bundle-size-check.js b/lib/test/bundle-size-check.js index 4f71e372..2fd2fc86 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: 37 * 1024, // 37 kb in bytes + limit: 38.5 * 1024, // 38.5 kb in bytes compress: false }, { name: "es6-min-full", path: "../bundle/es6/umd/ts-utils.min.js", - limit: 36 * 1024, // 36 kb in bytes + limit: 37.5 * 1024, // 37.5 kb in bytes compress: false }, { name: "es5-min-zip", path: "../bundle/es5/umd/ts-utils.min.js", - limit: 14.5 * 1024, // 14.5 kb in bytes + limit: 15 * 1024, // 15 kb in bytes compress: true }, { name: "es6-min-zip", path: "../bundle/es6/umd/ts-utils.min.js", - limit: 14.5 * 1024, // 14.5 kb in bytes + limit: 15 * 1024, // 15 kb in bytes compress: true }, { diff --git a/lib/test/src/common/array/concat.test.ts b/lib/test/src/common/array/concat.test.ts new file mode 100644 index 00000000..b1a4c69b --- /dev/null +++ b/lib/test/src/common/array/concat.test.ts @@ -0,0 +1,41 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { assert } from "@nevware21/tripwire-chai"; +import { arrConcat } from "../../../../src/array/concat"; +import { createArrayIterator } from "../../../../src/iterator/array"; + +describe("arrConcat helper", () => { + it("arrConcat returns a new concatenated array", () => { + let target = [1]; + let result = arrConcat(target, [2, 3]); + + assert.notStrictEqual(result, target, "arrConcat should return a new array instance"); + assert.deepEqual(result, [1, 2, 3]); + assert.deepEqual(target, [1], "arrConcat should not modify target"); + }); + + it("arrConcat does not modify source arrays", () => { + let left = [1, 2]; + let right = [3, 4]; + let result = arrConcat(left, right); + + assert.deepEqual(result, [1, 2, 3, 4]); + assert.deepEqual(left, [1, 2], "left array should not be modified"); + assert.deepEqual(right, [3, 4], "right array should not be modified"); + }); + + it("arrConcat keeps non-array values as a single element", () => { + let target = [1]; + let iter = createArrayIterator([2, 3]); + let result = arrConcat(target, iter as any); + + assert.deepEqual(result, [1, iter], "concat should keep non-array values as a single element"); + assert.deepEqual(target, [1], "target should not be modified"); + }); +}); diff --git a/lib/test/src/common/funcs/funcs.test.ts b/lib/test/src/common/funcs/funcs.test.ts index 9d7bbf00..7d852eb9 100644 --- a/lib/test/src/common/funcs/funcs.test.ts +++ b/lib/test/src/common/funcs/funcs.test.ts @@ -8,6 +8,7 @@ import { assert } from "@nevware21/tripwire-chai"; import { fnApply, fnBind, fnCall } from "../../../../src/funcs/funcs"; +import { fnBindArgs } from "../../../../src/funcs/fnBindArgs"; describe("function helpers", () => { describe("fnBind", () => { @@ -34,6 +35,22 @@ describe("function helpers", () => { module2.getX = fnBind(module1.getX, module1); assert.equal(module2.getX(), 21); }); + + it("binds this and pre-bound arguments", () => { + const module1 = { + prefix: "Hello", + log(value: string, punctuation: string) { + return this.prefix + " " + value + punctuation; + } + }; + + const module2 = { + prefix: "my" + }; + + const bound = fnBind(module1.log, module2, "friend"); + assert.equal(bound("!"), "my friend!", "pre-bound arguments should be preserved"); + }); }); describe("fnApply", () => { @@ -68,6 +85,26 @@ describe("function helpers", () => { }); }); + describe("fnBindArgs", () => { + it("binds this and pre-bound arguments from an array", () => { + const module1 = { + prefix: "Hello", + x: 21, + log(value: string, punctuation: string) { + return this.prefix + " " + value + punctuation + " : " + this.x; + } + }; + + let module2 = { + prefix: "my", + x: 42 + }; + + let bound = fnBindArgs(module1.log, module2, [ "friend" ]); + assert.equal(bound("!"), "my friend! : 42"); + }); + }); + describe("fnCall", () => { it("examples", () => { assert.equal(fnCall(Math.max, null, 21, 42, 84, 168, 7, 3), 168); diff --git a/lib/test/src/common/helpers/array.test.ts b/lib/test/src/common/helpers/array.test.ts index 1aa33d23..f165f7f1 100644 --- a/lib/test/src/common/helpers/array.test.ts +++ b/lib/test/src/common/helpers/array.test.ts @@ -195,6 +195,14 @@ describe("array helpers", () => { }); describe("arrAppend", () => { + it("returns the same array instance", () => { + let target = [1]; + let result = arrAppend(target, [2, 3]); + + assert.strictEqual(result, target, "arrAppend should return the same array instance"); + assert.deepEqual(target, [1, 2, 3]); + }); + it("Validate passing null values doesn't throw", () => { let target = arrAppend(null, null); assert.ok(true, "Passing null values"); diff --git a/lib/test/src/common/timer/microtask.test.ts b/lib/test/src/common/timer/microtask.test.ts index f70d43b2..095d2905 100644 --- a/lib/test/src/common/timer/microtask.test.ts +++ b/lib/test/src/common/timer/microtask.test.ts @@ -11,9 +11,10 @@ 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 { _addMicrotaskQueue, _resetSharedTimer } from "../../../../src/timer/microtasks/timerQueue"; import { _runMicroTask } from "../../../../src/timer/microtasks/runMicrotask"; import { scheduleTimeout, setTimeoutOverrides } from "../../../../src/timer/timeout"; +import { _clearTaskQueues } from "../../../../src/timer/microtasks/taskQueue"; describe("microtask tests", () => { let orgPromise: any; @@ -36,7 +37,8 @@ describe("microtask tests", () => { afterEach(() => { (getGlobal()).Promise = orgPromise; (getGlobal()).queueMicrotask = orgQueueMicrotask; - _resetMicrotaskQueue(); + _clearTaskQueues(); + _resetSharedTimer(); setTimeoutOverrides(); setMicroTaskFallbackOptions(); setBypassLazyCache(false); @@ -124,6 +126,52 @@ describe("microtask tests", () => { }, 0); }); + it("supports callback arguments with available microtask queue", (done) => { + let calledName: string; + let calledCount: number; + + let handler = scheduleMicrotask((name: string, count: number) => { + calledName = name; + calledCount = count; + }, ["micro", 42]); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(calledName, "micro", "Expected callback argument 'name' to be passed"); + assert.equal(calledCount, 42, "Expected callback argument 'count' to be passed"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 0); + }); + + it("forwards callback arguments via native queueMicrotask implementation", (done) => { + let theGlobal: any = getGlobal(); + let queueCalls = 0; + let calledName: string; + let calledCount: number; + + theGlobal.queueMicrotask = (callback: () => void) => { + queueCalls++; + orgSetTimeout(callback, 0); + }; + + let handler = scheduleMicrotask((name: string, count: number) => { + calledName = name; + calledCount = count; + }, ["native", 101]); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(queueCalls, 1, "Expected native queueMicrotask implementation to be used once"); + assert.equal(calledName, "native", "Expected callback argument 'name' to be passed"); + assert.equal(calledCount, 101, "Expected callback argument 'count' to be passed"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + it("enabled=false then enabled=true re-schedules with available microtask queue", (done) => { let called = 0; let handler = scheduleMicrotask(() => { @@ -221,6 +269,25 @@ describe("microtask tests", () => { }, 0); }); + it("supports callback arguments via Promise fallback", (done) => { + let calledName: string; + let calledCount: number; + + let handler = scheduleMicrotask((name: string, count: number) => { + calledName = name; + calledCount = count; + }, ["promise", 6]); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(calledName, "promise", "Expected callback argument 'name' to be passed"); + assert.equal(calledCount, 6, "Expected callback argument 'count' to be passed"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + it("uses timeout fallback when Promise fallback is disabled", () => { let clock = sinon.useFakeTimers(); try { @@ -262,6 +329,53 @@ describe("microtask tests", () => { }, 10); }); + it("supports callback arguments via custom fallback scheduling function", (done) => { + let scheduleCalls = 0; + let calledName: string; + let calledCount: number; + + let handler = scheduleMicrotask((name: string, count: number) => { + calledName = name; + calledCount = count; + }, ["custom", 9], { + scheduleFn: (cb) => { + scheduleCalls++; + orgSetTimeout(cb, 0); + } + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(scheduleCalls, 1, "Custom fallback scheduler should be used"); + assert.equal(calledName, "custom", "Expected callback argument 'name' to be passed"); + assert.equal(calledCount, 9, "Expected callback argument 'count' to be passed"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("supports callback arguments with options in fallback mode", (done) => { + let calledName: string; + let calledCount: number; + + let handler = scheduleMicrotask((name: string, count: number) => { + calledName = name; + calledCount = count; + }, ["fallback", 7], { + useTimeout: true + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(calledName, "fallback", "Expected callback argument 'name' to be passed"); + assert.equal(calledCount, 7, "Expected callback argument 'count' to be passed"); + 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({ @@ -343,7 +457,8 @@ describe("microtask tests", () => { setMicroTaskFallbackOptions(); (getGlobal()).queueMicrotask = null; (getGlobal()).Promise = null; - _resetMicrotaskQueue(); + _clearTaskQueues(); + _resetSharedTimer(); }); it("uses scheduleTimeout fallback", (done) => { @@ -429,12 +544,12 @@ describe("microtask tests", () => { } ]); - _addMicrotaskToQueue(() => { + _addMicrotaskQueue(() => { flushedBatches.push("batch-1"); }); orgSetTimeout(() => { - _addMicrotaskToQueue(() => { + _addMicrotaskQueue(() => { flushedBatches.push("batch-2"); }); diff --git a/lib/test/src/common/timer/nextTick.test.ts b/lib/test/src/common/timer/nextTick.test.ts new file mode 100644 index 00000000..c9e39786 --- /dev/null +++ b/lib/test/src/common/timer/nextTick.test.ts @@ -0,0 +1,730 @@ +/* + * @nevware21/ts-utils + * https://github.com/nevware21/ts-utils + * + * Copyright (c) 2026 NevWare21 Solutions LLC + * Licensed under the MIT license. + */ + +import { assert } from "@nevware21/tripwire-chai"; +import { getGlobal, isNode } from "../../../../src/helpers/environment"; +import { setBypassLazyCache } from "../../../../src/helpers/lazy"; +import { scheduleMicrotask, setMicroTaskFallbackOptions } from "../../../../src/timer/microtask"; +import { getProcessNextTick, hasProcessNextTick, scheduleNextTick, setNextTickFallbackOptions } from "../../../../src/timer/nextTick"; +import { scheduleTimeout } from "../../../../src/timer/timeout"; +import { _clearTaskQueues } from "../../../../src/timer/microtasks/taskQueue"; +import { _resetSharedTimer } from "../../../../src/timer/microtasks/timerQueue"; + +describe("nextTick tests", () => { + let orgProcess: any; + let orgPromise: any; + let orgQueueMicrotask: any; + let orgSetTimeout: any; + + before(() => { + let theGlobal: any = getGlobal(); + orgProcess = theGlobal.process; + orgPromise = theGlobal.Promise; + orgQueueMicrotask = theGlobal.queueMicrotask; + orgSetTimeout = setTimeout; + }); + + beforeEach(() => { + let theGlobal: any = getGlobal(); + setBypassLazyCache(true); + setMicroTaskFallbackOptions(); + setNextTickFallbackOptions(); + theGlobal.process = orgProcess; + theGlobal.Promise = orgPromise; + theGlobal.queueMicrotask = orgQueueMicrotask; + }); + + afterEach(() => { + let theGlobal: any = getGlobal(); + theGlobal.process = orgProcess; + theGlobal.Promise = orgPromise; + theGlobal.queueMicrotask = orgQueueMicrotask; + _clearTaskQueues(); + _resetSharedTimer(); + setMicroTaskFallbackOptions(); + setNextTickFallbackOptions(); + setBypassLazyCache(false); + }); + + it("hasProcessNextTick", () => { + let hasNativeNextTick = !!(isNode() && orgProcess && orgProcess.nextTick); + assert.equal(hasProcessNextTick(), hasNativeNextTick, "Check if we have process.nextTick support"); + assert.equal(!!getProcessNextTick(), hasNativeNextTick, "Check direct process.nextTick getter"); + }); + + it("uses process.nextTick when available in Node runtimes", (done) => { + let calls = 0; + let theGlobal: any = getGlobal(); + let processInst: any = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions + }; + theGlobal.process = processInst; + + processInst.nextTick = (callback: () => void) => { + calls++; + orgSetTimeout(callback, 0); + }; + + let handler = scheduleNextTick(() => { + // no-op + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + orgSetTimeout(() => { + assert.equal(calls, isNode() ? 1 : 0, "Expected process.nextTick usage only in Node runtimes"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("cancel prevents callback with native process.nextTick", (done) => { + if (!isNode()) { + done(); + return; + } + + let theGlobal: any = getGlobal(); + let called = 0; + let processCalls = 0; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: (callback: () => void) => { + processCalls++; + orgSetTimeout(callback, 0); + } + }; + + let handler = scheduleNextTick(() => { + 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(processCalls, 1, "Expected process.nextTick to be called once"); + assert.equal(called, 0, "Expected callback to not run after cancel"); + done(); + }, 10); + }); + + it("refresh reschedules callback with native process.nextTick", (done) => { + if (!isNode()) { + done(); + return; + } + + let theGlobal: any = getGlobal(); + let called = 0; + let processCalls = 0; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: (callback: () => void) => { + processCalls++; + orgSetTimeout(callback, 0); + } + }; + + let handler = scheduleNextTick(() => { + 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(processCalls, 2, "Expected refresh to enqueue a replacement process.nextTick callback"); + assert.equal(called, 1, "Expected callback to run once after refresh"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("supports callback arguments with native/fallback nextTick", (done) => { + let calledName: string; + let calledCount: number; + + let handler = scheduleNextTick((name: string, count: number) => { + calledName = name; + calledCount = count; + }, ["nextTick", 21]); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + orgSetTimeout(() => { + assert.equal(calledName, "nextTick", "Expected callback argument 'name' to be passed"); + assert.equal(calledCount, 21, "Expected callback argument 'count' to be passed"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("forwards callback arguments via native process.nextTick implementation", (done) => { + let theGlobal: any = getGlobal(); + let processCalls = 0; + let calledName: string; + let calledCount: number; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: (callback: () => void) => { + processCalls++; + orgSetTimeout(callback, 0); + } + }; + + let handler = scheduleNextTick((name: string, count: number) => { + calledName = name; + calledCount = count; + }, ["native", 33]); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(processCalls, isNode() ? 1 : 0, "Expected process.nextTick usage only in Node runtimes"); + assert.equal(calledName, "native", "Expected callback argument 'name' to be passed"); + assert.equal(calledCount, 33, "Expected callback argument 'count' to be passed"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("executes nextTick before a previously scheduled native setTimeout", (done) => { + let events: string[] = []; + + orgSetTimeout(() => { + events.push("timer"); + }, 0); + + scheduleNextTick(() => { + events.push("nextTick"); + }); + + orgSetTimeout(() => { + assert.deepEqual(events, ["nextTick", "timer"], "Expected nextTick before a previously scheduled native setTimeout"); + done(); + }, 10); + }); + + it("supports custom fallback scheduling function", (done) => { + let theGlobal: any = getGlobal(); + let scheduleCalls = 0; + let called = 0; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + + let handler = scheduleNextTick(() => { + 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 callback arguments via Promise fallback", (done) => { + let theGlobal: any = getGlobal(); + let calledName: string; + let calledCount: number; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + + let handler = scheduleNextTick((name: string, count: number) => { + calledName = name; + calledCount = count; + }, ["promise", 5]); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(calledName, "promise", "Expected callback argument 'name' to be passed"); + assert.equal(calledCount, 5, "Expected callback argument 'count' to be passed"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("cancel prevents callback via Promise fallback", (done) => { + let theGlobal: any = getGlobal(); + let called = 0; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + + let handler = scheduleNextTick(() => { + 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(); + }, 10); + }); + + it("refresh reschedules callback via Promise fallback", (done) => { + let theGlobal: any = getGlobal(); + let called = 0; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + + let handler = scheduleNextTick(() => { + 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(); + }, 10); + }); + + it("cancel prevents callback via timer-queue fallback", (done) => { + let theGlobal: any = getGlobal(); + let called = 0; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + + let handler = scheduleNextTick(() => { + called++; + }, { + useTimeout: 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, 0, "Expected callback to not run after cancel"); + done(); + }, 10); + }); + + it("refresh reschedules callback via timer-queue fallback", (done) => { + let theGlobal: any = getGlobal(); + let called = 0; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + + let handler = scheduleNextTick(() => { + called++; + }, { + useTimeout: true + }); + + 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(); + }, 10); + }); + + it("supports callback arguments via custom fallback scheduling function", (done) => { + let theGlobal: any = getGlobal(); + let scheduleCalls = 0; + let calledName: string; + let calledCount: number; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + + let handler = scheduleNextTick((name: string, count: number) => { + calledName = name; + calledCount = count; + }, ["custom", 12], { + scheduleFn: (cb) => { + scheduleCalls++; + orgSetTimeout(cb, 0); + } + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + + orgSetTimeout(() => { + assert.equal(scheduleCalls, 1, "Custom fallback scheduler should be used"); + assert.equal(calledName, "custom", "Expected callback argument 'name' to be passed"); + assert.equal(calledCount, 12, "Expected callback argument 'count' to be passed"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("supports callback arguments with options in fallback mode", (done) => { + let theGlobal: any = getGlobal(); + let calledName: string; + let calledCount: number; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + + let handler = scheduleNextTick((name: string, count: number) => { + calledName = name; + calledCount = count; + }, ["fallback", 8], { + useTimeout: true + }); + + assert.equal(handler.enabled, true, "Check that the handler is running"); + orgSetTimeout(() => { + assert.equal(calledName, "fallback", "Expected callback argument 'name' to be passed"); + assert.equal(calledCount, 8, "Expected callback argument 'count' to be passed"); + assert.equal(handler.enabled, false, "Check that the handler is stopped"); + done(); + }, 10); + }); + + it("supports global custom fallback scheduling function", (done) => { + let theGlobal: any = getGlobal(); + let scheduleCalls = 0; + let called = 0; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + + setNextTickFallbackOptions({ + scheduleFn: (cb) => { + scheduleCalls++; + orgSetTimeout(cb, 0); + } + }); + + let handler = scheduleNextTick(() => { + 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 nextTick scheduled from first microtask before remaining microtasks and before timer", (done) => { + let theGlobal: any = getGlobal(); + let events: string[] = []; + + // Force the timer-backed fallback path for both microtask and nextTick behavior. + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + theGlobal.queueMicrotask = null; + theGlobal.Promise = null; + + scheduleMicrotask(() => { + events.push("microtask-1"); + scheduleNextTick(() => { + events.push("nextTick"); + }); + }); + + scheduleMicrotask(() => { + events.push("microtask-2"); + }); + + scheduleTimeout(() => { + events.push("timer"); + }, 0); + + orgSetTimeout(() => { + try { + assert.deepEqual(events, ["microtask-1", "nextTick", "microtask-2", "timer"], "Expected nested fallback order microtask-1 -> nextTick -> microtask-2 -> timer"); + done(); + } catch (e) { + done(e as Error); + } + }, 20); + }); + + it("uses a single Promise task for mixed nextTick and microtask fallback queues", (done) => { + let theGlobal: any = getGlobal(); + let events: string[] = []; + let resolveCalls = 0; + let orgPromiseResolve = orgPromise && orgPromise.resolve; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + theGlobal.queueMicrotask = null; + + if (orgPromise && orgPromiseResolve) { + theGlobal.Promise = orgPromise; + theGlobal.Promise.resolve = function() { + resolveCalls++; + return orgPromiseResolve.call(orgPromise); + }; + } + + scheduleNextTick(() => { + events.push("nextTick"); + }); + + scheduleMicrotask(() => { + events.push("microtask"); + }); + + orgSetTimeout(() => { + try { + assert.deepEqual(events, ["nextTick", "microtask"], "Expected nextTick queue to flush before microtask queue"); + assert.equal(resolveCalls, 1, "Expected a single Promise.resolve() call for the mixed Promise fallback batch"); + done(); + } catch (e) { + done(e as Error); + } finally { + if (orgPromise && orgPromiseResolve) { + orgPromise.resolve = orgPromiseResolve; + } + } + }, 10); + }); + + it("does not use stale Promise cache after Promise becomes unavailable", (done) => { + let theGlobal: any = getGlobal(); + let promiseResolveCalls = 0; + let orgPromiseResolve = orgPromise && orgPromise.resolve; + let called = 0; + + if (!orgPromise || !orgPromiseResolve) { + done(); + return; + } + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + theGlobal.queueMicrotask = null; + + theGlobal.Promise = orgPromise; + theGlobal.Promise.resolve = function() { + promiseResolveCalls++; + return orgPromiseResolve.call(orgPromise); + }; + + scheduleNextTick(() => { + called++; + }); + + orgSetTimeout(() => { + try { + assert.equal(promiseResolveCalls, 1, "Expected Promise fallback to be used for first callback"); + + theGlobal.Promise = null; + scheduleNextTick(() => { + called++; + }); + + orgSetTimeout(() => { + try { + assert.equal(promiseResolveCalls, 1, "Expected no additional Promise.resolve() calls once Promise is unavailable"); + assert.equal(called, 2, "Expected both callbacks to execute"); + done(); + } catch (e) { + done(e as Error); + } + }, 10); + } catch (e) { + done(e as Error); + } finally { + orgPromise.resolve = orgPromiseResolve; + } + }, 10); + }); + + it("throws RangeError when maxQueuedTasks is exceeded", () => { + let theGlobal: any = getGlobal(); + let thrown: Error | undefined; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + theGlobal.Promise = { + resolve: () => ({ + then: () => { + // Keep the queue pending so depth can be exceeded synchronously. + } + }) + }; + + scheduleNextTick(() => { + // no-op + }, { + maxQueuedTasks: 2 + }); + + scheduleNextTick(() => { + // no-op + }, { + maxQueuedTasks: 2 + }); + + try { + scheduleNextTick(() => { + // no-op + }, { + maxQueuedTasks: 2 + }); + } catch (e) { + thrown = e as Error; + } + + assert.isOk(thrown, "Expected an error when maxQueuedTasks is exceeded"); + assert.equal(thrown && thrown.name, "RangeError", "Expected a RangeError"); + assert.equal(thrown && thrown.message, "scheduleNextTick() queue depth exceeded [2]", "Expected Node-like nextTick depth error message"); + }); + + it("uses Node-compatible default maxQueuedTasks depth for nextTick fallback", () => { + let theGlobal: any = getGlobal(); + let thrown: Error | undefined; + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + theGlobal.Promise = { + resolve: () => ({ + then: () => { + // Keep the queue pending so depth can be exceeded synchronously. + } + }) + }; + + for (let lp = 0; lp < 1000; lp++) { + scheduleNextTick(() => { + // no-op + }); + } + + try { + scheduleNextTick(() => { + // no-op + }); + } catch (e) { + thrown = e as Error; + } + + assert.isOk(thrown, "Expected an error when default nextTick depth is exceeded"); + assert.equal(thrown && thrown.name, "RangeError", "Expected a RangeError"); + assert.equal(thrown && thrown.message, "scheduleNextTick() queue depth exceeded [1000]", "Expected Node-like nextTick depth error message"); + }); + + it("recovers Promise fallback when Promise implementation changes while previous batch is stuck", (done) => { + let theGlobal: any = getGlobal(); + let events: string[] = []; + + if (!orgPromise || !orgPromise.resolve) { + done(); + return; + } + + theGlobal.process = { + version: orgProcess && orgProcess.version, + versions: orgProcess && orgProcess.versions, + nextTick: null + }; + + // Simulate a Promise implementation that never resolves callbacks, leaving pending state stuck. + theGlobal.Promise = { + resolve: () => ({ + then: () => { + // Never invoke callback + } + }) + }; + + scheduleNextTick(() => { + events.push("first"); + }); + + // Switch to a working Promise implementation; scheduling again should recover and flush the queue. + theGlobal.Promise = orgPromise; + scheduleNextTick(() => { + events.push("second"); + }); + + orgSetTimeout(() => { + try { + assert.deepEqual(events, ["first", "second"], "Expected queued nextTick callbacks to flush after Promise implementation change"); + done(); + } catch (e) { + done(e as Error); + } + }, 10); + }); +}); diff --git a/lib/test/src/common/timer/timeout.test.ts b/lib/test/src/common/timer/timeout.test.ts index f9d9a7e6..6765492d 100644 --- a/lib/test/src/common/timer/timeout.test.ts +++ b/lib/test/src/common/timer/timeout.test.ts @@ -9,7 +9,6 @@ import * as sinon from "sinon"; import { assert } from "@nevware21/tripwire-chai"; import { createTimeout, createTimeoutWith, scheduleTimeout, scheduleTimeoutWith, setTimeoutOverrides, setGlobalTimeoutOverrides, TimeoutOverrideFuncs } from "../../../../src/timer/timeout"; -import { isNode } from "../../../../src/helpers/environment"; describe("timeout tests", () => { let clock: sinon.SinonFakeTimers; @@ -326,6 +325,12 @@ describe("timeout tests", () => { it("basic timeout", () => { let timeoutCalled = false; + let timerId = setTimeout(() => { + // no-op + }, 1); + let supportsTimerRefresh = !!(timerId && (timerId as any).refresh); + clearTimeout(timerId); + let theTimeout = scheduleTimeoutWith(newSetTimeoutFn, () => { timeoutCalled = true; }, 100); @@ -363,19 +368,19 @@ describe("timeout tests", () => { clock.tick(1); assert.equal(theTimeout.enabled, true, "Check that the handler is stopped"); assert.equal(timeoutCalled, false, "Timeout should have been called yet"); - assert.equal(overrideCalled, isNode() ? 2 : 3, "The override should have been called"); + assert.equal(overrideCalled, supportsTimerRefresh ? 2 : 3, "The override should have been called"); clock.tick(98); assert.equal(theTimeout.enabled, true, "Check that the handler is stopped"); assert.equal(timeoutCalled, false, "Timeout should have been called yet"); - assert.equal(overrideCalled, isNode() ? 2 : 3, "The override should have been called"); + assert.equal(overrideCalled, supportsTimerRefresh ? 2 : 3, "The override should have been called"); clock.tick(1); assert.equal(theTimeout.enabled, false, "Check that the handler is stopped"); assert.equal(timeoutCalled, true, "Timeout should have been called yet"); - assert.equal(overrideCalled, isNode() ? 2 : 3, "The override should have been called"); + assert.equal(overrideCalled, supportsTimerRefresh ? 2 : 3, "The override should have been called"); }); }); diff --git a/package.json b/package.json index 7c878a40..81391a21 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,10 @@ "interval", "microtask", "queueMicrotask", + "nextTick", + "process-nexttick", "cancellable microtask", + "function-bind", "includes", "string contains", "html encode", @@ -147,6 +150,7 @@ "array-reduce", "array-filter", "array-find", + "array-concat", "object-copy", "object-keys", "lightweight",