/** * connectPushChannel -- subscribes to a daemon's push-invalidation channel with real connection * resilience. Split out of daemon-client.ts's own bundled concerns -- still zero runtime imports * of its own, using only the global WebSocket (see daemon-client.ts's own module doc comment for * why that invariant matters for Pi's jiti-based extension loader). */ export type PushChannelState = "connecting" | "open" | "degraded" | "closed"; export interface PushChannelClientOptions { /** * e.g. "ws://127.0.0.1:PORT/push" -- `token` is appended as a query * parameter automatically (the WHATWG WebSocket constructor cannot set an * Authorization header). A function is re-invoked on every reconnect * attempt, not just the first -- required for a daemon that rebinds a new * random port on every restart (the same problem connectWithPolicy solves * for one-shot RPC by re-reading the handle file each time); a plain * string only works if the daemon's port never changes across restarts. */ url: string | (() => string | Promise); token: string; /** Re-sent as `{op:"subscribe",topic}` after every successful (re)connect -- a reconnect must not silently lose a subscription. */ topics: readonly string[]; onMessage: (topic: string, payload: unknown) => void; /** Fires on every state transition; useful for a status surface (see daemonStatus) or logging. */ onStateChange?: (state: PushChannelState) => void; /** Defaults to 1000ms. */ minReconnectDelayMs?: number; /** Defaults to 30000ms. */ maxReconnectDelayMs?: number; /** Defaults to 1.5. */ reconnectionDelayGrowFactor?: number; /** A connection must stay open this long before it counts as genuinely stable -- a drop before this elapses keeps the backoff climbing instead of resetting on every brief open. Defaults to 5000ms, mirroring the reference this is modeled on (partysocket's own minUptime). */ minUptimeMs?: number; /** Defaults to 20000ms. */ heartbeatIntervalMs?: number; /** No message (including a pong) received within this long after the last one means the connection is treated as dead even though it never fired a close event -- a TCP socket can stay open while the peer process is hung. Defaults to 45000ms. */ heartbeatTimeoutMs?: number; /** Defaults to the global WebSocket. Injectable for tests. */ WebSocketImpl?: typeof WebSocket; } export interface PushChannelClient { state(): PushChannelState; /** Permanently closes the connection -- no further reconnect attempts. */ close(): void; } /** * Subscribes to a daemon's push-invalidation channel (push-channel.ts) with * real connection resilience, not a naive reconnect-on-close: * * - Exponential backoff (min/max/growFactor) gated by minUptimeMs, mirroring * partysocket (the maintained continuation of reconnecting-websocket): a * connection that opens then drops again immediately keeps the backoff * climbing instead of resetting to fast retries on every brief open -- * the actual mechanism behind detecting "degraded", not just "down". * - Jitter added on top of that reference algorithm (which has none) -- the * real shape here is several concurrent Pi sessions reconnecting to one * Vehicle server after a restart; unjittered synchronized backoff would * create a reconnect storm the moment the daemon comes back up. * - A heartbeat ping/timeout (mirroring ws-heartbeat) detects a socket that * stays open while the daemon process itself is hung -- a plain * reconnect-on-close strategy would never notice that. * - Re-subscribes every requested topic after each successful (re)connect. * * Uses only the global WebSocket -- no import, keeping this module's * "no imports of its own" invariant for Pi's jiti loader (see the module * doc comment). Node 22+ and Bun both provide it as a global. */ export declare function connectPushChannel(options: PushChannelClientOptions): PushChannelClient;