homebridge-plugin-utils 1.34.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.
Files changed (198) hide show
  1. package/README.md +130 -2
  2. package/build/eslint-plugin/README.md +164 -0
  3. package/build/eslint-plugin/config.mjs +308 -0
  4. package/build/eslint-plugin/index.mjs +7 -0
  5. package/build/eslint-plugin/plugin.mjs +46 -0
  6. package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
  7. package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
  8. package/build/eslint-plugin/rules/comment-style.mjs +190 -0
  9. package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
  10. package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
  11. package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
  12. package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
  13. package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
  14. package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
  15. package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
  16. package/build/eslint-plugin/test-setup.mjs +21 -0
  17. package/build/fs-ops.mjs +184 -0
  18. package/build/tsconfig.json +18 -3
  19. package/dist/backpressure.d.ts +128 -0
  20. package/dist/backpressure.js +273 -0
  21. package/dist/backpressure.js.map +1 -0
  22. package/dist/cli/index.d.ts +95 -0
  23. package/dist/cli/index.js +355 -0
  24. package/dist/cli/index.js.map +1 -0
  25. package/dist/clock-double.d.ts +93 -0
  26. package/dist/clock-double.js +141 -0
  27. package/dist/clock-double.js.map +1 -0
  28. package/dist/clock.d.ts +39 -0
  29. package/dist/clock.js +34 -0
  30. package/dist/clock.js.map +1 -0
  31. package/dist/eslint-plugin/config.d.mts +193 -0
  32. package/dist/eslint-plugin/index.d.mts +2 -0
  33. package/dist/eslint-plugin/plugin.d.mts +87 -0
  34. package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
  35. package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
  36. package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
  37. package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
  38. package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
  39. package/dist/featureOptions-docs.d.ts +96 -0
  40. package/dist/featureOptions-docs.js +193 -0
  41. package/dist/featureOptions-docs.js.map +1 -0
  42. package/dist/featureOptions.d.ts +674 -0
  43. package/dist/featureOptions.js +870 -0
  44. package/dist/featureOptions.js.map +1 -0
  45. package/dist/ffmpeg/codecs.d.ts +256 -72
  46. package/dist/ffmpeg/codecs.js +477 -257
  47. package/dist/ffmpeg/codecs.js.map +1 -1
  48. package/dist/ffmpeg/dgram-util.d.ts +46 -0
  49. package/dist/ffmpeg/dgram-util.js +38 -0
  50. package/dist/ffmpeg/dgram-util.js.map +1 -0
  51. package/dist/ffmpeg/exec.d.ts +83 -64
  52. package/dist/ffmpeg/exec.js +77 -86
  53. package/dist/ffmpeg/exec.js.map +1 -1
  54. package/dist/ffmpeg/fmp4.d.ts +70 -4
  55. package/dist/ffmpeg/fmp4.js +100 -11
  56. package/dist/ffmpeg/fmp4.js.map +1 -1
  57. package/dist/ffmpeg/hap-enums.d.ts +214 -0
  58. package/dist/ffmpeg/hap-enums.js +92 -0
  59. package/dist/ffmpeg/hap-enums.js.map +1 -0
  60. package/dist/ffmpeg/index.d.ts +16 -9
  61. package/dist/ffmpeg/index.js +6 -0
  62. package/dist/ffmpeg/index.js.map +1 -1
  63. package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
  64. package/dist/ffmpeg/mp4-assembler.js +387 -0
  65. package/dist/ffmpeg/mp4-assembler.js.map +1 -0
  66. package/dist/ffmpeg/mp4-parser.d.ts +94 -0
  67. package/dist/ffmpeg/mp4-parser.js +130 -0
  68. package/dist/ffmpeg/mp4-parser.js.map +1 -0
  69. package/dist/ffmpeg/options.d.ts +62 -149
  70. package/dist/ffmpeg/options.js +608 -499
  71. package/dist/ffmpeg/options.js.map +1 -1
  72. package/dist/ffmpeg/process.d.ts +143 -91
  73. package/dist/ffmpeg/process.js +406 -265
  74. package/dist/ffmpeg/process.js.map +1 -1
  75. package/dist/ffmpeg/record.d.ts +302 -188
  76. package/dist/ffmpeg/record.js +408 -560
  77. package/dist/ffmpeg/record.js.map +1 -1
  78. package/dist/ffmpeg/recording-process-double.d.ts +144 -0
  79. package/dist/ffmpeg/recording-process-double.js +168 -0
  80. package/dist/ffmpeg/recording-process-double.js.map +1 -0
  81. package/dist/ffmpeg/rtp-parser.d.ts +70 -0
  82. package/dist/ffmpeg/rtp-parser.js +77 -0
  83. package/dist/ffmpeg/rtp-parser.js.map +1 -0
  84. package/dist/ffmpeg/rtp.d.ts +198 -141
  85. package/dist/ffmpeg/rtp.js +473 -244
  86. package/dist/ffmpeg/rtp.js.map +1 -1
  87. package/dist/ffmpeg/settings.d.ts +5 -1
  88. package/dist/ffmpeg/settings.js +20 -2
  89. package/dist/ffmpeg/settings.js.map +1 -1
  90. package/dist/ffmpeg/stream.d.ts +57 -107
  91. package/dist/ffmpeg/stream.js +121 -148
  92. package/dist/ffmpeg/stream.js.map +1 -1
  93. package/dist/formatters.d.ts +106 -0
  94. package/dist/formatters.js +174 -0
  95. package/dist/formatters.js.map +1 -0
  96. package/dist/homebridge-enums.d.ts +30 -0
  97. package/dist/homebridge-enums.js +17 -0
  98. package/dist/homebridge-enums.js.map +1 -0
  99. package/dist/index.d.ts +12 -5
  100. package/dist/index.js +8 -2
  101. package/dist/index.js.map +1 -1
  102. package/dist/logclient/auth.d.ts +114 -0
  103. package/dist/logclient/auth.js +199 -0
  104. package/dist/logclient/auth.js.map +1 -0
  105. package/dist/logclient/cli-run.d.ts +76 -0
  106. package/dist/logclient/cli-run.js +639 -0
  107. package/dist/logclient/cli-run.js.map +1 -0
  108. package/dist/logclient/cli.d.ts +3 -0
  109. package/dist/logclient/cli.js +97 -0
  110. package/dist/logclient/cli.js.map +1 -0
  111. package/dist/logclient/client.d.ts +145 -0
  112. package/dist/logclient/client.js +600 -0
  113. package/dist/logclient/client.js.map +1 -0
  114. package/dist/logclient/config.d.ts +173 -0
  115. package/dist/logclient/config.js +199 -0
  116. package/dist/logclient/config.js.map +1 -0
  117. package/dist/logclient/endpoints.d.ts +54 -0
  118. package/dist/logclient/endpoints.js +73 -0
  119. package/dist/logclient/endpoints.js.map +1 -0
  120. package/dist/logclient/filter.d.ts +45 -0
  121. package/dist/logclient/filter.js +51 -0
  122. package/dist/logclient/filter.js.map +1 -0
  123. package/dist/logclient/frame.d.ts +93 -0
  124. package/dist/logclient/frame.js +203 -0
  125. package/dist/logclient/frame.js.map +1 -0
  126. package/dist/logclient/index.d.ts +31 -0
  127. package/dist/logclient/index.js +12 -0
  128. package/dist/logclient/index.js.map +1 -0
  129. package/dist/logclient/parser.d.ts +211 -0
  130. package/dist/logclient/parser.js +393 -0
  131. package/dist/logclient/parser.js.map +1 -0
  132. package/dist/logclient/rest.d.ts +41 -0
  133. package/dist/logclient/rest.js +111 -0
  134. package/dist/logclient/rest.js.map +1 -0
  135. package/dist/logclient/settings.d.ts +15 -0
  136. package/dist/logclient/settings.js +64 -0
  137. package/dist/logclient/settings.js.map +1 -0
  138. package/dist/logclient/socket-double.d.ts +201 -0
  139. package/dist/logclient/socket-double.js +384 -0
  140. package/dist/logclient/socket-double.js.map +1 -0
  141. package/dist/logclient/socket.d.ts +257 -0
  142. package/dist/logclient/socket.js +620 -0
  143. package/dist/logclient/socket.js.map +1 -0
  144. package/dist/logclient/stitch.d.ts +83 -0
  145. package/dist/logclient/stitch.js +146 -0
  146. package/dist/logclient/stitch.js.map +1 -0
  147. package/dist/logclient/time-expression.d.ts +42 -0
  148. package/dist/logclient/time-expression.js +181 -0
  149. package/dist/logclient/time-expression.js.map +1 -0
  150. package/dist/logclient/time-window.d.ts +38 -0
  151. package/dist/logclient/time-window.js +53 -0
  152. package/dist/logclient/time-window.js.map +1 -0
  153. package/dist/logclient/types.d.ts +107 -0
  154. package/dist/logclient/types.js +6 -0
  155. package/dist/logclient/types.js.map +1 -0
  156. package/dist/mqttClient.d.ts +287 -0
  157. package/dist/mqttClient.js +433 -0
  158. package/dist/mqttClient.js.map +1 -0
  159. package/dist/service.d.ts +64 -15
  160. package/dist/service.js +93 -66
  161. package/dist/service.js.map +1 -1
  162. package/dist/ui/featureOptions.js +870 -0
  163. package/dist/ui/featureOptions.js.map +1 -0
  164. package/dist/ui/formatters.js +174 -0
  165. package/dist/ui/formatters.js.map +1 -0
  166. package/dist/ui/pluginConfigSession.mjs +141 -0
  167. package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
  168. package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
  169. package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
  170. package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
  171. package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
  172. package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
  173. package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
  174. package/dist/ui/webUi-featureOptions/state.mjs +319 -0
  175. package/dist/ui/webUi-featureOptions/store.mjs +181 -0
  176. package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
  177. package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
  178. package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
  179. package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
  180. package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
  181. package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
  182. package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
  183. package/dist/ui/webUi-featureOptions.mjs +677 -0
  184. package/dist/ui/webUi.mjs +192 -87
  185. package/dist/util.d.ts +597 -45
  186. package/dist/util.js +787 -68
  187. package/dist/util.js.map +1 -1
  188. package/package.json +33 -15
  189. package/build/eslint-rules.mjs +0 -511
  190. package/dist/featureoptions.d.ts +0 -264
  191. package/dist/featureoptions.js +0 -480
  192. package/dist/featureoptions.js.map +0 -1
  193. package/dist/mqttclient.d.ts +0 -178
  194. package/dist/mqttclient.js +0 -310
  195. package/dist/mqttclient.js.map +0 -1
  196. package/dist/ui/featureoptions.js +0 -480
  197. package/dist/ui/featureoptions.js.map +0 -1
  198. package/dist/ui/webUi-featureoptions.mjs +0 -3765
