homebridge-plugin-utils 1.35.0 → 2.0.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 +95 -0
- package/dist/cli/index.js +355 -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/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 +120 -0
- package/dist/ffmpeg/mp4-assembler.js +387 -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 +302 -188
- package/dist/ffmpeg/record.js +408 -567
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/recording-process-double.d.ts +144 -0
- package/dist/ffmpeg/recording-process-double.js +168 -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 +473 -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 +12 -6
- package/dist/index.js +7 -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 +200 -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 +327 -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 +677 -0
- package/dist/ui/webUi.mjs +192 -87
- package/dist/util.d.ts +602 -45
- package/dist/util.js +783 -78
- 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
package/dist/util.d.ts
CHANGED
|
@@ -1,3 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript Utilities.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { Logging } from "homebridge";
|
|
7
|
+
/**
|
|
8
|
+
* The canonical set of abort reasons used across `homebridge-plugin-utils`.
|
|
9
|
+
*
|
|
10
|
+
* Every long-lived resource class in the library exposes an {@link AbortSignal} whose abort reason is normally an {@link HbpuAbortError} carrying one of these names.
|
|
11
|
+
* Consumers discriminate on the `.name` field. Platform errors produced by `AbortSignal.timeout()` and bare `controller.abort()` interoperate by matching names:
|
|
12
|
+
* `TimeoutError` and `AbortError` from the platform both flow through the same discrimination paths unchanged.
|
|
13
|
+
*
|
|
14
|
+
* @remarks When to use each reason:
|
|
15
|
+
*
|
|
16
|
+
* - `"closed"` - resource ended naturally (process exited with code 0, socket closed by peer, MQTT disconnected cleanly).
|
|
17
|
+
* - `"failed"` - resource ended because of an error (non-zero exit, spawn ENOENT, upstream error). Attach the underlying error via `cause`.
|
|
18
|
+
* - `"replaced"` - a newer operation superseded this one (new stream request, livestream discontinuity, new MQTT subscription overwriting the old handler).
|
|
19
|
+
* - `"shutdown"` - orderly teardown from parent lifecycle (plugin stop, controller close, session end). Default when `abort()` is called with no reason.
|
|
20
|
+
* - `"timeout"` - resource was stuck and exceeded a watchdog window. `AbortSignal.timeout()`'s platform `TimeoutError` carries a matching `.name`.
|
|
21
|
+
*
|
|
22
|
+
* @category Utilities
|
|
23
|
+
*/
|
|
24
|
+
export type HbpuAbortReason = "closed" | "failed" | "replaced" | "shutdown" | "timeout";
|
|
25
|
+
/**
|
|
26
|
+
* Options accepted by {@link HbpuAbortError}'s constructor.
|
|
27
|
+
*
|
|
28
|
+
* @category Utilities
|
|
29
|
+
*/
|
|
30
|
+
export interface HbpuAbortErrorOptions {
|
|
31
|
+
/**
|
|
32
|
+
* The underlying cause of the abort. For `"failed"` reasons this is typically the upstream error. For `"failed"` exits from child processes, this is idiomatically a
|
|
33
|
+
* structured object carrying diagnostic context (e.g., `{ exitCode, exitSignal }`) - specialized subclasses may tighten this later.
|
|
34
|
+
*/
|
|
35
|
+
cause?: unknown;
|
|
36
|
+
/**
|
|
37
|
+
* Optional human-readable message. When omitted, the error's `message` defaults to the reason name, which is sufficient for discrimination-based handling.
|
|
38
|
+
*/
|
|
39
|
+
message?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The canonical abort error used across `homebridge-plugin-utils`.
|
|
43
|
+
*
|
|
44
|
+
* `HbpuAbortError` is a lightweight subclass of `Error` whose `name` field is one of the values in {@link HbpuAbortReason}. It is the value passed to
|
|
45
|
+
* `AbortController.abort(reason)` by every HBPU-owned resource class and is surfaced back to callers as a signal's `reason` or as the rejection of any HBPU-awaited
|
|
46
|
+
* promise that ends because of an abort.
|
|
47
|
+
*
|
|
48
|
+
* @remarks The base class is intentionally minimal. Domain-specific context (FFmpeg exit code, MQTT packet id, etc.) travels on `cause` as a structured object rather
|
|
49
|
+
* than as additional fields on this class, so that every consumer that catches an `HbpuAbortError` reads the same shape. Specialized subclasses (e.g.,
|
|
50
|
+
* `FfmpegAbortError` carrying typed exit context) may be introduced later when there is a concrete need - not preemptively.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { HbpuAbortError, isHbpuAbortReason } from "homebridge-plugin-utils";
|
|
56
|
+
*
|
|
57
|
+
* try {
|
|
58
|
+
*
|
|
59
|
+
* await recording.segments().next();
|
|
60
|
+
* } catch(error: unknown) {
|
|
61
|
+
*
|
|
62
|
+
* if(isHbpuAbortReason(error, "replaced")) {
|
|
63
|
+
*
|
|
64
|
+
* // Stream was superseded; this is expected during a livestream discontinuity.
|
|
65
|
+
* return;
|
|
66
|
+
* }
|
|
67
|
+
*
|
|
68
|
+
* throw error;
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @category Utilities
|
|
73
|
+
*/
|
|
74
|
+
export declare class HbpuAbortError extends Error {
|
|
75
|
+
/**
|
|
76
|
+
* The discriminator. Matches one of {@link HbpuAbortReason}.
|
|
77
|
+
*/
|
|
78
|
+
readonly name: HbpuAbortReason;
|
|
79
|
+
/**
|
|
80
|
+
* Construct a new `HbpuAbortError`.
|
|
81
|
+
*
|
|
82
|
+
* @param reason - The abort reason (also assigned to `.name`).
|
|
83
|
+
* @param options - Optional `cause` for structured diagnostic context, and an optional human-readable `message`.
|
|
84
|
+
*/
|
|
85
|
+
constructor(reason: HbpuAbortReason, options?: HbpuAbortErrorOptions);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Type guard: returns `true` if `error` is an {@link HbpuAbortError}.
|
|
89
|
+
*
|
|
90
|
+
* Use this to discriminate HBPU's canonical abort errors from arbitrary thrown values, without nesting `instanceof` checks.
|
|
91
|
+
*
|
|
92
|
+
* @param error - The value to test.
|
|
93
|
+
*
|
|
94
|
+
* @returns `true` if `error` is an `HbpuAbortError` instance.
|
|
95
|
+
*
|
|
96
|
+
* @category Utilities
|
|
97
|
+
*/
|
|
98
|
+
export declare function isHbpuAbortError(error: unknown): error is HbpuAbortError;
|
|
99
|
+
/**
|
|
100
|
+
* Convenience type predicate: returns `true` if `error` is an {@link HbpuAbortError} whose `.name` matches `reason`, and narrows the type so callers can read
|
|
101
|
+
* `error.cause` and related fields without further casts.
|
|
102
|
+
*
|
|
103
|
+
* Collapses the common "is this an HBPU abort, and was it this specific reason?" question into a single call, avoiding the `instanceof` + `.name` nesting that appears
|
|
104
|
+
* throughout consuming code. The generic parameter `R` preserves the specific reason string in the narrowed type so callers that discriminate further by name get the
|
|
105
|
+
* literal narrowed form automatically.
|
|
106
|
+
*
|
|
107
|
+
* @typeParam R - The specific reason being matched. Defaulted by inference from `reason`.
|
|
108
|
+
* @param error - The value to test.
|
|
109
|
+
* @param reason - The abort reason to match.
|
|
110
|
+
*
|
|
111
|
+
* @returns `true` if `error` is an `HbpuAbortError` with the given reason.
|
|
112
|
+
*
|
|
113
|
+
* @category Utilities
|
|
114
|
+
*/
|
|
115
|
+
export declare function isHbpuAbortReason<R extends HbpuAbortReason>(error: unknown, reason: R): error is HbpuAbortError & {
|
|
116
|
+
name: R;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Test whether an abort reason indicates a timeout. Matches both the canonical {@link HbpuAbortError} with `"timeout"` name - produced by project watchdogs
|
|
120
|
+
* ({@link Watchdog}, the inactivity monitors on `FfmpegProcess` / `RtpDemuxer` / `Mp4SegmentAssembler`) - and the platform {@link DOMException}/`Error` whose
|
|
121
|
+
* `.name === "TimeoutError"` - produced by `AbortSignal.timeout()`. Consumers discriminate on a single predicate regardless of which code path originated the timeout.
|
|
122
|
+
*
|
|
123
|
+
* Exists because every long-lived resource class exposes an `isTimedOut` getter with identical branching logic; routing all of them through this single predicate
|
|
124
|
+
* enforces one taxonomy and eliminates drift if the project ever needs to add, say, a third timeout shape (e.g., an upstream-framework cancellation).
|
|
125
|
+
*
|
|
126
|
+
* @param reason - Any value found on `AbortSignal.reason`. Plain objects, non-errors, and `undefined` all return `false`.
|
|
127
|
+
*
|
|
128
|
+
* @returns `true` when the reason is a timeout in either supported shape.
|
|
129
|
+
*
|
|
130
|
+
* @category Utilities
|
|
131
|
+
*/
|
|
132
|
+
export declare function isTimeoutReason(reason: unknown): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Register a one-shot abort handler on `signal` and return a {@link Disposable} whose `[Symbol.dispose]` removes the listener. If `signal` is already aborted at
|
|
135
|
+
* call time, `handler` runs inline and the returned handle is a no-op disposer.
|
|
136
|
+
*
|
|
137
|
+
* Closes the well-known pitfall in `AbortSignal.addEventListener("abort", ...)`: listeners attached to an already-aborted signal **do not fire**, so constructors
|
|
138
|
+
* that take a parent signal and attach teardown logic via `addEventListener` silently skip that teardown when the parent is pre-aborted. This helper unifies the
|
|
139
|
+
* register-or-dispatch-immediately shape so every caller handles both cases without re-implementing the check.
|
|
140
|
+
*
|
|
141
|
+
* Returning a `Disposable` serves two patterns through one primitive:
|
|
142
|
+
*
|
|
143
|
+
* - **Long-lived resource-class registrations** (the common case): every HBPU resource class registers its `#teardown` handler in its constructor, intending the
|
|
144
|
+
* listener to live until the composed signal aborts. These callers discard the return value; the `{ once: true }` listener auto-unregisters on fire.
|
|
145
|
+
* - **Scope-bound transient registrations**: observers that only need the listener for a bounded scope (e.g., {@link waitWithSignal}) capture the handle with
|
|
146
|
+
* `using` so the listener is deterministically removed on scope exit even when the promise resolves before the signal aborts. This prevents listener accumulation
|
|
147
|
+
* on long-lived signals that see many short waits.
|
|
148
|
+
*
|
|
149
|
+
* The handler runs at most once: on normal abort, via the `{ once: true }` option on `addEventListener`; on pre-aborted signals, via a direct call here. The caller
|
|
150
|
+
* still decides what to do with the rest of its setup - a constructor that wants to short-circuit further initialization after a pre-aborted signal typically pairs
|
|
151
|
+
* this call with a subsequent `if(signal.aborted) return;` check.
|
|
152
|
+
*
|
|
153
|
+
* @param signal - The abort signal to observe.
|
|
154
|
+
* @param handler - The teardown or cleanup action to run once on abort. Invoked synchronously when `signal.aborted` is already `true` at call time; otherwise
|
|
155
|
+
* attached as a one-shot `"abort"` listener.
|
|
156
|
+
*
|
|
157
|
+
* @returns A {@link Disposable} handle. `[Symbol.dispose]` removes the abort listener (no-op on the pre-aborted path and after the listener has already fired).
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
*
|
|
161
|
+
* ```ts
|
|
162
|
+
* // Long-lived resource-class registration: discard the returned disposer. The listener lives until the composed signal aborts and `{ once: true }` cleans it up.
|
|
163
|
+
* constructor(init: { signal?: AbortSignal }) {
|
|
164
|
+
*
|
|
165
|
+
* this.signal = composeSignals(init.signal, this.#controller.signal);
|
|
166
|
+
*
|
|
167
|
+
* onAbort(this.signal, () => this.#teardown());
|
|
168
|
+
*
|
|
169
|
+
* if(this.signal.aborted) {
|
|
170
|
+
*
|
|
171
|
+
* return;
|
|
172
|
+
* }
|
|
173
|
+
*
|
|
174
|
+
* // ...proceed with setup that only makes sense on a live signal.
|
|
175
|
+
* }
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
*
|
|
180
|
+
* ```ts
|
|
181
|
+
* // Scope-bound transient registration: capture the handle with `using` so the listener auto-removes when the scope exits, even if the signal never aborts.
|
|
182
|
+
* async function abortableWait<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
183
|
+
*
|
|
184
|
+
* using _registration = onAbort(signal, () => {
|
|
185
|
+
* // Abort-driven action goes here.
|
|
186
|
+
* });
|
|
187
|
+
*
|
|
188
|
+
* // `return await promise` (not a bare `return promise`) is load-bearing inside an async function. `using` disposes when the enclosing function body finishes
|
|
189
|
+
* // executing, and without an `await` the body finishes synchronously at the `return` statement - even though the returned promise is still pending. The
|
|
190
|
+
* // listener would therefore be removed the instant the function returned, well before the promise settles. Adding `await` creates a suspension point that
|
|
191
|
+
* // keeps the `using` scope alive until the promise actually settles, which is what the "scope-bound registration" pattern relies on.
|
|
192
|
+
* return await promise;
|
|
193
|
+
* }
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* @category Utilities
|
|
197
|
+
*/
|
|
198
|
+
export declare function onAbort(signal: AbortSignal, handler: () => void): Disposable;
|
|
199
|
+
/**
|
|
200
|
+
* Attach a shared no-op rejection handler to `promise` so that if it rejects and no other observer is attached, Node does not emit an `UnhandledPromiseRejection`
|
|
201
|
+
* warning. Returns the original promise so callers can mark-and-assign in one expression.
|
|
202
|
+
*
|
|
203
|
+
* Use this on internal promise handles (`ready`, `exited`, init segments) that a class exposes for callers who may or may not choose to observe them. Callers who
|
|
204
|
+
* `await` the promise or attach their own `.catch` still see the rejection through their own chain - this helper only marks the promise as observed for Node's
|
|
205
|
+
* unhandled-rejection tracker.
|
|
206
|
+
*
|
|
207
|
+
* @typeParam T - The resolved value type.
|
|
208
|
+
* @param promise - The promise to mark handled.
|
|
209
|
+
*
|
|
210
|
+
* @returns The same promise, for chained assignment.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
*
|
|
214
|
+
* ```ts
|
|
215
|
+
* this.ready = markHandled(readyResolvers.promise);
|
|
216
|
+
* ```
|
|
217
|
+
*
|
|
218
|
+
* @category Utilities
|
|
219
|
+
*/
|
|
220
|
+
export declare function markHandled<T>(promise: Promise<T>): Promise<T>;
|
|
1
221
|
/**
|
|
2
222
|
* A utility type that recursively makes all properties of an object, including nested objects, optional.
|
|
3
223
|
*
|
|
@@ -40,6 +260,7 @@ export type DeepPartial<T> = {
|
|
|
40
260
|
*
|
|
41
261
|
* ```ts
|
|
42
262
|
* type Original = {
|
|
263
|
+
*
|
|
43
264
|
* id: string;
|
|
44
265
|
* nested: { value: number };
|
|
45
266
|
* };
|
|
@@ -157,93 +378,428 @@ export interface HomebridgePluginLogging {
|
|
|
157
378
|
warn: (message: string, ...parameters: unknown[]) => void;
|
|
158
379
|
}
|
|
159
380
|
/**
|
|
160
|
-
* A
|
|
381
|
+
* A shippable no-op {@link HomebridgePluginLogging}: every method accepts the logging signature and discards its arguments. A module-scope singleton - the methods are
|
|
382
|
+
* stateless and side-effect-free, so one shared instance is safe to reuse everywhere - which keeps the omitted-logger path allocation-free. This is the SSOT no-op
|
|
383
|
+
* logger: callers that need a CONCRETE logger but want no output default to it (e.g. a subsystem whose lower layer requires a non-optional logger), and the test-only
|
|
384
|
+
* `silentLog` helper derives from it rather than re-declaring the empty sink.
|
|
385
|
+
*
|
|
386
|
+
* @category Utilities
|
|
387
|
+
*/
|
|
388
|
+
export declare const noOpLog: HomebridgePluginLogging;
|
|
389
|
+
/**
|
|
390
|
+
* Logger union accepted by FFmpeg subsystem APIs that interoperate with both Homebridge's built-in logger and the plugin-side {@link HomebridgePluginLogging} interface.
|
|
391
|
+
* Provides one alias for sites that need this union, keeping the SSOT discipline applied elsewhere in the package consistent for the logger surface.
|
|
392
|
+
*
|
|
393
|
+
* @category Utilities
|
|
394
|
+
*/
|
|
395
|
+
export type Logger = HomebridgePluginLogging | Logging;
|
|
396
|
+
export { formatBps, formatBytes, formatMs, formatPercent, formatSeconds } from "./formatters.ts";
|
|
397
|
+
/**
|
|
398
|
+
* Render an arbitrary thrown value as a clean log-suffix string. Real `Error` instances surface their `.message`; everything else is coerced through `String(...)`.
|
|
399
|
+
* A trailing period is stripped in either case so the embedding log line (which itself ends in a period) does not produce ".." at the end of the rendered output.
|
|
400
|
+
*
|
|
401
|
+
* @param error - The thrown value, typically caught from a `try` block or rejected Promise.
|
|
402
|
+
*
|
|
403
|
+
* @returns The cleaned message ready to interpolate into a log format string.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
*
|
|
407
|
+
* ```ts
|
|
408
|
+
* try {
|
|
409
|
+
*
|
|
410
|
+
* await someOperation();
|
|
411
|
+
* } catch(error) {
|
|
412
|
+
*
|
|
413
|
+
* log.error("Operation failed: %s.", formatErrorMessage(error));
|
|
414
|
+
* }
|
|
415
|
+
* ```
|
|
416
|
+
*
|
|
417
|
+
* @category Utilities
|
|
418
|
+
*/
|
|
419
|
+
export declare function formatErrorMessage(error: unknown): string;
|
|
420
|
+
/**
|
|
421
|
+
* The default backoff policy used by {@link retry}: exponential with a 30-second ceiling, starting at 1 second for the second attempt (`attempt = 2`).
|
|
422
|
+
*
|
|
423
|
+
* @param attempt - The attempt number about to be run (1-indexed; never called with `attempt === 1`, since the first attempt runs immediately).
|
|
424
|
+
*
|
|
425
|
+
* @returns The delay, in milliseconds, to wait before executing `attempt`.
|
|
426
|
+
*
|
|
427
|
+
* @category Utilities
|
|
428
|
+
*/
|
|
429
|
+
export declare function defaultRetryBackoff(attempt: number): number;
|
|
430
|
+
/**
|
|
431
|
+
* Options accepted by {@link retry}.
|
|
432
|
+
*
|
|
433
|
+
* @category Utilities
|
|
434
|
+
*/
|
|
435
|
+
export interface RetryOptions {
|
|
436
|
+
/**
|
|
437
|
+
* Total number of attempts, including the first. Must be >= 1. Defaults to 3. Values less than 1 throw synchronously (rejected promise) at the top of `retry()`. Pass
|
|
438
|
+
* `Infinity` for unbounded attempts - the loop then terminates only on success, an abort, or a `shouldRetry` veto, never on an exhausted budget.
|
|
439
|
+
*/
|
|
440
|
+
attempts?: number;
|
|
441
|
+
/**
|
|
442
|
+
* Backoff policy, invoked with the attempt number (1-indexed) about to be run. The returned value is the delay in milliseconds before running that attempt. Called
|
|
443
|
+
* only between attempts (i.e., never with `attempt === 1`). Defaults to {@link defaultRetryBackoff} (exponential with a 30-second ceiling).
|
|
444
|
+
*/
|
|
445
|
+
backoff?: (attempt: number) => number;
|
|
446
|
+
/**
|
|
447
|
+
* Optional predicate consulted after an attempt throws and attempts remain. Receives the rejected error and the 1-indexed number of the attempt that just failed;
|
|
448
|
+
* return `false` to stop immediately and rethrow that error (no backoff wait, no further attempts), or `true` to retry per the backoff policy. When omitted, every
|
|
449
|
+
* error is retried until `attempts` is exhausted - the existing behavior, unchanged. This is the seam that lets a caller retry some failures and fail fast on others
|
|
450
|
+
* (e.g. retry network faults but give up on an authentication error) without owning the attempt loop itself.
|
|
451
|
+
*/
|
|
452
|
+
shouldRetry?: (error: unknown, attemptNumber: number) => boolean;
|
|
453
|
+
/**
|
|
454
|
+
* Optional abort signal. Aborting cancels any in-flight backoff wait and is forwarded verbatim to `operation` as its own signal argument, so well-behaved operations
|
|
455
|
+
* cancel too. An abort at any point - mid-attempt, mid-backoff, or before the first attempt - rejects the outer promise with the signal's reason.
|
|
456
|
+
*/
|
|
457
|
+
signal?: AbortSignal;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Retry an async operation with configurable attempts and backoff, with first-class abort signal support.
|
|
161
461
|
*
|
|
162
|
-
* @
|
|
462
|
+
* The operation receives the caller's {@link AbortSignal} directly (or a permanent never-aborted sentinel when no caller signal was provided). Well-behaved operations
|
|
463
|
+
* forward this signal to any cancellation-aware API they call (`fetch`, `events.once`, etc.) so the in-flight attempt actually cancels. Between-attempt waits use
|
|
464
|
+
* `node:timers/promises` `setTimeout` with the signal, so abort also interrupts the backoff.
|
|
465
|
+
*
|
|
466
|
+
* @typeParam T - The successful resolution type of `operation`.
|
|
467
|
+
* @param operation - The async work to perform. Receives the composed abort signal; must resolve with a value on success, or throw/reject on failure.
|
|
468
|
+
* @param options - Retry options. See {@link RetryOptions}.
|
|
469
|
+
*
|
|
470
|
+
* @returns Resolves with the first successful operation result. Rejects with the operation's error once the attempt budget is exhausted or a `shouldRetry` predicate
|
|
471
|
+
* vetoes a further attempt, or with the signal's reason if aborted mid-attempt or mid-backoff.
|
|
163
472
|
*
|
|
164
|
-
* @returns Returns the value as a human-readable string.
|
|
165
473
|
* @example
|
|
166
474
|
*
|
|
167
475
|
* ```ts
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
476
|
+
* import { retry } from "homebridge-plugin-utils";
|
|
477
|
+
*
|
|
478
|
+
* const controller = new AbortController();
|
|
479
|
+
*
|
|
480
|
+
* const device = await retry(async (signal) => fetchDevice(id, { signal }), {
|
|
481
|
+
*
|
|
482
|
+
* attempts: 5,
|
|
483
|
+
* backoff: (attempt) => 1_000 * attempt,
|
|
484
|
+
* signal: controller.signal
|
|
485
|
+
* });
|
|
173
486
|
* ```
|
|
487
|
+
*
|
|
488
|
+
* @category Utilities
|
|
174
489
|
*/
|
|
175
|
-
export declare function
|
|
490
|
+
export declare function retry<T>(operation: (signal: AbortSignal) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
176
491
|
/**
|
|
177
|
-
*
|
|
492
|
+
* Wait for `promise` to settle, bailing out early if `signal` aborts before it does.
|
|
178
493
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
494
|
+
* The canonical primitive for "observe this promise but let a caller cancel the wait." Useful inside async flows that reference an external promise (e.g., a resource
|
|
495
|
+
* class's internal state) and need to honor a per-call abort signal without modifying the underlying promise. Whichever settles first wins: `promise` resolves/rejects
|
|
496
|
+
* normally, or the signal aborts and `waitWithSignal` rejects with `signal.reason` - including when the signal was already aborted at call time.
|
|
182
497
|
*
|
|
183
|
-
*
|
|
498
|
+
* The abort listener is attached with `{ once: true }` and explicitly removed when the helper settles, so there is no listener leak regardless of which side wins the
|
|
499
|
+
* race. `promise` is ALWAYS observed via `.then(resolve, reject)` - including on the pre-aborted-signal path - which means attaching `waitWithSignal` to a promise
|
|
500
|
+
* marks it as handled for Node's unhandled-rejection tracker. Callers do not need to wrap `promise` in {@link markHandled} separately.
|
|
184
501
|
*
|
|
185
|
-
* @
|
|
502
|
+
* @typeParam T - The resolved value type of `promise`.
|
|
503
|
+
* @param promise - The promise to wait on.
|
|
504
|
+
* @param signal - The abort signal whose firing interrupts the wait.
|
|
505
|
+
*
|
|
506
|
+
* @returns The promise's resolved value.
|
|
507
|
+
*
|
|
508
|
+
* @throws `signal.reason` if the signal aborts before `promise` settles, or the original rejection if `promise` rejects first.
|
|
186
509
|
*
|
|
187
510
|
* @example
|
|
511
|
+
*
|
|
188
512
|
* ```ts
|
|
189
|
-
*
|
|
190
|
-
* let attempt = 0;
|
|
191
|
-
* const result = await retry(async () => {
|
|
513
|
+
* import { waitWithSignal } from "homebridge-plugin-utils";
|
|
192
514
|
*
|
|
193
|
-
*
|
|
515
|
+
* try {
|
|
194
516
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* }, 1000, 5);
|
|
517
|
+
* const initSegment = await waitWithSignal(assembler.initSegment, callerSignal);
|
|
518
|
+
* } catch {
|
|
198
519
|
*
|
|
199
|
-
*
|
|
520
|
+
* // Caller aborted, or the assembler rejected init. Either way, unwind cleanly.
|
|
521
|
+
* return;
|
|
522
|
+
* }
|
|
200
523
|
* ```
|
|
201
524
|
*
|
|
202
525
|
* @category Utilities
|
|
203
526
|
*/
|
|
204
|
-
export declare function
|
|
527
|
+
export declare function waitWithSignal<T>(promise: Promise<T>, signal: AbortSignal): Promise<T>;
|
|
205
528
|
/**
|
|
206
|
-
*
|
|
529
|
+
* Drain an async iterable and retain only its last `n` values, returned in original (oldest-to-newest) order.
|
|
207
530
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
531
|
+
* The implementation is a true fixed-capacity ring buffer: it allocates a single backing array of length `n` once and overwrites slots modulo `n` as values arrive, so
|
|
532
|
+
* memory stays bounded at `n` entries no matter how long the source runs. It deliberately does NOT accumulate every value and slice the tail at the end - that naive
|
|
533
|
+
* shape would grow without bound on a long-running source (the canonical use here is "the last ~500 lines of a multi-MB log seed"), defeating the entire point of a
|
|
534
|
+
* bounded retainer. When the source yields `n` or fewer values the result is simply those values in order; when it yields more, only the most recent `n` survive.
|
|
211
535
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
536
|
+
* Consumption is eager and complete: the source is iterated to exhaustion before returning, so callers must only pass iterables that terminate (a finite seed window,
|
|
537
|
+
* not an unbounded live stream). A non-positive `n` retains nothing and returns an empty array without iterating the source at all.
|
|
538
|
+
*
|
|
539
|
+
* @typeParam T - The element type of the source.
|
|
540
|
+
* @param source - The async iterable to drain. Must terminate.
|
|
541
|
+
* @param n - The maximum number of trailing values to retain. Values `<= 0` retain nothing.
|
|
542
|
+
*
|
|
543
|
+
* @returns The last `n` values produced by `source`, in original order.
|
|
214
544
|
*
|
|
215
545
|
* @example
|
|
546
|
+
*
|
|
216
547
|
* ```ts
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* // Resolves in 1000ms, timeout is 500ms, so it resolves to null.
|
|
222
|
-
* const slowPromise = new Promise<number>(resolve => setTimeout(() => resolve(42), 1000));
|
|
223
|
-
* const result2 = await runWithTimeout(slowPromise, 500);
|
|
224
|
-
* console.log(result2); // null
|
|
548
|
+
* import { takeLast } from "homebridge-plugin-utils";
|
|
549
|
+
*
|
|
550
|
+
* // Retain only the most recent 500 seed lines from a bounded history window, regardless of how many the source emits.
|
|
551
|
+
* const recent = await takeLast(seedLines, 500);
|
|
225
552
|
* ```
|
|
226
553
|
*
|
|
227
554
|
* @category Utilities
|
|
228
555
|
*/
|
|
229
|
-
export declare function
|
|
556
|
+
export declare function takeLast<T>(source: AsyncIterable<T>, n: number): Promise<T[]>;
|
|
230
557
|
/**
|
|
231
|
-
*
|
|
558
|
+
* Compose one or more optional {@link AbortSignal} sources into a single signal that aborts when any input aborts.
|
|
559
|
+
*
|
|
560
|
+
* Collapses the recurring `parent ? AbortSignal.any([ parent, internal ]) : internal` pattern into a single call, used by every resource class in this library to
|
|
561
|
+
* compose its lifetime signal. Filters out `undefined` inputs, returns the sole defined signal unchanged (no unnecessary `any()` wrapper), and composes two or more
|
|
562
|
+
* defined signals with `AbortSignal.any()`.
|
|
563
|
+
* Throws a {@link TypeError} when every input is `undefined`, because a class whose lifetime is defined by a signal must always have at least one concrete signal to
|
|
564
|
+
* compose against.
|
|
565
|
+
*
|
|
566
|
+
* @param signals - Ordered list of signal sources. `undefined` entries are filtered out; order is preserved among defined entries.
|
|
567
|
+
*
|
|
568
|
+
* @returns The single defined signal when only one was supplied; otherwise a new signal that aborts as soon as any input aborts, carrying the first aborting input's
|
|
569
|
+
* reason as its own `reason`.
|
|
570
|
+
*
|
|
571
|
+
* @throws `TypeError` if every input is `undefined` - the caller passed no concrete signal to compose.
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
*
|
|
575
|
+
* ```ts
|
|
576
|
+
* // Class constructor composing an optional parent signal with the internal controller's signal.
|
|
577
|
+
* this.signal = composeSignals(init.signal, this.#controller.signal);
|
|
578
|
+
*
|
|
579
|
+
* // Per-call composition of the class signal with a caller-supplied per-call signal.
|
|
580
|
+
* const composed = composeSignals(this.signal, init.signal);
|
|
232
581
|
*
|
|
233
|
-
*
|
|
582
|
+
* // Compose an optional caller signal with a derived watchdog timeout.
|
|
583
|
+
* const composed = composeSignals(init.signal, AbortSignal.timeout(PROBE_DEFAULT_TIMEOUT_MS));
|
|
584
|
+
* ```
|
|
234
585
|
*
|
|
235
|
-
* @
|
|
586
|
+
* @category Utilities
|
|
587
|
+
*/
|
|
588
|
+
export declare function composeSignals(...signals: (AbortSignal | undefined)[]): AbortSignal;
|
|
589
|
+
/**
|
|
590
|
+
* Supervise a detached, signal-bound async loop: run the loop, resolve quietly when it ends or its signal aborts, and route any genuine fault to a caller-supplied
|
|
591
|
+
* handler exactly once.
|
|
592
|
+
*
|
|
593
|
+
* Resilient background loops - membership observers, reachability probes, telemetry firehoses - all share one subtle, correctness-critical invariant: a throw is a
|
|
594
|
+
* *fault* only when we did not cause it. When the bound signal is aborted, a throw is the orderly unwinding of a loop the caller already tore down, so it is swallowed
|
|
595
|
+
* silently. Any other throw is a genuine fault and is handed to `onError` exactly once. Hand-copying that swallow-on-abort-versus-surface-once discrimination across
|
|
596
|
+
* call sites that share no ancestor is how it drifts apart; owning it in one generic primitive is how it stays consistent.
|
|
597
|
+
*
|
|
598
|
+
* The home is here, beside {@link composeSignals}, because the envelope is fully generic - it carries no logging policy, no message wording, and makes no detachment
|
|
599
|
+
* decision of its own. When the loops to supervise live on objects with no common base class (so the shared logic cannot be a method), this free function is the only
|
|
600
|
+
* shared home. The returned promise NEVER rejects as a consequence of the loop: it resolves when the loop returns (a finite source ending), when the signal aborts
|
|
601
|
+
* (orderly teardown, swallowed), or once a genuine fault has been delivered to `onError`. The caller owns the rest - `void` the result to fire-and-forget a detached
|
|
602
|
+
* loop, or `await` it for orderly shutdown and in tests.
|
|
603
|
+
*
|
|
604
|
+
* @param options - Supervision inputs.
|
|
605
|
+
* @param options.loop - The loop to run, once. It receives the bound {@link AbortSignal} so it can wire cancellation into `observe()` / `fetch()` / stream reads.
|
|
606
|
+
* @param options.onError - Invoked at most once, with the thrown value unchanged, when the loop faults while the signal is NOT aborted. It carries the caller's entire
|
|
607
|
+
* fault policy (logging, wording, recovery), which is why the primitive itself stays logging-free. A throw from `onError` is a defect in the
|
|
608
|
+
* handler and propagates - the never-rejects guarantee covers the loop, not the handler.
|
|
609
|
+
* @param options.signal - The signal the loop is bound to. Its aborted state is the single source of truth for "did we cause this throw?": aborted means swallow,
|
|
610
|
+
* not aborted means surface.
|
|
611
|
+
*
|
|
612
|
+
* @returns A promise that resolves when the loop ends, the signal aborts, or a fault has been delivered to `onError`. It does not reject for any of those outcomes.
|
|
236
613
|
*
|
|
237
614
|
* @example
|
|
238
|
-
* To sleep for 3 seconds before continuing execute:
|
|
239
615
|
*
|
|
240
616
|
* ```ts
|
|
241
|
-
*
|
|
617
|
+
* import { superviseLoop } from "homebridge-plugin-utils";
|
|
618
|
+
*
|
|
619
|
+
* // Fire-and-forget a detached observer that survives transient faults until its controller is torn down. Aborting `this.signal` unwinds the loop silently; any other
|
|
620
|
+
* // failure is surfaced once through the caller's own wording.
|
|
621
|
+
* void superviseLoop({
|
|
622
|
+
*
|
|
623
|
+
* loop: async (signal) => {
|
|
624
|
+
*
|
|
625
|
+
* for await (const event of client.observe(selector, { signal })) {
|
|
626
|
+
*
|
|
627
|
+
* this.handle(event);
|
|
628
|
+
* }
|
|
629
|
+
* },
|
|
630
|
+
* onError: (error) => this.log.error("The membership observer stopped unexpectedly and will not restart until the next reload: %s", formatErrorMessage(error)),
|
|
631
|
+
* signal: this.signal
|
|
632
|
+
* });
|
|
242
633
|
* ```
|
|
243
634
|
*
|
|
244
635
|
* @category Utilities
|
|
245
636
|
*/
|
|
246
|
-
export declare function
|
|
637
|
+
export declare function superviseLoop(options: {
|
|
638
|
+
loop: (signal: AbortSignal) => Promise<void>;
|
|
639
|
+
onError: (error: unknown) => void;
|
|
640
|
+
signal: AbortSignal;
|
|
641
|
+
}): Promise<void>;
|
|
642
|
+
/**
|
|
643
|
+
* Build the standard {@link superviseLoop} `onError` handler: a reporter that logs a faulted supervised loop with one canonical message, rendering the thrown value
|
|
644
|
+
* through {@link formatErrorMessage}.
|
|
645
|
+
*
|
|
646
|
+
* `superviseLoop` is deliberately logging-free - it owns the swallow-on-abort-versus-surface-once control flow and nothing else, so the wording of what to say when a
|
|
647
|
+
* loop dies lives here, in an explicitly logging companion, never in the primitive itself. Plugins that supervise the same shape of loop - a client observe-loop bound
|
|
648
|
+
* to a terminal shutdown signal with no auto-respawn - all owe the operator the same report: the fault is terminal until the next restart, so the message says exactly
|
|
649
|
+
* that and hands over the one actionable hint. Single-sourcing the template and the formatting here keeps that report from being hand-copied (and quietly drifting)
|
|
650
|
+
* across plugins that share no ancestor - the same "no shared home, so a free function is the home" situation {@link superviseLoop} itself answers.
|
|
651
|
+
*
|
|
652
|
+
* The wording is specific to that bound-to-shutdown, no-respawn lifecycle. A consumer whose loops recover on their own - reconnecting, re-arming, respawning - has
|
|
653
|
+
* different news to deliver and should pass its own `onError` to {@link superviseLoop} rather than this reporter.
|
|
654
|
+
*
|
|
655
|
+
* @param log - The plugin logger the report is written to; its `error` method receives the canonical format string and arguments.
|
|
656
|
+
* @param label - The loop's name, interpolated as the `%s` in `"HomeKit updates for %s ..."` so anyone reading the log can tell which supervised loop died.
|
|
657
|
+
*
|
|
658
|
+
* @returns The `(error) => void` handler to hand to {@link superviseLoop}'s `onError`. It logs exactly once per fault and returns nothing.
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
*
|
|
662
|
+
* ```ts
|
|
663
|
+
* import { loopFaultReporter, superviseLoop } from "homebridge-plugin-utils";
|
|
664
|
+
*
|
|
665
|
+
* // The standard supervised observer: swallow on shutdown, and on a genuine fault log the canonical "<label> loop died, restart to recover" report exactly once.
|
|
666
|
+
* void superviseLoop({
|
|
667
|
+
*
|
|
668
|
+
* loop: (signal) => this.observeMembership(signal),
|
|
669
|
+
* onError: loopFaultReporter(this.log, "membership"),
|
|
670
|
+
* signal: this.signal
|
|
671
|
+
* });
|
|
672
|
+
* ```
|
|
673
|
+
*
|
|
674
|
+
* @category Utilities
|
|
675
|
+
*/
|
|
676
|
+
export declare function loopFaultReporter(log: HomebridgePluginLogging, label: string): (error: unknown) => void;
|
|
677
|
+
/**
|
|
678
|
+
* Options for {@link runWithAbort}. At least one of `signal` or `timeout` must be provided so there is always an abort mechanism. TypeScript enforces this at compile
|
|
679
|
+
* time through a discriminated union - the "no abort mechanism" case is unrepresentable.
|
|
680
|
+
*
|
|
681
|
+
* @category Utilities
|
|
682
|
+
*/
|
|
683
|
+
export type RunWithAbortOptions = {
|
|
684
|
+
signal: AbortSignal;
|
|
685
|
+
timeout?: number;
|
|
686
|
+
} | {
|
|
687
|
+
timeout: number;
|
|
688
|
+
};
|
|
689
|
+
/**
|
|
690
|
+
* Run an abortable operation with signal-based cancellation.
|
|
691
|
+
*
|
|
692
|
+
* The caller provides a factory function that receives an {@link AbortSignal}. The signal fires when the timeout expires, when the caller's own signal aborts, or
|
|
693
|
+
* whichever comes first when both are provided. The factory must forward this signal to any API that accepts one (`events.once`, `fetch`, Node stream methods, etc.) so
|
|
694
|
+
* the underlying work is actually cancelled. When the signal fires and the factory rejects, the rejection is caught and `null` is returned. Genuine (non-abort) errors
|
|
695
|
+
* from the factory propagate normally.
|
|
696
|
+
*
|
|
697
|
+
* @typeParam T - The type of value the factory's promise resolves with.
|
|
698
|
+
* @param fn - A factory that receives the composed abort signal and returns the promise to await.
|
|
699
|
+
* @param options - Abort options. Provide `timeout` (milliseconds), an external `signal`, or both.
|
|
700
|
+
*
|
|
701
|
+
* @returns Resolves with the factory's result if it completes before abort, or `null` if the signal fires first.
|
|
702
|
+
*
|
|
703
|
+
* @example
|
|
704
|
+
* ```ts
|
|
705
|
+
* // Timeout only - cancel after 500ms.
|
|
706
|
+
* const result = await runWithAbort((signal) => fetch(url, { signal }), { timeout: 500 });
|
|
707
|
+
*
|
|
708
|
+
* // External signal only - cancel on demand.
|
|
709
|
+
* const controller = new AbortController();
|
|
710
|
+
* const result2 = await runWithAbort((signal) => once(emitter, "data", { signal }), { signal: controller.signal });
|
|
711
|
+
* controller.abort();
|
|
712
|
+
*
|
|
713
|
+
* // Both - cancel on demand or after 5 seconds, whichever comes first.
|
|
714
|
+
* const result3 = await runWithAbort((signal) => once(emitter, "data", { signal }), { signal: controller.signal, timeout: 5000 });
|
|
715
|
+
* ```
|
|
716
|
+
*
|
|
717
|
+
* @category Utilities
|
|
718
|
+
*/
|
|
719
|
+
export declare function runWithAbort<T>(fn: (signal: AbortSignal) => Promise<T>, options: RunWithAbortOptions): Promise<Nullable<T>>;
|
|
720
|
+
/**
|
|
721
|
+
* Construction-time options for {@link Watchdog}.
|
|
722
|
+
*
|
|
723
|
+
* @property onFire - Callback invoked when the watchdog window lapses without a re-arm. Typically aborts an owning controller (`() => this.#controller.abort(new
|
|
724
|
+
* HbpuAbortError("timeout"))`) but the watchdog itself is agnostic about what the fire does. Runs only when the observed signal has not already
|
|
725
|
+
* aborted; if the signal fires before the timer, `onFire` is skipped entirely.
|
|
726
|
+
* @property signal - The lifetime signal the watchdog observes. When the signal aborts for any reason the pending timer is cleared and no further arms take effect.
|
|
727
|
+
* Typically the consumer's composed lifetime signal (`this.signal`) so both parent-initiated and internal aborts wind the watchdog down.
|
|
728
|
+
* @property timeoutMs - Inactivity window in milliseconds. The first `arm()` schedules a fire at now + `timeoutMs`; each subsequent `arm()` restarts the clock.
|
|
729
|
+
*
|
|
730
|
+
* @category Utilities
|
|
731
|
+
*/
|
|
732
|
+
export interface WatchdogInit {
|
|
733
|
+
onFire: () => void;
|
|
734
|
+
signal: AbortSignal;
|
|
735
|
+
timeoutMs: number;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Re-armable inactivity watchdog.
|
|
739
|
+
*
|
|
740
|
+
* Every long-lived resource class in this library that cares about liveness - an FFmpeg stream's return-port UDP socket, the fMP4 segment assembler's inter-segment
|
|
741
|
+
* pacing, the RTP demuxer's inbound-packet cadence - composes a single `Watchdog` instance to implement the shared "abort if no activity within window" pattern.
|
|
742
|
+
*
|
|
743
|
+
* The semantics are minimal on purpose:
|
|
744
|
+
*
|
|
745
|
+
* - `arm()` starts the window. If a previous arm is still pending, it is replaced; if the observed signal has already aborted or the watchdog has been disposed, the
|
|
746
|
+
* call is a no-op.
|
|
747
|
+
* - If nothing calls `arm()` again within `timeoutMs`, `onFire` runs - but only if the signal is still unaborted at that instant, so a last-moment concurrent abort
|
|
748
|
+
* wins the race and the callback is skipped.
|
|
749
|
+
* - When the observed signal aborts for any reason, the watchdog self-cleans its pending timer; the consumer never needs to unwire it at teardown.
|
|
750
|
+
* - `clear()` cancels any pending fire without aborting anything and leaves the watchdog re-armable.
|
|
751
|
+
* - `[Symbol.dispose]` clears the pending fire and marks the watchdog permanently dead: subsequent `arm()` calls are no-ops. This matches the scope-bound semantics
|
|
752
|
+
* callers expect from `using` - the resource is dead when the block exits, not merely quiescent.
|
|
753
|
+
*
|
|
754
|
+
* This is a `Disposable` (synchronous) rather than `AsyncDisposable` because cancelling a timer is synchronous; there is no background work to await.
|
|
755
|
+
*
|
|
756
|
+
* @example
|
|
757
|
+
*
|
|
758
|
+
* ```ts
|
|
759
|
+
* using watchdog = new Watchdog({
|
|
760
|
+
*
|
|
761
|
+
* onFire: () => this.#controller.abort(new HbpuAbortError("timeout")),
|
|
762
|
+
* signal: this.signal,
|
|
763
|
+
* timeoutMs: this.#inactivityWindowMs
|
|
764
|
+
* });
|
|
765
|
+
*
|
|
766
|
+
* // Each time a packet / segment / message arrives, re-arm so the fire never fires.
|
|
767
|
+
* this.#source.on("data", () => watchdog.arm());
|
|
768
|
+
* watchdog.arm();
|
|
769
|
+
* ```
|
|
770
|
+
*
|
|
771
|
+
* @category Utilities
|
|
772
|
+
*/
|
|
773
|
+
export declare class Watchdog implements Disposable {
|
|
774
|
+
#private;
|
|
775
|
+
/**
|
|
776
|
+
* Construct a new watchdog. The watchdog is dormant until the first `arm()` call, so construction itself schedules no timers.
|
|
777
|
+
*
|
|
778
|
+
* A cleanup handler is registered on `init.signal` through {@link onAbort} so the watchdog auto-cleans when the lifetime signal aborts - consumers do not need to
|
|
779
|
+
* wire teardown manually. On a pre-aborted signal `onAbort` runs the cleanup inline; `clear()` is a no-op on a freshly-constructed watchdog (no timer has been
|
|
780
|
+
* armed yet), so the pre-aborted path unwinds harmlessly. A later `arm()` short-circuits on the same aborted check, so no timer is ever scheduled either way.
|
|
781
|
+
*
|
|
782
|
+
* @param init - Required init options. See {@link WatchdogInit}.
|
|
783
|
+
*/
|
|
784
|
+
constructor(init: WatchdogInit);
|
|
785
|
+
/**
|
|
786
|
+
* Start or restart the inactivity window. The pending timer (if any) is cancelled and a fresh one is scheduled for `timeoutMs` in the future. A no-op when the
|
|
787
|
+
* observed signal has already aborted or the watchdog has been disposed - in either state there is nothing live to protect, and scheduling a timer would violate the
|
|
788
|
+
* `using` contract callers rely on.
|
|
789
|
+
*/
|
|
790
|
+
arm(): void;
|
|
791
|
+
/**
|
|
792
|
+
* Cancel any pending fire without aborting anything and without marking the watchdog as permanently dead. Subsequent `arm()` calls continue to work. Safe to call
|
|
793
|
+
* when no arm is pending - the method is idempotent.
|
|
794
|
+
*/
|
|
795
|
+
clear(): void;
|
|
796
|
+
/**
|
|
797
|
+
* `Disposable` implementation. Clears any pending fire AND permanently disables the watchdog: after this runs, `arm()` is a no-op and no further `onFire` calls can
|
|
798
|
+
* occur. This is the contract `using watchdog = new Watchdog(...)` relies on - the resource is dead when the block exits, not merely quiescent. Idempotent; repeated
|
|
799
|
+
* disposal is a no-op. Because the class does not own an abort controller, disposal does not signal anything to the rest of the system.
|
|
800
|
+
*/
|
|
801
|
+
[Symbol.dispose](): void;
|
|
802
|
+
}
|
|
247
803
|
/**
|
|
248
804
|
* Start case a string, capitalizing the first letter of each word unconditionally.
|
|
249
805
|
*
|
|
@@ -311,3 +867,4 @@ export declare function sanitizeName(name: string): string;
|
|
|
311
867
|
* @category Utilities
|
|
312
868
|
*/
|
|
313
869
|
export declare function validateName(name: string): boolean;
|
|
870
|
+
//# sourceMappingURL=util.d.ts.map
|