import type { HomebridgePluginLogging } from "../util.ts"; /** * A minimal WebSocket surface the {@link LogSocket} depends on, so the concrete implementation (the platform global `WebSocket`, or a test double) is an injected seam. * * The shape is the subset of the DOM `WebSocket` interface the socket actually uses: the four lifecycle events via `addEventListener`, `send` for outbound frames, * `close` for teardown, and `readyState` (compared against {@link WEBSOCKET_OPEN}) so teardown can gate the namespace-disconnect frame on an open connection. Modeling * the seam as this narrow interface rather than the full `WebSocket` keeps a test double small and makes the socket's exact dependency surface explicit. * * @category Log Client */ export interface WebSocketLike { addEventListener(type: "close", listener: (event: { readonly code: number; }) => void): void; addEventListener(type: "error", listener: (event: unknown) => void): void; addEventListener(type: "message", listener: (event: { readonly data: unknown; }) => void): void; addEventListener(type: "open", listener: () => void): void; close(code?: number): void; readonly readyState: number; send(data: string): void; } /** * The factory seam that constructs a {@link WebSocketLike} for a given connect URL. * * The production factory wraps the platform global `WebSocket`; a test substitutes a factory that returns a controllable double, so the whole socket state machine - * handshake, ping/pong, reconnect, teardown - is exercised without a real network. * * @param url - The fully-formed `ws(s)://...` connect URL, already carrying `EIO=4`, the transport selector, and the raw token in its query string. * * @returns A {@link WebSocketLike} that begins connecting immediately, exactly as the platform `WebSocket` constructor does. * * @category Log Client */ export type WebSocketFactory = (url: string) => WebSocketLike; /** * Re-acquire a fresh raw bearer token. Invoked once per connect attempt so a reconnect after a token has expired re-authenticates from the stored credentials. The * provider may reject with a permanent authentication failure, which the reconnect loop classifies terminal via {@link isPermanentAuthError}. * * @param signal - The connect attempt's abort signal, forwarded so a token acquisition in flight is cancelled when the attempt is aborted. * * @returns A promise resolving to the raw bearer token (the bare JWT, no `Bearer` prefix). * * @category Log Client */ export type TokenProvider = (signal: AbortSignal) => Promise; /** * The numeric `readyState` value denoting an open WebSocket. The DOM `WebSocket.OPEN` constant is `1`; we name it as a module constant so the seam interface does not * have to carry the static constant and teardown can gate on it without depending on the concrete class. * * @category Log Client */ export declare const WEBSOCKET_OPEN = 1; /** * The production {@link WebSocketFactory}: constructs the platform global `WebSocket`. A consumer holds the factory typed as the seam abstraction; a test substitutes a * double. The platform `WebSocket` begins connecting on construction, which is exactly the factory contract. * * @param url - The connect URL. * * @returns A live platform `WebSocket`, typed as the {@link WebSocketLike} seam. * * @category Log Client */ export declare const webSocketFactory: WebSocketFactory; /** * Construction-time options for {@link LogSocket}. * * @property backoff - Optional override for the connect-phase backoff policy, invoked with the 1-indexed attempt about to run and returning the delay in * milliseconds. Defaults to the log client's own jittered exponential curve - a `RECONNECT_BASE_MS` base doubling each attempt and * capped at `RECONNECT_CAP_MS`, plus up to `JITTER_FRACTION` upward jitter. Overridden in tests with a near-zero delay so the * reconnect loop runs without real waits. * @property host - The hostname or IP of the homebridge-config-ui-x server. * @property log - Logger for connection lifecycle and overflow diagnostics. * @property port - The TCP port the server listens on. Defaults to `8581`. * @property random - Injectable source of `[0, 1)` randomness for backoff jitter. Defaults to `Math.random`; pinned in tests for deterministic backoff. * @property refreshable - Whether the credential backing {@link LogSocketInit.tokenProvider} can mint a fresh token on a reconnect. `true` for `password`/`noauth` * credentials (each connect re-authenticates), `false` for a static `token`. When `false`, a handshake/namespace auth rejection is raised as * a permanent {@link LogAuthError} so the connect-phase retry veto makes it terminal rather than retrying a token that cannot be refreshed. * @property signal - Optional parent {@link AbortSignal} composed with the socket's internal controller. When the parent aborts, the socket tears down. * @property stdoutHighWater - Optional high-water mark for the bounded stdout queue. Defaults to `10000`. Overflow drops the oldest lines. * @property tls - When `true`, use the secure (`wss`) scheme; when `false` or omitted, plaintext (`ws`). * @property tokenProvider - Re-acquires a fresh token per connect attempt. See {@link TokenProvider}. * @property webSocketFactory - The factory that constructs the underlying WebSocket. Defaults to {@link webSocketFactory}. * * @category Log Client */ export interface LogSocketInit { readonly backoff?: (attempt: number) => number; readonly host: string; readonly log: HomebridgePluginLogging; readonly port?: number; readonly random?: () => number; readonly refreshable: boolean; readonly signal?: AbortSignal; readonly stdoutHighWater?: number; readonly tls?: boolean; readonly tokenProvider: TokenProvider; readonly webSocketFactory?: WebSocketFactory; } /** * The log client's connect-phase reconnect backoff policy: a dev-tuned jittered exponential curve with a low ceiling. * * This is the single source of truth for the socket's default reconnect timing. The base delay is `RECONNECT_BASE_MS` (500 ms) and it doubles with each successive * connect attempt, plateauing at the `RECONNECT_CAP_MS` ceiling (5 s) - deliberately snappier than `defaultRetryBackoff`'s 30-second ceiling, because a log-tailing * dev tool should resume the tail promptly after the frequent Homebridge restarts a plugin developer does rather than back off to a half-minute lag. Up to * `JITTER_FRACTION` of the computed base is added as upward jitter so a fleet of clients does not reconnect in lockstep after a shared outage. * * It is exported (rather than left inline in the constructor) so the bare schedule is a directly unit-testable function: with `random` pinned to `0` the curve yields the * exact, deterministic 500, 1000, 2000, 4000, 5000, 5000, ... sequence. `retry` invokes the socket's backoff 1-indexed with the attempt about to run and never with * `attempt === 1` (the first attempt runs immediately), so `attempt - 2` is the zero-based exponent for the second-and-later attempts. * * @param attempt - The 1-indexed connect attempt about to run. Called only for the second and later attempts (the first runs with no wait). * @param random - Source of `[0, 1)` randomness for the jitter. Defaults to `Math.random`; pinned in tests for a deterministic schedule. * * @returns The delay, in milliseconds, to wait before running `attempt`. * * @category Log Client */ export declare function reconnectBackoff(attempt: number, random?: () => number): number; /** * The consumer-facing surface of a live-log socket: the minimal interface a client reads off a {@link LogSocket}. This is the product half of the socket * dependency-inversion seam, so a client depends on this narrow interface rather than the concrete {@link LogSocket} and a test can substitute a fake that yields * caller-supplied lines without standing up a WebSocket. Every member is defined on {@link LogSocket}, so the real class satisfies it by `implements` with zero runtime * change. * * @category Log Client */ export interface LogSocketLike extends AsyncDisposable { /** * Abort the socket and tear it down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied. * * @param reason - Optional abort reason. Typically an {@link HbpuAbortError}. */ abort(reason?: unknown): void; /** * `true` once the socket's lifetime signal has aborted. */ readonly aborted: boolean; /** * The number of stdout lines dropped because the consumer fell behind the high-water mark. Zero in steady state. */ readonly droppedLines: number; /** * The composed abort signal representing the socket's lifetime. Aborts exactly once; the reason on `signal.reason` names the cause. */ readonly signal: AbortSignal; /** * The bounded push-to-pull stream of raw log lines the server streams over the log namespace. Terminates when the socket aborts. * * @returns An async generator yielding raw log lines in stream order. */ stdout(): AsyncGenerator; } /** * The creational half of the socket dependency-inversion seam: build a {@link LogSocketLike} from the socket init. A client holds this factory typed as the abstraction * and constructs through it, so a test can substitute a factory that returns a fake socket. The production factory is {@link logSocketFactory}, whose `create` is exactly * the {@link LogSocket} constructor call, so routing construction through this seam is behavior-neutral - mirroring the `RecordingProcessFactory` precedent. * * @category Log Client */ export interface LogSocketFactory { /** * Construct a live-log socket for the supplied init. * * @param init - The socket init options. See {@link LogSocketInit}. * * @returns A new {@link LogSocketLike}. */ create(init: LogSocketInit): LogSocketLike; } /** * AsyncDisposable client for the live Homebridge log stream over a single Socket.IO WebSocket, with automatic reconnect, ping liveness, and a bounded push-to-pull line * stream. * * @example * * ```ts * await using socket = new LogSocket({ * * host: "localhost", * log, * refreshable: true, * tokenProvider: (signal) => acquireToken(credentials, { host: "localhost", port: 8581, signal }), * signal: session.signal * }); * * for await (const line of socket.stdout()) { * * process.stdout.write(line + "\n"); * } * ``` * * @category Log Client */ export declare class LogSocket implements LogSocketLike { #private; /** * The composed abort signal representing this socket's lifetime. Aborts exactly once - when {@link LogSocket.abort} is called, the parent signal fires, or the * reconnect loop gives up on a permanent failure - and `signal.reason` names the cause. */ readonly signal: AbortSignal; /** * Construct and start a new live-log socket. The reconnect loop starts synchronously as part of construction: by the time the constructor returns, the first connect * attempt is already in flight (unless the signal was pre-aborted, in which case the socket tears down immediately). * * @param init - Required init options. See {@link LogSocketInit}. */ constructor(init: LogSocketInit); /** * Abort the socket and tear it down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied; explicit reasons pass through unchanged. * * Safe to call more than once: the underlying signal aborts only once, so subsequent calls are no-ops. * * @param reason - Optional abort reason. Typically an {@link HbpuAbortError}. */ abort(reason?: unknown): void; /** * `AsyncDisposable` implementation. Aborts the socket (defaulting to `"shutdown"`) and awaits the reconnect loop's completion before returning, so callers using * `await using` are guaranteed the connection is closed and the loop has unwound by the time the block exits. * * @returns A promise that resolves once the reconnect loop has fully exited. */ [Symbol.asyncDispose](): Promise; /** * `true` once `this.signal` has aborted. Derived from the signal; no independent state. */ get aborted(): boolean; /** * The number of stdout lines dropped so far because the consumer fell behind the high-water mark. Zero in steady state; non-zero only after the bounded queue * overflowed, which is also logged once. */ get droppedLines(): number; /** * The bounded push-to-pull stream of raw log lines (ANSI intact, terminators removed) the server streams over the log namespace's `stdout` events. * * The server delivers `stdout` as raw text chunks whose boundaries do not align with log lines; the socket runs each chunk through a per-session * {@link LogLineSplitter}, yields complete lines here, and flushes it on each session's close so the final line is never stranded. Mirroring * `Mp4SegmentAssembler.segments`, a bounded queue decouples the WebSocket producer from this consumer, and a single parked waiter blocks the consumer when the queue is * empty until a line is pushed or the socket aborts. The queue survives reconnects - the same iterable keeps yielding across a drop-and-reconnect - so a consumer * iterates it once for the whole socket lifetime. The stream terminates (returns) when the socket aborts; the queue is drained before it returns, so a line already * staged before teardown is never lost. * * **Single-consumer only.** The parked-waiter slot is single-writer; iterating `stdout()` concurrently from two consumers is unsupported. * * @returns An async generator yielding raw log lines in stream order. */ stdout(): AsyncGenerator; } /** * The production {@link LogSocketFactory}: builds the concrete WebSocket-backed {@link LogSocket}. A client holds the factory typed as the seam abstraction; a test * substitutes a fake factory. `create` is exactly the {@link LogSocket} constructor call, so wiring construction through this seam is behavior-neutral. * * @category Log Client */ export declare const logSocketFactory: LogSocketFactory; //# sourceMappingURL=socket.d.ts.map