@orkestrel/timeout 0.0.9 → 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 CHANGED
@@ -1,13 +1,14 @@
1
1
  # @orkestrel/timeout
2
2
 
3
- A typed, **controllable** `setTimeout` wrapper — a deadline handle that
4
- exposes an `AbortSignal` which fires on expiry, for racing against work.
5
- Deliberately small: `start()` arms the deadline, `clear()` cancels it without
6
- firing, and calling `start()` again after expiry reuses the handle for a
7
- fresh deadline without re-construction. An optional parent `signal` links in
8
- without inheriting `AbortSignal.any` semantics — a parent abort during the
9
- timing window _clears_ the timeout (it never expires) rather than firing it.
10
- Part of the `@orkestrel` line.
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
 
@@ -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
- * Names the largest timeout duration accepted by the package, in milliseconds.
5
+ * Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds.
6
6
  *
7
7
  * @example
8
8
  * ```ts
@@ -13,7 +13,8 @@ var MAX_TIMEOUT_MS = 2147483647;
13
13
  //#endregion
14
14
  //#region src/core/validators.ts
15
15
  /**
16
- * Determines whether a value is an accepted timeout duration.
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
20
  * @returns True if the value is an integer in the inclusive timeout range; false otherwise
@@ -28,12 +29,12 @@ function isTimeoutDuration(value) {
28
29
  return (0, _orkestrel_contract.isInteger)(value) && value >= 0 && value <= 2147483647;
29
30
  }
30
31
  /**
31
- * Determines whether a value is a genuine native `AbortSignal`.
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. The boundary
35
- * contains revoked proxies and hostile receivers, returning `false` instead of
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
40
  * @returns True if the native `AbortSignal` getter accepts the value; false otherwise
@@ -56,11 +57,11 @@ function isTimeoutSignal(value) {
56
57
  //#endregion
57
58
  //#region src/core/helpers.ts
58
59
  /**
59
- * Validates and normalizes timeout construction options.
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 exactly once before validation. The returned object is
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
@@ -142,7 +143,8 @@ function validateTimeoutOptions(options) {
142
143
  //#endregion
143
144
  //#region src/core/Timeout.ts
144
145
  /**
145
- * Represents a controllable deadline whose native `AbortSignal` aborts when it expires.
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
@@ -223,7 +225,8 @@ var Timeout = class {
223
225
  //#endregion
224
226
  //#region src/core/factories.ts
225
227
  /**
226
- * Creates a controllable deadline whose native signal aborts on expiry.
228
+ * Creates a deadline handle from validated `TimeoutOptions` and returns it as a
229
+ * `TimeoutInterface`.
227
230
  *
228
231
  * @remarks
229
232
  * `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.
