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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
{
"name": "es5-full",
"path": "lib/dist/es5/mod/ts-utils.js",
"limit": "37 kb",
"limit": "37.5 kb",
"brotli": false,
"running": false
},
{
"name": "es6-full",
"path": "lib/dist/es6/mod/ts-utils.js",
"limit": "35.5 kb",
"limit": "36 kb",
"brotli": false,
"running": false
},
Expand All @@ -23,7 +23,7 @@
{
"name": "es6-full-brotli",
"path": "lib/dist/es6/mod/ts-utils.js",
"limit": "12.5 kb",
"limit": "12.75 kb",
"brotli": true,
"running": false
},
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
- Added `arrToMap` helper in the array module and moved callback/type declarations into `iterator/types`
- Refactored iterator helper implementation/tests into per-function files and updated root exports
- Added NaN regression coverage and switched iterator set-operation membership checks to `arrIncludes` semantics for parity with array helpers
- [#576](https://github.com/nevware21/ts-utils/pull/576) Refactor timer microtask/nextTick shared types and environment helpers, with expanded fallback coverage
- Moved `MicrotaskFn` and `ScheduleMicrotaskFn` to `helpers/types` and updated timer internals to consume shared type definitions
- Moved `getQueueMicrotask` and `hasQueueMicrotask` to `helpers/environment`, updated root exports, and aligned `scheduleNextTick` fallback resolution to prefer `queueMicrotask` when available
- Added internal `microtaskQueue` scheduler helper wiring and expanded tests for queue fallback behavior, Promise ordering, and no-Promise edge cases

### Bug Fixes

Expand Down
60 changes: 60 additions & 0 deletions docs/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This guide provides practical examples for using the @nevware21/ts-utils library
- [String Functions](#string-functions)
- [Safe Operations](#safe-operations)
- [Runtime Environment Helpers](#runtime-environment-helpers)
- [Scheduling Semantics and Known Limitations](#scheduling-semantics-and-known-limitations)
- [Advanced Usage](#advanced-usage)
- [Working with Iterators](#working-with-iterators)
- [Lazy Evaluation](#lazy-evaluation)
Expand Down Expand Up @@ -274,6 +275,65 @@ const global = getGlobal();
const doc = hasDocument() ? getDocument() : null;
```

## Scheduling Semantics and Known Limitations

When using timer helpers, especially `scheduleNextTick()`, ordering depends on the runtime capabilities and whether you mix direct native APIs with ts-utils wrappers.

### Fallback priority used by `scheduleNextTick()`

`scheduleNextTick()` resolves scheduling in this order:

1. Native `process.nextTick` (Node.js)
2. Native `queueMicrotask`
3. Promise microtask fallback (`Promise.resolve().then(...)`)
4. Timer-backed fallback (`setTimeout(..., 0)`)

### Mixing direct native calls with `scheduleNextTick()`

- In modern browser/worker runtimes (where native `process.nextTick` is not available), `scheduleNextTick()` typically uses native `queueMicrotask`.
- If you directly call native `queueMicrotask()` and also call `scheduleNextTick()`, both callbacks are queued in the same microtask queue, so ordering is insertion/FIFO based.
- This means `scheduleNextTick()` does not get special priority over a previously queued native `queueMicrotask()` callback in browser/worker environments.
- For deterministic ordering, prefer one strategy per critical path: either use only native microtask APIs directly or use ts-utils wrappers (`scheduleNextTick()` / `scheduleMicrotask()`) consistently.

Node.js note:

- In Node.js, native `process.nextTick` has existed for a long time and `scheduleNextTick()` uses it when available, so the browser/worker `queueMicrotask` mixing concern is generally not the primary issue.

### Known limitation in timer-backed fallback environments

In older runtimes where `process.nextTick`, `queueMicrotask`, and Promise are not available, `scheduleNextTick()` falls back to `setTimeout(..., 0)`.

In this mode:

- `scheduleNextTick()` is no longer a true microtask.
- It cannot preempt a user's directly scheduled native timers that were already queued.
- Ordering between user timers and `scheduleNextTick()` becomes macrotask queue ordering and may differ from microtask-like expectations.

Example:

```typescript
import { scheduleNextTick } from "@nevware21/ts-utils";

setTimeout(() => {
console.log("user timeout");
}, 0);

scheduleNextTick(() => {
console.log("scheduleNextTick");
});

// In timer-backed fallback environments, output may be:
// "user timeout"
// "scheduleNextTick"
```

### Practical guidance

- only use `scheduleTimeout()`, `scheduleMicrotask()` and `scheduleNextTick()` to ensure the correct execution order.
- Use `scheduleNextTick()` for cross-runtime behavior, but do not assume it always has higher priority than directly queued native `queueMicrotasks` in browser/worker environments.
- Avoid relying on strict microtask-before-timer guarantees in environments that require timer fallback (unless you only use the `scheduleXXXXX()` functions)
- For app-level deterministic ordering, pick one scheduling strategy per critical execution path and avoid mixing native and wrapped scheduling primitives.

## Advanced Usage

### Working with Iterators
Expand Down
40 changes: 40 additions & 0 deletions lib/src/helpers/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { _getGlobalValue } from "../internal/global";
import { ILazyValue, _globalLazyTestHooks, _initTestHooks, getLazy } from "./lazy";
import { ICachedValue, createCachedValue } from "./cache";
import { safe } from "./safe";
import { ScheduleMicrotaskFn } from "./types";

const WINDOW = "window";

Expand Down Expand Up @@ -238,3 +239,42 @@ export const isNode = (/*#__PURE__*/_getGlobalInstFn<boolean>(() => {
export const isWebWorker = (/*#__PURE__*/_getGlobalInstFn<boolean>(() => {
return !!safe(() => self && self instanceof WorkerGlobalScope).v;
}));


/**
* Returns the global `queueMicrotask` function if available, or `null` when unavailable.
*
* @function
* @since 0.15.0
* @group Timer
* @group Environment
* @example
* ```ts
* const queueFn = getQueueMicrotask();
* if (queueFn) {
* queueFn(() => {
* console.log("microtask");
* });
* }
* ```
*/
export const getQueueMicrotask = (/*#__PURE__*/_getGlobalInstFn<ScheduleMicrotaskFn>(getInst as any, ["queueMicrotask"]));

/**
* Identifies if the runtime supports the `queueMicrotask` API.
*
* @since 0.15.0
* @group Timer
* @group Environment
* @returns True if the runtime supports `queueMicrotask` otherwise false.
* @example
* ```ts
* if (hasQueueMicrotask()) {
* console.log("Native queueMicrotask support is available");
* }
* ```
*/
/*#__NO_SIDE_EFFECTS__*/
export function hasQueueMicrotask(): boolean {
return !!( /*#__PURE__*/getQueueMicrotask());
}
22 changes: 22 additions & 0 deletions lib/src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,25 @@ export type ValueOf<T> = T[keyof T];
* ```
*/
export type NonEmptyArray<T> = [T, ...T[]];

/**
* Type alias for a microtask callback function, which is a function that is scheduled to run in the microtask
* queue after the current execution context completes.
* @since 0.15.0
* @group Timer
* @group Environment
*/
export type MicrotaskFn = () => void;

/**
* Type alias for a function that is used to schedule a microtask, which is a function
* that takes a callback and schedules it to run
*
* @since 0.15.0
* @group Timer
* @group Environment
* @param callback - The microtask callback function to schedule.
* @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, maxQueuedTasks?: number) => void | boolean;
12 changes: 6 additions & 6 deletions lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export {
} from "./helpers/enum";
export {
getGlobal, getInst, lazySafeGetInst, hasDocument, getDocument, hasWindow, getWindow, hasNavigator, getNavigator, hasHistory,
getHistory, isNode, isWebWorker
getHistory, isNode, isWebWorker, hasQueueMicrotask, getQueueMicrotask
} from "./helpers/environment";
export {
encodeAsHtml, encodeAsJson, normalizeJsName, encodeAsBase64, decodeBase64, encodeAsBase64Url,
Expand All @@ -87,7 +87,10 @@ export { safe, ISafeReturn, SafeReturnType } from "./helpers/safe";
export { safeGet } from "./helpers/safe_get";
export { safeGetLazy, safeGetWritableLazy, safeGetDeferred, safeGetWritableDeferred } from "./helpers/safe_lazy";
export { throwError, throwTypeError, throwRangeError } from "./helpers/throw";
export { ReadonlyRecord, DeepPartial, DeepReadonly, Mutable, DeepRequired, ValueOf, NonEmptyArray } from "./helpers/types";
export {
ReadonlyRecord, DeepPartial, DeepReadonly, Mutable, DeepRequired, ValueOf, NonEmptyArray,
ScheduleMicrotaskFn, MicrotaskFn
} from "./helpers/types";
export { hasValue } from "./helpers/value";
export { createArrayIterator } from "./iterator/array";
export { CreateIteratorContext, createIterator, createIterable, createIterableIterator, makeIterable } from "./iterator/create";
Expand Down Expand Up @@ -180,10 +183,7 @@ export {
getIdleCallback, getCancelIdleCallback, RequestIdleCallback, CancelIdleCallback
} from "./timer/idle";
export { scheduleInterval } from "./timer/interval";
export {
hasQueueMicrotask, scheduleMicrotask, getQueueMicrotask,
ScheduleMicrotaskFn, MicroTaskOptions, setMicroTaskFallbackOptions
} from "./timer/microtask";
export { scheduleMicrotask, MicroTaskOptions, setMicroTaskFallbackOptions } from "./timer/microtask";
export { hasProcessNextTick, scheduleNextTick, getProcessNextTick, NextTickOptions, ProcessNextTickFn, setNextTickFallbackOptions } from "./timer/nextTick";
export {
TimeoutOverrideFn, ClearTimeoutOverrideFn, TimeoutOverrideFuncs, scheduleTimeout, scheduleTimeoutWith,
Expand Down
63 changes: 2 additions & 61 deletions lib/src/timer/microtask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Licensed under the MIT license.
*/

import { _getGlobalInstFn, getInst } from "../helpers/environment";
import { getQueueMicrotask } from "../helpers/environment";
import { ITimerHandler } from "./handler";
import { _createCancellableTask } from "./microtasks/cancellableTask";
import { isArray } from "../helpers/base";
Expand All @@ -15,31 +15,10 @@ import { _eTaskQueueType } from "./microtasks/taskQueue";
import { _addMicrotaskQueue } from "./microtasks/timerQueue";
import { fnBindArgs } from "../funcs/fnBindArgs";
import { UNDEF_VALUE } from "../internal/constants";
import { MicrotaskFn, ScheduleMicrotaskFn } from "../helpers/types";

let _defaultOptions: MicroTaskOptions | undefined;

/**
* Type alias for a microtask callback function, which is a function that is scheduled to run in the microtask
* queue after the current execution context completes.
* @since 0.15.0
* @group Timer
* @group Environment
*/
export type MicrotaskFn = () => void;

/**
* Type alias for a function that is used to schedule a microtask, which is a function
* that takes a callback and schedules it to run
*
* @since 0.15.0
* @group Timer
* @group Environment
* @param callback - The microtask callback function to schedule.
* @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, maxQueuedTasks?: number) => void | boolean;

/**
* Controls how `scheduleMicrotask` chooses fallback behavior when native
* `queueMicrotask` is not available.
Expand All @@ -65,44 +44,6 @@ export interface MicroTaskOptions {
useTimeout?: boolean;
}

/**
* Returns the global `queueMicrotask` function if available, or `null` when unavailable.
*
* @function
* @since 0.15.0
* @group Timer
* @group Environment
* @example
* ```ts
* const queueFn = getQueueMicrotask();
* if (queueFn) {
* queueFn(() => {
* console.log("microtask");
* });
* }
* ```
*/
export const getQueueMicrotask = (/*#__PURE__*/_getGlobalInstFn<ScheduleMicrotaskFn>(getInst as any, ["queueMicrotask"]));

/**
* Identifies if the runtime supports the `queueMicrotask` API.
*
* @since 0.15.0
* @group Timer
* @group Environment
* @returns True if the runtime supports `queueMicrotask` otherwise false.
* @example
* ```ts
* if (hasQueueMicrotask()) {
* console.log("Native queueMicrotask support is available");
* }
* ```
*/
/*#__NO_SIDE_EFFECTS__*/
export function hasQueueMicrotask(): boolean {
return !!( /*#__PURE__*/getQueueMicrotask());
}

/**
* Sets the default fallback behavior for {@link scheduleMicrotask} when
* `queueMicrotask` is not available.
Expand Down
2 changes: 1 addition & 1 deletion lib/src/timer/microtasks/cancellableTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* Licensed under the MIT license.
*/

import { ScheduleMicrotaskFn } from "../../helpers/types";
import { ITimerHandler, _TimerHandler, _createTimerHandler } from "../handler";
import { ScheduleMicrotaskFn } from "../microtask";

/**
* @internal
Expand Down
Loading