@orkestrel/timeout 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -11
- package/dist/src/core/index.cjs +49 -41
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +62 -39
- package/dist/src/core/index.d.ts +62 -39
- package/dist/src/core/index.js +50 -42
- package/dist/src/core/index.js.map +1 -1
- package/package.json +18 -15
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# @orkestrel/timeout
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
the
|
|
3
|
+
> The time-bound half of the substrate's time-and-cancellation pair: a
|
|
4
|
+
> controllable `setTimeout` wrapper carrying a trace `id` and a deadline `ms`,
|
|
5
|
+
> whose native `AbortSignal` aborts on expiry.
|
|
6
|
+
|
|
7
|
+
Create a handle with the `createTimeout` function, call `start()` to arm the
|
|
8
|
+
deadline, and hand its signal to the fetch, stream, or queue task the deadline
|
|
9
|
+
bounds; call `clear()` when that work finishes first. Pass a parent signal
|
|
10
|
+
where an outer cancellation must retire the deadline rather than let it expire;
|
|
11
|
+
that link carries no `AbortSignal.any` semantics. Part of the `@orkestrel` line.
|
|
11
12
|
|
|
12
13
|
## Install
|
|
13
14
|
|
|
@@ -17,8 +18,8 @@ npm install @orkestrel/timeout
|
|
|
17
18
|
|
|
18
19
|
## Requirements
|
|
19
20
|
|
|
20
|
-
- Node.js >=
|
|
21
|
-
- ESM
|
|
21
|
+
- Node.js >= 22.12.0, matching the `engines` field in `package.json`
|
|
22
|
+
- ESM (`import`) and CommonJS (`require`) through the `exports` field
|
|
22
23
|
|
|
23
24
|
## Usage
|
|
24
25
|
|
|
@@ -61,7 +62,7 @@ once-read helper that returns a fresh copy without absent optional keys.
|
|
|
61
62
|
## Guide
|
|
62
63
|
|
|
63
64
|
For the full surface — the deadline lifecycle, parent-signal linking, and
|
|
64
|
-
reuse semantics — see [`guides/
|
|
65
|
+
reuse semantics — see [`guides/timeout.md`](guides/timeout.md).
|
|
65
66
|
|
|
66
67
|
## Package
|
|
67
68
|
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
3
|
//#region src/core/constants.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
@@ -13,10 +13,11 @@ var MAX_TIMEOUT_MS = 2147483647;
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/core/validators.ts
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Determines whether a value is an integer in the inclusive range from `0` through
|
|
17
|
+
* `MAX_TIMEOUT_MS`, staying total for every input.
|
|
17
18
|
*
|
|
18
19
|
* @param value - Unknown duration candidate
|
|
19
|
-
* @returns
|
|
20
|
+
* @returns True if the value is an integer in the inclusive timeout range; false otherwise
|
|
20
21
|
*
|
|
21
22
|
* @example
|
|
22
23
|
* ```ts
|
|
@@ -25,18 +26,18 @@ var MAX_TIMEOUT_MS = 2147483647;
|
|
|
25
26
|
* ```
|
|
26
27
|
*/
|
|
27
28
|
function isTimeoutDuration(value) {
|
|
28
|
-
return (0, _orkestrel_contract.
|
|
29
|
+
return (0, _orkestrel_contract.isInteger)(value) && value >= 0 && value <= 2147483647;
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
+
* Determines whether a value is a genuine native `AbortSignal`, staying total for a
|
|
33
|
+
* structural spoof and for a hostile or revoked proxy.
|
|
32
34
|
*
|
|
33
35
|
* @remarks
|
|
34
|
-
* The intrinsic `aborted` getter performs the native brand check
|
|
35
|
-
*
|
|
36
|
-
* allowing their errors to escape.
|
|
36
|
+
* The intrinsic `aborted` getter performs the native brand check, and the boundary
|
|
37
|
+
* returns `false` rather than letting a receiver's error escape.
|
|
37
38
|
*
|
|
38
39
|
* @param value - Unknown signal candidate
|
|
39
|
-
* @returns
|
|
40
|
+
* @returns True if the native `AbortSignal` getter accepts the value; false otherwise
|
|
40
41
|
*
|
|
41
42
|
* @example
|
|
42
43
|
* ```ts
|
|
@@ -56,17 +57,17 @@ function isTimeoutSignal(value) {
|
|
|
56
57
|
//#endregion
|
|
57
58
|
//#region src/core/helpers.ts
|
|
58
59
|
/**
|
|
59
|
-
*
|
|
60
|
+
* Validates once-read timeout construction options and returns a fresh normalized copy
|
|
61
|
+
* omitting absent optional keys.
|
|
60
62
|
*
|
|
61
63
|
* @remarks
|
|
62
|
-
* Each property is read
|
|
63
|
-
* a fresh copy and omits absent optional properties. No timer, controller, or
|
|
64
|
+
* Each property is read before any validation runs, and no timer, controller, or
|
|
64
65
|
* listener lifecycle begins at this boundary.
|
|
65
66
|
*
|
|
66
67
|
* @param options - Potentially untrusted timeout options
|
|
67
68
|
* @returns A fresh validated `TimeoutOptions` object
|
|
68
|
-
* @throws {@link import('@orkestrel/contract').ContractError}
|
|
69
|
-
* does not satisfy `TimeoutOptions`
|
|
69
|
+
* @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
|
|
70
|
+
* input does not satisfy `TimeoutOptions`
|
|
70
71
|
*
|
|
71
72
|
* @example
|
|
72
73
|
* ```ts
|
|
@@ -142,7 +143,8 @@ function validateTimeoutOptions(options) {
|
|
|
142
143
|
//#endregion
|
|
143
144
|
//#region src/core/Timeout.ts
|
|
144
145
|
/**
|
|
145
|
-
*
|
|
146
|
+
* Implements `TimeoutInterface` exactly, as a controllable `setTimeout` wrapper over one
|
|
147
|
+
* owned `AbortController` whose signal aborts when the deadline expires.
|
|
146
148
|
*
|
|
147
149
|
* @remarks
|
|
148
150
|
* `start()` arms or replaces the current deadline. Expiry sets `expired` and
|
|
@@ -167,21 +169,26 @@ function validateTimeoutOptions(options) {
|
|
|
167
169
|
* ```
|
|
168
170
|
*/
|
|
169
171
|
var Timeout = class {
|
|
170
|
-
id;
|
|
171
|
-
ms;
|
|
172
|
+
#id;
|
|
173
|
+
#ms;
|
|
172
174
|
#parent;
|
|
173
175
|
#listener;
|
|
174
176
|
#controller;
|
|
175
177
|
#handle;
|
|
176
|
-
#linked = false;
|
|
177
178
|
constructor(options) {
|
|
178
179
|
const input = validateTimeoutOptions(options);
|
|
179
|
-
this
|
|
180
|
-
this
|
|
180
|
+
this.#id = input.id === void 0 ? crypto.randomUUID() : input.id;
|
|
181
|
+
this.#ms = input.ms;
|
|
181
182
|
this.#parent = input.signal;
|
|
182
183
|
this.#controller = new AbortController();
|
|
183
184
|
this.#listener = this.clear.bind(this);
|
|
184
185
|
}
|
|
186
|
+
get id() {
|
|
187
|
+
return this.#id;
|
|
188
|
+
}
|
|
189
|
+
get ms() {
|
|
190
|
+
return this.#ms;
|
|
191
|
+
}
|
|
185
192
|
get signal() {
|
|
186
193
|
return this.#controller.signal;
|
|
187
194
|
}
|
|
@@ -196,15 +203,12 @@ var Timeout = class {
|
|
|
196
203
|
}
|
|
197
204
|
this.#detach();
|
|
198
205
|
if (this.#controller.signal.aborted) this.#controller = new AbortController();
|
|
199
|
-
if (this.#parent !== void 0) {
|
|
200
|
-
this.#parent.addEventListener("abort", this.#listener, { once: true });
|
|
201
|
-
this.#linked = true;
|
|
202
|
-
}
|
|
206
|
+
if (this.#parent !== void 0) this.#parent.addEventListener("abort", this.#listener, { once: true });
|
|
203
207
|
this.#handle = setTimeout(() => {
|
|
204
208
|
this.#handle = void 0;
|
|
205
209
|
this.#detach();
|
|
206
210
|
this.#controller.abort();
|
|
207
|
-
}, this
|
|
211
|
+
}, this.#ms);
|
|
208
212
|
}
|
|
209
213
|
clear() {
|
|
210
214
|
if (this.#handle !== void 0) {
|
|
@@ -215,39 +219,43 @@ var Timeout = class {
|
|
|
215
219
|
if (this.#controller.signal.aborted) this.#controller = new AbortController();
|
|
216
220
|
}
|
|
217
221
|
#detach() {
|
|
218
|
-
|
|
219
|
-
this.#parent?.removeEventListener("abort", this.#listener);
|
|
220
|
-
this.#linked = false;
|
|
221
|
-
}
|
|
222
|
+
this.#parent?.removeEventListener("abort", this.#listener);
|
|
222
223
|
}
|
|
223
224
|
};
|
|
224
225
|
//#endregion
|
|
225
226
|
//#region src/core/factories.ts
|
|
226
227
|
/**
|
|
227
|
-
*
|
|
228
|
+
* Creates a deadline handle from validated `TimeoutOptions` and returns it as a
|
|
229
|
+
* `TimeoutInterface`.
|
|
228
230
|
*
|
|
229
231
|
* @remarks
|
|
230
232
|
* `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.
|
|
231
233
|
* A parent `options.signal` clears an armed timeout without aborting the
|
|
232
|
-
* timeout's own signal.
|
|
233
|
-
*
|
|
234
|
-
*
|
|
234
|
+
* timeout's own signal. `start()` returns without arming when the parent
|
|
235
|
+
* `signal` supplied at construction has already aborted, so a handle whose
|
|
236
|
+
* parent aborted never expires again and its `signal` never fires. Omitted
|
|
237
|
+
* `options.id` values generate a random UUID. Malformed or unreadable options
|
|
238
|
+
* use contract code `bound`; invalid `id`, `ms`, and `signal` values use
|
|
239
|
+
* `literal`, `range`, and `placement`.
|
|
235
240
|
*
|
|
236
241
|
* @param options - Validated deadline, optional trace label, and optional native parent signal
|
|
237
242
|
* @returns A reusable timeout handle
|
|
238
|
-
* @throws {@link import('@orkestrel/contract').ContractError}
|
|
243
|
+
* @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
|
|
239
244
|
* JavaScript input does not satisfy `TimeoutOptions`
|
|
240
245
|
*
|
|
241
|
-
* @example
|
|
246
|
+
* @example Race work against a deadline
|
|
242
247
|
* ```ts
|
|
243
248
|
* import { createTimeout } from '@orkestrel/timeout'
|
|
244
249
|
*
|
|
245
|
-
*
|
|
246
|
-
* timeout
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
250
|
+
* async function fetchWithDeadline(url: string, ms: number): Promise<Response> {
|
|
251
|
+
* const timeout = createTimeout({ ms })
|
|
252
|
+
* timeout.start()
|
|
253
|
+
*
|
|
254
|
+
* try {
|
|
255
|
+
* return await fetch(url, { signal: timeout.signal })
|
|
256
|
+
* } finally {
|
|
257
|
+
* timeout.clear() // cancels the still-armed deadline when the fetch won the race
|
|
258
|
+
* }
|
|
251
259
|
* }
|
|
252
260
|
* ```
|
|
253
261
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["#parent","#listener","#controller","#handle","#detach","#linked"],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/Timeout.ts","../../../src/core/factories.ts"],"sourcesContent":["/**\n * Largest timeout duration accepted by the package, in milliseconds.\n *\n * @example\n * ```ts\n * createTimeout({ ms: MAX_TIMEOUT_MS })\n * ```\n */\nexport const MAX_TIMEOUT_MS = 2_147_483_647\n","import { isFiniteNumber, isInteger } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\n\n/**\n * Determine whether a value is an accepted timeout duration.\n *\n * @param value - Unknown duration candidate\n * @returns `true` only for an integer in the inclusive timeout range\n *\n * @example\n * ```ts\n * isTimeoutDuration(0) // true\n * isTimeoutDuration(1.5) // false\n * ```\n */\nexport function isTimeoutDuration(value: unknown): value is number {\n\treturn isFiniteNumber(value) && isInteger(value) && value >= 0 && value <= MAX_TIMEOUT_MS\n}\n\n/**\n * Determine whether a value is a genuine native `AbortSignal`.\n *\n * @remarks\n * The intrinsic `aborted` getter performs the native brand check. The boundary\n * contains revoked proxies and hostile receivers, returning `false` instead of\n * allowing their errors to escape.\n *\n * @param value - Unknown signal candidate\n * @returns `true` only when the native `AbortSignal` getter accepts the value\n *\n * @example\n * ```ts\n * isTimeoutSignal(new AbortController().signal) // true\n * isTimeoutSignal({ aborted: false }) // false\n * ```\n */\nexport function isTimeoutSignal(value: unknown): value is AbortSignal {\n\ttry {\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted')\n\t\tif (descriptor?.get === undefined) return false\n\t\treturn typeof Reflect.apply(descriptor.get, value, []) === 'boolean'\n\t} catch {\n\t\treturn false\n\t}\n}\n","import type { TimeoutOptions } from './types.js'\nimport { ContractError, isRecord, isString, preview } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\nimport { isTimeoutDuration, isTimeoutSignal } from './validators.js'\n\n/**\n * Validate and normalize timeout construction options.\n *\n * @remarks\n * Each property is read exactly once before validation. The returned object is\n * a fresh copy and omits absent optional properties. No timer, controller, or\n * listener lifecycle begins at this boundary.\n *\n * @param options - Potentially untrusted timeout options\n * @returns A fresh validated `TimeoutOptions` object\n * @throws {@link import('@orkestrel/contract').ContractError} When the input\n * does not satisfy `TimeoutOptions`\n *\n * @example\n * ```ts\n * const options = validateTimeoutOptions({ ms: 5_000 })\n * ```\n */\nexport function validateTimeoutOptions(options: TimeoutOptions): TimeoutOptions {\n\tif (!isRecord(options)) {\n\t\tthrow new ContractError('Timeout: options must be a plain record', {\n\t\t\tcode: 'bound',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options'],\n\t\t\t\tlimit: 'plain record',\n\t\t\t\treceived: preview(options),\n\t\t\t},\n\t\t})\n\t}\n\n\tlet id: TimeoutOptions['id']\n\tlet ms: TimeoutOptions['ms']\n\tlet signal: TimeoutOptions['signal']\n\ttry {\n\t\tid = options.id\n\t\tms = options.ms\n\t\tsignal = options.signal\n\t} catch (cause) {\n\t\tthrow new ContractError('Timeout: options could not be read', {\n\t\t\tcode: 'bound',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options'],\n\t\t\t\tlimit: 'readable plain record',\n\t\t\t\treceived: preview(options),\n\t\t\t},\n\t\t\tcause,\n\t\t})\n\t}\n\n\tif (id !== undefined && !isString(id)) {\n\t\tthrow new ContractError('Timeout: id must be a string when defined', {\n\t\t\tcode: 'literal',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'id'],\n\t\t\t\tlimit: 'string or undefined',\n\t\t\t\treceived: preview(id),\n\t\t\t},\n\t\t})\n\t}\n\tif (!isTimeoutDuration(ms)) {\n\t\tthrow new ContractError('Timeout: ms must be an integer in the supported range', {\n\t\t\tcode: 'range',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'ms'],\n\t\t\t\tlimit: `integer in [0, ${MAX_TIMEOUT_MS}]`,\n\t\t\t\treceived: preview(ms),\n\t\t\t},\n\t\t})\n\t}\n\tif (signal !== undefined && !isTimeoutSignal(signal)) {\n\t\tthrow new ContractError('Timeout: signal must be a native AbortSignal when defined', {\n\t\t\tcode: 'placement',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'signal'],\n\t\t\t\tlimit: 'native AbortSignal or undefined',\n\t\t\t\treceived: preview(signal),\n\t\t\t},\n\t\t})\n\t}\n\n\tif (id !== undefined && signal !== undefined) return { id, ms, signal }\n\tif (id !== undefined) return { id, ms }\n\tif (signal !== undefined) return { ms, signal }\n\treturn { ms }\n}\n","import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { validateTimeoutOptions } from './helpers.js'\n\n/**\n * A controllable deadline whose native `AbortSignal` aborts when it expires.\n *\n * @remarks\n * `start()` arms or replaces the current deadline. Expiry sets `expired` and\n * aborts the current signal once. `clear()` cancels without aborting and resets\n * `expired`. Starting or clearing after expiry installs a fresh signal. A parent\n * abort deliberately clears the timeout without aborting its signal; once the\n * parent has aborted, later starts are inert.\n *\n * Construction validates the complete JavaScript boundary before creating an\n * `AbortController`. A malformed or unreadable options record throws a\n * `bound`-coded {@link import('@orkestrel/contract').ContractError}; an invalid\n * `id`, `ms`, or `signal` uses `literal`, `range`, or `placement`, respectively.\n *\n * @example\n * ```ts\n * import { Timeout } from '@orkestrel/timeout'\n *\n * const timeout = new Timeout({ ms: 5_000 })\n * timeout.start()\n * timeout.signal.addEventListener('abort', () => giveUp(), { once: true })\n * timeout.clear()\n * ```\n */\nexport class Timeout implements TimeoutInterface {\n\treadonly id: string\n\treadonly ms: number\n\treadonly #parent: AbortSignal | undefined\n\treadonly #listener: () => void\n\t#controller: AbortController\n\t#handle: ReturnType<typeof setTimeout> | undefined\n\t#linked = false\n\n\tconstructor(options: TimeoutOptions) {\n\t\tconst input = validateTimeoutOptions(options)\n\t\tthis.id = input.id === undefined ? crypto.randomUUID() : input.id\n\t\tthis.ms = input.ms\n\t\tthis.#parent = input.signal\n\t\tthis.#controller = new AbortController()\n\t\tthis.#listener = this.clear.bind(this)\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.#controller.signal\n\t}\n\n\tget expired(): boolean {\n\t\treturn this.#controller.signal.aborted\n\t}\n\n\tstart(): void {\n\t\tif (this.#parent?.aborted === true) return\n\t\tif (this.#handle !== undefined) {\n\t\t\tclearTimeout(this.#handle)\n\t\t\tthis.#handle = undefined\n\t\t}\n\t\tthis.#detach()\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t\tif (this.#parent !== undefined) {\n\t\t\tthis.#parent.addEventListener('abort', this.#listener, { once: true })\n\t\t\tthis.#linked = true\n\t\t}\n\t\tthis.#handle = setTimeout(() => {\n\t\t\tthis.#handle = undefined\n\t\t\tthis.#detach()\n\t\t\tthis.#controller.abort()\n\t\t}, this.ms)\n\t}\n\n\tclear(): void {\n\t\tif (this.#handle !== undefined) {\n\t\t\tclearTimeout(this.#handle)\n\t\t\tthis.#handle = undefined\n\t\t}\n\t\tthis.#detach()\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t}\n\n\t#detach(): void {\n\t\tif (this.#linked) {\n\t\t\tthis.#parent?.removeEventListener('abort', this.#listener)\n\t\t\tthis.#linked = false\n\t\t}\n\t}\n}\n","import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { Timeout } from './Timeout.js'\n\n/**\n * Create a controllable deadline whose native signal aborts on expiry.\n *\n * @remarks\n * `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.\n * A parent `options.signal` clears an armed timeout without aborting the\n * timeout's own signal. Omitted `options.id` values generate a random UUID.\n * Malformed or unreadable options use contract code `bound`; invalid `id`,\n * `ms`, and `signal` values use `literal`, `range`, and `placement`.\n *\n * @param options - Validated deadline, optional trace label, and optional native parent signal\n * @returns A reusable timeout handle\n * @throws {@link import('@orkestrel/contract').ContractError} When the\n * JavaScript input does not satisfy `TimeoutOptions`\n *\n * @example\n * ```ts\n * import { createTimeout } from '@orkestrel/timeout'\n *\n * const timeout = createTimeout({ ms: 5_000 })\n * timeout.start()\n * try {\n * \tawait fetch('/work', { signal: timeout.signal })\n * } finally {\n * \ttimeout.clear()\n * }\n * ```\n */\nexport function createTimeout(options: TimeoutOptions): TimeoutInterface {\n\treturn new Timeout(options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/Timeout.ts","../../../src/core/factories.ts"],"sourcesContent":["/**\n * Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds.\n *\n * @example\n * ```ts\n * createTimeout({ ms: MAX_TIMEOUT_MS })\n * ```\n */\nexport const MAX_TIMEOUT_MS = 2_147_483_647\n","import { isInteger } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\n\n/**\n * Determines whether a value is an integer in the inclusive range from `0` through\n * `MAX_TIMEOUT_MS`, staying total for every input.\n *\n * @param value - Unknown duration candidate\n * @returns True if the value is an integer in the inclusive timeout range; false otherwise\n *\n * @example\n * ```ts\n * isTimeoutDuration(0) // true\n * isTimeoutDuration(1.5) // false\n * ```\n */\nexport function isTimeoutDuration(value: unknown): value is number {\n\treturn isInteger(value) && value >= 0 && value <= MAX_TIMEOUT_MS\n}\n\n/**\n * Determines whether a value is a genuine native `AbortSignal`, staying total for a\n * structural spoof and for a hostile or revoked proxy.\n *\n * @remarks\n * The intrinsic `aborted` getter performs the native brand check, and the boundary\n * returns `false` rather than letting a receiver's error escape.\n *\n * @param value - Unknown signal candidate\n * @returns True if the native `AbortSignal` getter accepts the value; false otherwise\n *\n * @example\n * ```ts\n * isTimeoutSignal(new AbortController().signal) // true\n * isTimeoutSignal({ aborted: false }) // false\n * ```\n */\nexport function isTimeoutSignal(value: unknown): value is AbortSignal {\n\ttry {\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted')\n\t\tif (descriptor?.get === undefined) return false\n\t\treturn typeof Reflect.apply(descriptor.get, value, []) === 'boolean'\n\t} catch {\n\t\treturn false\n\t}\n}\n","import type { TimeoutOptions } from './types.js'\nimport { ContractError, isRecord, isString, preview } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\nimport { isTimeoutDuration, isTimeoutSignal } from './validators.js'\n\n/**\n * Validates once-read timeout construction options and returns a fresh normalized copy\n * omitting absent optional keys.\n *\n * @remarks\n * Each property is read before any validation runs, and no timer, controller, or\n * listener lifecycle begins at this boundary.\n *\n * @param options - Potentially untrusted timeout options\n * @returns A fresh validated `TimeoutOptions` object\n * @throws {@link import('@orkestrel/contract').ContractError} Thrown when the\n * input does not satisfy `TimeoutOptions`\n *\n * @example\n * ```ts\n * const options = validateTimeoutOptions({ ms: 5_000 })\n * ```\n */\nexport function validateTimeoutOptions(options: TimeoutOptions): TimeoutOptions {\n\tif (!isRecord(options)) {\n\t\tthrow new ContractError('Timeout: options must be a plain record', {\n\t\t\tcode: 'bound',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options'],\n\t\t\t\tlimit: 'plain record',\n\t\t\t\treceived: preview(options),\n\t\t\t},\n\t\t})\n\t}\n\n\tlet id: TimeoutOptions['id']\n\tlet ms: TimeoutOptions['ms']\n\tlet signal: TimeoutOptions['signal']\n\ttry {\n\t\tid = options.id\n\t\tms = options.ms\n\t\tsignal = options.signal\n\t} catch (cause) {\n\t\tthrow new ContractError('Timeout: options could not be read', {\n\t\t\tcode: 'bound',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options'],\n\t\t\t\tlimit: 'readable plain record',\n\t\t\t\treceived: preview(options),\n\t\t\t},\n\t\t\tcause,\n\t\t})\n\t}\n\n\tif (id !== undefined && !isString(id)) {\n\t\tthrow new ContractError('Timeout: id must be a string when defined', {\n\t\t\tcode: 'literal',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'id'],\n\t\t\t\tlimit: 'string or undefined',\n\t\t\t\treceived: preview(id),\n\t\t\t},\n\t\t})\n\t}\n\tif (!isTimeoutDuration(ms)) {\n\t\tthrow new ContractError('Timeout: ms must be an integer in the supported range', {\n\t\t\tcode: 'range',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'ms'],\n\t\t\t\tlimit: `integer in [0, ${MAX_TIMEOUT_MS}]`,\n\t\t\t\treceived: preview(ms),\n\t\t\t},\n\t\t})\n\t}\n\tif (signal !== undefined && !isTimeoutSignal(signal)) {\n\t\tthrow new ContractError('Timeout: signal must be a native AbortSignal when defined', {\n\t\t\tcode: 'placement',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'signal'],\n\t\t\t\tlimit: 'native AbortSignal or undefined',\n\t\t\t\treceived: preview(signal),\n\t\t\t},\n\t\t})\n\t}\n\n\tif (id !== undefined && signal !== undefined) return { id, ms, signal }\n\tif (id !== undefined) return { id, ms }\n\tif (signal !== undefined) return { ms, signal }\n\treturn { ms }\n}\n","import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { validateTimeoutOptions } from './helpers.js'\n\n/**\n * Implements `TimeoutInterface` exactly, as a controllable `setTimeout` wrapper over one\n * owned `AbortController` whose signal aborts when the deadline expires.\n *\n * @remarks\n * `start()` arms or replaces the current deadline. Expiry sets `expired` and\n * aborts the current signal once. `clear()` cancels without aborting and resets\n * `expired`. Starting or clearing after expiry installs a fresh signal. A parent\n * abort deliberately clears the timeout without aborting its signal; once the\n * parent has aborted, later starts are inert.\n *\n * Construction validates the complete JavaScript boundary before creating an\n * `AbortController`. A malformed or unreadable options record throws a\n * `bound`-coded {@link import('@orkestrel/contract').ContractError}; an invalid\n * `id`, `ms`, or `signal` uses `literal`, `range`, or `placement`, respectively.\n *\n * @example\n * ```ts\n * import { Timeout } from '@orkestrel/timeout'\n *\n * const timeout = new Timeout({ ms: 5_000 })\n * timeout.start()\n * timeout.signal.addEventListener('abort', () => giveUp(), { once: true })\n * timeout.clear()\n * ```\n */\nexport class Timeout implements TimeoutInterface {\n\treadonly #id: string\n\treadonly #ms: number\n\treadonly #parent: AbortSignal | undefined\n\treadonly #listener: () => void\n\t#controller: AbortController\n\t#handle: ReturnType<typeof setTimeout> | undefined\n\n\tconstructor(options: TimeoutOptions) {\n\t\tconst input = validateTimeoutOptions(options)\n\t\tthis.#id = input.id === undefined ? crypto.randomUUID() : input.id\n\t\tthis.#ms = input.ms\n\t\tthis.#parent = input.signal\n\t\tthis.#controller = new AbortController()\n\t\tthis.#listener = this.clear.bind(this)\n\t}\n\n\tget id(): string {\n\t\treturn this.#id\n\t}\n\n\tget ms(): number {\n\t\treturn this.#ms\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.#controller.signal\n\t}\n\n\tget expired(): boolean {\n\t\treturn this.#controller.signal.aborted\n\t}\n\n\tstart(): void {\n\t\tif (this.#parent?.aborted === true) return\n\t\tif (this.#handle !== undefined) {\n\t\t\tclearTimeout(this.#handle)\n\t\t\tthis.#handle = undefined\n\t\t}\n\t\tthis.#detach()\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t\tif (this.#parent !== undefined) {\n\t\t\tthis.#parent.addEventListener('abort', this.#listener, { once: true })\n\t\t}\n\t\tthis.#handle = setTimeout(() => {\n\t\t\tthis.#handle = undefined\n\t\t\tthis.#detach()\n\t\t\tthis.#controller.abort()\n\t\t}, this.#ms)\n\t}\n\n\tclear(): void {\n\t\tif (this.#handle !== undefined) {\n\t\t\tclearTimeout(this.#handle)\n\t\t\tthis.#handle = undefined\n\t\t}\n\t\tthis.#detach()\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t}\n\n\t#detach(): void {\n\t\tthis.#parent?.removeEventListener('abort', this.#listener)\n\t}\n}\n","import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { Timeout } from './Timeout.js'\n\n/**\n * Creates a deadline handle from validated `TimeoutOptions` and returns it as a\n * `TimeoutInterface`.\n *\n * @remarks\n * `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.\n * A parent `options.signal` clears an armed timeout without aborting the\n * timeout's own signal. `start()` returns without arming when the parent\n * `signal` supplied at construction has already aborted, so a handle whose\n * parent aborted never expires again and its `signal` never fires. Omitted\n * `options.id` values generate a random UUID. Malformed or unreadable options\n * use contract code `bound`; invalid `id`, `ms`, and `signal` values use\n * `literal`, `range`, and `placement`.\n *\n * @param options - Validated deadline, optional trace label, and optional native parent signal\n * @returns A reusable timeout handle\n * @throws {@link import('@orkestrel/contract').ContractError} Thrown when the\n * JavaScript input does not satisfy `TimeoutOptions`\n *\n * @example Race work against a deadline\n * ```ts\n * import { createTimeout } from '@orkestrel/timeout'\n *\n * async function fetchWithDeadline(url: string, ms: number): Promise<Response> {\n * \tconst timeout = createTimeout({ ms })\n * \ttimeout.start()\n *\n * \ttry {\n * \t\treturn await fetch(url, { signal: timeout.signal })\n * \t} finally {\n * \t\ttimeout.clear() // cancels the still-armed deadline when the fetch won the race\n * \t}\n * }\n * ```\n */\nexport function createTimeout(options: TimeoutOptions): TimeoutInterface {\n\treturn new Timeout(options)\n}\n"],"mappings":"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"}
|
|
@@ -1,38 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Creates a deadline handle from validated `TimeoutOptions` and returns it as a
|
|
3
|
+
* `TimeoutInterface`.
|
|
3
4
|
*
|
|
4
5
|
* @remarks
|
|
5
6
|
* `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.
|
|
6
7
|
* A parent `options.signal` clears an armed timeout without aborting the
|
|
7
|
-
* timeout's own signal.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* timeout's own signal. `start()` returns without arming when the parent
|
|
9
|
+
* `signal` supplied at construction has already aborted, so a handle whose
|
|
10
|
+
* parent aborted never expires again and its `signal` never fires. Omitted
|
|
11
|
+
* `options.id` values generate a random UUID. Malformed or unreadable options
|
|
12
|
+
* use contract code `bound`; invalid `id`, `ms`, and `signal` values use
|
|
13
|
+
* `literal`, `range`, and `placement`.
|
|
10
14
|
*
|
|
11
15
|
* @param options - Validated deadline, optional trace label, and optional native parent signal
|
|
12
16
|
* @returns A reusable timeout handle
|
|
13
|
-
* @throws {@link import('@orkestrel/contract').ContractError}
|
|
17
|
+
* @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
|
|
14
18
|
* JavaScript input does not satisfy `TimeoutOptions`
|
|
15
19
|
*
|
|
16
|
-
* @example
|
|
20
|
+
* @example Race work against a deadline
|
|
17
21
|
* ```ts
|
|
18
22
|
* import { createTimeout } from '@orkestrel/timeout'
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
* timeout
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* async function fetchWithDeadline(url: string, ms: number): Promise<Response> {
|
|
25
|
+
* const timeout = createTimeout({ ms })
|
|
26
|
+
* timeout.start()
|
|
27
|
+
*
|
|
28
|
+
* try {
|
|
29
|
+
* return await fetch(url, { signal: timeout.signal })
|
|
30
|
+
* } finally {
|
|
31
|
+
* timeout.clear() // cancels the still-armed deadline when the fetch won the race
|
|
32
|
+
* }
|
|
26
33
|
* }
|
|
27
34
|
* ```
|
|
28
35
|
*/
|
|
29
36
|
export declare function createTimeout(options: TimeoutOptions): TimeoutInterface;
|
|
30
37
|
|
|
31
38
|
/**
|
|
32
|
-
*
|
|
39
|
+
* Determines whether a value is an integer in the inclusive range from `0` through
|
|
40
|
+
* `MAX_TIMEOUT_MS`, staying total for every input.
|
|
33
41
|
*
|
|
34
42
|
* @param value - Unknown duration candidate
|
|
35
|
-
* @returns
|
|
43
|
+
* @returns True if the value is an integer in the inclusive timeout range; false otherwise
|
|
36
44
|
*
|
|
37
45
|
* @example
|
|
38
46
|
* ```ts
|
|
@@ -43,15 +51,15 @@
|
|
|
43
51
|
export declare function isTimeoutDuration(value: unknown): value is number;
|
|
44
52
|
|
|
45
53
|
/**
|
|
46
|
-
*
|
|
54
|
+
* Determines whether a value is a genuine native `AbortSignal`, staying total for a
|
|
55
|
+
* structural spoof and for a hostile or revoked proxy.
|
|
47
56
|
*
|
|
48
57
|
* @remarks
|
|
49
|
-
* The intrinsic `aborted` getter performs the native brand check
|
|
50
|
-
*
|
|
51
|
-
* allowing their errors to escape.
|
|
58
|
+
* The intrinsic `aborted` getter performs the native brand check, and the boundary
|
|
59
|
+
* returns `false` rather than letting a receiver's error escape.
|
|
52
60
|
*
|
|
53
61
|
* @param value - Unknown signal candidate
|
|
54
|
-
* @returns
|
|
62
|
+
* @returns True if the native `AbortSignal` getter accepts the value; false otherwise
|
|
55
63
|
*
|
|
56
64
|
* @example
|
|
57
65
|
* ```ts
|
|
@@ -62,7 +70,7 @@
|
|
|
62
70
|
export declare function isTimeoutSignal(value: unknown): value is AbortSignal;
|
|
63
71
|
|
|
64
72
|
/**
|
|
65
|
-
*
|
|
73
|
+
* Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds.
|
|
66
74
|
*
|
|
67
75
|
* @example
|
|
68
76
|
* ```ts
|
|
@@ -72,7 +80,8 @@
|
|
|
72
80
|
export declare const MAX_TIMEOUT_MS = 2147483647;
|
|
73
81
|
|
|
74
82
|
/**
|
|
75
|
-
*
|
|
83
|
+
* Implements `TimeoutInterface` exactly, as a controllable `setTimeout` wrapper over one
|
|
84
|
+
* owned `AbortController` whose signal aborts when the deadline expires.
|
|
76
85
|
*
|
|
77
86
|
* @remarks
|
|
78
87
|
* `start()` arms or replaces the current deadline. Expiry sets `expired` and
|
|
@@ -98,9 +107,9 @@
|
|
|
98
107
|
*/
|
|
99
108
|
export declare class Timeout implements TimeoutInterface {
|
|
100
109
|
#private;
|
|
101
|
-
readonly id: string;
|
|
102
|
-
readonly ms: number;
|
|
103
110
|
constructor(options: TimeoutOptions);
|
|
111
|
+
get id(): string;
|
|
112
|
+
get ms(): number;
|
|
104
113
|
get signal(): AbortSignal;
|
|
105
114
|
get expired(): boolean;
|
|
106
115
|
start(): void;
|
|
@@ -108,7 +117,7 @@
|
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
/**
|
|
111
|
-
*
|
|
120
|
+
* Represents a controllable deadline exposing a native `AbortSignal` that aborts on expiry.
|
|
112
121
|
*
|
|
113
122
|
* @example
|
|
114
123
|
* ```ts
|
|
@@ -118,22 +127,36 @@
|
|
|
118
127
|
* ```
|
|
119
128
|
*/
|
|
120
129
|
export declare interface TimeoutInterface {
|
|
121
|
-
/**
|
|
130
|
+
/** Holds the trace label supplied at construction or generated as a random UUID. */
|
|
122
131
|
readonly id: string;
|
|
123
|
-
/**
|
|
132
|
+
/** Holds the validated integer deadline in milliseconds. */
|
|
124
133
|
readonly ms: number;
|
|
125
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Holds the native signal that aborts once when the current deadline
|
|
136
|
+
* expires. A `start()` or `clear()` call after an expiry installs a fresh
|
|
137
|
+
* signal, so a caller holding an earlier reference keeps the aborted one.
|
|
138
|
+
*/
|
|
126
139
|
readonly signal: AbortSignal;
|
|
127
|
-
/**
|
|
140
|
+
/** Reports whether the owned signal has aborted, derived directly from that signal. */
|
|
128
141
|
readonly expired: boolean;
|
|
129
142
|
/**
|
|
130
|
-
*
|
|
143
|
+
* Arms or re-arms the deadline for `ms`, installing a fresh `signal` when the
|
|
144
|
+
* current one has already aborted.
|
|
145
|
+
*
|
|
146
|
+
* @remarks
|
|
147
|
+
* Returns without arming when the parent `signal` supplied at construction
|
|
148
|
+
* has already aborted, so a handle whose parent aborted never expires again
|
|
149
|
+
* and its `signal` never fires.
|
|
131
150
|
*
|
|
132
151
|
* @returns Nothing
|
|
133
152
|
*/
|
|
134
153
|
start(): void;
|
|
135
154
|
/**
|
|
136
|
-
*
|
|
155
|
+
* Cancels an armed deadline without aborting its `signal`, and resets expiry by
|
|
156
|
+
* installing a fresh signal when the current one has already aborted.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* A signal that never fired keeps its identity.
|
|
137
160
|
*
|
|
138
161
|
* @returns Nothing
|
|
139
162
|
*/
|
|
@@ -141,7 +164,7 @@
|
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
/**
|
|
144
|
-
*
|
|
167
|
+
* Represents the options `createTimeout` and the `Timeout` constructor accept.
|
|
145
168
|
*
|
|
146
169
|
* @remarks
|
|
147
170
|
* `ms` is an integer from `0` through `2_147_483_647`, inclusive. `id`
|
|
@@ -155,26 +178,26 @@
|
|
|
155
178
|
* ```
|
|
156
179
|
*/
|
|
157
180
|
export declare interface TimeoutOptions {
|
|
158
|
-
/**
|
|
181
|
+
/** Holds the trace label for the timeout; omission generates a random UUID. */
|
|
159
182
|
readonly id?: string;
|
|
160
|
-
/**
|
|
183
|
+
/** Holds the integer deadline in milliseconds, inclusive from `0` through `2_147_483_647`. */
|
|
161
184
|
readonly ms: number;
|
|
162
|
-
/**
|
|
185
|
+
/** Holds the native parent signal whose abort clears an armed timeout. */
|
|
163
186
|
readonly signal?: AbortSignal;
|
|
164
187
|
}
|
|
165
188
|
|
|
166
189
|
/**
|
|
167
|
-
*
|
|
190
|
+
* Validates once-read timeout construction options and returns a fresh normalized copy
|
|
191
|
+
* omitting absent optional keys.
|
|
168
192
|
*
|
|
169
193
|
* @remarks
|
|
170
|
-
* Each property is read
|
|
171
|
-
* a fresh copy and omits absent optional properties. No timer, controller, or
|
|
194
|
+
* Each property is read before any validation runs, and no timer, controller, or
|
|
172
195
|
* listener lifecycle begins at this boundary.
|
|
173
196
|
*
|
|
174
197
|
* @param options - Potentially untrusted timeout options
|
|
175
198
|
* @returns A fresh validated `TimeoutOptions` object
|
|
176
|
-
* @throws {@link import('@orkestrel/contract').ContractError}
|
|
177
|
-
* does not satisfy `TimeoutOptions`
|
|
199
|
+
* @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
|
|
200
|
+
* input does not satisfy `TimeoutOptions`
|
|
178
201
|
*
|
|
179
202
|
* @example
|
|
180
203
|
* ```ts
|
package/dist/src/core/index.d.ts
CHANGED
|
@@ -1,38 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Creates a deadline handle from validated `TimeoutOptions` and returns it as a
|
|
3
|
+
* `TimeoutInterface`.
|
|
3
4
|
*
|
|
4
5
|
* @remarks
|
|
5
6
|
* `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.
|
|
6
7
|
* A parent `options.signal` clears an armed timeout without aborting the
|
|
7
|
-
* timeout's own signal.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* timeout's own signal. `start()` returns without arming when the parent
|
|
9
|
+
* `signal` supplied at construction has already aborted, so a handle whose
|
|
10
|
+
* parent aborted never expires again and its `signal` never fires. Omitted
|
|
11
|
+
* `options.id` values generate a random UUID. Malformed or unreadable options
|
|
12
|
+
* use contract code `bound`; invalid `id`, `ms`, and `signal` values use
|
|
13
|
+
* `literal`, `range`, and `placement`.
|
|
10
14
|
*
|
|
11
15
|
* @param options - Validated deadline, optional trace label, and optional native parent signal
|
|
12
16
|
* @returns A reusable timeout handle
|
|
13
|
-
* @throws {@link import('@orkestrel/contract').ContractError}
|
|
17
|
+
* @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
|
|
14
18
|
* JavaScript input does not satisfy `TimeoutOptions`
|
|
15
19
|
*
|
|
16
|
-
* @example
|
|
20
|
+
* @example Race work against a deadline
|
|
17
21
|
* ```ts
|
|
18
22
|
* import { createTimeout } from '@orkestrel/timeout'
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
* timeout
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* async function fetchWithDeadline(url: string, ms: number): Promise<Response> {
|
|
25
|
+
* const timeout = createTimeout({ ms })
|
|
26
|
+
* timeout.start()
|
|
27
|
+
*
|
|
28
|
+
* try {
|
|
29
|
+
* return await fetch(url, { signal: timeout.signal })
|
|
30
|
+
* } finally {
|
|
31
|
+
* timeout.clear() // cancels the still-armed deadline when the fetch won the race
|
|
32
|
+
* }
|
|
26
33
|
* }
|
|
27
34
|
* ```
|
|
28
35
|
*/
|
|
29
36
|
export declare function createTimeout(options: TimeoutOptions): TimeoutInterface;
|
|
30
37
|
|
|
31
38
|
/**
|
|
32
|
-
*
|
|
39
|
+
* Determines whether a value is an integer in the inclusive range from `0` through
|
|
40
|
+
* `MAX_TIMEOUT_MS`, staying total for every input.
|
|
33
41
|
*
|
|
34
42
|
* @param value - Unknown duration candidate
|
|
35
|
-
* @returns
|
|
43
|
+
* @returns True if the value is an integer in the inclusive timeout range; false otherwise
|
|
36
44
|
*
|
|
37
45
|
* @example
|
|
38
46
|
* ```ts
|
|
@@ -43,15 +51,15 @@
|
|
|
43
51
|
export declare function isTimeoutDuration(value: unknown): value is number;
|
|
44
52
|
|
|
45
53
|
/**
|
|
46
|
-
*
|
|
54
|
+
* Determines whether a value is a genuine native `AbortSignal`, staying total for a
|
|
55
|
+
* structural spoof and for a hostile or revoked proxy.
|
|
47
56
|
*
|
|
48
57
|
* @remarks
|
|
49
|
-
* The intrinsic `aborted` getter performs the native brand check
|
|
50
|
-
*
|
|
51
|
-
* allowing their errors to escape.
|
|
58
|
+
* The intrinsic `aborted` getter performs the native brand check, and the boundary
|
|
59
|
+
* returns `false` rather than letting a receiver's error escape.
|
|
52
60
|
*
|
|
53
61
|
* @param value - Unknown signal candidate
|
|
54
|
-
* @returns
|
|
62
|
+
* @returns True if the native `AbortSignal` getter accepts the value; false otherwise
|
|
55
63
|
*
|
|
56
64
|
* @example
|
|
57
65
|
* ```ts
|
|
@@ -62,7 +70,7 @@
|
|
|
62
70
|
export declare function isTimeoutSignal(value: unknown): value is AbortSignal;
|
|
63
71
|
|
|
64
72
|
/**
|
|
65
|
-
*
|
|
73
|
+
* Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds.
|
|
66
74
|
*
|
|
67
75
|
* @example
|
|
68
76
|
* ```ts
|
|
@@ -72,7 +80,8 @@
|
|
|
72
80
|
export declare const MAX_TIMEOUT_MS = 2147483647;
|
|
73
81
|
|
|
74
82
|
/**
|
|
75
|
-
*
|
|
83
|
+
* Implements `TimeoutInterface` exactly, as a controllable `setTimeout` wrapper over one
|
|
84
|
+
* owned `AbortController` whose signal aborts when the deadline expires.
|
|
76
85
|
*
|
|
77
86
|
* @remarks
|
|
78
87
|
* `start()` arms or replaces the current deadline. Expiry sets `expired` and
|
|
@@ -98,9 +107,9 @@
|
|
|
98
107
|
*/
|
|
99
108
|
export declare class Timeout implements TimeoutInterface {
|
|
100
109
|
#private;
|
|
101
|
-
readonly id: string;
|
|
102
|
-
readonly ms: number;
|
|
103
110
|
constructor(options: TimeoutOptions);
|
|
111
|
+
get id(): string;
|
|
112
|
+
get ms(): number;
|
|
104
113
|
get signal(): AbortSignal;
|
|
105
114
|
get expired(): boolean;
|
|
106
115
|
start(): void;
|
|
@@ -108,7 +117,7 @@
|
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
/**
|
|
111
|
-
*
|
|
120
|
+
* Represents a controllable deadline exposing a native `AbortSignal` that aborts on expiry.
|
|
112
121
|
*
|
|
113
122
|
* @example
|
|
114
123
|
* ```ts
|
|
@@ -118,22 +127,36 @@
|
|
|
118
127
|
* ```
|
|
119
128
|
*/
|
|
120
129
|
export declare interface TimeoutInterface {
|
|
121
|
-
/**
|
|
130
|
+
/** Holds the trace label supplied at construction or generated as a random UUID. */
|
|
122
131
|
readonly id: string;
|
|
123
|
-
/**
|
|
132
|
+
/** Holds the validated integer deadline in milliseconds. */
|
|
124
133
|
readonly ms: number;
|
|
125
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Holds the native signal that aborts once when the current deadline
|
|
136
|
+
* expires. A `start()` or `clear()` call after an expiry installs a fresh
|
|
137
|
+
* signal, so a caller holding an earlier reference keeps the aborted one.
|
|
138
|
+
*/
|
|
126
139
|
readonly signal: AbortSignal;
|
|
127
|
-
/**
|
|
140
|
+
/** Reports whether the owned signal has aborted, derived directly from that signal. */
|
|
128
141
|
readonly expired: boolean;
|
|
129
142
|
/**
|
|
130
|
-
*
|
|
143
|
+
* Arms or re-arms the deadline for `ms`, installing a fresh `signal` when the
|
|
144
|
+
* current one has already aborted.
|
|
145
|
+
*
|
|
146
|
+
* @remarks
|
|
147
|
+
* Returns without arming when the parent `signal` supplied at construction
|
|
148
|
+
* has already aborted, so a handle whose parent aborted never expires again
|
|
149
|
+
* and its `signal` never fires.
|
|
131
150
|
*
|
|
132
151
|
* @returns Nothing
|
|
133
152
|
*/
|
|
134
153
|
start(): void;
|
|
135
154
|
/**
|
|
136
|
-
*
|
|
155
|
+
* Cancels an armed deadline without aborting its `signal`, and resets expiry by
|
|
156
|
+
* installing a fresh signal when the current one has already aborted.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* A signal that never fired keeps its identity.
|
|
137
160
|
*
|
|
138
161
|
* @returns Nothing
|
|
139
162
|
*/
|
|
@@ -141,7 +164,7 @@
|
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
/**
|
|
144
|
-
*
|
|
167
|
+
* Represents the options `createTimeout` and the `Timeout` constructor accept.
|
|
145
168
|
*
|
|
146
169
|
* @remarks
|
|
147
170
|
* `ms` is an integer from `0` through `2_147_483_647`, inclusive. `id`
|
|
@@ -155,26 +178,26 @@
|
|
|
155
178
|
* ```
|
|
156
179
|
*/
|
|
157
180
|
export declare interface TimeoutOptions {
|
|
158
|
-
/**
|
|
181
|
+
/** Holds the trace label for the timeout; omission generates a random UUID. */
|
|
159
182
|
readonly id?: string;
|
|
160
|
-
/**
|
|
183
|
+
/** Holds the integer deadline in milliseconds, inclusive from `0` through `2_147_483_647`. */
|
|
161
184
|
readonly ms: number;
|
|
162
|
-
/**
|
|
185
|
+
/** Holds the native parent signal whose abort clears an armed timeout. */
|
|
163
186
|
readonly signal?: AbortSignal;
|
|
164
187
|
}
|
|
165
188
|
|
|
166
189
|
/**
|
|
167
|
-
*
|
|
190
|
+
* Validates once-read timeout construction options and returns a fresh normalized copy
|
|
191
|
+
* omitting absent optional keys.
|
|
168
192
|
*
|
|
169
193
|
* @remarks
|
|
170
|
-
* Each property is read
|
|
171
|
-
* a fresh copy and omits absent optional properties. No timer, controller, or
|
|
194
|
+
* Each property is read before any validation runs, and no timer, controller, or
|
|
172
195
|
* listener lifecycle begins at this boundary.
|
|
173
196
|
*
|
|
174
197
|
* @param options - Potentially untrusted timeout options
|
|
175
198
|
* @returns A fresh validated `TimeoutOptions` object
|
|
176
|
-
* @throws {@link import('@orkestrel/contract').ContractError}
|
|
177
|
-
* does not satisfy `TimeoutOptions`
|
|
199
|
+
* @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
|
|
200
|
+
* input does not satisfy `TimeoutOptions`
|
|
178
201
|
*
|
|
179
202
|
* @example
|
|
180
203
|
* ```ts
|
package/dist/src/core/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ContractError,
|
|
1
|
+
import { ContractError, isInteger, isRecord, isString, preview } from "@orkestrel/contract";
|
|
2
2
|
//#region src/core/constants.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds.
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* ```ts
|
|
@@ -12,10 +12,11 @@ var MAX_TIMEOUT_MS = 2147483647;
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region src/core/validators.ts
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Determines whether a value is an integer in the inclusive range from `0` through
|
|
16
|
+
* `MAX_TIMEOUT_MS`, staying total for every input.
|
|
16
17
|
*
|
|
17
18
|
* @param value - Unknown duration candidate
|
|
18
|
-
* @returns
|
|
19
|
+
* @returns True if the value is an integer in the inclusive timeout range; false otherwise
|
|
19
20
|
*
|
|
20
21
|
* @example
|
|
21
22
|
* ```ts
|
|
@@ -24,18 +25,18 @@ var MAX_TIMEOUT_MS = 2147483647;
|
|
|
24
25
|
* ```
|
|
25
26
|
*/
|
|
26
27
|
function isTimeoutDuration(value) {
|
|
27
|
-
return
|
|
28
|
+
return isInteger(value) && value >= 0 && value <= 2147483647;
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
+
* Determines whether a value is a genuine native `AbortSignal`, staying total for a
|
|
32
|
+
* structural spoof and for a hostile or revoked proxy.
|
|
31
33
|
*
|
|
32
34
|
* @remarks
|
|
33
|
-
* The intrinsic `aborted` getter performs the native brand check
|
|
34
|
-
*
|
|
35
|
-
* allowing their errors to escape.
|
|
35
|
+
* The intrinsic `aborted` getter performs the native brand check, and the boundary
|
|
36
|
+
* returns `false` rather than letting a receiver's error escape.
|
|
36
37
|
*
|
|
37
38
|
* @param value - Unknown signal candidate
|
|
38
|
-
* @returns
|
|
39
|
+
* @returns True if the native `AbortSignal` getter accepts the value; false otherwise
|
|
39
40
|
*
|
|
40
41
|
* @example
|
|
41
42
|
* ```ts
|
|
@@ -55,17 +56,17 @@ function isTimeoutSignal(value) {
|
|
|
55
56
|
//#endregion
|
|
56
57
|
//#region src/core/helpers.ts
|
|
57
58
|
/**
|
|
58
|
-
*
|
|
59
|
+
* Validates once-read timeout construction options and returns a fresh normalized copy
|
|
60
|
+
* omitting absent optional keys.
|
|
59
61
|
*
|
|
60
62
|
* @remarks
|
|
61
|
-
* Each property is read
|
|
62
|
-
* a fresh copy and omits absent optional properties. No timer, controller, or
|
|
63
|
+
* Each property is read before any validation runs, and no timer, controller, or
|
|
63
64
|
* listener lifecycle begins at this boundary.
|
|
64
65
|
*
|
|
65
66
|
* @param options - Potentially untrusted timeout options
|
|
66
67
|
* @returns A fresh validated `TimeoutOptions` object
|
|
67
|
-
* @throws {@link import('@orkestrel/contract').ContractError}
|
|
68
|
-
* does not satisfy `TimeoutOptions`
|
|
68
|
+
* @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
|
|
69
|
+
* input does not satisfy `TimeoutOptions`
|
|
69
70
|
*
|
|
70
71
|
* @example
|
|
71
72
|
* ```ts
|
|
@@ -141,7 +142,8 @@ function validateTimeoutOptions(options) {
|
|
|
141
142
|
//#endregion
|
|
142
143
|
//#region src/core/Timeout.ts
|
|
143
144
|
/**
|
|
144
|
-
*
|
|
145
|
+
* Implements `TimeoutInterface` exactly, as a controllable `setTimeout` wrapper over one
|
|
146
|
+
* owned `AbortController` whose signal aborts when the deadline expires.
|
|
145
147
|
*
|
|
146
148
|
* @remarks
|
|
147
149
|
* `start()` arms or replaces the current deadline. Expiry sets `expired` and
|
|
@@ -166,21 +168,26 @@ function validateTimeoutOptions(options) {
|
|
|
166
168
|
* ```
|
|
167
169
|
*/
|
|
168
170
|
var Timeout = class {
|
|
169
|
-
id;
|
|
170
|
-
ms;
|
|
171
|
+
#id;
|
|
172
|
+
#ms;
|
|
171
173
|
#parent;
|
|
172
174
|
#listener;
|
|
173
175
|
#controller;
|
|
174
176
|
#handle;
|
|
175
|
-
#linked = false;
|
|
176
177
|
constructor(options) {
|
|
177
178
|
const input = validateTimeoutOptions(options);
|
|
178
|
-
this
|
|
179
|
-
this
|
|
179
|
+
this.#id = input.id === void 0 ? crypto.randomUUID() : input.id;
|
|
180
|
+
this.#ms = input.ms;
|
|
180
181
|
this.#parent = input.signal;
|
|
181
182
|
this.#controller = new AbortController();
|
|
182
183
|
this.#listener = this.clear.bind(this);
|
|
183
184
|
}
|
|
185
|
+
get id() {
|
|
186
|
+
return this.#id;
|
|
187
|
+
}
|
|
188
|
+
get ms() {
|
|
189
|
+
return this.#ms;
|
|
190
|
+
}
|
|
184
191
|
get signal() {
|
|
185
192
|
return this.#controller.signal;
|
|
186
193
|
}
|
|
@@ -195,15 +202,12 @@ var Timeout = class {
|
|
|
195
202
|
}
|
|
196
203
|
this.#detach();
|
|
197
204
|
if (this.#controller.signal.aborted) this.#controller = new AbortController();
|
|
198
|
-
if (this.#parent !== void 0) {
|
|
199
|
-
this.#parent.addEventListener("abort", this.#listener, { once: true });
|
|
200
|
-
this.#linked = true;
|
|
201
|
-
}
|
|
205
|
+
if (this.#parent !== void 0) this.#parent.addEventListener("abort", this.#listener, { once: true });
|
|
202
206
|
this.#handle = setTimeout(() => {
|
|
203
207
|
this.#handle = void 0;
|
|
204
208
|
this.#detach();
|
|
205
209
|
this.#controller.abort();
|
|
206
|
-
}, this
|
|
210
|
+
}, this.#ms);
|
|
207
211
|
}
|
|
208
212
|
clear() {
|
|
209
213
|
if (this.#handle !== void 0) {
|
|
@@ -214,39 +218,43 @@ var Timeout = class {
|
|
|
214
218
|
if (this.#controller.signal.aborted) this.#controller = new AbortController();
|
|
215
219
|
}
|
|
216
220
|
#detach() {
|
|
217
|
-
|
|
218
|
-
this.#parent?.removeEventListener("abort", this.#listener);
|
|
219
|
-
this.#linked = false;
|
|
220
|
-
}
|
|
221
|
+
this.#parent?.removeEventListener("abort", this.#listener);
|
|
221
222
|
}
|
|
222
223
|
};
|
|
223
224
|
//#endregion
|
|
224
225
|
//#region src/core/factories.ts
|
|
225
226
|
/**
|
|
226
|
-
*
|
|
227
|
+
* Creates a deadline handle from validated `TimeoutOptions` and returns it as a
|
|
228
|
+
* `TimeoutInterface`.
|
|
227
229
|
*
|
|
228
230
|
* @remarks
|
|
229
231
|
* `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.
|
|
230
232
|
* A parent `options.signal` clears an armed timeout without aborting the
|
|
231
|
-
* timeout's own signal.
|
|
232
|
-
*
|
|
233
|
-
*
|
|
233
|
+
* timeout's own signal. `start()` returns without arming when the parent
|
|
234
|
+
* `signal` supplied at construction has already aborted, so a handle whose
|
|
235
|
+
* parent aborted never expires again and its `signal` never fires. Omitted
|
|
236
|
+
* `options.id` values generate a random UUID. Malformed or unreadable options
|
|
237
|
+
* use contract code `bound`; invalid `id`, `ms`, and `signal` values use
|
|
238
|
+
* `literal`, `range`, and `placement`.
|
|
234
239
|
*
|
|
235
240
|
* @param options - Validated deadline, optional trace label, and optional native parent signal
|
|
236
241
|
* @returns A reusable timeout handle
|
|
237
|
-
* @throws {@link import('@orkestrel/contract').ContractError}
|
|
242
|
+
* @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
|
|
238
243
|
* JavaScript input does not satisfy `TimeoutOptions`
|
|
239
244
|
*
|
|
240
|
-
* @example
|
|
245
|
+
* @example Race work against a deadline
|
|
241
246
|
* ```ts
|
|
242
247
|
* import { createTimeout } from '@orkestrel/timeout'
|
|
243
248
|
*
|
|
244
|
-
*
|
|
245
|
-
* timeout
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
249
|
+
* async function fetchWithDeadline(url: string, ms: number): Promise<Response> {
|
|
250
|
+
* const timeout = createTimeout({ ms })
|
|
251
|
+
* timeout.start()
|
|
252
|
+
*
|
|
253
|
+
* try {
|
|
254
|
+
* return await fetch(url, { signal: timeout.signal })
|
|
255
|
+
* } finally {
|
|
256
|
+
* timeout.clear() // cancels the still-armed deadline when the fetch won the race
|
|
257
|
+
* }
|
|
250
258
|
* }
|
|
251
259
|
* ```
|
|
252
260
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#parent","#listener","#controller","#handle","#detach","#linked"],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/Timeout.ts","../../../src/core/factories.ts"],"sourcesContent":["/**\n * Largest timeout duration accepted by the package, in milliseconds.\n *\n * @example\n * ```ts\n * createTimeout({ ms: MAX_TIMEOUT_MS })\n * ```\n */\nexport const MAX_TIMEOUT_MS = 2_147_483_647\n","import { isFiniteNumber, isInteger } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\n\n/**\n * Determine whether a value is an accepted timeout duration.\n *\n * @param value - Unknown duration candidate\n * @returns `true` only for an integer in the inclusive timeout range\n *\n * @example\n * ```ts\n * isTimeoutDuration(0) // true\n * isTimeoutDuration(1.5) // false\n * ```\n */\nexport function isTimeoutDuration(value: unknown): value is number {\n\treturn isFiniteNumber(value) && isInteger(value) && value >= 0 && value <= MAX_TIMEOUT_MS\n}\n\n/**\n * Determine whether a value is a genuine native `AbortSignal`.\n *\n * @remarks\n * The intrinsic `aborted` getter performs the native brand check. The boundary\n * contains revoked proxies and hostile receivers, returning `false` instead of\n * allowing their errors to escape.\n *\n * @param value - Unknown signal candidate\n * @returns `true` only when the native `AbortSignal` getter accepts the value\n *\n * @example\n * ```ts\n * isTimeoutSignal(new AbortController().signal) // true\n * isTimeoutSignal({ aborted: false }) // false\n * ```\n */\nexport function isTimeoutSignal(value: unknown): value is AbortSignal {\n\ttry {\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted')\n\t\tif (descriptor?.get === undefined) return false\n\t\treturn typeof Reflect.apply(descriptor.get, value, []) === 'boolean'\n\t} catch {\n\t\treturn false\n\t}\n}\n","import type { TimeoutOptions } from './types.js'\nimport { ContractError, isRecord, isString, preview } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\nimport { isTimeoutDuration, isTimeoutSignal } from './validators.js'\n\n/**\n * Validate and normalize timeout construction options.\n *\n * @remarks\n * Each property is read exactly once before validation. The returned object is\n * a fresh copy and omits absent optional properties. No timer, controller, or\n * listener lifecycle begins at this boundary.\n *\n * @param options - Potentially untrusted timeout options\n * @returns A fresh validated `TimeoutOptions` object\n * @throws {@link import('@orkestrel/contract').ContractError} When the input\n * does not satisfy `TimeoutOptions`\n *\n * @example\n * ```ts\n * const options = validateTimeoutOptions({ ms: 5_000 })\n * ```\n */\nexport function validateTimeoutOptions(options: TimeoutOptions): TimeoutOptions {\n\tif (!isRecord(options)) {\n\t\tthrow new ContractError('Timeout: options must be a plain record', {\n\t\t\tcode: 'bound',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options'],\n\t\t\t\tlimit: 'plain record',\n\t\t\t\treceived: preview(options),\n\t\t\t},\n\t\t})\n\t}\n\n\tlet id: TimeoutOptions['id']\n\tlet ms: TimeoutOptions['ms']\n\tlet signal: TimeoutOptions['signal']\n\ttry {\n\t\tid = options.id\n\t\tms = options.ms\n\t\tsignal = options.signal\n\t} catch (cause) {\n\t\tthrow new ContractError('Timeout: options could not be read', {\n\t\t\tcode: 'bound',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options'],\n\t\t\t\tlimit: 'readable plain record',\n\t\t\t\treceived: preview(options),\n\t\t\t},\n\t\t\tcause,\n\t\t})\n\t}\n\n\tif (id !== undefined && !isString(id)) {\n\t\tthrow new ContractError('Timeout: id must be a string when defined', {\n\t\t\tcode: 'literal',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'id'],\n\t\t\t\tlimit: 'string or undefined',\n\t\t\t\treceived: preview(id),\n\t\t\t},\n\t\t})\n\t}\n\tif (!isTimeoutDuration(ms)) {\n\t\tthrow new ContractError('Timeout: ms must be an integer in the supported range', {\n\t\t\tcode: 'range',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'ms'],\n\t\t\t\tlimit: `integer in [0, ${MAX_TIMEOUT_MS}]`,\n\t\t\t\treceived: preview(ms),\n\t\t\t},\n\t\t})\n\t}\n\tif (signal !== undefined && !isTimeoutSignal(signal)) {\n\t\tthrow new ContractError('Timeout: signal must be a native AbortSignal when defined', {\n\t\t\tcode: 'placement',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'signal'],\n\t\t\t\tlimit: 'native AbortSignal or undefined',\n\t\t\t\treceived: preview(signal),\n\t\t\t},\n\t\t})\n\t}\n\n\tif (id !== undefined && signal !== undefined) return { id, ms, signal }\n\tif (id !== undefined) return { id, ms }\n\tif (signal !== undefined) return { ms, signal }\n\treturn { ms }\n}\n","import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { validateTimeoutOptions } from './helpers.js'\n\n/**\n * A controllable deadline whose native `AbortSignal` aborts when it expires.\n *\n * @remarks\n * `start()` arms or replaces the current deadline. Expiry sets `expired` and\n * aborts the current signal once. `clear()` cancels without aborting and resets\n * `expired`. Starting or clearing after expiry installs a fresh signal. A parent\n * abort deliberately clears the timeout without aborting its signal; once the\n * parent has aborted, later starts are inert.\n *\n * Construction validates the complete JavaScript boundary before creating an\n * `AbortController`. A malformed or unreadable options record throws a\n * `bound`-coded {@link import('@orkestrel/contract').ContractError}; an invalid\n * `id`, `ms`, or `signal` uses `literal`, `range`, or `placement`, respectively.\n *\n * @example\n * ```ts\n * import { Timeout } from '@orkestrel/timeout'\n *\n * const timeout = new Timeout({ ms: 5_000 })\n * timeout.start()\n * timeout.signal.addEventListener('abort', () => giveUp(), { once: true })\n * timeout.clear()\n * ```\n */\nexport class Timeout implements TimeoutInterface {\n\treadonly id: string\n\treadonly ms: number\n\treadonly #parent: AbortSignal | undefined\n\treadonly #listener: () => void\n\t#controller: AbortController\n\t#handle: ReturnType<typeof setTimeout> | undefined\n\t#linked = false\n\n\tconstructor(options: TimeoutOptions) {\n\t\tconst input = validateTimeoutOptions(options)\n\t\tthis.id = input.id === undefined ? crypto.randomUUID() : input.id\n\t\tthis.ms = input.ms\n\t\tthis.#parent = input.signal\n\t\tthis.#controller = new AbortController()\n\t\tthis.#listener = this.clear.bind(this)\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.#controller.signal\n\t}\n\n\tget expired(): boolean {\n\t\treturn this.#controller.signal.aborted\n\t}\n\n\tstart(): void {\n\t\tif (this.#parent?.aborted === true) return\n\t\tif (this.#handle !== undefined) {\n\t\t\tclearTimeout(this.#handle)\n\t\t\tthis.#handle = undefined\n\t\t}\n\t\tthis.#detach()\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t\tif (this.#parent !== undefined) {\n\t\t\tthis.#parent.addEventListener('abort', this.#listener, { once: true })\n\t\t\tthis.#linked = true\n\t\t}\n\t\tthis.#handle = setTimeout(() => {\n\t\t\tthis.#handle = undefined\n\t\t\tthis.#detach()\n\t\t\tthis.#controller.abort()\n\t\t}, this.ms)\n\t}\n\n\tclear(): void {\n\t\tif (this.#handle !== undefined) {\n\t\t\tclearTimeout(this.#handle)\n\t\t\tthis.#handle = undefined\n\t\t}\n\t\tthis.#detach()\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t}\n\n\t#detach(): void {\n\t\tif (this.#linked) {\n\t\t\tthis.#parent?.removeEventListener('abort', this.#listener)\n\t\t\tthis.#linked = false\n\t\t}\n\t}\n}\n","import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { Timeout } from './Timeout.js'\n\n/**\n * Create a controllable deadline whose native signal aborts on expiry.\n *\n * @remarks\n * `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.\n * A parent `options.signal` clears an armed timeout without aborting the\n * timeout's own signal. Omitted `options.id` values generate a random UUID.\n * Malformed or unreadable options use contract code `bound`; invalid `id`,\n * `ms`, and `signal` values use `literal`, `range`, and `placement`.\n *\n * @param options - Validated deadline, optional trace label, and optional native parent signal\n * @returns A reusable timeout handle\n * @throws {@link import('@orkestrel/contract').ContractError} When the\n * JavaScript input does not satisfy `TimeoutOptions`\n *\n * @example\n * ```ts\n * import { createTimeout } from '@orkestrel/timeout'\n *\n * const timeout = createTimeout({ ms: 5_000 })\n * timeout.start()\n * try {\n * \tawait fetch('/work', { signal: timeout.signal })\n * } finally {\n * \ttimeout.clear()\n * }\n * ```\n */\nexport function createTimeout(options: TimeoutOptions): TimeoutInterface {\n\treturn new Timeout(options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/Timeout.ts","../../../src/core/factories.ts"],"sourcesContent":["/**\n * Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds.\n *\n * @example\n * ```ts\n * createTimeout({ ms: MAX_TIMEOUT_MS })\n * ```\n */\nexport const MAX_TIMEOUT_MS = 2_147_483_647\n","import { isInteger } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\n\n/**\n * Determines whether a value is an integer in the inclusive range from `0` through\n * `MAX_TIMEOUT_MS`, staying total for every input.\n *\n * @param value - Unknown duration candidate\n * @returns True if the value is an integer in the inclusive timeout range; false otherwise\n *\n * @example\n * ```ts\n * isTimeoutDuration(0) // true\n * isTimeoutDuration(1.5) // false\n * ```\n */\nexport function isTimeoutDuration(value: unknown): value is number {\n\treturn isInteger(value) && value >= 0 && value <= MAX_TIMEOUT_MS\n}\n\n/**\n * Determines whether a value is a genuine native `AbortSignal`, staying total for a\n * structural spoof and for a hostile or revoked proxy.\n *\n * @remarks\n * The intrinsic `aborted` getter performs the native brand check, and the boundary\n * returns `false` rather than letting a receiver's error escape.\n *\n * @param value - Unknown signal candidate\n * @returns True if the native `AbortSignal` getter accepts the value; false otherwise\n *\n * @example\n * ```ts\n * isTimeoutSignal(new AbortController().signal) // true\n * isTimeoutSignal({ aborted: false }) // false\n * ```\n */\nexport function isTimeoutSignal(value: unknown): value is AbortSignal {\n\ttry {\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted')\n\t\tif (descriptor?.get === undefined) return false\n\t\treturn typeof Reflect.apply(descriptor.get, value, []) === 'boolean'\n\t} catch {\n\t\treturn false\n\t}\n}\n","import type { TimeoutOptions } from './types.js'\nimport { ContractError, isRecord, isString, preview } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\nimport { isTimeoutDuration, isTimeoutSignal } from './validators.js'\n\n/**\n * Validates once-read timeout construction options and returns a fresh normalized copy\n * omitting absent optional keys.\n *\n * @remarks\n * Each property is read before any validation runs, and no timer, controller, or\n * listener lifecycle begins at this boundary.\n *\n * @param options - Potentially untrusted timeout options\n * @returns A fresh validated `TimeoutOptions` object\n * @throws {@link import('@orkestrel/contract').ContractError} Thrown when the\n * input does not satisfy `TimeoutOptions`\n *\n * @example\n * ```ts\n * const options = validateTimeoutOptions({ ms: 5_000 })\n * ```\n */\nexport function validateTimeoutOptions(options: TimeoutOptions): TimeoutOptions {\n\tif (!isRecord(options)) {\n\t\tthrow new ContractError('Timeout: options must be a plain record', {\n\t\t\tcode: 'bound',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options'],\n\t\t\t\tlimit: 'plain record',\n\t\t\t\treceived: preview(options),\n\t\t\t},\n\t\t})\n\t}\n\n\tlet id: TimeoutOptions['id']\n\tlet ms: TimeoutOptions['ms']\n\tlet signal: TimeoutOptions['signal']\n\ttry {\n\t\tid = options.id\n\t\tms = options.ms\n\t\tsignal = options.signal\n\t} catch (cause) {\n\t\tthrow new ContractError('Timeout: options could not be read', {\n\t\t\tcode: 'bound',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options'],\n\t\t\t\tlimit: 'readable plain record',\n\t\t\t\treceived: preview(options),\n\t\t\t},\n\t\t\tcause,\n\t\t})\n\t}\n\n\tif (id !== undefined && !isString(id)) {\n\t\tthrow new ContractError('Timeout: id must be a string when defined', {\n\t\t\tcode: 'literal',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'id'],\n\t\t\t\tlimit: 'string or undefined',\n\t\t\t\treceived: preview(id),\n\t\t\t},\n\t\t})\n\t}\n\tif (!isTimeoutDuration(ms)) {\n\t\tthrow new ContractError('Timeout: ms must be an integer in the supported range', {\n\t\t\tcode: 'range',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'ms'],\n\t\t\t\tlimit: `integer in [0, ${MAX_TIMEOUT_MS}]`,\n\t\t\t\treceived: preview(ms),\n\t\t\t},\n\t\t})\n\t}\n\tif (signal !== undefined && !isTimeoutSignal(signal)) {\n\t\tthrow new ContractError('Timeout: signal must be a native AbortSignal when defined', {\n\t\t\tcode: 'placement',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'signal'],\n\t\t\t\tlimit: 'native AbortSignal or undefined',\n\t\t\t\treceived: preview(signal),\n\t\t\t},\n\t\t})\n\t}\n\n\tif (id !== undefined && signal !== undefined) return { id, ms, signal }\n\tif (id !== undefined) return { id, ms }\n\tif (signal !== undefined) return { ms, signal }\n\treturn { ms }\n}\n","import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { validateTimeoutOptions } from './helpers.js'\n\n/**\n * Implements `TimeoutInterface` exactly, as a controllable `setTimeout` wrapper over one\n * owned `AbortController` whose signal aborts when the deadline expires.\n *\n * @remarks\n * `start()` arms or replaces the current deadline. Expiry sets `expired` and\n * aborts the current signal once. `clear()` cancels without aborting and resets\n * `expired`. Starting or clearing after expiry installs a fresh signal. A parent\n * abort deliberately clears the timeout without aborting its signal; once the\n * parent has aborted, later starts are inert.\n *\n * Construction validates the complete JavaScript boundary before creating an\n * `AbortController`. A malformed or unreadable options record throws a\n * `bound`-coded {@link import('@orkestrel/contract').ContractError}; an invalid\n * `id`, `ms`, or `signal` uses `literal`, `range`, or `placement`, respectively.\n *\n * @example\n * ```ts\n * import { Timeout } from '@orkestrel/timeout'\n *\n * const timeout = new Timeout({ ms: 5_000 })\n * timeout.start()\n * timeout.signal.addEventListener('abort', () => giveUp(), { once: true })\n * timeout.clear()\n * ```\n */\nexport class Timeout implements TimeoutInterface {\n\treadonly #id: string\n\treadonly #ms: number\n\treadonly #parent: AbortSignal | undefined\n\treadonly #listener: () => void\n\t#controller: AbortController\n\t#handle: ReturnType<typeof setTimeout> | undefined\n\n\tconstructor(options: TimeoutOptions) {\n\t\tconst input = validateTimeoutOptions(options)\n\t\tthis.#id = input.id === undefined ? crypto.randomUUID() : input.id\n\t\tthis.#ms = input.ms\n\t\tthis.#parent = input.signal\n\t\tthis.#controller = new AbortController()\n\t\tthis.#listener = this.clear.bind(this)\n\t}\n\n\tget id(): string {\n\t\treturn this.#id\n\t}\n\n\tget ms(): number {\n\t\treturn this.#ms\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.#controller.signal\n\t}\n\n\tget expired(): boolean {\n\t\treturn this.#controller.signal.aborted\n\t}\n\n\tstart(): void {\n\t\tif (this.#parent?.aborted === true) return\n\t\tif (this.#handle !== undefined) {\n\t\t\tclearTimeout(this.#handle)\n\t\t\tthis.#handle = undefined\n\t\t}\n\t\tthis.#detach()\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t\tif (this.#parent !== undefined) {\n\t\t\tthis.#parent.addEventListener('abort', this.#listener, { once: true })\n\t\t}\n\t\tthis.#handle = setTimeout(() => {\n\t\t\tthis.#handle = undefined\n\t\t\tthis.#detach()\n\t\t\tthis.#controller.abort()\n\t\t}, this.#ms)\n\t}\n\n\tclear(): void {\n\t\tif (this.#handle !== undefined) {\n\t\t\tclearTimeout(this.#handle)\n\t\t\tthis.#handle = undefined\n\t\t}\n\t\tthis.#detach()\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t}\n\n\t#detach(): void {\n\t\tthis.#parent?.removeEventListener('abort', this.#listener)\n\t}\n}\n","import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { Timeout } from './Timeout.js'\n\n/**\n * Creates a deadline handle from validated `TimeoutOptions` and returns it as a\n * `TimeoutInterface`.\n *\n * @remarks\n * `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.\n * A parent `options.signal` clears an armed timeout without aborting the\n * timeout's own signal. `start()` returns without arming when the parent\n * `signal` supplied at construction has already aborted, so a handle whose\n * parent aborted never expires again and its `signal` never fires. Omitted\n * `options.id` values generate a random UUID. Malformed or unreadable options\n * use contract code `bound`; invalid `id`, `ms`, and `signal` values use\n * `literal`, `range`, and `placement`.\n *\n * @param options - Validated deadline, optional trace label, and optional native parent signal\n * @returns A reusable timeout handle\n * @throws {@link import('@orkestrel/contract').ContractError} Thrown when the\n * JavaScript input does not satisfy `TimeoutOptions`\n *\n * @example Race work against a deadline\n * ```ts\n * import { createTimeout } from '@orkestrel/timeout'\n *\n * async function fetchWithDeadline(url: string, ms: number): Promise<Response> {\n * \tconst timeout = createTimeout({ ms })\n * \ttimeout.start()\n *\n * \ttry {\n * \t\treturn await fetch(url, { signal: timeout.signal })\n * \t} finally {\n * \t\ttimeout.clear() // cancels the still-armed deadline when the fetch won the race\n * \t}\n * }\n * ```\n */\nexport function createTimeout(options: TimeoutOptions): TimeoutInterface {\n\treturn new Timeout(options)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/timeout",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "A typed, controllable setTimeout wrapper — an AbortSignal deadline with start/clear lifecycle and parent-signal linking. Part of the @orkestrel line.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abort-signal",
|
|
@@ -46,40 +46,43 @@
|
|
|
46
46
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
47
47
|
"copy": "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
|
|
48
48
|
"scaffold": "scaffold",
|
|
49
|
-
"lint": "oxlint --config .oxlintrc.json --fix
|
|
49
|
+
"lint": "oxlint --config .oxlintrc.json --fix .",
|
|
50
50
|
"check": "tsc --noEmit --project tsconfig.json && npm run check:src",
|
|
51
51
|
"check:src": "npm run check:src:core",
|
|
52
52
|
"check:src:core": "tsc --noEmit -p configs/src/tsconfig.core.json",
|
|
53
53
|
"format": "oxfmt --config .oxfmtrc.json --write .",
|
|
54
54
|
"format:check": "oxfmt --config .oxfmtrc.json --check .",
|
|
55
55
|
"lint:check": "oxlint --config .oxlintrc.json --deny-warnings .",
|
|
56
|
-
"test": "npm run test:src && npm run test:policy && npm run test:config && npm run test:guides",
|
|
56
|
+
"test": "npm run test:src && npm run test:policy && npm run test:config && npm run test:setup && npm run test:guides",
|
|
57
57
|
"test:src": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
|
|
58
58
|
"test:src:core": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
|
|
59
59
|
"test:policy": "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy",
|
|
60
60
|
"test:config": "vitest run --config vite.config.ts --no-cache --reporter=dot --project config",
|
|
61
|
-
"test:guides": "
|
|
61
|
+
"test:guides": "node --experimental-strip-types tests/guides.test.ts",
|
|
62
62
|
"build": "npm run clean && npm run build:src",
|
|
63
63
|
"build:src": "npm run build:src:core",
|
|
64
64
|
"build:src:core": "vite build --config configs/src/vite.core.config.ts && npm run copy dist/src/core/index.d.ts dist/src/core/index.d.cts",
|
|
65
|
-
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
|
|
65
|
+
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test && npm run test:distribution -- --mode release",
|
|
66
|
+
"test:distribution": "vitest run --config vite.config.ts --no-cache --reporter=dot --project distribution",
|
|
67
|
+
"test:probe": "vitest run --config vite.config.ts --no-cache --reporter=verbose --project probe",
|
|
68
|
+
"test:bench": "vitest bench --config vite.config.ts --no-cache --project probe",
|
|
69
|
+
"prepack": "npm run build",
|
|
70
|
+
"test:setup": "vitest run --config vite.config.ts --no-cache --reporter=dot --project setup"
|
|
66
71
|
},
|
|
67
72
|
"dependencies": {
|
|
68
|
-
"@orkestrel/contract": "^0.0.
|
|
73
|
+
"@orkestrel/contract": "^0.0.17"
|
|
69
74
|
},
|
|
70
75
|
"devDependencies": {
|
|
71
76
|
"@microsoft/api-extractor": "^7.59.0",
|
|
72
|
-
"@orkestrel/guide": "^0.0.
|
|
73
|
-
"@orkestrel/probe": "^0.0.
|
|
74
|
-
"@orkestrel/scaffold": "^0.0.
|
|
75
|
-
"@orkestrel/test": "^0.0.
|
|
76
|
-
"@types/node": "^26.
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"oxlint": "^1.79.0",
|
|
77
|
+
"@orkestrel/guide": "^0.0.17",
|
|
78
|
+
"@orkestrel/probe": "^0.0.12",
|
|
79
|
+
"@orkestrel/scaffold": "^0.0.63",
|
|
80
|
+
"@orkestrel/test": "^0.0.14",
|
|
81
|
+
"@types/node": "^26.4.1",
|
|
82
|
+
"oxfmt": "^0.66.0",
|
|
83
|
+
"oxlint": "^1.81.0",
|
|
80
84
|
"typescript": "^6.0.3",
|
|
81
85
|
"vite": "^8.2.2",
|
|
82
|
-
"vite-plugin-dts": "^5.0.3",
|
|
83
86
|
"vitest": "^4.1.11"
|
|
84
87
|
},
|
|
85
88
|
"engines": {
|