/** * TypeScript Utilities. * * @module */ import type { Logging } from "homebridge"; /** * The canonical set of abort reasons used across `homebridge-plugin-utils`. * * 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. * Consumers branch on the `.name` field. Platform errors produced by `AbortSignal.timeout()` and bare `controller.abort()` interoperate by matching names: * `TimeoutError` and `AbortError` from the platform both flow through the same branching paths unchanged. * * @remarks When to use each reason: * * - `"closed"` - resource ended naturally (process exited with code 0, socket closed by peer, MQTT disconnected cleanly). * - `"failed"` - resource ended because of an error (non-zero exit, spawn ENOENT, upstream error). Attach the underlying error via `cause`. * - `"replaced"` - a newer operation superseded this one (new stream request, livestream discontinuity, new MQTT subscription overwriting the old handler). * - `"shutdown"` - orderly teardown from parent lifecycle (plugin stop, controller close, session end). Default when `abort()` is called with no reason. * - `"timeout"` - resource was stuck and exceeded a watchdog window. `AbortSignal.timeout()`'s platform `TimeoutError` carries a matching `.name`. * * @category Utilities */ export type HbpuAbortReason = "closed" | "failed" | "replaced" | "shutdown" | "timeout"; /** * Options accepted by {@link HbpuAbortError}'s constructor. * * @category Utilities */ export interface HbpuAbortErrorOptions { /** * 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 * structured object carrying diagnostic context (e.g., `{ exitCode, exitSignal }`) - specialized subclasses may tighten this later. */ cause?: unknown; /** * Optional human-readable message. When omitted, the error's `message` defaults to the reason name, which is sufficient for name-based handling. */ message?: string; } /** * The canonical abort error used across `homebridge-plugin-utils`. * * `HbpuAbortError` is a lightweight subclass of `Error` whose `name` field is one of the values in {@link HbpuAbortReason}. It is the value passed to * `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 * promise that ends because of an abort. * * @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 * than as additional fields on this class, so that every consumer that catches an `HbpuAbortError` reads the same shape. Specialized subclasses (e.g., * `FfmpegAbortError` carrying typed exit context) may be introduced later when there is a concrete need - not preemptively. * * @example * * ```ts * import { HbpuAbortError, isHbpuAbortReason } from "homebridge-plugin-utils"; * * try { * * await recording.segments().next(); * } catch(error: unknown) { * * if(isHbpuAbortReason(error, "replaced")) { * * // Stream was superseded; this is expected during a livestream discontinuity. * return; * } * * throw error; * } * ``` * * @category Utilities */ export declare class HbpuAbortError extends Error { /** * The tag. Matches one of {@link HbpuAbortReason}. */ readonly name: HbpuAbortReason; /** * Construct a new `HbpuAbortError`. * * @param reason - The abort reason (also assigned to `.name`). * @param options - Optional `cause` for structured diagnostic context, and an optional human-readable `message`. */ constructor(reason: HbpuAbortReason, options?: HbpuAbortErrorOptions); } /** * Type guard: returns `true` if `error` is an {@link HbpuAbortError}. * * Use this to distinguish HBPU's canonical abort errors from arbitrary thrown values, without nesting `instanceof` checks. * * @param error - The value to test. * * @returns `true` if `error` is an `HbpuAbortError` instance. * * @category Utilities */ export declare function isHbpuAbortError(error: unknown): error is HbpuAbortError; /** * Convenience type predicate: returns `true` if `error` is an {@link HbpuAbortError} whose `.name` matches `reason`, and narrows the type so callers can read * `error.cause` and related fields without further casts. * * 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 * 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 * literal narrowed form automatically. * * @typeParam R - The specific reason being matched. Defaulted by inference from `reason`. * @param error - The value to test. * @param reason - The abort reason to match. * * @returns `true` if `error` is an `HbpuAbortError` with the given reason. * * @category Utilities */ export declare function isHbpuAbortReason(error: unknown, reason: R): error is HbpuAbortError & { name: R; }; /** * Test whether an abort reason indicates a timeout. Matches both the canonical {@link HbpuAbortError} with `"timeout"` name - produced by project watchdogs * ({@link Watchdog}, the inactivity monitors on `FfmpegProcess` / `RtpDemuxer` / `Mp4SegmentAssembler`) - and the platform {@link DOMException}/`Error` whose * `.name === "TimeoutError"` - produced by `AbortSignal.timeout()`. Consumers branch on a single predicate regardless of which code path originated the timeout. * * Exists because every long-lived resource class exposes an `isTimedOut` getter with identical branching logic; routing all of them through this single predicate * enforces one taxonomy and eliminates drift if the project ever needs to add, say, a third timeout shape (e.g., an upstream-framework cancellation). * * @param reason - Any value found on `AbortSignal.reason`. Plain objects, non-errors, and `undefined` all return `false`. * * @returns `true` when the reason is a timeout in either supported shape. * * @category Utilities */ export declare function isTimeoutReason(reason: unknown): boolean; /** * 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 * call time, `handler` runs inline and the returned handle is a no-op disposer. * * Closes the well-known pitfall in `AbortSignal.addEventListener("abort", ...)`: listeners attached to an already-aborted signal **do not fire**, so constructors * 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 * register-or-dispatch-immediately shape so every caller handles both cases without re-implementing the check. * * Returning a `Disposable` serves two patterns through one primitive: * * - **Long-lived resource-class registrations** (the common case): every HBPU resource class registers its `#teardown` handler in its constructor, intending the * listener to live until the composed signal aborts. These callers discard the return value; the `{ once: true }` listener auto-unregisters on fire. * - **Scope-bound transient registrations**: observers that only need the listener for a bounded scope (e.g., {@link waitWithSignal}) capture the handle with * `using` so the listener is deterministically removed on scope exit even when the promise resolves before the signal aborts. This prevents listener accumulation * on long-lived signals that see many short waits. * * 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 * 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 * this call with a subsequent `if(signal.aborted) return;` check. * * @param signal - The abort signal to observe. * @param handler - The teardown or cleanup action to run once on abort. Invoked synchronously when `signal.aborted` is already `true` at call time; otherwise * attached as a one-shot `"abort"` listener. * * @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). * * @example * * ```ts * // Long-lived resource-class registration: discard the returned disposer. The listener lives until the composed signal aborts and `{ once: true }` cleans it up. * constructor(init: { signal?: AbortSignal }) { * * this.signal = composeSignals(init.signal, this.#controller.signal); * * onAbort(this.signal, () => this.#teardown()); * * if(this.signal.aborted) { * * return; * } * * // ...proceed with setup that only makes sense on a live signal. * } * ``` * * @example * * ```ts * // Scope-bound transient registration: capture the handle with `using` so the listener auto-removes when the scope exits, even if the signal never aborts. * async function abortableWait(promise: Promise, signal: AbortSignal): Promise { * * using _registration = onAbort(signal, () => { * // Abort-driven action goes here. * }); * * // `return await promise` (not a bare `return promise`) is required inside an async function. `using` disposes when the enclosing function body finishes * // executing, and without an `await` the body finishes synchronously at the `return` statement - even though the returned promise is still pending. The * // listener would therefore be removed the instant the function returned, well before the promise settles. Adding `await` creates a suspension point that * // keeps the `using` scope alive until the promise actually settles, which is what the "scope-bound registration" pattern relies on. * return await promise; * } * ``` * * @category Utilities */ export declare function onAbort(signal: AbortSignal, handler: () => void): Disposable; /** * 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` * warning. Returns the original promise so callers can mark-and-assign in one expression. * * 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 * `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 * unhandled-rejection tracker. * * @typeParam T - The resolved value type. * @param promise - The promise to mark handled. * * @returns The same promise, for chained assignment. * * @example * * ```ts * this.ready = markHandled(readyResolvers.promise); * ``` * * @category Utilities */ export declare function markHandled(promise: Promise): Promise; /** * A utility type that recursively makes all properties of an object, including nested objects, optional. * * This should only be used on JSON objects. If used on classes, class methods will also be marked as optional. * * @remarks Credit for this type goes to: https://github.com/joonhocho/tsdef. * * @typeParam T - The type to make recursively partial. * * @example * * ```ts * type Original = { * * id: string; * nested: { value: number }; * }; * * // All properties, including nested ones, are optional. * type PartialObj = DeepPartial; * * const obj: PartialObj = { nested: {} }; * ``` * * @category Utilities */ export type DeepPartial = { [P in keyof T]?: T[P] extends (infer I)[] ? DeepPartial[] : DeepPartial; }; /** * A utility type that recursively makes all properties of an object, including nested objects, readonly. * * This should only be used on JSON objects. If used on classes, class methods will also be marked as readonly. * * @remarks Credit for this type goes to: https://github.com/joonhocho/tsdef. * * @typeParam T - The type to make recursively readonly. * * @example * * ```ts * type Original = { * * id: string; * nested: { value: number }; * }; * * // All properties, including nested ones, are readonly. * type ReadonlyObj = DeepReadonly; * * const obj: ReadonlyObj = { id: "a", nested: { value: 1 } }; * // obj.id = "b"; // Error: cannot assign to readonly property. * ``` * * @category Utilities */ export type DeepReadonly = { readonly [P in keyof T]: T[P] extends (infer I)[] ? DeepReadonly[] : DeepReadonly; }; /** * Utility type that allows a value to be either the given type or `null`. * * This type is used to explicitly indicate that a variable, property, or return value may be either a specific type or `null`. * * @typeParam T - The type to make nullable. * * @example * * ```ts * let id: Nullable = null; * * // Later... * id = "device-001"; * ``` * * @category Utilities */ export type Nullable = T | null; /** * Makes all properties in `T` optional except for those specified by `K`, which remain required. * * @typeParam T - The base interface or type. * @typeParam K - The keys of `T` that should remain required. * * @example * * ```ts * interface Device { * * id: string; * name: string; * mac: string; * } * * type DeviceUpdate = PartialWithId; * * // Valid: Only 'id' is required, others are optional. * const update: DeviceUpdate = { id: "123" }; * * // Valid: Extra properties can be provided. * const another: DeviceUpdate = { id: "456", name: "SomeDevice" }; * * // Error: 'id' is missing. * const invalid: DeviceUpdate = { name: "SomeOtherDevice" }; // TypeScript error * ``` * * @category Utilities */ export type PartialWithId = Partial & Pick; /** * Logging interface for Homebridge plugins. * * This interface defines the standard logging methods (`debug`, `info`, `warn`, `error`) that plugins should use to output log messages at different severity levels. It * is intended to be compatible with Homebridge's builtin logger and can be implemented by any custom logger used within Homebridge plugins. * * @example * * ```ts * function example(log: HomebridgePluginLogging) { * * log.debug("Debug message: %s", "details"); * log.info("Informational message."); * log.warn("Warning message!"); * log.error("Error message: %s", "problem"); * } * ``` * * @category Utilities */ export interface HomebridgePluginLogging { /** * Logs a debug-level message. * * @param message - The message string, with optional format specifiers. * @param parameters - Optional parameters for message formatting. */ debug: (message: string, ...parameters: unknown[]) => void; /** * Logs an error-level message. * * @param message - The message string, with optional format specifiers. * @param parameters - Optional parameters for message formatting. */ error: (message: string, ...parameters: unknown[]) => void; /** * Logs an info-level message. * * @param message - The message string, with optional format specifiers. * @param parameters - Optional parameters for message formatting. */ info: (message: string, ...parameters: unknown[]) => void; /** * Logs a warning-level message. * * @param message - The message string, with optional format specifiers. * @param parameters - Optional parameters for message formatting. */ warn: (message: string, ...parameters: unknown[]) => void; } /** * A shippable no-op {@link HomebridgePluginLogging}: every method accepts the logging signature and discards its arguments. A module-scope singleton - the methods are * 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 * 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 * `silentLog` helper derives from it rather than re-declaring the empty sink. * * @category Utilities */ export declare const noOpLog: HomebridgePluginLogging; /** * Logger union accepted by FFmpeg subsystem APIs that interoperate with both Homebridge's built-in logger and the plugin-side {@link HomebridgePluginLogging} interface. * Provides one alias for sites that need this union, keeping the SSOT discipline applied elsewhere in the package consistent for the logger surface. * * @category Utilities */ export type Logger = HomebridgePluginLogging | Logging; export { formatBps, formatBytes, formatMs, formatPercent, formatSeconds } from "./formatters.ts"; /** * Render an arbitrary thrown value as a clean log-suffix string. Real `Error` instances surface their `.message`; everything else is coerced through `String(...)`. * 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. * * @param error - The thrown value, typically caught from a `try` block or rejected Promise. * * @returns The cleaned message ready to interpolate into a log format string. * * @example * * ```ts * try { * * await someOperation(); * } catch(error) { * * log.error("Operation failed: %s.", formatErrorMessage(error)); * } * ``` * * @category Utilities */ export declare function formatErrorMessage(error: unknown): string; /** * The default backoff policy used by {@link retry}: exponential with a 30-second ceiling, starting at 1 second for the second attempt (`attempt = 2`). * * @param attempt - The attempt number about to be run (1-indexed; never called with `attempt === 1`, since the first attempt runs immediately). * * @returns The delay, in milliseconds, to wait before executing `attempt`. * * @category Utilities */ export declare function defaultRetryBackoff(attempt: number): number; /** * Options accepted by {@link retry}. * * @category Utilities */ export interface RetryOptions { /** * 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 * `Infinity` for unbounded attempts - the loop then terminates only on success, an abort, or a `shouldRetry` veto, never on an exhausted budget. */ attempts?: number; /** * 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 * only between attempts (i.e., never with `attempt === 1`). Defaults to {@link defaultRetryBackoff} (exponential with a 30-second ceiling). */ backoff?: (attempt: number) => number; /** * 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; * 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 * 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 * (e.g. retry network faults but give up on an authentication error) without owning the attempt loop itself. */ shouldRetry?: (error: unknown, attemptNumber: number) => boolean; /** * 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 * 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. */ signal?: AbortSignal; } /** * Retry an async operation with configurable attempts and backoff, with first-class abort signal support. * * The operation receives the caller's {@link AbortSignal} directly (or a permanent never-aborted sentinel when no caller signal was provided). Well-behaved operations * 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 * `node:timers/promises` `setTimeout` with the signal, so abort also interrupts the backoff. * * @typeParam T - The successful resolution type of `operation`. * @param operation - The async work to perform. Receives the composed abort signal; must resolve with a value on success, or throw/reject on failure. * @param options - Retry options. See {@link RetryOptions}. * * @returns Resolves with the first successful operation result. Rejects with the operation's error once the attempt budget is exhausted or a `shouldRetry` predicate * vetoes a further attempt, or with the signal's reason if aborted mid-attempt or mid-backoff. * * @example * * ```ts * import { retry } from "homebridge-plugin-utils"; * * const controller = new AbortController(); * * const device = await retry(async (signal) => fetchDevice(id, { signal }), { * * attempts: 5, * backoff: (attempt) => 1_000 * attempt, * signal: controller.signal * }); * ``` * * @category Utilities */ export declare function retry(operation: (signal: AbortSignal) => Promise, options?: RetryOptions): Promise; /** * Wait for `promise` to settle, bailing out early if `signal` aborts before it does. * * 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 * 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 * normally, or the signal aborts and `waitWithSignal` rejects with `signal.reason` - including when the signal was already aborted at call time. * * 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 * race. `promise` is ALWAYS observed via `.then(resolve, reject)` - including on the pre-aborted-signal path - which means attaching `waitWithSignal` to a promise * marks it as handled for Node's unhandled-rejection tracker. Callers do not need to wrap `promise` in {@link markHandled} separately. * * @typeParam T - The resolved value type of `promise`. * @param promise - The promise to wait on. * @param signal - The abort signal whose firing interrupts the wait. * * @returns The promise's resolved value. * * @throws `signal.reason` if the signal aborts before `promise` settles, or the original rejection if `promise` rejects first. * * @example * * ```ts * import { waitWithSignal } from "homebridge-plugin-utils"; * * try { * * const initSegment = await waitWithSignal(assembler.initSegment, callerSignal); * } catch { * * // Caller aborted, or the assembler rejected init. Either way, unwind cleanly. * return; * } * ``` * * @category Utilities */ export declare function waitWithSignal(promise: Promise, signal: AbortSignal): Promise; /** * Drain an async iterable and retain only its last `n` values, returned in original (oldest-to-newest) order. * * 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 * 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 * 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 * 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. * * 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, * not an unbounded live stream). A non-positive `n` retains nothing and returns an empty array without iterating the source at all. * * @typeParam T - The element type of the source. * @param source - The async iterable to drain. Must terminate. * @param n - The maximum number of trailing values to retain. Values `<= 0` retain nothing. * * @returns The last `n` values produced by `source`, in original order. * * @example * * ```ts * import { takeLast } from "homebridge-plugin-utils"; * * // Retain only the most recent 500 seed lines from a bounded history window, regardless of how many the source emits. * const recent = await takeLast(seedLines, 500); * ``` * * @category Utilities */ export declare function takeLast(source: AsyncIterable, n: number): Promise; /** * Compose one or more optional {@link AbortSignal} sources into a single signal that aborts when any input aborts. * * Collapses the recurring `parent ? AbortSignal.any([ parent, internal ]) : internal` pattern into a single call, used by every resource class in this library to * compose its lifetime signal. Filters out `undefined` inputs, returns the sole defined signal unchanged (no unnecessary `any()` wrapper), and composes two or more * defined signals with `AbortSignal.any()`. * 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 * compose against. * * @param signals - Ordered list of signal sources. `undefined` entries are filtered out; order is preserved among defined entries. * * @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 * reason as its own `reason`. * * @throws `TypeError` if every input is `undefined` - the caller passed no concrete signal to compose. * * @example * * ```ts * // Class constructor composing an optional parent signal with the internal controller's signal. * this.signal = composeSignals(init.signal, this.#controller.signal); * * // Per-call composition of the class signal with a caller-supplied per-call signal. * const composed = composeSignals(this.signal, init.signal); * * // Compose an optional caller signal with a derived watchdog timeout. * const composed = composeSignals(init.signal, AbortSignal.timeout(PROBE_DEFAULT_TIMEOUT_MS)); * ``` * * @category Utilities */ export declare function composeSignals(...signals: (AbortSignal | undefined)[]): AbortSignal; /** * 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 * handler exactly once. * * Resilient background loops - membership observers, reachability probes, telemetry firehoses - all share one subtle, correctness-critical rule: a throw is a * *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 * 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 * call sites that share no ancestor is how it drifts apart; owning it in one generic primitive is how it stays consistent. * * 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 * 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 * 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 * (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 * loop, or `await` it for orderly shutdown and in tests. * * @param options - Supervision inputs. * @param options.loop - The loop to run, once. It receives the bound {@link AbortSignal} so it can wire cancellation into `observe()` / `fetch()` / stream reads. * @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 * fault policy (logging, wording, recovery), which is why the primitive itself stays logging-free. A throw from `onError` is a defect in the * handler and propagates - the never-rejects guarantee covers the loop, not the handler. * @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, * not aborted means surface. * * @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. * * @example * * ```ts * import { superviseLoop } from "homebridge-plugin-utils"; * * // 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 * // failure is surfaced once through the caller's own wording. * void superviseLoop({ * * loop: async (signal) => { * * for await (const event of client.observe(selector, { signal })) { * * this.handle(event); * } * }, * onError: (error) => this.log.error("The membership observer stopped unexpectedly and will not restart until the next reload: %s", formatErrorMessage(error)), * signal: this.signal * }); * ``` * * @category Utilities */ export declare function superviseLoop(options: { loop: (signal: AbortSignal) => Promise; onError: (error: unknown) => void; signal: AbortSignal; }): Promise; /** * Build the standard {@link superviseLoop} `onError` handler: a reporter that logs a faulted supervised loop with one canonical message, rendering the thrown value * through {@link formatErrorMessage}. * * `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 * 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 * 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 * 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) * across plugins that share no ancestor - the same "no shared home, so a free function is the home" situation {@link superviseLoop} itself answers. * * 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 * different news to deliver and should pass its own `onError` to {@link superviseLoop} rather than this reporter. * * @param log - The plugin logger the report is written to; its `error` method receives the canonical format string and arguments. * @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. * * @returns The `(error) => void` handler to hand to {@link superviseLoop}'s `onError`. It logs exactly once per fault and returns nothing. * * @example * * ```ts * import { loopFaultReporter, superviseLoop } from "homebridge-plugin-utils"; * * // The standard supervised observer: swallow on shutdown, and on a genuine fault log the canonical "