@orkestrel/timeout 0.0.4 → 0.0.5
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 +18 -9
- package/dist/src/core/index.cjs +183 -52
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +178 -98
- package/dist/src/core/index.d.ts +178 -98
- package/dist/src/core/index.js +181 -54
- package/dist/src/core/index.js.map +1 -1
- package/package.json +7 -7
package/dist/src/core/index.js
CHANGED
|
@@ -1,32 +1,168 @@
|
|
|
1
|
-
import { isString } from "@orkestrel/contract";
|
|
1
|
+
import { ContractError, isFiniteNumber, isInteger, isRecord, isString, preview } from "@orkestrel/contract";
|
|
2
|
+
//#region src/core/constants.ts
|
|
3
|
+
/**
|
|
4
|
+
* Largest timeout duration accepted by the package, in milliseconds.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* createTimeout({ ms: MAX_TIMEOUT_MS })
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/core/validators.ts
|
|
14
|
+
/**
|
|
15
|
+
* Determine whether a value is an accepted timeout duration.
|
|
16
|
+
*
|
|
17
|
+
* @param value - Unknown duration candidate
|
|
18
|
+
* @returns `true` only for an integer in the inclusive timeout range
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* isTimeoutDuration(0) // true
|
|
23
|
+
* isTimeoutDuration(1.5) // false
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function isTimeoutDuration(value) {
|
|
27
|
+
return isFiniteNumber(value) && isInteger(value) && value >= 0 && value <= 2147483647;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Determine whether a value is a genuine native `AbortSignal`.
|
|
31
|
+
*
|
|
32
|
+
* @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.
|
|
36
|
+
*
|
|
37
|
+
* @param value - Unknown signal candidate
|
|
38
|
+
* @returns `true` only when the native `AbortSignal` getter accepts the value
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* isTimeoutSignal(new AbortController().signal) // true
|
|
43
|
+
* isTimeoutSignal({ aborted: false }) // false
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function isTimeoutSignal(value) {
|
|
47
|
+
try {
|
|
48
|
+
const descriptor = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted");
|
|
49
|
+
if (descriptor?.get === void 0) return false;
|
|
50
|
+
return typeof Reflect.apply(descriptor.get, value, []) === "boolean";
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/core/helpers.ts
|
|
57
|
+
/**
|
|
58
|
+
* Validate and normalize timeout construction options.
|
|
59
|
+
*
|
|
60
|
+
* @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
|
+
* listener lifecycle begins at this boundary.
|
|
64
|
+
*
|
|
65
|
+
* @param options - Potentially untrusted timeout options
|
|
66
|
+
* @returns A fresh validated `TimeoutOptions` object
|
|
67
|
+
* @throws {@link import('@orkestrel/contract').ContractError} When the input
|
|
68
|
+
* does not satisfy `TimeoutOptions`
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const options = validateTimeoutOptions({ ms: 5_000 })
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
function validateTimeoutOptions(options) {
|
|
76
|
+
if (!isRecord(options)) throw new ContractError("Timeout: options must be a plain record", {
|
|
77
|
+
code: "bound",
|
|
78
|
+
context: {
|
|
79
|
+
path: ["options"],
|
|
80
|
+
limit: "plain record",
|
|
81
|
+
received: preview(options)
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
let id;
|
|
85
|
+
let ms;
|
|
86
|
+
let signal;
|
|
87
|
+
try {
|
|
88
|
+
id = options.id;
|
|
89
|
+
ms = options.ms;
|
|
90
|
+
signal = options.signal;
|
|
91
|
+
} catch (cause) {
|
|
92
|
+
throw new ContractError("Timeout: options could not be read", {
|
|
93
|
+
code: "bound",
|
|
94
|
+
context: {
|
|
95
|
+
path: ["options"],
|
|
96
|
+
limit: "readable plain record",
|
|
97
|
+
received: preview(options)
|
|
98
|
+
},
|
|
99
|
+
cause
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (id !== void 0 && !isString(id)) throw new ContractError("Timeout: id must be a string when defined", {
|
|
103
|
+
code: "literal",
|
|
104
|
+
context: {
|
|
105
|
+
path: ["options", "id"],
|
|
106
|
+
limit: "string or undefined",
|
|
107
|
+
received: preview(id)
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (!isTimeoutDuration(ms)) throw new ContractError("Timeout: ms must be an integer in the supported range", {
|
|
111
|
+
code: "range",
|
|
112
|
+
context: {
|
|
113
|
+
path: ["options", "ms"],
|
|
114
|
+
limit: `integer in [0, ${MAX_TIMEOUT_MS}]`,
|
|
115
|
+
received: preview(ms)
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
if (signal !== void 0 && !isTimeoutSignal(signal)) throw new ContractError("Timeout: signal must be a native AbortSignal when defined", {
|
|
119
|
+
code: "placement",
|
|
120
|
+
context: {
|
|
121
|
+
path: ["options", "signal"],
|
|
122
|
+
limit: "native AbortSignal or undefined",
|
|
123
|
+
received: preview(signal)
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
if (id !== void 0 && signal !== void 0) return {
|
|
127
|
+
id,
|
|
128
|
+
ms,
|
|
129
|
+
signal
|
|
130
|
+
};
|
|
131
|
+
if (id !== void 0) return {
|
|
132
|
+
id,
|
|
133
|
+
ms
|
|
134
|
+
};
|
|
135
|
+
if (signal !== void 0) return {
|
|
136
|
+
ms,
|
|
137
|
+
signal
|
|
138
|
+
};
|
|
139
|
+
return { ms };
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
2
142
|
//#region src/core/Timeout.ts
|
|
3
143
|
/**
|
|
4
|
-
* A deadline
|
|
5
|
-
* `AbortSignal` which fires when the timeout expires, for racing against work.
|
|
144
|
+
* A controllable deadline whose native `AbortSignal` aborts when it expires.
|
|
6
145
|
*
|
|
7
146
|
* @remarks
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* CLEARS it: the parent listener is attached only while a timer is armed (added
|
|
19
|
-
* on `start()`, removed on expiry or `clear()`), so a parent abort during the
|
|
20
|
-
* timing window cancels the pending expiry and a later `start()` is a no-op once
|
|
21
|
-
* the parent has aborted. An idle handle holds no parent listener.
|
|
22
|
-
* - **Event-free.** A pure functional primitive — no Emitter, no events.
|
|
147
|
+
* `start()` arms or replaces the current deadline. Expiry sets `expired` and
|
|
148
|
+
* aborts the current signal once. `clear()` cancels without aborting and resets
|
|
149
|
+
* `expired`. Starting or clearing after expiry installs a fresh signal. A parent
|
|
150
|
+
* abort deliberately clears the timeout without aborting its signal; once the
|
|
151
|
+
* parent has aborted, later starts are inert.
|
|
152
|
+
*
|
|
153
|
+
* Construction validates the complete JavaScript boundary before creating an
|
|
154
|
+
* `AbortController`. A malformed or unreadable options record throws a
|
|
155
|
+
* `bound`-coded {@link import('@orkestrel/contract').ContractError}; an invalid
|
|
156
|
+
* `id`, `ms`, or `signal` uses `literal`, `range`, or `placement`, respectively.
|
|
23
157
|
*
|
|
24
158
|
* @example
|
|
25
159
|
* ```ts
|
|
160
|
+
* import { Timeout } from '@orkestrel/timeout'
|
|
161
|
+
*
|
|
26
162
|
* const timeout = new Timeout({ ms: 5_000 })
|
|
27
163
|
* timeout.start()
|
|
28
164
|
* timeout.signal.addEventListener('abort', () => giveUp(), { once: true })
|
|
29
|
-
* timeout.clear()
|
|
165
|
+
* timeout.clear()
|
|
30
166
|
* ```
|
|
31
167
|
*/
|
|
32
168
|
var Timeout = class {
|
|
@@ -34,21 +170,22 @@ var Timeout = class {
|
|
|
34
170
|
ms;
|
|
35
171
|
#parent;
|
|
36
172
|
#listener;
|
|
37
|
-
#controller
|
|
173
|
+
#controller;
|
|
38
174
|
#handle;
|
|
39
175
|
#linked = false;
|
|
40
|
-
#expired = false;
|
|
41
176
|
constructor(options) {
|
|
42
|
-
|
|
43
|
-
this.
|
|
44
|
-
this
|
|
177
|
+
const input = validateTimeoutOptions(options);
|
|
178
|
+
this.id = input.id === void 0 ? crypto.randomUUID() : input.id;
|
|
179
|
+
this.ms = input.ms;
|
|
180
|
+
this.#parent = input.signal;
|
|
181
|
+
this.#controller = new AbortController();
|
|
45
182
|
this.#listener = this.clear.bind(this);
|
|
46
183
|
}
|
|
47
184
|
get signal() {
|
|
48
185
|
return this.#controller.signal;
|
|
49
186
|
}
|
|
50
187
|
get expired() {
|
|
51
|
-
return this.#
|
|
188
|
+
return this.#controller.signal.aborted;
|
|
52
189
|
}
|
|
53
190
|
start() {
|
|
54
191
|
if (this.#parent?.aborted === true) return;
|
|
@@ -57,7 +194,6 @@ var Timeout = class {
|
|
|
57
194
|
this.#handle = void 0;
|
|
58
195
|
}
|
|
59
196
|
this.#detach();
|
|
60
|
-
this.#expired = false;
|
|
61
197
|
if (this.#controller.signal.aborted) this.#controller = new AbortController();
|
|
62
198
|
if (this.#parent !== void 0) {
|
|
63
199
|
this.#parent.addEventListener("abort", this.#listener, { once: true });
|
|
@@ -65,9 +201,8 @@ var Timeout = class {
|
|
|
65
201
|
}
|
|
66
202
|
this.#handle = setTimeout(() => {
|
|
67
203
|
this.#handle = void 0;
|
|
68
|
-
this.#expired = true;
|
|
69
|
-
this.#controller.abort();
|
|
70
204
|
this.#detach();
|
|
205
|
+
this.#controller.abort();
|
|
71
206
|
}, this.ms);
|
|
72
207
|
}
|
|
73
208
|
clear() {
|
|
@@ -76,7 +211,6 @@ var Timeout = class {
|
|
|
76
211
|
this.#handle = void 0;
|
|
77
212
|
}
|
|
78
213
|
this.#detach();
|
|
79
|
-
this.#expired = false;
|
|
80
214
|
if (this.#controller.signal.aborted) this.#controller = new AbortController();
|
|
81
215
|
}
|
|
82
216
|
#detach() {
|
|
@@ -89,44 +223,37 @@ var Timeout = class {
|
|
|
89
223
|
//#endregion
|
|
90
224
|
//#region src/core/factories.ts
|
|
91
225
|
/**
|
|
92
|
-
* Create a deadline
|
|
93
|
-
* `AbortSignal` which fires when the timeout expires, for racing against work.
|
|
226
|
+
* Create a controllable deadline whose native signal aborts on expiry.
|
|
94
227
|
*
|
|
95
228
|
* @remarks
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* @
|
|
105
|
-
*
|
|
106
|
-
* optional parent `signal` whose abort clears the timeout
|
|
107
|
-
* @returns A working {@link TimeoutInterface}
|
|
229
|
+
* `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.
|
|
230
|
+
* A parent `options.signal` clears an armed timeout without aborting the
|
|
231
|
+
* timeout's own signal. Omitted `options.id` values generate a random UUID.
|
|
232
|
+
* Malformed or unreadable options use contract code `bound`; invalid `id`,
|
|
233
|
+
* `ms`, and `signal` values use `literal`, `range`, and `placement`.
|
|
234
|
+
*
|
|
235
|
+
* @param options - Validated deadline, optional trace label, and optional native parent signal
|
|
236
|
+
* @returns A reusable timeout handle
|
|
237
|
+
* @throws {@link import('@orkestrel/contract').ContractError} When the
|
|
238
|
+
* JavaScript input does not satisfy `TimeoutOptions`
|
|
108
239
|
*
|
|
109
240
|
* @example
|
|
110
241
|
* ```ts
|
|
111
|
-
* import { createTimeout } from '@
|
|
242
|
+
* import { createTimeout } from '@orkestrel/timeout'
|
|
112
243
|
*
|
|
113
244
|
* const timeout = createTimeout({ ms: 5_000 })
|
|
114
245
|
* timeout.start()
|
|
115
|
-
*
|
|
116
|
-
* work
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* }),
|
|
121
|
-
* ),
|
|
122
|
-
* ])
|
|
123
|
-
* timeout.clear() // work finished first — cancel the deadline
|
|
246
|
+
* try {
|
|
247
|
+
* await fetch('/work', { signal: timeout.signal })
|
|
248
|
+
* } finally {
|
|
249
|
+
* timeout.clear()
|
|
250
|
+
* }
|
|
124
251
|
* ```
|
|
125
252
|
*/
|
|
126
253
|
function createTimeout(options) {
|
|
127
254
|
return new Timeout(options);
|
|
128
255
|
}
|
|
129
256
|
//#endregion
|
|
130
|
-
export { Timeout, createTimeout };
|
|
257
|
+
export { MAX_TIMEOUT_MS, Timeout, createTimeout, isTimeoutDuration, isTimeoutSignal, validateTimeoutOptions };
|
|
131
258
|
|
|
132
259
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","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,KAAK,SAAS,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.js","names":["#parent","#listener","#controller","#handle","#detach","#linked"],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/Timeout.ts","../../../src/core/factories.ts"],"sourcesContent":["/**\n * Largest timeout duration accepted by the package, in milliseconds.\n *\n * @example\n * ```ts\n * createTimeout({ ms: MAX_TIMEOUT_MS })\n * ```\n */\nexport const MAX_TIMEOUT_MS = 2_147_483_647\n","import { isFiniteNumber, isInteger } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\n\n/**\n * Determine whether a value is an accepted timeout duration.\n *\n * @param value - Unknown duration candidate\n * @returns `true` only for an integer in the inclusive timeout range\n *\n * @example\n * ```ts\n * isTimeoutDuration(0) // true\n * isTimeoutDuration(1.5) // false\n * ```\n */\nexport function isTimeoutDuration(value: unknown): value is number {\n\treturn isFiniteNumber(value) && isInteger(value) && value >= 0 && value <= MAX_TIMEOUT_MS\n}\n\n/**\n * Determine whether a value is a genuine native `AbortSignal`.\n *\n * @remarks\n * The intrinsic `aborted` getter performs the native brand check. The boundary\n * contains revoked proxies and hostile receivers, returning `false` instead of\n * allowing their errors to escape.\n *\n * @param value - Unknown signal candidate\n * @returns `true` only when the native `AbortSignal` getter accepts the value\n *\n * @example\n * ```ts\n * isTimeoutSignal(new AbortController().signal) // true\n * isTimeoutSignal({ aborted: false }) // false\n * ```\n */\nexport function isTimeoutSignal(value: unknown): value is AbortSignal {\n\ttry {\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted')\n\t\tif (descriptor?.get === undefined) return false\n\t\treturn typeof Reflect.apply(descriptor.get, value, []) === 'boolean'\n\t} catch {\n\t\treturn false\n\t}\n}\n","import type { TimeoutOptions } from './types.js'\nimport { ContractError, isRecord, isString, preview } from '@orkestrel/contract'\nimport { MAX_TIMEOUT_MS } from './constants.js'\nimport { isTimeoutDuration, isTimeoutSignal } from './validators.js'\n\n/**\n * Validate and normalize timeout construction options.\n *\n * @remarks\n * Each property is read exactly once before validation. The returned object is\n * a fresh copy and omits absent optional properties. No timer, controller, or\n * listener lifecycle begins at this boundary.\n *\n * @param options - Potentially untrusted timeout options\n * @returns A fresh validated `TimeoutOptions` object\n * @throws {@link import('@orkestrel/contract').ContractError} When the input\n * does not satisfy `TimeoutOptions`\n *\n * @example\n * ```ts\n * const options = validateTimeoutOptions({ ms: 5_000 })\n * ```\n */\nexport function validateTimeoutOptions(options: TimeoutOptions): TimeoutOptions {\n\tif (!isRecord(options)) {\n\t\tthrow new ContractError('Timeout: options must be a plain record', {\n\t\t\tcode: 'bound',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options'],\n\t\t\t\tlimit: 'plain record',\n\t\t\t\treceived: preview(options),\n\t\t\t},\n\t\t})\n\t}\n\n\tlet id: TimeoutOptions['id']\n\tlet ms: TimeoutOptions['ms']\n\tlet signal: TimeoutOptions['signal']\n\ttry {\n\t\tid = options.id\n\t\tms = options.ms\n\t\tsignal = options.signal\n\t} catch (cause) {\n\t\tthrow new ContractError('Timeout: options could not be read', {\n\t\t\tcode: 'bound',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options'],\n\t\t\t\tlimit: 'readable plain record',\n\t\t\t\treceived: preview(options),\n\t\t\t},\n\t\t\tcause,\n\t\t})\n\t}\n\n\tif (id !== undefined && !isString(id)) {\n\t\tthrow new ContractError('Timeout: id must be a string when defined', {\n\t\t\tcode: 'literal',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'id'],\n\t\t\t\tlimit: 'string or undefined',\n\t\t\t\treceived: preview(id),\n\t\t\t},\n\t\t})\n\t}\n\tif (!isTimeoutDuration(ms)) {\n\t\tthrow new ContractError('Timeout: ms must be an integer in the supported range', {\n\t\t\tcode: 'range',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'ms'],\n\t\t\t\tlimit: `integer in [0, ${MAX_TIMEOUT_MS}]`,\n\t\t\t\treceived: preview(ms),\n\t\t\t},\n\t\t})\n\t}\n\tif (signal !== undefined && !isTimeoutSignal(signal)) {\n\t\tthrow new ContractError('Timeout: signal must be a native AbortSignal when defined', {\n\t\t\tcode: 'placement',\n\t\t\tcontext: {\n\t\t\t\tpath: ['options', 'signal'],\n\t\t\t\tlimit: 'native AbortSignal or undefined',\n\t\t\t\treceived: preview(signal),\n\t\t\t},\n\t\t})\n\t}\n\n\tif (id !== undefined && signal !== undefined) return { id, ms, signal }\n\tif (id !== undefined) return { id, ms }\n\tif (signal !== undefined) return { ms, signal }\n\treturn { ms }\n}\n","import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { validateTimeoutOptions } from './helpers.js'\n\n/**\n * A controllable deadline whose native `AbortSignal` aborts when it expires.\n *\n * @remarks\n * `start()` arms or replaces the current deadline. Expiry sets `expired` and\n * aborts the current signal once. `clear()` cancels without aborting and resets\n * `expired`. Starting or clearing after expiry installs a fresh signal. A parent\n * abort deliberately clears the timeout without aborting its signal; once the\n * parent has aborted, later starts are inert.\n *\n * Construction validates the complete JavaScript boundary before creating an\n * `AbortController`. A malformed or unreadable options record throws a\n * `bound`-coded {@link import('@orkestrel/contract').ContractError}; an invalid\n * `id`, `ms`, or `signal` uses `literal`, `range`, or `placement`, respectively.\n *\n * @example\n * ```ts\n * import { Timeout } from '@orkestrel/timeout'\n *\n * const timeout = new Timeout({ ms: 5_000 })\n * timeout.start()\n * timeout.signal.addEventListener('abort', () => giveUp(), { once: true })\n * timeout.clear()\n * ```\n */\nexport class Timeout implements TimeoutInterface {\n\treadonly id: string\n\treadonly ms: number\n\treadonly #parent: AbortSignal | undefined\n\treadonly #listener: () => void\n\t#controller: AbortController\n\t#handle: ReturnType<typeof setTimeout> | undefined\n\t#linked = false\n\n\tconstructor(options: TimeoutOptions) {\n\t\tconst input = validateTimeoutOptions(options)\n\t\tthis.id = input.id === undefined ? crypto.randomUUID() : input.id\n\t\tthis.ms = input.ms\n\t\tthis.#parent = input.signal\n\t\tthis.#controller = new AbortController()\n\t\tthis.#listener = this.clear.bind(this)\n\t}\n\n\tget signal(): AbortSignal {\n\t\treturn this.#controller.signal\n\t}\n\n\tget expired(): boolean {\n\t\treturn this.#controller.signal.aborted\n\t}\n\n\tstart(): void {\n\t\tif (this.#parent?.aborted === true) return\n\t\tif (this.#handle !== undefined) {\n\t\t\tclearTimeout(this.#handle)\n\t\t\tthis.#handle = undefined\n\t\t}\n\t\tthis.#detach()\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t\tif (this.#parent !== undefined) {\n\t\t\tthis.#parent.addEventListener('abort', this.#listener, { once: true })\n\t\t\tthis.#linked = true\n\t\t}\n\t\tthis.#handle = setTimeout(() => {\n\t\t\tthis.#handle = undefined\n\t\t\tthis.#detach()\n\t\t\tthis.#controller.abort()\n\t\t}, this.ms)\n\t}\n\n\tclear(): void {\n\t\tif (this.#handle !== undefined) {\n\t\t\tclearTimeout(this.#handle)\n\t\t\tthis.#handle = undefined\n\t\t}\n\t\tthis.#detach()\n\t\tif (this.#controller.signal.aborted) this.#controller = new AbortController()\n\t}\n\n\t#detach(): void {\n\t\tif (this.#linked) {\n\t\t\tthis.#parent?.removeEventListener('abort', this.#listener)\n\t\t\tthis.#linked = false\n\t\t}\n\t}\n}\n","import type { TimeoutInterface, TimeoutOptions } from './types.js'\nimport { Timeout } from './Timeout.js'\n\n/**\n * Create a controllable deadline whose native signal aborts on expiry.\n *\n * @remarks\n * `options.ms` must be an integer from `0` through `2_147_483_647`, inclusive.\n * A parent `options.signal` clears an armed timeout without aborting the\n * timeout's own signal. Omitted `options.id` values generate a random UUID.\n * Malformed or unreadable options use contract code `bound`; invalid `id`,\n * `ms`, and `signal` values use `literal`, `range`, and `placement`.\n *\n * @param options - Validated deadline, optional trace label, and optional native parent signal\n * @returns A reusable timeout handle\n * @throws {@link import('@orkestrel/contract').ContractError} When the\n * JavaScript input does not satisfy `TimeoutOptions`\n *\n * @example\n * ```ts\n * import { createTimeout } from '@orkestrel/timeout'\n *\n * const timeout = createTimeout({ ms: 5_000 })\n * timeout.start()\n * try {\n * \tawait fetch('/work', { signal: timeout.signal })\n * } finally {\n * \ttimeout.clear()\n * }\n * ```\n */\nexport function createTimeout(options: TimeoutOptions): TimeoutInterface {\n\treturn new Timeout(options)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/timeout",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
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",
|
|
@@ -64,18 +64,18 @@
|
|
|
64
64
|
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@orkestrel/contract": "^0.0.
|
|
67
|
+
"@orkestrel/contract": "^0.0.9"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@microsoft/api-extractor": "^7.58.12",
|
|
71
|
-
"@orkestrel/guide": "^0.0.
|
|
72
|
-
"@orkestrel/scaffold": "^0.0.
|
|
71
|
+
"@orkestrel/guide": "^0.0.8",
|
|
72
|
+
"@orkestrel/scaffold": "^0.0.18",
|
|
73
73
|
"@types/node": "^26.1.2",
|
|
74
74
|
"@vitest/browser-playwright": "^4.1.10",
|
|
75
|
-
"oxfmt": "^0.
|
|
76
|
-
"oxlint": "^1.
|
|
75
|
+
"oxfmt": "^0.62.0",
|
|
76
|
+
"oxlint": "^1.77.0",
|
|
77
77
|
"typescript": "^6.0.3",
|
|
78
|
-
"vite": "^8.
|
|
78
|
+
"vite": "^8.2.0",
|
|
79
79
|
"vite-plugin-dts": "^5.0.3",
|
|
80
80
|
"vitest": "^4.1.10"
|
|
81
81
|
},
|