Skip to content
Buy me a coffee

Per-step deadlines

A request or job has an overall signal (client disconnect, shutdown, an overall budget). Its steps need their own, shorter deadlines. A linked controller per step gives both: it aborts when the parent does (with the parent’s reason) or when its own abortAfter fires.

const
const steps: readonly [readonly ["load user", 10], readonly ["render", 200]]
steps
= [
['load user', 10],
['render', 200],
] as
type const = readonly [readonly ["load user", 10], readonly ["render", 200]]
const
;
for (const [
const step: "load user" | "render"
step
,
const ms: 10 | 200
ms
] of
const steps: readonly [readonly ["load user", 10], readonly ["render", 200]]
steps
) {
// Aborts when the request aborts (with its reason) or after 100 ms, whichever comes first.
using
using stepController: EnhancedAbortController
stepController
=
class EnhancedAbortController

An AbortController with .NET CancellationTokenSource features. It is an AbortController, and controller.signal is a real AbortSignal: pass it to fetch, AbortSignal.any, streams, timers and any other API that takes a signal.

@example

const controller = new EnhancedAbortController().abortAfter(TimeSpan.fromSeconds(5));
const response = await fetch(url, { signal: controller.signal });

EnhancedAbortController
.
EnhancedAbortController.createLinkedController<EnhancedAbortController>(this: new () => EnhancedAbortController, ...signals: Array<AbortSignal | null | undefined>): EnhancedAbortController

A controller that aborts as soon as any of signals aborts, with that signal's reason, like .NET's CreateLinkedTokenSource. Aborting the linked controller does not affect the parents. Its listeners on the parents are removed when it aborts or is disposed, so short-lived links on a long-lived parent don't leak. undefined/null entries (optional signals) and EnhancedAbortSignal.none are skipped.

createLinkedController
(
const request: AbortController
request
.
AbortController.signal: AbortSignal

The signal read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.

MDN Reference

signal
).
EnhancedAbortController.abortAfter(delay: Delay): EnhancedAbortController

Aborts after delay, like .NET's CancelAfter: calling it again replaces the previous schedule, and Infinity / -1 / TimeSpan.infinite cancel it. The reason is a DOMException named TimeoutError, as with AbortSignal.timeout. The timer never keeps the process alive, and delays longer than 24.8 days work. Does nothing once aborted. Returns the controller, for chaining.

@throws ― RangeError for NaN, negative (other than -1) or non-numeric delays.

@throws ― Error if the controller was disposed.

abortAfter
(100);
try {
await
function sleep(ms: number, signal: AbortSignal): Promise<void>
sleep
(
const ms: 10 | 200
ms
,
using stepController: EnhancedAbortController
stepController
.
EnhancedAbortController.signal: EnhancedSignal

The controller's signal: a real AbortSignal, always the same object.

signal
);
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@see ― source

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since ― v0.1.100

log
(`${
const step: "load user" | "render"
step
}: ok`);
} catch (
var error: unknown
error
) {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@see ― source

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since ― v0.1.100

log
(`${
const step: "load user" | "render"
step
}: ${
function isTimeoutError(error: unknown): boolean

True if error is a timeout: a TimeoutError from abortAfter, timeout() or AbortSignal.timeout, also when wrapped: Node core APIs (fs/promises, timers/promises, streams, events.once) reject with an AbortError whose cause is the signal's reason.

isTimeoutError
(
var error: unknown
error
) ? 'timed out' : 'request cancelled'}`);
}
// Leaving the block disposes the step controller: its listener on request.signal is removed.
}

Output: load user: ok, then render: timed out. The request itself is still live: a step timing out doesn’t abort the parent.

The reason tells you. A step’s own deadline is a TimeoutError on the step; a parent abort is the parent’s reason (which may also be a TimeoutError if the parent has a deadline). To be exact, compare signals:

using
using s: EnhancedAbortController
s
=
class EnhancedAbortController

An AbortController with .NET CancellationTokenSource features. It is an AbortController, and controller.signal is a real AbortSignal: pass it to fetch, AbortSignal.any, streams, timers and any other API that takes a signal.

@example

const controller = new EnhancedAbortController().abortAfter(TimeSpan.fromSeconds(5));
const response = await fetch(url, { signal: controller.signal });

EnhancedAbortController
.
EnhancedAbortController.createLinkedController<EnhancedAbortController>(this: new () => EnhancedAbortController, ...signals: Array<AbortSignal | null | undefined>): EnhancedAbortController

A controller that aborts as soon as any of signals aborts, with that signal's reason, like .NET's CreateLinkedTokenSource. Aborting the linked controller does not affect the parents. Its listeners on the parents are removed when it aborts or is disposed, so short-lived links on a long-lived parent don't leak. undefined/null entries (optional signals) and EnhancedAbortSignal.none are skipped.

createLinkedController
(
const job: EnhancedAbortController
job
.
EnhancedAbortController.signal: EnhancedSignal

The controller's signal: a real AbortSignal, always the same object.

signal
).
EnhancedAbortController.abortAfter(delay: Delay): EnhancedAbortController

