Skip to content

Commit 35634aa

Browse files
committed
Add nextTick support with shared timer queue 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. - Export the new API from the package entrypoint and add coverage plus size-limit updates.
1 parent 34178c1 commit 35634aa

17 files changed

Lines changed: 903 additions & 207 deletions

‎.size-limit.json‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
{
33
"name": "es5-full",
44
"path": "lib/dist/es5/mod/ts-utils.js",
5-
"limit": "33.5 kb",
5+
"limit": "34.5 kb",
66
"brotli": false,
77
"running": false
88
},
99
{
1010
"name": "es6-full",
1111
"path": "lib/dist/es6/mod/ts-utils.js",
12-
"limit": "32 kb",
12+
"limit": "33.5 kb",
1313
"brotli": false,
1414
"running": false
1515
},
@@ -23,14 +23,14 @@
2323
{
2424
"name": "es6-full-brotli",
2525
"path": "lib/dist/es6/mod/ts-utils.js",
26-
"limit": "11.5 kb",
26+
"limit": "12 kb",
2727
"brotli": true,
2828
"running": false
2929
},
3030
{
3131
"name": "es5-zip",
3232
"path": "lib/dist/es5/mod/ts-utils.js",
33-
"limit": "13 Kb",
33+
"limit": "13.5 Kb",
3434
"gzip": true,
3535
"running": false
3636
},
@@ -107,9 +107,25 @@
107107
{
108108
"name": "es5-scheduleMicrotask",
109109
"path": "lib/dist/es5/mod/ts-utils.js",
110-
"limit": "1.5 kb",
110+
"limit": "1.75 kb",
111111
"import": "{ scheduleMicrotask }",
112112
"gzip": true,
113113
"running": false
114+
},
115+
{
116+
"name": "es5-scheduleNextTick",
117+
"path": "lib/dist/es5/mod/ts-utils.js",
118+
"limit": "2 kb",
119+
"import": "{ scheduleNextTick }",
120+
"gzip": true,
121+
"running": false
122+
},
123+
{
124+
"name": "es5-scheduleNextTickAndMicrotask",
125+
"path": "lib/dist/es5/mod/ts-utils.js",
126+
"limit": "2 kb",
127+
"import": "{ scheduleNextTick, scheduleMicrotask }",
128+
"gzip": true,
129+
"running": false
114130
}
115131
]