@@ -240,16 +243,19 @@ var Timeout = class {
240
243
  * @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
241
244
  * JavaScript input does not satisfy `TimeoutOptions`
242
245
  *
243
- * @example
246
+ * @example Race work against a deadline
244
247
  * ```ts
245
248
  * import { createTimeout } from '@orkestrel/timeout'
246
249
  *
247
- * const timeout = createTimeout({ ms: 5_000 })
248
- * timeout.start()
249
- * try {
250
- * await fetch('/work', { signal: timeout.signal })
251
- * } finally {
252
- * timeout.clear()
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
+ * }
253
259
  * }
254
260
  * ```
255
261
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["#id","#ms","#parent","#listener","#controller","#handle","#detach"],"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 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 { isInteger } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\n\n/**\n * Determines whether a value is an accepted timeout duration.\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`.\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 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 and normalizes 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} 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 * Represents 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\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 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. `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\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,5 +1,6 @@
1
1
  /**
2
- * Creates a controllable deadline whose native signal aborts on expiry.
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.
@@ -16,23 +17,27 @@
16
17
  * @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
17
18
  * JavaScript input does not satisfy `TimeoutOptions`
18
19
  *
19
- * @example
20
+ * @example Race work against a deadline
20
21
  * ```ts
21
22
  * import { createTimeout } from '@orkestrel/timeout'
22
23
  *
23
- * const timeout = createTimeout({ ms: 5_000 })
24
- * timeout.start()
25
- * try {
26
- * await fetch('/work', { signal: timeout.signal })
27
- * } finally {
28
- * timeout.clear()
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
+ * }
29
33
  * }
30
34
  * ```
31
35
  */
32
36
  export declare function createTimeout(options: TimeoutOptions): TimeoutInterface;
33
37
 
34
38
  /**
35
- * Determines whether a value is an accepted timeout duration.
39
+ * Determines whether a value is an integer in the inclusive range from `0` through
40
+ * `MAX_TIMEOUT_MS`, staying total for every input.
36
41
  *
37
42
  * @param value - Unknown duration candidate
38
43
  * @returns True if the value is an integer in the inclusive timeout range; false otherwise
@@ -46,12 +51,12 @@
46
51
  export declare function isTimeoutDuration(value: unknown): value is number;
47
52
 
48
53
  /**
49
- * Determines whether a value is a genuine native `AbortSignal`.
54
+ * Determines whether a value is a genuine native `AbortSignal`, staying total for a
55
+ * structural spoof and for a hostile or revoked proxy.
50
56
  *
51
57
  * @remarks
52
- * The intrinsic `aborted` getter performs the native brand check. The boundary
53
- * contains revoked proxies and hostile receivers, returning `false` instead of
54
- * 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.
55
60
  *
56
61
  * @param value - Unknown signal candidate
57
62
  * @returns True if the native `AbortSignal` getter accepts the value; false otherwise
@@ -65,7 +70,7 @@
65
70
  export declare function isTimeoutSignal(value: unknown): value is AbortSignal;
66
71
 
67
72
  /**
68
- * Names the largest timeout duration accepted by the package, in milliseconds.
73
+ * Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds.
69
74
  *
70
75
  * @example
71
76
  * ```ts
@@ -75,7 +80,8 @@
75
80
  export declare const MAX_TIMEOUT_MS = 2147483647;
76
81
 
77
82
  /**
78
- * Represents a controllable deadline whose native `AbortSignal` aborts when it expires.
83
+ * Implements `TimeoutInterface` exactly, as a controllable `setTimeout` wrapper over one
84
+ * owned `AbortController` whose signal aborts when the deadline expires.
79
85
  *
80
86
  * @remarks
81
87
  * `start()` arms or replaces the current deadline. Expiry sets `expired` and
@@ -134,7 +140,8 @@
134
140
  /** Reports whether the owned signal has aborted, derived directly from that signal. */
135
141
  readonly expired: boolean;
136
142
  /**
137
- * Arms or re-arms the deadline.
143
+ * Arms or re-arms the deadline for `ms`, installing a fresh `signal` when the
144
+ * current one has already aborted.
138
145
  *
139
146
  * @remarks
140
147
  * Returns without arming when the parent `signal` supplied at construction
@@ -145,11 +152,11 @@
145
152
  */
146
153
  start(): void;
147
154
  /**
148
- * Cancels an armed deadline without aborting its signal and resets expiry state.
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.
149
157
  *
150
158
  * @remarks
151
- * Installs a fresh non-aborted signal when the current signal has already
152
- * aborted; a signal that never fired keeps its identity.
159
+ * A signal that never fired keeps its identity.
153
160
  *
154
161
  * @returns Nothing
155
162
  */
@@ -157,7 +164,7 @@
157
164
  }
158
165
 
159
166
  /**
160
- * Represents the options for constructing a timeout deadline.
167
+ * Represents the options `createTimeout` and the `Timeout` constructor accept.
161
168
  *
162
169
  * @remarks
163
170
  * `ms` is an integer from `0` through `2_147_483_647`, inclusive. `id`
@@ -180,11 +187,11 @@
180
187
  }
181
188
 
182
189
  /**
183
- * Validates and normalizes timeout construction options.
190
+ * Validates once-read timeout construction options and returns a fresh normalized copy
191
+ * omitting absent optional keys.
184
192
  *
185
193
  * @remarks
186
- * Each property is read exactly once before validation. The returned object is
187
- * 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
188
195
  * listener lifecycle begins at this boundary.
189
196
  *
190
197
  * @param options - Potentially untrusted timeout options
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Creates a controllable deadline whose native signal aborts on expiry.
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.
@@ -16,23 +17,27 @@
16
17
  * @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
17
18
  * JavaScript input does not satisfy `TimeoutOptions`
18
19
  *
19
- * @example
20
+ * @example Race work against a deadline
20
21
  * ```ts
21
22
  * import { createTimeout } from '@orkestrel/timeout'
22
23
  *
23
- * const timeout = createTimeout({ ms: 5_000 })
24
- * timeout.start()
25
- * try {
26
- * await fetch('/work', { signal: timeout.signal })
27
- * } finally {
28
- * timeout.clear()
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
+ * }
29
33
  * }
30
34
  * ```
31
35
  */