package/dist/util.d.ts CHANGED
@@ -3,6 +3,221 @@
3
3
  *
4
4
  * @module
5
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>;
6
221
  /**
7
222
  * A utility type that recursively makes all properties of an object, including nested objects, optional.
8
223
  *
@@ -45,6 +260,7 @@ export type DeepPartial<T> = {
45
260
  *
46
261
  * ```ts
47
262
  * type Original = {
263
+ *
48
264
  * id: string;
49
265
  * nested: { value: number };
50
266
  * };
@@ -162,93 +378,428 @@ export interface HomebridgePluginLogging {
162
378
  warn: (message: string, ...parameters: unknown[]) => void;
163
379
  }
164
380
  /**
165
- * A utility method that formats a bitrate value into a human-readable form as kbps or Mbps.
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.
166
392
  *
167
- * @param value - The bitrate value to convert.
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.
168
404
  *
169
- * @returns Returns the value as a human-readable string.
170
405
  * @example
171
406
  *
172
407
  * ```ts
173
- * formatBps(500); // "500 bps".
174
- * formatBps(2000); // "2.0 kbps".
175
- * formatBps(15000); // "15.0 kbps".
176
- * formatBps(1000000); // "1.0 Mbps".
177
- * formatBps(2560000); // "2.6 Mbps".
408
+ * try {
409
+ *
410
+ * await someOperation();
411
+ * } catch(error) {
412
+ *
413
+ * log.error("Operation failed: %s.", formatErrorMessage(error));
414
+ * }
178
415
  * ```
416
+ *
417
+ * @category Utilities
179
418
  */