‎lib/src/helpers/environment.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,10 @@ export const getHistory = (/*#__PURE__*/_getGlobalInstFn<History>(getInst, ["his
223223
* @returns True if you are
224224
*/
225225
export const isNode = (/*#__PURE__*/_getGlobalInstFn<boolean>(() => {
226-
return !!safe(() => (process && (process.versions||{}).node)).v;
226+
return !!safe(() => {
227+
let processInst = getInst<any>("process");
228+
return processInst && (processInst.versions || {}).node;
229+
}).v;
227230
}));
228231

229232
/**

‎lib/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ export {
170170
hasQueueMicrotask, scheduleMicrotask, getQueueMicrotask,
171171
ScheduleMicrotaskFn, MicroTaskOptions, setMicroTaskFallbackOptions
172172
} from "./timer/microtask";
173+
export { hasProcessNextTick, scheduleNextTick, getProcessNextTick, NextTickOptions, setNextTickFallbackOptions } from "./timer/nextTick";
173174
export {
174175
TimeoutOverrideFn, ClearTimeoutOverrideFn, TimeoutOverrideFuncs, scheduleTimeout, scheduleTimeoutWith,
175176
createTimeout, createTimeoutWith, setTimeoutOverrides, setGlobalTimeoutOverrides

‎lib/src/timer/microtask.ts‎

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
* Licensed under the MIT license.
77
*/
88

9-
import { isStrictUndefined } from "../helpers/base";
109
import { _getGlobalInstFn, getInst } from "../helpers/environment";
11-
import { ITimerHandler, _TimerHandler, _createTimerHandler } from "./handler";
12-
import { _getPromiseMicrotaskFn } from "./microtasks/promiseMicrotask";
13-
import { _addMicrotaskToQueue } from "./microtasks/timerMicrotask";
10+
import { ITimerHandler } from "./handler";
11+
import { _createCancellableTask } from "./microtasks/cancellableTask";
12+
import { isStrictUndefined } from "../helpers/base";
13+
import { _getPromiseQueueFn } from "./microtasks/promiseQueue";
14+
import { _eTaskQueueType } from "./microtasks/taskQueue";
15+
import { _addMicrotaskQueue } from "./microtasks/timerQueue";
1416

1517
let _defaultOptions: MicroTaskOptions | undefined;
1618

@@ -31,8 +33,10 @@ export type MicrotaskFn = () => void;
3133
* @group Timer
3234
* @group Environment
3335
* @param callback - The microtask callback function to schedule.
36+
* @param maxQueuedTasks - Optional, the maximum number of queued tasks allowed before the scheduler
37+
* starts dropping tasks or throwing errors, depending on the implementation.
3438
*/
35-
export type ScheduleMicrotaskFn = (callback: MicrotaskFn) => void;
39+
export type ScheduleMicrotaskFn = (callback: MicrotaskFn, maxQueuedTasks?: number) => void | boolean;
3640

3741
/**
3842
* Controls how `scheduleMicrotask` chooses fallback behavior when native
@@ -183,45 +187,10 @@ export function scheduleMicrotask(callback: () => void, options?: MicroTaskOptio
183187
: (_defaultOptions && _defaultOptions.useTimeout));
184188
if (usePromise) {
185189
// Use the Promise based fallback if available, otherwise use setTimeout
186-
queueMicrotaskFn = _getPromiseMicrotaskFn();
187-
}
188-
}
189-
}
190-
191-
return _createCancellableMicroTask(callback, scheduleFn || queueMicrotaskFn || _addMicrotaskToQueue);
192-
}
193-
194-
/**
195-
* @internal
196-
* @since 0.15.0
197-
*/
198-
function _createCancellableMicroTask(callback: () => void, queueFn: ScheduleMicrotaskFn): ITimerHandler {
199-
let handler: _TimerHandler;
200-
// Used to track the currently scheduled task, incremented to cancel pending tasks when needed
201-
let currentTask = 0;
202-
203-
function _scheduleTask() {
204-
let taskId = ++currentTask;
205-
queueFn(() => {
206-
if (taskId === currentTask) {
207-
handler.dn();
208-
callback();
190+
queueMicrotaskFn = _getPromiseQueueFn(_eTaskQueueType.microtask);
209191
}
210-
});
211-
212-
return taskId;
213-
}
214-
215-
function _cancelTask(taskId: number) {
216-
if (taskId === currentTask) {
217-
currentTask++;
218192
}
219193
}
220194

221-
handler = _createTimerHandler(false, _scheduleTask, _cancelTask);
222-
// Start the timer only after handler is fully assigned so that any synchronous
223-
// queueFn implementation that fires the callback immediately can safely access handler.dn().
224-
handler.h.refresh();
225-
226-
return handler.h;
195+
return _createCancellableTask(callback, scheduleFn || queueMicrotaskFn || _addMicrotaskQueue);
227196
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* @nevware21/ts-utils
3+
* https://github.com/nevware21/ts-utils
4+
*
5+
* Copyright (c) 2026 NevWare21 Solutions LLC
6+
* Licensed under the MIT license.
7+
*/
8+
9+
import { ITimerHandler, _TimerHandler, _createTimerHandler } from "../handler";
10+
import { ScheduleMicrotaskFn } from "../microtask";
11+
12+
/**
13+
* @internal
14+
* @since 0.15.0
15+
*/
16+
export function _createCancellableTask(callback: () => void, queueFn: ScheduleMicrotaskFn, maxQueuedTasks?: number): ITimerHandler {
17+
let handler: _TimerHandler;
18+
let currentTask = 0;
19+
20+
function _scheduleTask() {
21+
let taskId = ++currentTask;
22+
let queued = queueFn(() => {
23+
if (taskId === currentTask) {
24+
handler.dn();
25+
callback();
26+
}
27+
}, maxQueuedTasks);
28+
29+
if (queued === false) {
30+
handler.dn();
31+
}
32+
33+
return taskId;
34+
}
35+
36+
function _cancelTask(taskId: number) {
37+
if (taskId === currentTask) {
38+
currentTask++;
39+
}
40+
}
41+
42+
handler = _createTimerHandler(false, _scheduleTask, _cancelTask);
43+
// Start after handler assignment in case queueFn executes synchronously.
44+
handler.h.refresh();
45+
46+
return handler.h;
47+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* @nevware21/ts-utils
3+
* https://github.com/nevware21/ts-utils
4+
*
5+
* Copyright (c) 2026 NevWare21 Solutions LLC
6+
* Licensed under the MIT license.
7+
*/
8+
9+
import { isFunction } from "../../helpers/base";
10+
import { createCachedValue, ICachedValue } from "../../helpers/cache";
11+
import { getInst, isNode } from "../../helpers/environment";
12+
import { _globalLazyTestHooks, _initTestHooks } from "../../helpers/lazy";
13+
import { UNDEF_VALUE } from "../../internal/constants";
14+
import { ScheduleMicrotaskFn } from "../microtask";
15+
import { _runMicroTask } from "./runMicrotask";
16+
17+
interface IProcessLike {
18+
nextTick?: (callback: () => void) => void;
19+
}
20+
21+
let _nextTickFn: ICachedValue<ScheduleMicrotaskFn | undefined>;
22+
23+
/**
24+
* @internal
25+
* @since 0.15.0
26+
*/
27+
function _nextTickScheduleFn(processInst: IProcessLike): ScheduleMicrotaskFn {
28+
return function(callback: () => void): void {
29+
processInst.nextTick(() => {
30+
_runMicroTask(callback);
31+
});
32+
};
33+
}
34+
35+
/**
36+
* @internal
37+
* Resolves the process.nextTick-based scheduler from the current global state without caching.
38+
* @since 0.15.0
39+
*/
40+
function _resolveProcessNextTickFn(): ScheduleMicrotaskFn | undefined {
41+
let processInst = getInst<IProcessLike>("process");
42+
return (isNode() && processInst && isFunction(processInst.nextTick)) ? _nextTickScheduleFn(processInst) : UNDEF_VALUE as any;
43+
}
44+
45+
/**
46+
* @internal
47+
* Returns the process.nextTick-based scheduler, re-resolving globals when lazy bypass is active (e.g. tests).
48+
* @since 0.15.0
49+
*/
50+
export function _getProcessNextTickFn(): ScheduleMicrotaskFn | undefined {
51+
!_globalLazyTestHooks && _initTestHooks();
52+
if (!_nextTickFn || _globalLazyTestHooks.lzy) {
53+
_nextTickFn = createCachedValue(_resolveProcessNextTickFn());
54+
}
55+
56+
return _nextTickFn.v;
57+
}

‎lib/src/timer/microtasks/promiseMicrotask.ts‎

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* @nevware21/ts-utils
3+
* https://github.com/nevware21/ts-utils
4+
*
5+
* Copyright (c) 2026 NevWare21 Solutions LLC
6+
* Licensed under the MIT license.
7+
*/
8+
9+
import { isFunction } from "../../helpers/base";
10+
import { createCachedValue, ICachedValue } from "../../helpers/cache";
11+
import { getInst } from "../../helpers/environment";
12+
import { _globalLazyTestHooks, _initTestHooks } from "../../helpers/lazy";
13+
import { ScheduleMicrotaskFn } from "../microtask";
14+
import { _addToQueue, _eTaskQueueType, _flushQueues } from "./taskQueue";
15+
16+
let _promiseCls: ICachedValue<PromiseConstructor | undefined>;
17+
let _promisePending = false;
18+
let _promiseQueueFns: { [key in _eTaskQueueType]?: ScheduleMicrotaskFn } | undefined;
19+
20+
function _ensurePromiseTask(promiseCls: PromiseConstructor): void {
21+
if (!_promisePending) {
22+
_promisePending = true;
23+
promiseCls.resolve().then(() => {
24+
_promisePending = false;
25+
_flushQueues();
26+
});
27+
}
28+
}
29+
30+
function _promiseScheduleFn(promiseCls: PromiseConstructor, queueType: _eTaskQueueType): ScheduleMicrotaskFn {
31+
return function(callback: () => void, maxQueuedTasks?: number): boolean {
32+
let added = _addToQueue(queueType, callback, maxQueuedTasks);
33+
if (added) {
34+
_ensurePromiseTask(promiseCls);
35+
}
36+
37+
return added;
38+
};
39+
}
40+
41+
/**
42+
* @internal
43+
* Returns the Promise-based microtask scheduler, re-resolving the global Promise when the lazy
44+
* bypass flag is active (e.g. during tests) so that changes to the global Promise are reflected.
45+
* @since 0.15.0
46+
*/
47+
export function _getPromiseQueueFn(queueType: _eTaskQueueType): ScheduleMicrotaskFn | undefined {
48+
!_globalLazyTestHooks && _initTestHooks();
49+
if (!_promiseCls || _globalLazyTestHooks.lzy) {
50+
let promClass = getInst<PromiseConstructor>("Promise");
51+
if (promClass && isFunction(promClass.resolve)) {
52+
_promiseCls = createCachedValue(promClass);
53+
_promiseQueueFns = undefined;
54+
}
55+
}
56+
57+
let result: ScheduleMicrotaskFn | undefined;
58+
if (_promiseCls && _promiseCls.v) {
59+
!_promiseQueueFns && (_promiseQueueFns = {});
60+
result = _promiseQueueFns[queueType];
61+
if (!result) {
62+
result = _promiseScheduleFn(_promiseCls.v, queueType);
63+
_promiseQueueFns[queueType] = result;
64+
}
65+
}
66+
67+
return result;
68+
}
69+

0 commit comments

Comments
 (0)