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
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 branch 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 branching 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 name-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 tag. 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 distinguish 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 distinguish 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 branch 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 required 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,505 @@ 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.
|
|
161
385
|
*
|
|
162
|
-
* @
|
|
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.
|
|
163
404
|
*
|
|
164
|
-
* @returns Returns the value as a human-readable string.
|
|
165
405
|
* @example
|
|
166
406
|
*
|
|
167
407
|
* ```ts
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
408
|
+
* try {
|
|
409
|
+
*
|
|
410
|
+
* await someOperation();
|
|
411
|
+
* } catch(error) {
|
|
412
|
+
*
|
|
413
|
+
* log.error("Operation failed: %s.", formatErrorMessage(error));
|
|
414
|
+
* }
|
|
173
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
|
|
174
428
|
*/
|
|
175
|
-
export declare function
|
|
429
|
+
export declare function defaultRetryBackoff(attempt: number): number;
|
|
176
430
|
/**
|
|
177
|
-
*
|
|
431
|
+
* Options accepted by {@link retry}.
|
|
178
432
|
*
|
|
179
|
-
* @
|
|
180
|
-
|
|
181
|
-
|
|
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.
|
|
182
461
|
*
|
|
183
|
-
*
|
|
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.
|
|
184
465
|
*
|
|
185
|
-
* @
|
|
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.
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
*
|
|
475
|
+
* ```ts
|
|
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
|
+
* });
|
|
486
|
+
* ```
|
|
487
|
+
*
|
|
488
|
+
* @category Utilities
|
|
489
|
+
*/
|
|
490
|
+
export declare function retry<T>(operation: (signal: AbortSignal) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
491
|
+
/**
|
|
492
|
+
* Wait for `promise` to settle, bailing out early if `signal` aborts before it does.
|
|
493
|
+
*
|
|
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.
|
|
497
|
+
*
|
|
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.
|
|
501
|
+
*
|
|
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
|
-
*
|
|
191
|
-
*
|
|
513
|
+
* import { waitWithSignal } from "homebridge-plugin-utils";
|
|
514
|
+
*
|
|
515
|
+
* try {
|
|
192
516
|
*
|
|
193
|
-
*
|
|
517
|
+
* const initSegment = await waitWithSignal(assembler.initSegment, callerSignal);
|
|
518
|
+
* } catch {
|
|
194
519
|
*
|
|
195
|
-
* //
|
|
196
|
-
* return
|
|
197
|
-
* }
|
|
520
|
+
* // Caller aborted, or the assembler rejected init. Either way, unwind cleanly.
|
|
521
|
+
* return;
|
|
522
|
+
* }
|
|
523
|
+
* ```
|
|
524
|
+
*
|
|
525
|
+
* @category Utilities
|
|
526
|
+
*/
|
|
527
|
+
export declare function waitWithSignal<T>(promise: Promise<T>, signal: AbortSignal): Promise<T>;
|
|
528
|
+
/**
|
|
529
|
+
* Drain an async iterable and retain only its last `n` values, returned in original (oldest-to-newest) order.
|
|
530
|
+
*
|
|
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.
|
|
535
|
+
*
|
|
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.
|
|
544
|
+
*
|
|
545
|
+
* @example
|
|
546
|
+
*
|
|
547
|
+
* ```ts
|
|
548
|
+
* import { takeLast } from "homebridge-plugin-utils";
|
|
198
549
|
*
|
|
199
|
-
*
|
|
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);
|
|
200
552
|
* ```
|
|
201
553
|
*
|
|
202
554
|
* @category Utilities
|
|
203
555
|
*/
|
|
204
|
-
export declare function
|
|
556
|
+
export declare function takeLast<T>(source: AsyncIterable<T>, n: number): Promise<T[]>;
|
|
205
557
|
/**
|
|
206
|
-
*
|
|
558
|
+
* Compose one or more optional {@link AbortSignal} sources into a single signal that aborts when any input aborts.
|
|
207
559
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
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.
|
|
211
565
|
*
|
|
212
|
-
* @
|
|
213
|
-
*
|
|
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.
|
|
214
572
|
*
|
|
215
573
|
* @example
|
|
574
|
+
*
|
|
216
575
|
* ```ts
|
|
217
|
-
* //
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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);
|
|
581
|
+
*
|
|
582
|
+
* // Compose an optional caller signal with a derived watchdog timeout.
|
|
583
|
+
* const composed = composeSignals(init.signal, AbortSignal.timeout(PROBE_DEFAULT_TIMEOUT_MS));
|
|
584
|
+
* ```
|
|
585
|
+
*
|
|
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 rule: 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 distinction 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.
|
|
613
|
+
*
|
|
614
|
+
* @example
|
|
615
|
+
*
|
|
616
|
+
* ```ts
|
|
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
|
+
* });
|
|
225
633
|
* ```
|
|
226
634
|
*
|
|
227
635
|
* @category Utilities
|
|
228
636
|
*/
|
|
229
|
-
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>;
|
|
230
642
|
/**
|
|
231
|
-
*
|
|
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.
|
|
232
654
|
*
|
|
233
|
-
* @param
|
|
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.
|
|
234
657
|
*
|
|
235
|
-
* @returns
|
|
658
|
+
* @returns The `(error) => void` handler to hand to {@link superviseLoop}'s `onError`. It logs exactly once per fault and returns nothing.
|
|
236
659
|
*
|
|
237
660
|
* @example
|
|
238
|
-
* To sleep for 3 seconds before continuing execute:
|
|
239
661
|
*
|
|
240
662
|
* ```ts
|
|
241
|
-
*
|
|
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
|
+
* });
|
|
242
672
|
* ```
|
|
243
673
|
*
|
|
244
674
|
* @category Utilities
|
|
245
675
|
*/
|
|
246
|
-
export declare function
|
|
676
|
+
export declare function loopFaultReporter(log: HomebridgePluginLogging, label: string): (error: unknown) => void;
|
|
677
|
+
/**
|
|
678
|
+
* The minimal completion-callback shape {@link guardedDispatch} guards. HomeKit's camera-delegate callbacks (snapshot, prepare-stream, stream-request) each answer with
|
|
679
|
+
* an optional leading `Error` and, in some cases, an optional trailing payload; every one is structurally assignable to this error-first shape, so `guardedDispatch`
|
|
680
|
+
* serves them all without importing any HomeKit or hap-nodejs type. A richer callback that also carries a success payload is preserved through the generic parameter on
|
|
681
|
+
* {@link guardedDispatch}, which keeps the caller's exact signature while still guarding it.
|
|
682
|
+
*
|
|
683
|
+
* @category Utilities
|
|
684
|
+
*/
|
|
685
|
+
export type DispatchCallback = (error?: Error) => void;
|
|
686
|
+
/**
|
|
687
|
+
* Run an async handler that HomeKit invokes without awaiting - a camera-delegate method whose interface return type is `void` - so a rejection can never float as an
|
|
688
|
+
* unhandled rejection and the delegate's completion callback is answered exactly once.
|
|
689
|
+
*
|
|
690
|
+
* HomeKit's camera-delegate methods (snapshot, prepare-stream, stream-request) are declared to return `void` yet are naturally written as `async`. Calling an async
|
|
691
|
+
* method in that position discards its promise: a rejection surfaces as a process-level unhandled rejection, and if the method faulted before answering its callback,
|
|
692
|
+
* HomeKit waits forever for a response that never comes. `guardedDispatch` closes both gaps. It owns a once-guard around the real callback and hands the guarded callback
|
|
693
|
+
* to `handler`, so however the handler behaves - answered then faulted, faulted before answering, or answered twice by mistake - the real callback fires exactly once:
|
|
694
|
+
* the first answer wins; a fault after an answer is logged and the earlier answer stands; a fault before any answer is delivered to HomeKit through the callback itself.
|
|
695
|
+
* The handler's promise is marked observed through {@link markHandled}, so nothing floats.
|
|
696
|
+
*
|
|
697
|
+
* @typeParam C - The caller's exact callback signature. Constrained to {@link DispatchCallback} (error-first) so any HomeKit delegate callback fits, while
|
|
698
|
+
* preserving a richer signature - one that also passes a snapshot buffer or stream response - for the handler to answer with.
|
|
699
|
+
* @param options - Dispatch inputs.
|
|
700
|
+
* @param options.callback - The real completion callback HomeKit supplied. It is wrapped in a once-guard and never invoked more than once.
|
|
701
|
+
* @param options.handler - The async work, given the guarded callback to answer with. A rejection, or a synchronous throw, is caught: while the callback is still open
|
|
702
|
+
* it receives the error, otherwise the error is logged.
|
|
703
|
+
* @param options.label - A short human-readable name for the operation (for example `"snapshot request"`), interpolated into the failure log line.
|
|
704
|
+
* @param options.log - The logger a post-answer fault is reported through.
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
*
|
|
708
|
+
* ```ts
|
|
709
|
+
* import { guardedDispatch } from "homebridge-plugin-utils";
|
|
710
|
+
*
|
|
711
|
+
* // A snapshot delegate HomeKit calls without awaiting. The method itself returns void; the async work and its one-shot callback are handed to guardedDispatch.
|
|
712
|
+
* public handleSnapshotRequest(request: SnapshotRequest, callback: SnapshotRequestCallback): void {
|
|
713
|
+
*
|
|
714
|
+
* guardedDispatch({ callback, handler: (answer) => this.snapshot(request, answer), label: "snapshot request", log: this.log });
|
|
715
|
+
* }
|
|
716
|
+
* ```
|
|
717
|
+
*
|
|
718
|
+
* @category Utilities
|
|
719
|
+
*/
|
|
720
|
+
export declare function guardedDispatch<C extends DispatchCallback>(options: {
|
|
721
|
+
callback: C;
|
|
722
|
+
handler: (callback: C) => Promise<void>;
|
|
723
|
+
label: string;
|
|
724
|
+
log: Logger;
|
|
725
|
+
}): void;
|
|
726
|
+
/**
|
|
727
|
+
* Run an async, callback-less handler that HomeKit (or any caller) invokes without awaiting, so a rejection can never float as an unhandled rejection. With no callback
|
|
728
|
+
* to carry a failure back, a fault is simply logged.
|
|
729
|
+
*
|
|
730
|
+
* @param options - Dispatch inputs.
|
|
731
|
+
* @param options.handler - The async work to run. A rejection, or a synchronous throw, is caught and logged.
|
|
732
|
+
* @param options.label - A short human-readable name for the operation (for example `"recording activation"`), interpolated into the failure log line.
|
|
733
|
+
* @param options.log - The logger a fault is reported through.
|
|
734
|
+
*
|
|
735
|
+
* @example
|
|
736
|
+
*
|
|
737
|
+
* ```ts
|
|
738
|
+
* import { guardedDispatch } from "homebridge-plugin-utils";
|
|
739
|
+
*
|
|
740
|
+
* // A recording-state update HomeKit calls without awaiting. There is no callback, so a fault is logged rather than reported back to HomeKit.
|
|
741
|
+
* public updateRecordingActive(active: boolean): void {
|
|
742
|
+
*
|
|
743
|
+
* guardedDispatch({ handler: () => this.applyRecordingActive(active), label: "recording activation", log: this.log });
|
|
744
|
+
* }
|
|
745
|
+
* ```
|
|
746
|
+
*
|
|
747
|
+
* @category Utilities
|
|
748
|
+
*/
|
|
749
|
+
export declare function guardedDispatch(options: {
|
|
750
|
+
handler: () => Promise<void>;
|
|
751
|
+
label: string;
|
|
752
|
+
log: Logger;
|
|
753
|
+
}): void;
|
|
754
|
+
/**
|
|
755
|
+
* 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
|
|
756
|
+
* time through a discriminated union - the "no abort mechanism" case is unrepresentable.
|
|
757
|
+
*
|
|
758
|
+
* @category Utilities
|
|
759
|
+
*/
|
|
760
|
+
export type RunWithAbortOptions = {
|
|
761
|
+
signal: AbortSignal;
|
|
762
|
+
timeout?: number;
|
|
763
|
+
} | {
|
|
764
|
+
timeout: number;
|
|
765
|
+
};
|
|
766
|
+
/**
|
|
767
|
+
* Run an abortable operation with signal-based cancellation.
|
|
768
|
+
*
|
|
769
|
+
* 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
|
|
770
|
+
* 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
|
|
771
|
+
* 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
|
|
772
|
+
* from the factory propagate normally.
|
|
773
|
+
*
|
|
774
|
+
* @typeParam T - The type of value the factory's promise resolves with.
|
|
775
|
+
* @param fn - A factory that receives the composed abort signal and returns the promise to await.
|
|
776
|
+
* @param options - Abort options. Provide `timeout` (milliseconds), an external `signal`, or both.
|
|
777
|
+
*
|
|
778
|
+
* @returns Resolves with the factory's result if it completes before abort, or `null` if the signal fires first.
|
|
779
|
+
*
|
|
780
|
+
* @example
|
|
781
|
+
* ```ts
|
|
782
|
+
* // Timeout only - cancel after 500ms.
|
|
783
|
+
* const result = await runWithAbort((signal) => fetch(url, { signal }), { timeout: 500 });
|
|
784
|
+
*
|
|
785
|
+
* // External signal only - cancel on demand.
|
|
786
|
+
* const controller = new AbortController();
|
|
787
|
+
* const result2 = await runWithAbort((signal) => once(emitter, "data", { signal }), { signal: controller.signal });
|
|
788
|
+
* controller.abort();
|
|
789
|
+
*
|
|
790
|
+
* // Both - cancel on demand or after 5 seconds, whichever comes first.
|
|
791
|
+
* const result3 = await runWithAbort((signal) => once(emitter, "data", { signal }), { signal: controller.signal, timeout: 5000 });
|
|
792
|
+
* ```
|
|
793
|
+
*
|
|
794
|
+
* @category Utilities
|
|
795
|
+
*/
|
|
796
|
+
export declare function runWithAbort<T>(fn: (signal: AbortSignal) => Promise<T>, options: RunWithAbortOptions): Promise<Nullable<T>>;
|
|
797
|
+
/**
|
|
798
|
+
* Construction-time options for {@link Watchdog}.
|
|
799
|
+
*
|
|
800
|
+
* @property onFire - Callback invoked when the watchdog window lapses without a re-arm. Typically aborts an owning controller (`() => this.#controller.abort(new
|
|
801
|
+
* HbpuAbortError("timeout"))`) but the watchdog itself is agnostic about what the fire does. Runs only when the observed signal has not already
|
|
802
|
+
* aborted; if the signal fires before the timer, `onFire` is skipped entirely.
|
|
803
|
+
* @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.
|
|
804
|
+
* Typically the consumer's composed lifetime signal (`this.signal`) so both parent-initiated and internal aborts wind the watchdog down.
|
|
805
|
+
* @property timeoutMs - Inactivity window in milliseconds. The first `arm()` schedules a fire at now + `timeoutMs`; each subsequent `arm()` restarts the clock.
|
|
806
|
+
*
|
|
807
|
+
* @category Utilities
|
|
808
|
+
*/
|
|
809
|
+
export interface WatchdogInit {
|
|
810
|
+
onFire: () => void;
|
|
811
|
+
signal: AbortSignal;
|
|
812
|
+
timeoutMs: number;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Re-armable inactivity watchdog.
|
|
816
|
+
*
|
|
817
|
+
* 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
|
|
818
|
+
* pacing, the RTP demuxer's inbound-packet cadence - composes a single `Watchdog` instance to implement the shared "abort if no activity within window" pattern.
|
|
819
|
+
*
|
|
820
|
+
* The semantics are minimal on purpose:
|
|
821
|
+
*
|
|
822
|
+
* - `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
|
|
823
|
+
* call is a no-op.
|
|
824
|
+
* - 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
|
|
825
|
+
* wins the race and the callback is skipped.
|
|
826
|
+
* - When the observed signal aborts for any reason, the watchdog self-cleans its pending timer; the consumer never needs to unwire it at teardown.
|
|
827
|
+
* - `clear()` cancels any pending fire without aborting anything and leaves the watchdog re-armable.
|
|
828
|
+
* - `[Symbol.dispose]` clears the pending fire and marks the watchdog permanently dead: subsequent `arm()` calls are no-ops. This matches the scope-bound semantics
|
|
829
|
+
* callers expect from `using` - the resource is dead when the block exits, not merely quiescent.
|
|
830
|
+
*
|
|
831
|
+
* This is a `Disposable` (synchronous) rather than `AsyncDisposable` because cancelling a timer is synchronous; there is no background work to await.
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
*
|
|
835
|
+
* ```ts
|
|
836
|
+
* using watchdog = new Watchdog({
|
|
837
|
+
*
|
|
838
|
+
* onFire: () => this.#controller.abort(new HbpuAbortError("timeout")),
|
|
839
|
+
* signal: this.signal,
|
|
840
|
+
* timeoutMs: this.#inactivityWindowMs
|
|
841
|
+
* });
|
|
842
|
+
*
|
|
843
|
+
* // Each time a packet / segment / message arrives, re-arm so the fire never fires.
|
|
844
|
+
* this.#source.on("data", () => watchdog.arm());
|
|
845
|
+
* watchdog.arm();
|
|
846
|
+
* ```
|
|
847
|
+
*
|
|
848
|
+
* @category Utilities
|
|
849
|
+
*/
|
|
850
|
+
export declare class Watchdog implements Disposable {
|
|
851
|
+
#private;
|
|
852
|
+
/**
|
|
853
|
+
* Construct a new watchdog. The watchdog is dormant until the first `arm()` call, so construction itself schedules no timers.
|
|
854
|
+
*
|
|
855
|
+
* 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
|
|
856
|
+
* 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
|
|
857
|
+
* 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.
|
|
858
|
+
*
|
|
859
|
+
* @param init - Required init options. See {@link WatchdogInit}.
|
|
860
|
+
*/
|
|
861
|
+
constructor(init: WatchdogInit);
|
|
862
|
+
/**
|
|
863
|
+
* 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
|
|
864
|
+
* 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
|
|
865
|
+
* `using` contract callers rely on.
|
|
866
|
+
*/
|
|
867
|
+
arm(): void;
|
|
868
|
+
/**
|
|
869
|
+
* Cancel any pending fire without aborting anything and without marking the watchdog as permanently dead. Subsequent `arm()` calls continue to work. Safe to call
|
|
870
|
+
* when no arm is pending - repeat calls are no-ops.
|
|
871
|
+
*/
|
|
872
|
+
clear(): void;
|
|
873
|
+
/**
|
|
874
|
+
* `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
|
|
875
|
+
* occur. This is the contract `using watchdog = new Watchdog(...)` relies on - the resource is dead when the block exits, not merely quiescent. Repeated
|
|
876
|
+
* 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.
|
|
877
|
+
*/
|
|
878
|
+
[Symbol.dispose](): void;
|
|
879
|
+
}
|
|
247
880
|
/**
|
|
248
881
|
* Start case a string, capitalizing the first letter of each word unconditionally.
|
|
249
882
|
*
|
|
@@ -311,3 +944,4 @@ export declare function sanitizeName(name: string): string;
|
|
|
311
944
|
* @category Utilities
|
|
312
945
|
*/
|
|
313
946
|
export declare function validateName(name: string): boolean;
|
|
947
|
+
//# sourceMappingURL=util.d.ts.map
|