32
36
  export declare function createTimeout(options: TimeoutOptions): TimeoutInterface;
33
37
 
34
38
  /**
35
- * Determines whether a value is an accepted timeout duration.
39
+ * Determines whether a value is an integer in the inclusive range from `0` through
40
+ * `MAX_TIMEOUT_MS`, staying total for every input.
36
41
  *
37
42
  * @param value - Unknown duration candidate
38
43
  * @returns True if the value is an integer in the inclusive timeout range; false otherwise
@@ -46,12 +51,12 @@
46
51
  export declare function isTimeoutDuration(value: unknown): value is number;
47
52
 
48
53
  /**
49
- * Determines whether a value is a genuine native `AbortSignal`.
54
+ * Determines whether a value is a genuine native `AbortSignal`, staying total for a
55
+ * structural spoof and for a hostile or revoked proxy.
50
56
  *
51
57
  * @remarks
52
- * The intrinsic `aborted` getter performs the native brand check. The boundary
53
- * contains revoked proxies and hostile receivers, returning `false` instead of
54
- * 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.
55
60
  *
56
61
  * @param value - Unknown signal candidate
57
62
  * @returns True if the native `AbortSignal` getter accepts the value; false otherwise
@@ -65,7 +70,7 @@
65
70
  export declare function isTimeoutSignal(value: unknown): value is AbortSignal;
66
71
 
67
72
  /**
68
- * Names the largest timeout duration accepted by the package, in milliseconds.
73
+ * Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds.
69
74
  *
70
75
  * @example
71
76
  * ```ts
@@ -75,7 +80,8 @@
75
80
  export declare const MAX_TIMEOUT_MS = 2147483647;
76
81
 
77
82
  /**
78
- * Represents a controllable deadline whose native `AbortSignal` aborts when it expires.
83
+ * Implements `TimeoutInterface` exactly, as a controllable `setTimeout` wrapper over one
84
+ * owned `AbortController` whose signal aborts when the deadline expires.
79
85
  *
80
86
  * @remarks
81
87
  * `start()` arms or replaces the current deadline. Expiry sets `expired` and
@@ -134,7 +140,8 @@
134
140
  /** Reports whether the owned signal has aborted, derived directly from that signal. */
135
141
  readonly expired: boolean;
136
142
  /**
137
- * Arms or re-arms the deadline.
143
+ * Arms or re-arms the deadline for `ms`, installing a fresh `signal` when the
144
+ * current one has already aborted.
138
145
  *
139
146
  * @remarks
140
147
  * Returns without arming when the parent `signal` supplied at construction
@@ -145,11 +152,11 @@
145
152
  */
146
153
  start(): void;
147
154
  /**
148
- * Cancels an armed deadline without aborting its signal and resets expiry state.
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.
149
157
  *
150
158
  * @remarks
151
- * Installs a fresh non-aborted signal when the current signal has already
152
- * aborted; a signal that never fired keeps its identity.
159
+ * A signal that never fired keeps its identity.
153
160
  *
154
161
  * @returns Nothing
155
162
  */
@@ -157,7 +164,7 @@
157
164
  }
158
165
 