Aborts after delay, like .NET's CancelAfter: calling it again replaces the previous schedule, and Infinity / -1 / TimeSpan.infinite cancel it. The reason is a DOMException named TimeoutError, as with AbortSignal.timeout. The timer never keeps the process alive, and delays longer than 24.8 days work. Does nothing once aborted. Returns the controller, for chaining.

@throws ― RangeError for NaN, negative (other than -1) or non-numeric delays.

@throws ― Error if the controller was disposed.

abortAfter
(2000);
try {
await
function step(signal: AbortSignal): Promise<void>
step
(
using s: EnhancedAbortController
s
.
EnhancedAbortController.signal: EnhancedSignal

The controller's signal: a real AbortSignal, always the same object.

signal
);
} catch (
var error: unknown
error
) {
if (
const job: EnhancedAbortController
job
.
EnhancedAbortController.signal: EnhancedSignal

The controller's signal: a real AbortSignal, always the same object.

signal
.
AbortSignal.aborted: boolean

The aborted read-only property returns a value that indicates whether the asynchronous operations the signal is communicating with are aborted (true) or not (false).

MDN Reference

aborted
) throw
var error: unknown
error
; // the whole job is over: stop
if (
using s: EnhancedAbortController
s
.
EnhancedAbortController.signal: EnhancedSignal

The controller's signal: a real AbortSignal, always the same object.

signal
.
AbortSignal.aborted: boolean

The aborted read-only property returns a value that indicates whether the asynchronous operations the signal is communicating with are aborted (true) or not (false).

MDN Reference

aborted
)
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@see ― source

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since ― v0.1.100

log
('this step was too slow; continue with a fallback');
else throw
var error: unknown
error
;
}

To split one overall budget across steps, compute what’s left and use it as the step deadline:

const
const budget: TimeSpan
budget
=
class TimeSpan

An immutable time interval, stored in milliseconds. Modeled on .NET's TimeSpan.

@example

controller.abortAfter(TimeSpan.fromSeconds(30));
TimeSpan.fromMinutes(1).add(TimeSpan.fromSeconds(30)).totalSeconds; // 90

TimeSpan
.
TimeSpan.fromSeconds(seconds: number): TimeSpan
fromSeconds
(5);
const
const started: number
started
=
var Date: DateConstructor

Enables basic storage and retrieval of dates and times.

Date
.
DateConstructor.now(): number

Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).

now
();
for (const
const run: (signal: AbortSignal) => Promise<void>
run
of
const steps: ((signal: AbortSignal) => Promise<void>)[]
steps
) {
const
const left: number
left
=
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.max(...values: number[]): number

Returns the larger of a set of supplied numeric expressions.

@param ― values Numeric expressions to be evaluated.

max
(0,
const budget: TimeSpan
budget
.
TimeSpan.milliseconds: number

The interval in milliseconds.

milliseconds
- (
var Date: DateConstructor

Enables basic storage and retrieval of dates and times.

Date
.
DateConstructor.now(): number

Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).

now
() -
const started: number
started
));
using
using step: EnhancedAbortController
step
=
class EnhancedAbortController

An AbortController with .NET CancellationTokenSource features. It is an AbortController, and controller.signal is a real AbortSignal: pass it to fetch, AbortSignal.any, streams, timers and any other API that takes a signal.

@example

const controller = new EnhancedAbortController().abortAfter(TimeSpan.fromSeconds(5));
const response = await fetch(url, { signal: controller.signal });

EnhancedAbortController
.
EnhancedAbortController.createLinkedController<EnhancedAbortController>(this: new () => EnhancedAbortController, ...signals: Array<AbortSignal | null | undefined>): EnhancedAbortController

A controller that aborts as soon as any of signals aborts, with that signal's reason, like .NET's CreateLinkedTokenSource. Aborting the linked controller does not affect the parents. Its listeners on the parents are removed when it aborts or is disposed, so short-lived links on a long-lived parent don't leak. undefined/null entries (optional signals) and EnhancedAbortSignal.none are skipped.

createLinkedController
(
const request: AbortSignal
request
).
EnhancedAbortController.abortAfter(delay: Delay): EnhancedAbortController

Aborts after delay, like .NET's CancelAfter: calling it again replaces the previous schedule, and Infinity / -1 / TimeSpan.infinite cancel it. The reason is a DOMException named TimeoutError, as with AbortSignal.timeout. The timer never keeps the process alive, and delays longer than 24.8 days work. Does nothing once aborted. Returns the controller, for chaining.

@throws ― RangeError for NaN, negative (other than -1) or non-numeric delays.

@throws ― Error if the controller was disposed.

abortAfter
(
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.min(...values: number[]): number

Returns the smaller of a set of supplied numeric expressions.

@param ― values Numeric expressions to be evaluated.

min
(
const left: number
left
, 2000));
await
const run: (signal: AbortSignal) => Promise<void>
run
(
using step: EnhancedAbortController
step
.
EnhancedAbortController.signal: EnhancedSignal

The controller's signal: a real AbortSignal, always the same object.

signal
);
}

Each step’s controller adds one abort listener to the parent. Disposing (or aborting) the step removes it. Without that, a long-lived parent (a shutdown signal, a websocket session) collects one listener per step ever run, and Node prints MaxListenersExceededWarning once there are more than 10. Watch the count go back to zero in the linked-signals playground.