@orkestrel/timeout 0.0.4 → 0.0.6

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
@@ -39,15 +39,24 @@ timeout.clear() // work finished first — cancel the deadline
39
39
  ```
40
40
 
41
41
  `createTimeout(options)` (or `new Timeout(options)`) returns a
42
- `TimeoutInterface`. `options.ms` is the deadline in milliseconds (a
43
- non-negative finite number — the host `setTimeout` clamps a negative, `NaN`,
44
- or over-2^31-1 value rather than throwing); an optional `options.id` labels the
45
- handle for tracing (defaults to a random UUID); an optional `options.signal`
46
- links a parent `AbortSignal` whose abort clears the timeout while a timer is
47
- armed. `start()` arms the deadline: on expiry `expired` flips `true` and
48
- `signal` fires. `clear()` cancels a pending expiry without firing `signal` and
49
- resets `expired` back to `false`. The handle is a pure functional primitive —
50
- no `Emitter`, no events.
42
+ `TimeoutInterface`. `options.ms` must be an integer from `0` through
43
+ `MAX_TIMEOUT_MS` (`2_147_483_647`), inclusive; invalid values throw a coded
44
+ `ContractError` before any timer lifecycle begins. Zero is intentionally a
45
+ next-turn deadline. An optional string `options.id` labels the handle for
46
+ tracing and defaults to a random UUID. An optional `options.signal` must be a
47
+ genuine native `AbortSignal`; its abort clears an armed timeout without
48
+ expiring it, aborting the timeout's own signal, or forwarding its reason.
49
+ `start()` arms or replaces the deadline. On expiry `expired` flips `true` and
50
+ the native `signal` aborts once. `clear()` cancels without aborting, resets
51
+ `expired`, and remains safe to call while idle. `isTimeoutDuration` and
52
+ `isTimeoutSignal` provide total validators for dynamic inputs.
53
+
54
+ Malformed or unreadable options throw with contract code `bound`. Invalid
55
+ defined `id`, `ms`, and `signal` values use `literal`, `range`, and `placement`,
56
+ respectively, with safe path, limit, and received-value context. `expired`
57
+ derives directly from the owned signal's `aborted` state.
58
+ `validateTimeoutOptions` exposes the same strict construction boundary as a
59
+ once-read helper that returns a fresh copy without absent optional keys.
51
60
 
52
61
  ## Guide
53
62
 
@@ -1,33 +1,169 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _orkestrel_contract = require("@orkestrel/contract");
3
+ //#region src/core/constants.ts
4
+ /**
5
+ * Largest timeout duration accepted by the package, in milliseconds.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * createTimeout({ ms: MAX_TIMEOUT_MS })
10
+ * ```
11
+ */
12
+ var MAX_TIMEOUT_MS = 2147483647;
13
+ //#endregion
14
+ //#region src/core/validators.ts
15
+ /**
16
+ * Determine whether a value is an accepted timeout duration.
17
+ *
18
+ * @param value - Unknown duration candidate
19
+ * @returns `true` only for an integer in the inclusive timeout range
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * isTimeoutDuration(0) // true
24
+ * isTimeoutDuration(1.5) // false
25
+ * ```
26
+ */
27
+ function isTimeoutDuration(value) {
28
+ return (0, _orkestrel_contract.isFiniteNumber)(value) && (0, _orkestrel_contract.isInteger)(value) && value >= 0 && value <= 2147483647;
29
+ }
30
+ /**
31
+ * Determine whether a value is a genuine native `AbortSignal`.
32
+ *
33
+ * @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.
37
+ *
38
+ * @param value - Unknown signal candidate
39
+ * @returns `true` only when the native `AbortSignal` getter accepts the value
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * isTimeoutSignal(new AbortController().signal) // true
44
+ * isTimeoutSignal({ aborted: false }) // false
45
+ * ```
46
+ */
47
+ function isTimeoutSignal(value) {
48
+ try {
49
+ const descriptor = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted");
50
+ if (descriptor?.get === void 0) return false;
51
+ return typeof Reflect.apply(descriptor.get, value, []) === "boolean";
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+ //#endregion
57
+ //#region src/core/helpers.ts
58
+ /**
59
+ * Validate and normalize timeout construction options.
60
+ *
61
+ * @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
+ * listener lifecycle begins at this boundary.
65
+ *
66
+ * @param options - Potentially untrusted timeout options
67
+ * @returns A fresh validated `TimeoutOptions` object
68
+ * @throws {@link import('@orkestrel/contract').ContractError} When the input
69
+ * does not satisfy `TimeoutOptions`
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const options = validateTimeoutOptions({ ms: 5_000 })
74
+ * ```
75
+ */
76
+ function validateTimeoutOptions(options) {
77
+ if (!(0, _orkestrel_contract.isRecord)(options)) throw new _orkestrel_contract.ContractError("Timeout: options must be a plain record", {
78
+ code: "bound",
79
+ context: {
80
+ path: ["options"],
81
+ limit: "plain record",
82
+ received: (0, _orkestrel_contract.preview)(options)
83
+ }
84
+ });
85
+ let id;
86
+ let ms;
87
+ let signal;
88
+ try {
89
+ id = options.id;
90
+ ms = options.ms;
91
+ signal = options.signal;
92
+ } catch (cause) {
93
+ throw new _orkestrel_contract.ContractError("Timeout: options could not be read", {
94
+ code: "bound",
95
+ context: {
96
+ path: ["options"],
97
+ limit: "readable plain record",
98
+ received: (0, _orkestrel_contract.preview)(options)
99
+ },
100
+ cause
101
+ });
102
+ }
103
+ if (id !== void 0 && !(0, _orkestrel_contract.isString)(id)) throw new _orkestrel_contract.ContractError("Timeout: id must be a string when defined", {
104
+ code: "literal",
105
+ context: {
106
+ path: ["options", "id"],
107
+ limit: "string or undefined",
108
+ received: (0, _orkestrel_contract.preview)(id)
109
+ }
110
+ });
111
+ if (!isTimeoutDuration(ms)) throw new _orkestrel_contract.ContractError("Timeout: ms must be an integer in the supported range", {
112
+ code: "range",
113
+ context: {
114
+ path: ["options", "ms"],
115
+ limit: `integer in [0, ${MAX_TIMEOUT_MS}]`,
116
+ received: (0, _orkestrel_contract.preview)(ms)
117
+ }
118
+ });
119
+ if (signal !== void 0 && !isTimeoutSignal(signal)) throw new _orkestrel_contract.ContractError("Timeout: signal must be a native AbortSignal when defined", {
120
+ code: "placement",
121
+ context: {
122
+ path: ["options", "signal"],
123
+ limit: "native AbortSignal or undefined",
124
+ received: (0, _orkestrel_contract.preview)(signal)
125
+ }
126
+ });
127
+ if (id !== void 0 && signal !== void 0) return {
128
+ id,
129
+ ms,
130
+ signal
131
+ };
132
+ if (id !== void 0) return {
133
+ id,
134
+ ms
135
+ };
136
+ if (signal !== void 0) return {
137
+ ms,
138
+ signal
139
+ };
140
+ return { ms };
141
+ }
142
+ //#endregion
3
143
  //#region src/core/Timeout.ts
4
144
  /**
5
- * A deadline handle — a controllable `setTimeout` wrapper that exposes an
6
- * `AbortSignal` which fires when the timeout expires, for racing against work.
145
+ * A controllable deadline whose native `AbortSignal` aborts when it expires.
7
146
  *
8
147
  * @remarks
9
- * - **Deadline signal.** `start()` arms a `setTimeout` for `ms`; on expiry it
10
- * flips `expired` and aborts the controller, firing `signal`. Race `signal`
11
- * against work to bound how long it may run.
12
- * - **Controllable.** `clear()` cancels a pending expiry WITHOUT firing `signal`,
13
- * and resets `expired` back to `false` (the §10 reset) — a cleared timeout
14
- * reports `expired === false` and a non-aborted `signal`.
15
- * - **Reusable.** Re-`start()` after an expiry swaps in a fresh controller and
16
- * resets `expired`, so `signal` / `expired` reflect the new run — the handle is
17
- * reusable across deadlines without re-construction.
18
- * - **Parent linking.** A parent `signal` does NOT expire this timeout — it
19
- * CLEARS it: the parent listener is attached only while a timer is armed (added
20
- * on `start()`, removed on expiry or `clear()`), so a parent abort during the
21
- * timing window cancels the pending expiry and a later `start()` is a no-op once
22
- * the parent has aborted. An idle handle holds no parent listener.
23
- * - **Event-free.** A pure functional primitive — no Emitter, no events.
148
+ * `start()` arms or replaces the current deadline. Expiry sets `expired` and
149
+ * aborts the current signal once. `clear()` cancels without aborting and resets
150
+ * `expired`. Starting or clearing after expiry installs a fresh signal. A parent
151
+ * abort deliberately clears the timeout without aborting its signal; once the
152
+ * parent has aborted, later starts are inert.
153
+ *
154
+ * Construction validates the complete JavaScript boundary before creating an
155
+ * `AbortController`. A malformed or unreadable options record throws a
156
+ * `bound`-coded {@link import('@orkestrel/contract').ContractError}; an invalid
157
+ * `id`, `ms`, or `signal` uses `literal`, `range`, or `placement`, respectively.
24
158
  *
25
159
  * @example
26
160
  * ```ts
161
+ * import { Timeout } from '@orkestrel/timeout'
162
+ *
27
163
  * const timeout = new Timeout({ ms: 5_000 })
28
164
  * timeout.start()
29
165
  * timeout.signal.addEventListener('abort', () => giveUp(), { once: true })
30
- * timeout.clear() // cancels the deadline before it fires
166
+ * timeout.clear()
31
167
  * ```
32
168
  */
33
169
  var Timeout = class {
@@ -35,21 +171,22 @@ var Timeout = class {
35
171
  ms;
36
172
  #parent;
37
173
  #listener;
38
- #controller = new AbortController();
174
+ #controller;
39
175
  #handle;
40
176
  #linked = false;
41
- #expired = false;
42
177
  constructor(options) {
43
- this.id = (0, _orkestrel_contract.isString)(options.id) ? options.id : crypto.randomUUID();
44
- this.ms = options.ms;
45
- this.#parent = options.signal;
178
+ const input = validateTimeoutOptions(options);
179
+ this.id = input.id === void 0 ? crypto.randomUUID() : input.id;
180
+ this.ms = input.ms;
181
+ this.#parent = input.signal;
182
+ this.#controller = new AbortController();
46
183
  this.#listener = this.clear.bind(this);
47
184
  }
48
185
  get signal() {
49
186
  return this.#controller.signal;
50
187
  }
51
188
  get expired() {
52
- return this.#expired;
189
+ return this.#controller.signal.aborted;
53
190
  }
54
191
  start() {
55
192
  if (this.#parent?.aborted === true) return;
@@ -58,7 +195,6 @@ var Timeout = class {
58
195
  this.#handle = void 0;
59
196
  }
60
197
  this.#detach();
61
- this.#expired = false;
62
198
  if (this.#controller.signal.aborted) this.#controller = new AbortController();
63
199
  if (this.#parent !== void 0) {
64
200
  this.#parent.addEventListener("abort", this.#listener, { once: true });
@@ -66,9 +202,8 @@ var Timeout = class {
66
202
  }
67
203
  this.#handle = setTimeout(() => {
68
204
  this.#handle = void 0;
69
- this.#expired = true;
70
- this.#controller.abort();
71
205
  this.#detach();
206
+ this.#controller.abort();
72
207
  }, this.ms);
73
208
  }
74
209
  clear() {
@@ -77,7 +212,6 @@ var Timeout = class {
77
212
  this.#handle = void 0;
78
213
  }
79
214
  this.#detach();
80
- this.#expired = false;
81
215
  if (this.#controller.signal.aborted) this.#controller = new AbortController();
82
216
  }
83
217
  #detach() {
@@ -90,45 +224,42 @@ var Timeout = class {
90
224
  //#endregion
91
225
  //#region src/core/factories.ts
92
226
  /**
93
- * Create a deadline handle — a controllable `setTimeout` wrapper that exposes an
94
- * `AbortSignal` which fires when the timeout expires, for racing against work.
227
+ * Create a controllable deadline whose native signal aborts on expiry.
95
228
  *
96
229
  * @remarks
97
- * Call `start()` to arm the deadline for `ms` milliseconds; on expiry the handle's
98
- * `signal` fires and `expired` flips `true`. Call `clear()` to cancel a pending
99
- * deadline without firing. When `options.signal` is given, a parent abort CLEARS
100
- * the timeout (it never expires) rather than firing it. Pass `options.id` to label
101
- * the handle for tracing, or let it default to a random UUID. `ms` must be a
102
- * non-negative finite number; the host `setTimeout` clamps a negative or `NaN`
103
- * value to roughly `0` (firing on the next macrotask) rather than throwing.
104
- *
105
- * @param options - `ms` (the deadline in milliseconds, a non-negative finite
106
- * number), an optional `id` (a trace label; defaults to a random UUID), and an
107
- * optional parent `signal` whose abort clears the timeout
108
- * @returns A working {@link TimeoutInterface}
230
+ * `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.
231
+ * A parent `options.signal` clears an armed timeout without aborting the
232
+ * timeout's own signal. Omitted `options.id` values generate a random UUID.
233
+ * Malformed or unreadable options use contract code `bound`; invalid `id`,
234
+ * `ms`, and `signal` values use `literal`, `range`, and `placement`.
235
+ *
236
+ * @param options - Validated deadline, optional trace label, and optional native parent signal
237
+ * @returns A reusable timeout handle
238
+ * @throws {@link import('@orkestrel/contract').ContractError} When the
239
+ * JavaScript input does not satisfy `TimeoutOptions`
109
240
  *
110
241
  * @example
111
242
  * ```ts
112
- * import { createTimeout } from '@src/core'
243
+ * import { createTimeout } from '@orkestrel/timeout'
113
244
  *
114
245
  * const timeout = createTimeout({ ms: 5_000 })
115
246
  * timeout.start()
116
- * const result = await Promise.race([
117
- * work(),
118
- * new Promise((_, reject) =>
119
- * timeout.signal.addEventListener('abort', () => reject(new Error('timed out')), {
120
- * once: true,
121
- * }),
122
- * ),
123
- * ])
124
- * timeout.clear() // work finished first — cancel the deadline
247
+ * try {
248
+ * await fetch('/work', { signal: timeout.signal })
249
+ * } finally {
250
+ * timeout.clear()
251
+ * }
125
252
  * ```
126
253
  */
127
254
  function createTimeout(options) {
128
255
  return new Timeout(options);
129
256
  }
130
257
  //#endregion
258
+ exports.MAX_TIMEOUT_MS = MAX_TIMEOUT_MS;
131
259
  exports.Timeout = Timeout;
132
260
  exports.createTimeout = createTimeout;
261
+ exports.isTimeoutDuration = isTimeoutDuration;
262
+ exports.isTimeoutSignal = isTimeoutSignal;
263
+ exports.validateTimeoutOptions = validateTimeoutOptions;
133
264
 
134
265
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["#parent","#listener","#controller","#expired","#handle","#detach","#linked"],"sources":["../../../src/core/Timeout.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { isString } from '@orkestrel/contract'\n\n/**\n * A deadline handle — a controllable `setTimeout` wrapper that exposes an\n * `AbortSignal` which fires when the timeout expires, for racing against work.\n *\n * @remarks\n * - **Deadline signal.** `start()` arms a `setTimeout` for `ms`; on expiry it\n * flips `expired` and aborts the controller, firing `signal`. Race `signal`\n * against work to bound how long it may run.\n * - **Controllable.** `clear()` cancels a pending expiry WITHOUT firing `signal`,\n * and resets `expired` back to `false` (the §10 reset) — a cleared timeout\n * reports `expired === false` and a non-aborted `signal`.\n * - **Reusable.** Re-`start()` after an expiry swaps in a fresh controller and\n * resets `expired`, so `signal` / `expired` reflect the new run — the handle is\n * reusable across deadlines without re-construction.\n * - **Parent linking.** A parent `signal` does NOT expire this timeout — it\n * CLEARS it: the parent listener is attached only while a timer is armed (added\n * on `start()`, removed on expiry or `clear()`), so a parent abort during the\n * timing window cancels the pending expiry and a later `start()` is a no-op once\n * the parent has aborted. An idle handle holds no parent listener.\n * - **Event-free.** A pure functional primitive — no Emitter, no events.\n *\n * @example\n * ```ts\n * const timeout = new Timeout({ ms: 5_000 })\n * timeout.start()\n * timeout.signal.addEventListener('abort', () => giveUp(), { once: true })\n * timeout.clear() // cancels the deadline before it fires\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 = new AbortController()\n\t#handle: ReturnType<typeof setTimeout> | undefined\n\t#linked = false\n\t#expired = false\n\n\tconstructor(options: TimeoutOptions) {\n\t\t// Construction is the defensive JS boundary — validate here so the\n\t\t// start()/clear() hot paths stay dependency-free.\n\t\tthis.id = isString(options.id) ? options.id : crypto.randomUUID()\n\t\tthis.ms = options.ms\n\t\tthis.#parent = options.signal\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.#expired\n\t}\n\n\tstart(): void {\n\t\t// Once the parent has aborted, the deadline must never fire.\n\t\tif (this.#parent?.aborted === true) return\n\t\t// Re-arm cleanly: drop any pending timer + parent listener from a prior run.\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\tthis.#expired = false\n\t\t// Swap in a fresh controller only when the current one has already fired, so a\n\t\t// re-armed run gets a clean `signal` while a listener attached before the FIRST\n\t\t// `start()` (on the construction-time signal) survives to fire on expiry.\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t\t// Link the parent only for the lifetime of this timer — a parent abort CLEARS\n\t\t// the timeout (so it never expires); the listener is removed when the timer settles.\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.#expired = true\n\t\t\tthis.#controller.abort()\n\t\t\t// The timer fired — drop the parent listener so a later parent abort can no\n\t\t\t// longer reach `clear()` and un-expire this legitimate expiry.\n\t\t\tthis.#detach()\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\tthis.#expired = false\n\t\t// If the controller had already fired, swap a fresh one so a cleared timeout\n\t\t// reports `expired === false` AND a non-aborted `signal`, consistently.\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t}\n\n\t// Remove the parent-abort listener if one is currently attached.\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 deadline handle — a controllable `setTimeout` wrapper that exposes an\n * `AbortSignal` which fires when the timeout expires, for racing against work.\n *\n * @remarks\n * Call `start()` to arm the deadline for `ms` milliseconds; on expiry the handle's\n * `signal` fires and `expired` flips `true`. Call `clear()` to cancel a pending\n * deadline without firing. When `options.signal` is given, a parent abort CLEARS\n * the timeout (it never expires) rather than firing it. Pass `options.id` to label\n * the handle for tracing, or let it default to a random UUID. `ms` must be a\n * non-negative finite number; the host `setTimeout` clamps a negative or `NaN`\n * value to roughly `0` (firing on the next macrotask) rather than throwing.\n *\n * @param options - `ms` (the deadline in milliseconds, a non-negative finite\n * number), an optional `id` (a trace label; defaults to a random UUID), and an\n * optional parent `signal` whose abort clears the timeout\n * @returns A working {@link TimeoutInterface}\n *\n * @example\n * ```ts\n * import { createTimeout } from '@src/core'\n *\n * const timeout = createTimeout({ ms: 5_000 })\n * timeout.start()\n * const result = await Promise.race([\n * \twork(),\n * \tnew Promise((_, reject) =>\n * \t\ttimeout.signal.addEventListener('abort', () => reject(new Error('timed out')), {\n * \t\t\tonce: true,\n * \t\t}),\n * \t),\n * ])\n * timeout.clear() // work finished first — cancel the deadline\n * ```\n */\nexport function createTimeout(options: TimeoutOptions): TimeoutInterface {\n\treturn new Timeout(options)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,UAAb,MAAiD;CAChD;CACA;CACA;CACA;CACA,cAAc,IAAI,gBAAgB;CAClC;CACA,UAAU;CACV,WAAW;CAEX,YAAY,SAAyB;EAGpC,KAAK,MAAA,GAAA,oBAAA,SAAA,CAAc,QAAQ,EAAE,IAAI,QAAQ,KAAK,OAAO,WAAW;EAChE,KAAK,KAAK,QAAQ;EAClB,KAAKA,UAAU,QAAQ;EACvB,KAAKC,YAAY,KAAK,MAAM,KAAK,IAAI;CACtC;CAEA,IAAI,SAAsB;EACzB,OAAO,KAAKC,YAAY;CACzB;CAEA,IAAI,UAAmB;EACtB,OAAO,KAAKC;CACb;CAEA,QAAc;EAEb,IAAI,KAAKH,SAAS,YAAY,MAAM;EAEpC,IAAI,KAAKI,YAAY,KAAA,GAAW;GAC/B,aAAa,KAAKA,OAAO;GACzB,KAAKA,UAAU,KAAA;EAChB;EACA,KAAKC,QAAQ;EACb,KAAKF,WAAW;EAIhB,IAAI,KAAKD,YAAY,OAAO,SAAS,KAAKA,cAAc,IAAI,gBAAgB;EAG5E,IAAI,KAAKF,YAAY,KAAA,GAAW;GAC/B,KAAKA,QAAQ,iBAAiB,SAAS,KAAKC,WAAW,EAAE,MAAM,KAAK,CAAC;GACrE,KAAKK,UAAU;EAChB;EACA,KAAKF,UAAU,iBAAiB;GAC/B,KAAKA,UAAU,KAAA;GACf,KAAKD,WAAW;GAChB,KAAKD,YAAY,MAAM;GAGvB,KAAKG,QAAQ;EACd,GAAG,KAAK,EAAE;CACX;CAEA,QAAc;EACb,IAAI,KAAKD,YAAY,KAAA,GAAW;GAC/B,aAAa,KAAKA,OAAO;GACzB,KAAKA,UAAU,KAAA;EAChB;EACA,KAAKC,QAAQ;EACb,KAAKF,WAAW;EAGhB,IAAI,KAAKD,YAAY,OAAO,SAAS,KAAKA,cAAc,IAAI,gBAAgB;CAC7E;CAGA,UAAgB;EACf,IAAI,KAAKI,SAAS;GACjB,KAAKN,SAAS,oBAAoB,SAAS,KAAKC,SAAS;GACzD,KAAKK,UAAU;EAChB;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACtEA,SAAgB,cAAc,SAA2C;CACxE,OAAO,IAAI,QAAQ,OAAO;AAC3B"}
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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"}