159
166
  /**
160
- * Represents the options for constructing a timeout deadline.
167
+ * Represents the options `createTimeout` and the `Timeout` constructor accept.
161
168
  *
162
169
  * @remarks
163
170
  * `ms` is an integer from `0` through `2_147_483_647`, inclusive. `id`
@@ -180,11 +187,11 @@
180
187
  }
181
188
 
182
189
  /**
183
- * Validates and normalizes timeout construction options.
190
+ * Validates once-read timeout construction options and returns a fresh normalized copy
191
+ * omitting absent optional keys.
184
192
  *
185
193
  * @remarks
186
- * Each property is read exactly once before validation. The returned object is
187
- * 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
188
195
  * listener lifecycle begins at this boundary.
189
196
  *
190
197
  * @param options - Potentially untrusted timeout options
@@ -1,7 +1,7 @@
1
1
  import { ContractError, isInteger, isRecord, isString, preview } from "@orkestrel/contract";
2
2
  //#region src/core/constants.ts
3
3
  /**
4
- * Names the largest timeout duration accepted by the package, in milliseconds.
4
+ * Names the largest timeout duration the package accepts, `2_147_483_647` milliseconds.
5
5
  *
6
6
  * @example
7
7
  * ```ts
@@ -12,7 +12,8 @@ var MAX_TIMEOUT_MS = 2147483647;
12
12
  //#endregion
13
13
  //#region src/core/validators.ts
14
14
  /**
15
- * Determines whether a value is an accepted timeout duration.
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
19
  * @returns True if the value is an integer in the inclusive timeout range; false otherwise
@@ -27,12 +28,12 @@ function isTimeoutDuration(value) {
27
28
  return isInteger(value) && value >= 0 && value <= 2147483647;
28
29
  }
29
30
  /**
30
- * Determines whether a value is a genuine native `AbortSignal`.
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. The boundary
34
- * contains revoked proxies and hostile receivers, returning `false` instead of
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
39
  * @returns True if the native `AbortSignal` getter accepts the value; false otherwise
@@ -55,11 +56,11 @@ function isTimeoutSignal(value) {
55
56
  //#endregion
56
57
  //#region src/core/helpers.ts
57
58
  /**
58
- * Validates and normalizes timeout construction options.
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 exactly once before validation. The returned object is
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
@@ -141,7 +142,8 @@ function validateTimeoutOptions(options) {
141
142
  //#endregion
142
143
  //#region src/core/Timeout.ts
143
144
  /**
144
- * Represents a controllable deadline whose native `AbortSignal` aborts when it expires.
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
@@ -222,7 +224,8 @@ var Timeout = class {
222
224
  //#endregion
223
225
  //#region src/core/factories.ts
224
226
  /**
225
- * Creates a controllable deadline whose native signal aborts on expiry.
227
+ * Creates a deadline handle from validated `TimeoutOptions` and returns it as a
228
+ * `TimeoutInterface`.
226
229
  *
227
230
  * @remarks
228
231
  * `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.
@@ -239,16 +242,19 @@ var Timeout = class {
239
242
  * @throws {@link import('@orkestrel/contract').ContractError} Thrown when the
240
243
  * JavaScript input does not satisfy `TimeoutOptions`
241
244
  *
242
- * @example
245
+ * @example Race work against a deadline
243
246
  * ```ts
244
247
  * import { createTimeout } from '@orkestrel/timeout'
245
248
  *
246
- * const timeout = createTimeout({ ms: 5_000 })
247
- * timeout.start()
248
- * try {
249
- * await fetch('/work', { signal: timeout.signal })
250
- * } finally {
251
- * timeout.clear()
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
+ * }
252
258
  * }
253
259
  * ```
254
260
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#id","#ms","#parent","#listener","#controller","#handle","#detach"],"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 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 { isInteger } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\n\n/**\n * Determines whether a value is an accepted timeout duration.\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`.\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 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 and normalizes 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} 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 * Represents 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\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 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. `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\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.9",
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",
@@ -58,7 +58,7 @@
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": "vitest run --config vite.config.ts --no-cache --reporter=dot --project 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",
@@ -70,20 +70,19 @@
70
70
  "test:setup": "vitest run --config vite.config.ts --no-cache --reporter=dot --project setup"
71
71
  },
72
72
  "dependencies": {
73
- "@orkestrel/contract": "^0.0.16"
73
+ "@orkestrel/contract": "^0.0.17"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@microsoft/api-extractor": "^7.59.0",
77
- "@orkestrel/guide": "^0.0.16",
78
- "@orkestrel/probe": "^0.0.11",
79
- "@orkestrel/scaffold": "^0.0.61",
80
- "@orkestrel/test": "^0.0.13",
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
81
  "@types/node": "^26.4.1",
82
82
  "oxfmt": "^0.66.0",
83
83
  "oxlint": "^1.81.0",
84
84
  "typescript": "^6.0.3",
85
85
  "vite": "^8.2.2",
86
- "vite-plugin-dts": "^5.1.0",
87
86
  "vitest": "^4.1.11"
88
87
  },
89
88
  "engines": {