180
- export declare function formatBps(value: number): string;
419
+ export declare function formatErrorMessage(error: unknown): string;
181
420
  /**
182
- * A utility method that retries an operation at a specific interval for up to an absolute total number of retries.
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`).
183
422
  *
184
- * @param operation - The operation callback to try until successful.
185
- * @param retryInterval - Interval to retry, in milliseconds.
186
- * @param totalRetries - Optionally, specify the total number of retries.
423
+ * @param attempt - The attempt number about to be run (1-indexed; never called with `attempt === 1`, since the first attempt runs immediately).
187
424
  *
188
- * @returns Returns `true` when the operation is successful, `false` otherwise or if the total number of retries has been exceeded.
425
+ * @returns The delay, in milliseconds, to wait before executing `attempt`.
189
426
  *
190
- * @remarks `operation` must be an asynchronous function that returns `true` when successful, and `false` otherwise.
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.
461
+ *
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.
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.
191
509
  *
192
510
  * @example
511
+ *
193
512
  * ```ts
194
- * // Example: Retry an async operation up to 5 times, waiting 1 second between each try.
195
- * let attempt = 0;
196
- * const result = await retry(async () => {
513
+ * import { waitWithSignal } from "homebridge-plugin-utils";
197
514
  *
198
- * attempt++;
515
+ * try {
199
516
  *
200
- * // Simulate a 50% chance of success
201
- * return Math.random() > 0.5 || attempt === 5;
202
- * }, 1000, 5);
517
+ * const initSegment = await waitWithSignal(assembler.initSegment, callerSignal);
518
+ * } catch {
203
519
  *
204
- * console.log(result); // true if operation succeeded within 5 tries, otherwise false.
520
+ * // Caller aborted, or the assembler rejected init. Either way, unwind cleanly.
521
+ * return;
522
+ * }
205
523
  * ```
206
524
  *
207
525
  * @category Utilities
208
526
  */
