homebridge-plugin-utils 1.35.0 → 2.1.0
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 +130 -2
- package/build/eslint-plugin/README.md +164 -0
- package/build/eslint-plugin/config.mjs +308 -0
- package/build/eslint-plugin/index.mjs +7 -0
- package/build/eslint-plugin/plugin.mjs +46 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
- package/build/eslint-plugin/rules/comment-style.mjs +190 -0
- package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
- package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
- package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
- package/build/eslint-plugin/test-setup.mjs +21 -0
- package/build/fs-ops.mjs +184 -0
- package/build/tsconfig.json +18 -3
- package/dist/backpressure.d.ts +94 -45
- package/dist/backpressure.js +229 -93
- package/dist/backpressure.js.map +1 -1
- package/dist/cli/index.d.ts +166 -0
- package/dist/cli/index.js +551 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/clock-double.d.ts +93 -0
- package/dist/clock-double.js +141 -0
- package/dist/clock-double.js.map +1 -0
- package/dist/clock.d.ts +39 -0
- package/dist/clock.js +34 -0
- package/dist/clock.js.map +1 -0
- package/dist/disposable-stack.d.ts +59 -0
- package/dist/disposable-stack.js +155 -0
- package/dist/disposable-stack.js.map +1 -0
- package/dist/docChrome.d.ts +260 -0
- package/dist/docChrome.js +361 -0
- package/dist/docChrome.js.map +1 -0
- package/dist/eslint-plugin/config.d.mts +193 -0
- package/dist/eslint-plugin/index.d.mts +2 -0
- package/dist/eslint-plugin/plugin.d.mts +87 -0
- package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
- package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
- package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
- package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
- package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
- package/dist/featureOptions-docs.d.ts +96 -0
- package/dist/featureOptions-docs.js +193 -0
- package/dist/featureOptions-docs.js.map +1 -0
- package/dist/featureOptions.d.ts +674 -0
- package/dist/featureOptions.js +870 -0
- package/dist/featureOptions.js.map +1 -0
- package/dist/ffmpeg/codecs.d.ts +256 -72
- package/dist/ffmpeg/codecs.js +477 -262
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/dgram-util.d.ts +46 -0
- package/dist/ffmpeg/dgram-util.js +38 -0
- package/dist/ffmpeg/dgram-util.js.map +1 -0
- package/dist/ffmpeg/exec.d.ts +83 -64
- package/dist/ffmpeg/exec.js +77 -86
- package/dist/ffmpeg/exec.js.map +1 -1
- package/dist/ffmpeg/fmp4.d.ts +49 -2
- package/dist/ffmpeg/fmp4.js +47 -11
- package/dist/ffmpeg/fmp4.js.map +1 -1
- package/dist/ffmpeg/hap-enums.d.ts +214 -0
- package/dist/ffmpeg/hap-enums.js +92 -0
- package/dist/ffmpeg/hap-enums.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +16 -9
- package/dist/ffmpeg/index.js +6 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/mp4-assembler.d.ts +161 -0
- package/dist/ffmpeg/mp4-assembler.js +424 -0
- package/dist/ffmpeg/mp4-assembler.js.map +1 -0
- package/dist/ffmpeg/mp4-parser.d.ts +94 -0
- package/dist/ffmpeg/mp4-parser.js +130 -0
- package/dist/ffmpeg/mp4-parser.js.map +1 -0
- package/dist/ffmpeg/options.d.ts +62 -149
- package/dist/ffmpeg/options.js +608 -499
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +142 -96
- package/dist/ffmpeg/process.js +406 -278
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +325 -186
- package/dist/ffmpeg/record.js +418 -565
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/recording-process-double.d.ts +157 -0
- package/dist/ffmpeg/recording-process-double.js +190 -0
- package/dist/ffmpeg/recording-process-double.js.map +1 -0
- package/dist/ffmpeg/rtp-parser.d.ts +70 -0
- package/dist/ffmpeg/rtp-parser.js +77 -0
- package/dist/ffmpeg/rtp-parser.js.map +1 -0
- package/dist/ffmpeg/rtp.d.ts +198 -141
- package/dist/ffmpeg/rtp.js +474 -251
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -2
- package/dist/ffmpeg/settings.js +20 -5
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -150
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/formatters.d.ts +106 -0
- package/dist/formatters.js +174 -0
- package/dist/formatters.js.map +1 -0
- package/dist/homebridge-enums.d.ts +30 -0
- package/dist/homebridge-enums.js +17 -0
- package/dist/homebridge-enums.js.map +1 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/logclient/auth.d.ts +114 -0
- package/dist/logclient/auth.js +199 -0
- package/dist/logclient/auth.js.map +1 -0
- package/dist/logclient/cli-run.d.ts +76 -0
- package/dist/logclient/cli-run.js +639 -0
- package/dist/logclient/cli-run.js.map +1 -0
- package/dist/logclient/cli.d.ts +3 -0
- package/dist/logclient/cli.js +97 -0
- package/dist/logclient/cli.js.map +1 -0
- package/dist/logclient/client.d.ts +145 -0
- package/dist/logclient/client.js +600 -0
- package/dist/logclient/client.js.map +1 -0
- package/dist/logclient/config.d.ts +173 -0
- package/dist/logclient/config.js +199 -0
- package/dist/logclient/config.js.map +1 -0
- package/dist/logclient/endpoints.d.ts +54 -0
- package/dist/logclient/endpoints.js +73 -0
- package/dist/logclient/endpoints.js.map +1 -0
- package/dist/logclient/filter.d.ts +45 -0
- package/dist/logclient/filter.js +51 -0
- package/dist/logclient/filter.js.map +1 -0
- package/dist/logclient/frame.d.ts +93 -0
- package/dist/logclient/frame.js +203 -0
- package/dist/logclient/frame.js.map +1 -0
- package/dist/logclient/index.d.ts +31 -0
- package/dist/logclient/index.js +12 -0
- package/dist/logclient/index.js.map +1 -0
- package/dist/logclient/parser.d.ts +211 -0
- package/dist/logclient/parser.js +393 -0
- package/dist/logclient/parser.js.map +1 -0
- package/dist/logclient/rest.d.ts +41 -0
- package/dist/logclient/rest.js +111 -0
- package/dist/logclient/rest.js.map +1 -0
- package/dist/logclient/settings.d.ts +15 -0
- package/dist/logclient/settings.js +64 -0
- package/dist/logclient/settings.js.map +1 -0
- package/dist/logclient/socket-double.d.ts +201 -0
- package/dist/logclient/socket-double.js +384 -0
- package/dist/logclient/socket-double.js.map +1 -0
- package/dist/logclient/socket.d.ts +257 -0
- package/dist/logclient/socket.js +620 -0
- package/dist/logclient/socket.js.map +1 -0
- package/dist/logclient/stitch.d.ts +83 -0
- package/dist/logclient/stitch.js +146 -0
- package/dist/logclient/stitch.js.map +1 -0
- package/dist/logclient/time-expression.d.ts +42 -0
- package/dist/logclient/time-expression.js +181 -0
- package/dist/logclient/time-expression.js.map +1 -0
- package/dist/logclient/time-window.d.ts +38 -0
- package/dist/logclient/time-window.js +53 -0
- package/dist/logclient/time-window.js.map +1 -0
- package/dist/logclient/types.d.ts +107 -0
- package/dist/logclient/types.js +6 -0
- package/dist/logclient/types.js.map +1 -0
- package/dist/mqttClient.d.ts +287 -0
- package/dist/mqttClient.js +433 -0
- package/dist/mqttClient.js.map +1 -0
- package/dist/service.d.ts +64 -15
- package/dist/service.js +93 -66
- package/dist/service.js.map +1 -1
- package/dist/ui/featureOptions.js +870 -0
- package/dist/ui/featureOptions.js.map +1 -0
- package/dist/ui/formatters.js +174 -0
- package/dist/ui/formatters.js.map +1 -0
- package/dist/ui/pluginConfigSession.mjs +141 -0
- package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
- package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
- package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
- package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
- package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
- package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
- package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
- package/dist/ui/webUi-featureOptions/state.mjs +319 -0
- package/dist/ui/webUi-featureOptions/store.mjs +181 -0
- package/dist/ui/webUi-featureOptions/utils.mjs +213 -0
- package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
- package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
- package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
- package/dist/ui/webUi-featureOptions/views/nav.mjs +341 -0
- package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
- package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
- package/dist/ui/webUi-featureOptions.mjs +702 -0
- package/dist/ui/webUi.mjs +225 -88
- package/dist/util.d.ts +679 -45
- package/dist/util.js +830 -77
- package/dist/util.js.map +1 -1
- package/package.json +33 -15
- package/build/eslint-rules.mjs +0 -511
- package/dist/featureoptions.d.ts +0 -264
- package/dist/featureoptions.js +0 -480
- package/dist/featureoptions.js.map +0 -1
- package/dist/mqttclient.d.ts +0 -178
- package/dist/mqttclient.js +0 -310
- package/dist/mqttclient.js.map +0 -1
- package/dist/ui/featureoptions.js +0 -480
- package/dist/ui/featureoptions.js.map +0 -1
- package/dist/ui/webUi-featureoptions.mjs +0 -3765
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reusable, controllable {@link Clock} test double.
|
|
3
|
+
*
|
|
4
|
+
* The {@link Clock} seam in `clock.ts` exists so a consuming plugin's time-dependent code can be driven without real wall-clock waits. This module ships the fake that
|
|
5
|
+
* cashes that in: a {@link TestClock} over a virtual timeline a test advances explicitly. `now()` returns the virtual time; `delay()` registers a pending wait that
|
|
6
|
+
* resolves only when {@link TestClock.advance} crosses its deadline, or rejects when its signal aborts - matching `node:timers/promises` `setTimeout`'s `AbortError`
|
|
7
|
+
* shape. No real timers and no wall-clock are used, so a consumer's pacing/timeout/duration path runs deterministically and instantly under test.
|
|
8
|
+
*
|
|
9
|
+
* The double builds on the library's own primitives rather than hand-rolling them: {@link onAbort} wires the abort listener and yields the `Disposable` that detaches
|
|
10
|
+
* it, and `Promise.withResolvers` captures each pending wait's deferred. The abort listener is detached on EITHER resolution path (deadline-crossed or aborted), so no
|
|
11
|
+
* listener leaks onto a long-lived signal across many short waits.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { Clock } from "./clock.ts";
|
|
16
|
+
/**
|
|
17
|
+
* A controllable {@link Clock} double over virtual time. `now()` returns the current virtual time; `delay()` registers a pending wait that resolves only when
|
|
18
|
+
* {@link TestClock.advance} crosses its deadline (in ascending-deadline order), or rejects with an `AbortError` (matching `node:timers/promises` `setTimeout` - `name`
|
|
19
|
+
* `"AbortError"`, `code` `"ABORT_ERR"`, NOT the signal's reason) when its signal aborts. No real timers or wall-clock are used.
|
|
20
|
+
*
|
|
21
|
+
* The virtual time is a RELATIVE timeline seeded at `start` (default `0`), NOT real epoch milliseconds. A consumer that compares `now()` against an absolute real-epoch
|
|
22
|
+
* constant would diverge; consumers must only compare `now()` values to each other (deriving elapsed intervals from differences), which is the only use a
|
|
23
|
+
* `Date.now()`-style read serves in the consuming pacing path - all its time reads come from the one injected clock.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { TestClock } from "homebridge-plugin-utils";
|
|
29
|
+
*
|
|
30
|
+
* const clock = new TestClock();
|
|
31
|
+
*
|
|
32
|
+
* const waited = clock.delay(100);
|
|
33
|
+
*
|
|
34
|
+
* // Nothing resolves until virtual time crosses the deadline.
|
|
35
|
+
* clock.advance(100);
|
|
36
|
+
*
|
|
37
|
+
* await waited;
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @see Clock
|
|
41
|
+
*
|
|
42
|
+
* @category Testing
|
|
43
|
+
*/
|
|
44
|
+
export declare class TestClock implements Clock {
|
|
45
|
+
#private;
|
|
46
|
+
/**
|
|
47
|
+
* Construct a clock seeded at `start` (default `0`). The seed is the initial value `now()` returns; `advance` moves it forward (or back, for a negative delta).
|
|
48
|
+
*
|
|
49
|
+
* @param start - The initial virtual time, in the consumer's relative timeline. Defaults to `0`.
|
|
50
|
+
*/
|
|
51
|
+
constructor(start?: number);
|
|
52
|
+
/**
|
|
53
|
+
* Advance virtual time by `ms` and resolve every delay whose deadline the new time has reached. The delta is applied regardless of sign, so a negative `ms` moves time
|
|
54
|
+
* backward; `advance(0)` moves time nowhere but STILL flushes any already-due entry (a `delay(0)` or a `delay` with a non-positive `ms`), so a zero or negative delay
|
|
55
|
+
* is never a lost wakeup.
|
|
56
|
+
*
|
|
57
|
+
* Due entries resolve in ASCENDING deadline order; entries that share a deadline keep their FIFO registration order, because the snapshot is taken before any removal
|
|
58
|
+
* and the numeric sort is stable - matching how `setTimeout` fires equal-deadline timers in scheduling order. Each due entry is removed by identity and has its abort
|
|
59
|
+
* listener detached before it resolves, so the resolve path leaks no listener and the iteration is immune to the index shifts a forward in-place splice would cause.
|
|
60
|
+
*
|
|
61
|
+
* @param ms - The amount of virtual time to advance, in milliseconds. May be zero or negative.
|
|
62
|
+
*/
|
|
63
|
+
advance(ms: number): void;
|
|
64
|
+
/**
|
|
65
|
+
* Register a delay that resolves when virtual time reaches `this.now() + ms`, or rejects with an `AbortError` (matching `node:timers/promises`) if `init.signal` aborts
|
|
66
|
+
* first. A non-positive `ms` yields a deadline at or before the current time, which the very next {@link TestClock.advance} (including `advance(0)`) flushes.
|
|
67
|
+
*
|
|
68
|
+
* A pre-aborted signal rejects on the executor's microtask exactly as `systemClock` does (NOT a synchronous throw): {@link onAbort} fires the handler inline, which
|
|
69
|
+
* removes the just-registered entry and rejects, so the entry never lingers in `pending`.
|
|
70
|
+
*
|
|
71
|
+
* @param ms - The delay, in milliseconds. May be zero or negative (flushed by the next `advance`).
|
|
72
|
+
* @param init - Optional init options. A supplied `signal` rejects the wait with an `AbortError` when it aborts.
|
|
73
|
+
*
|
|
74
|
+
* @returns A promise that resolves when the deadline is crossed, or rejects with an `AbortError` if the signal aborts first.
|
|
75
|
+
*/
|
|
76
|
+
delay(ms: number, init?: {
|
|
77
|
+
signal?: AbortSignal;
|
|
78
|
+
}): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Return the current virtual time. Compare these values to each other to derive elapsed intervals - they are a relative timeline, not real epoch milliseconds.
|
|
81
|
+
*
|
|
82
|
+
* @returns The current virtual time.
|
|
83
|
+
*/
|
|
84
|
+
now(): number;
|
|
85
|
+
/**
|
|
86
|
+
* The number of registered delays that have neither resolved nor rejected. A test reads this to assert a consumer registered its waits and later cleared them (no
|
|
87
|
+
* leak).
|
|
88
|
+
*
|
|
89
|
+
* @returns The count of unsettled delays.
|
|
90
|
+
*/
|
|
91
|
+
get pending(): number;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=clock-double.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* clock-double.ts: A reusable, controllable Clock test double - virtual time a test advances explicitly - for the injectable Clock seam in clock.ts.
|
|
4
|
+
*/
|
|
5
|
+
import { onAbort } from "./util.js";
|
|
6
|
+
/**
|
|
7
|
+
* Construct the rejection a {@link TestClock} `delay` produces when its signal aborts, matching `node:timers/promises` `setTimeout` exactly: a plain `Error` whose `name`
|
|
8
|
+
* is `"AbortError"` and whose `code` is the STRING `"ABORT_ERR"`. The real primitive's rejection is a dedicated internal class (not a `DOMException`, whose `code` is the
|
|
9
|
+
* numeric `20`, and there is no constructable `AbortError` global), so the double cannot match the constructor or prototype identity - it matches the observable `name`
|
|
10
|
+
* and `code` a consumer branches on, which is the contract that matters.
|
|
11
|
+
*
|
|
12
|
+
* @returns The `AbortError`-shaped rejection.
|
|
13
|
+
*/
|
|
14
|
+
function abortError() {
|
|
15
|
+
const error = new Error("The operation was aborted");
|
|
16
|
+
error.name = "AbortError";
|
|
17
|
+
// `code` is not a standard `Error` field, so assign it through an indexed widening rather than declaring a one-off subclass. The STRING value is what the real
|
|
18
|
+
// `node:timers/promises` rejection carries and what a consumer's `error.code === "ABORT_ERR"` check reads.
|
|
19
|
+
error.code = "ABORT_ERR";
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A controllable {@link Clock} double over virtual time. `now()` returns the current virtual time; `delay()` registers a pending wait that resolves only when
|
|
24
|
+
* {@link TestClock.advance} crosses its deadline (in ascending-deadline order), or rejects with an `AbortError` (matching `node:timers/promises` `setTimeout` - `name`
|
|
25
|
+
* `"AbortError"`, `code` `"ABORT_ERR"`, NOT the signal's reason) when its signal aborts. No real timers or wall-clock are used.
|
|
26
|
+
*
|
|
27
|
+
* The virtual time is a RELATIVE timeline seeded at `start` (default `0`), NOT real epoch milliseconds. A consumer that compares `now()` against an absolute real-epoch
|
|
28
|
+
* constant would diverge; consumers must only compare `now()` values to each other (deriving elapsed intervals from differences), which is the only use a
|
|
29
|
+
* `Date.now()`-style read serves in the consuming pacing path - all its time reads come from the one injected clock.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { TestClock } from "homebridge-plugin-utils";
|
|
35
|
+
*
|
|
36
|
+
* const clock = new TestClock();
|
|
37
|
+
*
|
|
38
|
+
* const waited = clock.delay(100);
|
|
39
|
+
*
|
|
40
|
+
* // Nothing resolves until virtual time crosses the deadline.
|
|
41
|
+
* clock.advance(100);
|
|
42
|
+
*
|
|
43
|
+
* await waited;
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @see Clock
|
|
47
|
+
*
|
|
48
|
+
* @category Testing
|
|
49
|
+
*/
|
|
50
|
+
export class TestClock {
|
|
51
|
+
// The current virtual time. Seeded by the constructor and moved only by `advance`.
|
|
52
|
+
#now;
|
|
53
|
+
// The registered, not-yet-settled delays. An entry leaves this list exactly once - either when `advance` crosses its deadline or when its signal aborts - via
|
|
54
|
+
// `#remove`, which splices by identity so a mixed resolve-and-abort sequence never strands or mis-removes an entry.
|
|
55
|
+
#pending = [];
|
|
56
|
+
/**
|
|
57
|
+
* Construct a clock seeded at `start` (default `0`). The seed is the initial value `now()` returns; `advance` moves it forward (or back, for a negative delta).
|
|
58
|
+
*
|
|
59
|
+
* @param start - The initial virtual time, in the consumer's relative timeline. Defaults to `0`.
|
|
60
|
+
*/
|
|
61
|
+
constructor(start = 0) {
|
|
62
|
+
this.#now = start;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Advance virtual time by `ms` and resolve every delay whose deadline the new time has reached. The delta is applied regardless of sign, so a negative `ms` moves time
|
|
66
|
+
* backward; `advance(0)` moves time nowhere but STILL flushes any already-due entry (a `delay(0)` or a `delay` with a non-positive `ms`), so a zero or negative delay
|
|
67
|
+
* is never a lost wakeup.
|
|
68
|
+
*
|
|
69
|
+
* Due entries resolve in ASCENDING deadline order; entries that share a deadline keep their FIFO registration order, because the snapshot is taken before any removal
|
|
70
|
+
* and the numeric sort is stable - matching how `setTimeout` fires equal-deadline timers in scheduling order. Each due entry is removed by identity and has its abort
|
|
71
|
+
* listener detached before it resolves, so the resolve path leaks no listener and the iteration is immune to the index shifts a forward in-place splice would cause.
|
|
72
|
+
*
|
|
73
|
+
* @param ms - The amount of virtual time to advance, in milliseconds. May be zero or negative.
|
|
74
|
+
*/
|
|
75
|
+
advance(ms) {
|
|
76
|
+
this.#now += ms;
|
|
77
|
+
// Snapshot the due entries BEFORE mutating `#pending`, then sort them into deadline order. Filtering off a live array while removing from it would shift indices and
|
|
78
|
+
// strand entries; the snapshot first decouples the iteration from the removal. The sort is a stable numeric comparator, so equal deadlines preserve FIFO order.
|
|
79
|
+
const due = this.#pending.filter((entry) => entry.deadline <= this.#now).sort((a, b) => a.deadline - b.deadline);
|
|
80
|
+
for (const entry of due) {
|
|
81
|
+
// Remove by identity first so a re-entrant observer sees the correct `pending` count, then detach the abort listener (present only when this delay had a signal) so
|
|
82
|
+
// the resolve path leaves no listener on a long-lived signal, then settle the caller's promise.
|
|
83
|
+
this.#remove(entry);
|
|
84
|
+
entry.dispose?.[Symbol.dispose]();
|
|
85
|
+
entry.resolve();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Register a delay that resolves when virtual time reaches `this.now() + ms`, or rejects with an `AbortError` (matching `node:timers/promises`) if `init.signal` aborts
|
|
90
|
+
* first. A non-positive `ms` yields a deadline at or before the current time, which the very next {@link TestClock.advance} (including `advance(0)`) flushes.
|
|
91
|
+
*
|
|
92
|
+
* A pre-aborted signal rejects on the executor's microtask exactly as `systemClock` does (NOT a synchronous throw): {@link onAbort} fires the handler inline, which
|
|
93
|
+
* removes the just-registered entry and rejects, so the entry never lingers in `pending`.
|
|
94
|
+
*
|
|
95
|
+
* @param ms - The delay, in milliseconds. May be zero or negative (flushed by the next `advance`).
|
|
96
|
+
* @param init - Optional init options. A supplied `signal` rejects the wait with an `AbortError` when it aborts.
|
|
97
|
+
*
|
|
98
|
+
* @returns A promise that resolves when the deadline is crossed, or rejects with an `AbortError` if the signal aborts first.
|
|
99
|
+
*/
|
|
100
|
+
delay(ms, init) {
|
|
101
|
+
const { promise, reject, resolve } = Promise.withResolvers();
|
|
102
|
+
const entry = { deadline: this.#now + ms, resolve };
|
|
103
|
+
// Register the entry FIRST so a pre-aborted signal's inline `onAbort` handler (below) can find and remove it. `onAbort` runs the handler synchronously when the
|
|
104
|
+
// signal is already aborted, so for a pre-aborted signal the entry is pushed and then immediately removed-and-rejected within this call - settling on the executor's
|
|
105
|
+
// microtask, identical to `systemClock`. When no signal is supplied, `dispose` stays undefined and the entry only ever leaves via `advance`.
|
|
106
|
+
this.#pending.push(entry);
|
|
107
|
+
if (init?.signal !== undefined) {
|
|
108
|
+
entry.dispose = onAbort(init.signal, () => {
|
|
109
|
+
this.#remove(entry);
|
|
110
|
+
reject(abortError());
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return promise;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Return the current virtual time. Compare these values to each other to derive elapsed intervals - they are a relative timeline, not real epoch milliseconds.
|
|
117
|
+
*
|
|
118
|
+
* @returns The current virtual time.
|
|
119
|
+
*/
|
|
120
|
+
now() {
|
|
121
|
+
return this.#now;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The number of registered delays that have neither resolved nor rejected. A test reads this to assert a consumer registered its waits and later cleared them (no
|
|
125
|
+
* leak).
|
|
126
|
+
*
|
|
127
|
+
* @returns The count of unsettled delays.
|
|
128
|
+
*/
|
|
129
|
+
get pending() {
|
|
130
|
+
return this.#pending.length;
|
|
131
|
+
}
|
|
132
|
+
// Remove `entry` from `#pending` by identity. A guarded `indexOf` + `splice` makes a second removal (e.g. an abort that races a resolve) a safe no-op and
|
|
133
|
+
// never removes the wrong entry, so `#pending` stays consistent across any resolve-and-abort interleaving.
|
|
134
|
+
#remove(entry) {
|
|
135
|
+
const index = this.#pending.indexOf(entry);
|
|
136
|
+
if (index !== -1) {
|
|
137
|
+
this.#pending.splice(index, 1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=clock-double.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock-double.js","sourceRoot":"","sources":["../src/clock-double.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiBH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC;;;;;;;GAOG;AACH,SAAS,UAAU;IAEjB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAErD,KAAK,CAAC,IAAI,GAAG,YAAY,CAAC;IAE1B,+JAA+J;IAC/J,2GAA2G;IAC1G,KAAkC,CAAC,IAAI,GAAG,WAAW,CAAC;IAEvD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,OAAO,SAAS;IAEpB,mFAAmF;IACnF,IAAI,CAAS;IAEb,8JAA8J;IAC9J,oHAAoH;IAC3G,QAAQ,GAAiB,EAAE,CAAC;IAErC;;;;OAIG;IACH,YAAmB,KAAK,GAAG,CAAC;QAE1B,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;IACpB,CAAC;IAED;;;;;;;;;;OAUG;IACI,OAAO,CAAC,EAAU;QAEvB,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;QAEhB,qKAAqK;QACrK,gKAAgK;QAChK,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;QAEjH,KAAI,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;YAEvB,oKAAoK;YACpK,gGAAgG;YAChG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACpB,KAAK,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAClC,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,CAAC;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACI,KAAK,CAAC,EAAU,EAAE,IAA+B;QAEtD,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,GAA+B,OAAO,CAAC,aAAa,EAAE,CAAC;QACzF,MAAM,KAAK,GAAe,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC;QAEhE,gKAAgK;QAChK,qKAAqK;QACrK,6IAA6I;QAC7I,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE1B,IAAG,IAAI,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;YAE9B,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;gBAExC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBACpB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;YACvB,CAAC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;OAIG;IACI,GAAG;QAER,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;;;OAKG;IACH,IAAW,OAAO;QAEhB,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,0JAA0J;IAC1J,2GAA2G;IAC3G,OAAO,CAAC,KAAiB;QAEvB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAE3C,IAAG,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YAEhB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;CACF"}
|
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The injectable wall-clock contract: the platform time primitives time-dependent code reads. A consumer holds a `Clock` rather than calling `Date.now()` /
|
|
3
|
+
* `node:timers/promises` `setTimeout` directly, so a test can substitute a controllable double (`TestClock`) and drive time deterministically while production behavior
|
|
4
|
+
* stays unchanged through {@link systemClock}.
|
|
5
|
+
*
|
|
6
|
+
* @see systemClock
|
|
7
|
+
*
|
|
8
|
+
* @category Utilities
|
|
9
|
+
*/
|
|
10
|
+
export interface Clock {
|
|
11
|
+
/**
|
|
12
|
+
* Resolve after `ms` milliseconds, or reject if `init.signal` aborts first. The production {@link systemClock} implements this as `node:timers/promises` `setTimeout`,
|
|
13
|
+
* so an abort rejects with that primitive's `AbortError` (`name` `"AbortError"`, `code` `"ABORT_ERR"`) rather than the signal's reason.
|
|
14
|
+
*
|
|
15
|
+
* @param ms - The delay, in milliseconds.
|
|
16
|
+
* @param init - Optional init options. `signal` cancels the delay - resolving the wait early with a rejection - when it aborts.
|
|
17
|
+
*
|
|
18
|
+
* @returns A promise that resolves after the delay, or rejects with an `AbortError` if the signal aborts first.
|
|
19
|
+
*/
|
|
20
|
+
delay(ms: number, init?: {
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Return the current time as epoch milliseconds. The production {@link systemClock} implements this as `Date.now()`.
|
|
25
|
+
*
|
|
26
|
+
* @returns The current time in epoch milliseconds.
|
|
27
|
+
*/
|
|
28
|
+
now(): number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The behavior-neutral production {@link Clock}: `now()` IS `Date.now()` and `delay()` IS `node:timers/promises` `setTimeout`. A consumer that routes its time reads
|
|
32
|
+
* through this clock instead of calling those primitives directly cannot observe any behavior change - it is the same two calls, one indirection removed at test time.
|
|
33
|
+
*
|
|
34
|
+
* @see Clock
|
|
35
|
+
*
|
|
36
|
+
* @category Utilities
|
|
37
|
+
*/
|
|
38
|
+
export declare const systemClock: Clock;
|
|
39
|
+
//# sourceMappingURL=clock.d.ts.map
|
package/dist/clock.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* clock.ts: An injectable wall-clock seam - the current time and an abortable delay - so time-dependent code can be driven deterministically under test.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* An injectable wall-clock time seam.
|
|
7
|
+
*
|
|
8
|
+
* Time-dependent code reads the platform time primitives it needs: the current epoch time (`Date.now()`) and a delay that can be cancelled (`node:timers/promises`
|
|
9
|
+
* `setTimeout`). Calling those directly bakes real wall-clock time into the code, so a test cannot exercise a pacing or timeout path without multi-second real waits, and
|
|
10
|
+
* `node:test`'s mock timers do not patch the `node:timers/promises` primitives. Holding a {@link Clock} instead - the abstraction over those primitives - inverts the
|
|
11
|
+
* dependency: production wires {@link systemClock}, whose `now()` IS `Date.now()` and whose `delay()` IS `node:timers/promises` `setTimeout`, so routing through the seam
|
|
12
|
+
* is behavior-neutral; a test wires a `TestClock` (see `clock-double.ts`) that advances virtual time explicitly, so the consumer's time-dependent path runs
|
|
13
|
+
* deterministically and instantly.
|
|
14
|
+
*
|
|
15
|
+
* This module imports `node:timers/promises` and is therefore Node-only (not browser-safe), like `util.ts`. A browser-targeted consumer cannot resolve that import.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
20
|
+
/**
|
|
21
|
+
* The behavior-neutral production {@link Clock}: `now()` IS `Date.now()` and `delay()` IS `node:timers/promises` `setTimeout`. A consumer that routes its time reads
|
|
22
|
+
* through this clock instead of calling those primitives directly cannot observe any behavior change - it is the same two calls, one indirection removed at test time.
|
|
23
|
+
*
|
|
24
|
+
* @see Clock
|
|
25
|
+
*
|
|
26
|
+
* @category Utilities
|
|
27
|
+
*/
|
|
28
|
+
export const systemClock = {
|
|
29
|
+
// `setTimeout(ms, value, options)` from `node:timers/promises`: pass `undefined` for the resolution value and forward `init` as the options so the caller's signal
|
|
30
|
+
// cancels the wait. `setTimeout(ms, undefined, opts)` resolves to `Promise<undefined>`, which is assignable to the `Promise<void>` the contract declares.
|
|
31
|
+
delay: (ms, init) => delay(ms, undefined, init),
|
|
32
|
+
now: () => Date.now()
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=clock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.js","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAgC3D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAU;IAEhC,mKAAmK;IACnK,0JAA0J;IAC1J,KAAK,EAAE,CAAC,EAAU,EAAE,IAA+B,EAAiB,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC;IACjG,GAAG,EAAE,GAAW,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;CAC9B,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A container that aggregates disposable resources and disposes them, in reverse (last-in-first-out) order, when the stack itself is disposed.
|
|
3
|
+
*
|
|
4
|
+
* The class satisfies the platform `DisposableStack` interface by construction - the `implements` clause below binds it to `globalThis.DisposableStack`, so the
|
|
5
|
+
* compiler enforces that the name's promise (the platform contract) is kept at the definition site.
|
|
6
|
+
*
|
|
7
|
+
* @category Utilities
|
|
8
|
+
*/
|
|
9
|
+
export declare class DisposableStack implements InstanceType<typeof globalThis.DisposableStack> {
|
|
10
|
+
#private;
|
|
11
|
+
readonly [Symbol.toStringTag] = "DisposableStack";
|
|
12
|
+
/**
|
|
13
|
+
* Whether this stack has been disposed.
|
|
14
|
+
*/
|
|
15
|
+
get disposed(): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Register a {@link Disposable} whose `[Symbol.dispose]()` runs when this stack is disposed, returning the value unchanged.
|
|
18
|
+
*
|
|
19
|
+
* `null` and `undefined` pass through without being registered. The dispose method is captured at registration time and invoked with the value as its receiver, so
|
|
20
|
+
* a later mutation of `value[Symbol.dispose]` cannot change what runs.
|
|
21
|
+
*
|
|
22
|
+
* @param value - The resource to register, or `null`/`undefined` to skip registration.
|
|
23
|
+
* @returns The provided `value`.
|
|
24
|
+
*/
|
|
25
|
+
use<T extends Disposable | null | undefined>(value: T): T;
|
|
26
|
+
/**
|
|
27
|
+
* Register a value together with an explicit disposal callback, returning the value unchanged. The callback is invoked with the value as its first argument when
|
|
28
|
+
* this stack is disposed.
|
|
29
|
+
*
|
|
30
|
+
* @param value - The value to associate with the callback.
|
|
31
|
+
* @param onDispose - The disposal callback, invoked with `value`.
|
|
32
|
+
* @returns The provided `value`.
|
|
33
|
+
*/
|
|
34
|
+
adopt<T>(value: T, onDispose: (value: T) => void): T;
|
|
35
|
+
/**
|
|
36
|
+
* Register a callback to run when this stack is disposed.
|
|
37
|
+
*
|
|
38
|
+
* @param onDispose - The callback to run on disposal.
|
|
39
|
+
*/
|
|
40
|
+
defer(onDispose: () => void): void;
|
|
41
|
+
/**
|
|
42
|
+
* Move every pending disposer out of this stack into a fresh {@link DisposableStack}, preserving registration order, and mark this stack disposed without running
|
|
43
|
+
* anything. This is the "commit" primitive: after a successful acquire sequence, moving the disposers away disarms this stack's scope-bound cleanup while handing
|
|
44
|
+
* responsibility for those resources to the returned stack.
|
|
45
|
+
*
|
|
46
|
+
* @returns A new stack owning the transferred disposers.
|
|
47
|
+
*/
|
|
48
|
+
move(): DisposableStack;
|
|
49
|
+
/**
|
|
50
|
+
* Dispose every registered resource in reverse (last-in-first-out) order. A second call is a no-op. Every disposer runs even when an earlier one throws: a single
|
|
51
|
+
* failure is rethrown after the sweep completes, and multiple failures chain through `SuppressedError` (the newest failure wrapping the accumulated one).
|
|
52
|
+
*/
|
|
53
|
+
dispose(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Dispose this stack. Enables `using` semantics by delegating to {@link dispose}.
|
|
56
|
+
*/
|
|
57
|
+
[Symbol.dispose](): void;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=disposable-stack.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* disposable-stack.ts: An in-package stand-in for the platform `DisposableStack` global that the library's own port allocator relies on.
|
|
4
|
+
*/
|
|
5
|
+
// Build a `SuppressedError` that links a newer disposal failure to the error it supersedes, matching the spec's multi-failure aggregation shape. We read the platform
|
|
6
|
+
// `SuppressedError` global fresh on every call rather than caching a reference, because that constructor is itself part of the Node 24 explicit-resource-management
|
|
7
|
+
// surface and can be absent on the engines floor this module exists to serve. When it is missing we synthesize a structurally-identical stand-in - an `Error` whose
|
|
8
|
+
// name is "SuppressedError" carrying `error` and `suppressed` - so multi-failure aggregation is observably identical whether or not the global is present. The typed
|
|
9
|
+
// optional read tells the truth the platform lib cannot: on this engines floor the global may not exist.
|
|
10
|
+
function createSuppressedError(error, suppressed) {
|
|
11
|
+
const suppressedErrorConstructor = globalThis.SuppressedError;
|
|
12
|
+
if (suppressedErrorConstructor !== undefined) {
|
|
13
|
+
return new suppressedErrorConstructor(error, suppressed);
|
|
14
|
+
}
|
|
15
|
+
const synthesized = new Error();
|
|
16
|
+
synthesized.error = error;
|
|
17
|
+
synthesized.name = "SuppressedError";
|
|
18
|
+
synthesized.suppressed = suppressed;
|
|
19
|
+
return synthesized;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A container that aggregates disposable resources and disposes them, in reverse (last-in-first-out) order, when the stack itself is disposed.
|
|
23
|
+
*
|
|
24
|
+
* The class satisfies the platform `DisposableStack` interface by construction - the `implements` clause below binds it to `globalThis.DisposableStack`, so the
|
|
25
|
+
* compiler enforces that the name's promise (the platform contract) is kept at the definition site.
|
|
26
|
+
*
|
|
27
|
+
* @category Utilities
|
|
28
|
+
*/
|
|
29
|
+
export class DisposableStack {
|
|
30
|
+
// The pending disposers in registration order. A `null` array is the disposed state: pairing "disposed" with "no pending disposers" in a single field makes the
|
|
31
|
+
// disposed-with-pending-work state unrepresentable, so every member checks this one field to decide whether it may still register or run work.
|
|
32
|
+
#disposers = [];
|
|
33
|
+
// The platform tag reported by `Object.prototype.toString`, matching the native class.
|
|
34
|
+
[Symbol.toStringTag] = "DisposableStack";
|
|
35
|
+
/**
|
|
36
|
+
* Whether this stack has been disposed.
|
|
37
|
+
*/
|
|
38
|
+
get disposed() {
|
|
39
|
+
return this.#disposers === null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Register a {@link Disposable} whose `[Symbol.dispose]()` runs when this stack is disposed, returning the value unchanged.
|
|
43
|
+
*
|
|
44
|
+
* `null` and `undefined` pass through without being registered. The dispose method is captured at registration time and invoked with the value as its receiver, so
|
|
45
|
+
* a later mutation of `value[Symbol.dispose]` cannot change what runs.
|
|
46
|
+
*
|
|
47
|
+
* @param value - The resource to register, or `null`/`undefined` to skip registration.
|
|
48
|
+
* @returns The provided `value`.
|
|
49
|
+
*/
|
|
50
|
+
use(value) {
|
|
51
|
+
if (this.#disposers === null) {
|
|
52
|
+
throw new ReferenceError("Cannot use a resource on a disposed DisposableStack.");
|
|
53
|
+
}
|
|
54
|
+
// Skip registration for null and undefined, returning them unchanged. We test undefined with `typeof` so the generic parameter narrows cleanly to a Disposable for
|
|
55
|
+
// the member access below.
|
|
56
|
+
if ((value === null) || (typeof value === "undefined")) {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
// Capture the dispose method now, at registration time, matching the spec's registration-time capture... a later mutation of `value[Symbol.dispose]` must not change
|
|
60
|
+
// what runs. We read it through `unknown` because use() is a trust boundary: a value can satisfy the Disposable type nominally yet present a non-callable member at
|
|
61
|
+
// runtime, which the spec requires we reject with a TypeError.
|
|
62
|
+
const disposeMethod = value[Symbol.dispose];
|
|
63
|
+
if (typeof disposeMethod !== "function") {
|
|
64
|
+
throw new TypeError("The value passed to DisposableStack.use() is not disposable.");
|
|
65
|
+
}
|
|
66
|
+
const boundDispose = disposeMethod;
|
|
67
|
+
this.#disposers.push(() => {
|
|
68
|
+
boundDispose.call(value);
|
|
69
|
+
});
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Register a value together with an explicit disposal callback, returning the value unchanged. The callback is invoked with the value as its first argument when
|
|
74
|
+
* this stack is disposed.
|
|
75
|
+
*
|
|
76
|
+
* @param value - The value to associate with the callback.
|
|
77
|
+
* @param onDispose - The disposal callback, invoked with `value`.
|
|
78
|
+
* @returns The provided `value`.
|
|
79
|
+
*/
|
|
80
|
+
adopt(value, onDispose) {
|
|
81
|
+
if (this.#disposers === null) {
|
|
82
|
+
throw new ReferenceError("Cannot adopt a resource on a disposed DisposableStack.");
|
|
83
|
+
}
|
|
84
|
+
if (typeof onDispose !== "function") {
|
|
85
|
+
throw new TypeError("The onDispose callback passed to DisposableStack.adopt() is not a function.");
|
|
86
|
+
}
|
|
87
|
+
this.#disposers.push(() => onDispose(value));
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Register a callback to run when this stack is disposed.
|
|
92
|
+
*
|
|
93
|
+
* @param onDispose - The callback to run on disposal.
|
|
94
|
+
*/
|
|
95
|
+
defer(onDispose) {
|
|
96
|
+
if (this.#disposers === null) {
|
|
97
|
+
throw new ReferenceError("Cannot defer a callback on a disposed DisposableStack.");
|
|
98
|
+
}
|
|
99
|
+
if (typeof onDispose !== "function") {
|
|
100
|
+
throw new TypeError("The onDispose callback passed to DisposableStack.defer() is not a function.");
|
|
101
|
+
}
|
|
102
|
+
this.#disposers.push(onDispose);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Move every pending disposer out of this stack into a fresh {@link DisposableStack}, preserving registration order, and mark this stack disposed without running
|
|
106
|
+
* anything. This is the "commit" primitive: after a successful acquire sequence, moving the disposers away disarms this stack's scope-bound cleanup while handing
|
|
107
|
+
* responsibility for those resources to the returned stack.
|
|
108
|
+
*
|
|
109
|
+
* @returns A new stack owning the transferred disposers.
|
|
110
|
+
*/
|
|
111
|
+
move() {
|
|
112
|
+
if (this.#disposers === null) {
|
|
113
|
+
throw new ReferenceError("Cannot move a disposed DisposableStack.");
|
|
114
|
+
}
|
|
115
|
+
const moved = new DisposableStack();
|
|
116
|
+
// Transfer the array by reference so order is preserved, then mark this stack disposed without running any disposer.
|
|
117
|
+
moved.#disposers = this.#disposers;
|
|
118
|
+
this.#disposers = null;
|
|
119
|
+
return moved;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Dispose every registered resource in reverse (last-in-first-out) order. A second call is a no-op. Every disposer runs even when an earlier one throws: a single
|
|
123
|
+
* failure is rethrown after the sweep completes, and multiple failures chain through `SuppressedError` (the newest failure wrapping the accumulated one).
|
|
124
|
+
*/
|
|
125
|
+
dispose() {
|
|
126
|
+
if (this.#disposers === null) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Detach the array and mark disposed before running anything, so a disposer that re-enters this stack observes the disposed state and cannot double-run work.
|
|
130
|
+
const disposers = this.#disposers;
|
|
131
|
+
this.#disposers = null;
|
|
132
|
+
let hasError = false;
|
|
133
|
+
let heldError;
|
|
134
|
+
// Reverse the detached array in place - we hold the only reference to it, so mutating it is safe - and run the disposers last-in-first-out.
|
|
135
|
+
for (const disposer of disposers.reverse()) {
|
|
136
|
+
try {
|
|
137
|
+
disposer();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
heldError = hasError ? createSuppressedError(error, heldError) : error;
|
|
141
|
+
hasError = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (hasError) {
|
|
145
|
+
throw heldError;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Dispose this stack. Enables `using` semantics by delegating to {@link dispose}.
|
|
150
|
+
*/
|
|
151
|
+
[Symbol.dispose]() {
|
|
152
|
+
this.dispose();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=disposable-stack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disposable-stack.js","sourceRoot":"","sources":["../src/disposable-stack.ts"],"names":[],"mappings":"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"}
|