209
- export declare function retry(operation: () => Promise<boolean>, retryInterval: number, totalRetries?: number): Promise<boolean>;
527
+ export declare function waitWithSignal<T>(promise: Promise<T>, signal: AbortSignal): Promise<T>;
210
528
  /**
211
- * Run a promise with a guaranteed timeout to complete.
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.
212
535
  *
213
- * @typeParam T - The type of value the promise resolves with.
214
- * @param promise - The promise you want to run.
215
- * @param timeout - The amount of time, in milliseconds, to wait for the promise to resolve.
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.
216
538
  *
217
- * @returns Returns the result of resolving the promise it's been passed if it completes before timeout, or null if the timeout expires. The internal timer is cleaned
218
- * up regardless of which outcome wins the race. Note that the underlying promise is not cancelled...it continues to run, but its result is ignored.
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.
219
544
  *
220
545
  * @example
546
+ *
221
547
  * ```ts
222
- * // Resolves in 100ms, timeout is 500ms, so it resolves to 42.
223
- * const result = await runWithTimeout(Promise.resolve(42), 500);
224
- * console.log(result); // 42
225
- *
226
- * // Resolves in 1000ms, timeout is 500ms, so it resolves to null.
227
- * const slowPromise = new Promise<number>(resolve => setTimeout(() => resolve(42), 1000));
228
- * const result2 = await runWithTimeout(slowPromise, 500);
229
- * 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);
230
552
  * ```
231
553
  *
232
554
  * @category Utilities
233
555
  */
234
- export declare function runWithTimeout<T>(promise: Promise<T>, timeout: number): Promise<Nullable<T>>;
556
+ export declare function takeLast<T>(source: AsyncIterable<T>, n: number): Promise<T[]>;
235
557
  /**
236
- * Emulate a sleep function.
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);
237
578
  *
238
- * @param sleepTimer - The amount of time to sleep, in milliseconds.
579
+ * // Per-call composition of the class signal with a caller-supplied per-call signal.
580
+ * const composed = composeSignals(this.signal, init.signal);
239
581
  *
240
- * @returns Returns a promise that resolves after the specified time elapses.
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 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.
241
613
  *
242
614
  * @example
243
- * To sleep for 3 seconds before continuing execute:
244
615
  *
245
616
  * ```ts
246
- * await sleep(3000)
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
+ * });
247
633
  * ```
248
634
  *
249
635
  * @category Utilities
250
636
  */
251
- export declare function sleep(sleepTimer: number): Promise<void>;
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
+ }
252
803
  /**
253
804
  * Start case a string, capitalizing the first letter of each word unconditionally.
254
805
  *
@@ -316,3 +867,4 @@ export declare function sanitizeName(name: string): string;
316
867
  * @category Utilities
317
868
  */
318
869
  export declare function validateName(name: string): boolean;
870
+ //# sourceMappingURL=util.d.ts.map