# SignalingClient _(no submodules)_ ## Purpose `SignalingClient` owns the WebSocket lifecycle to the WebRTC signaling server. The signaling layer carries SDP offers and answers, ICE candidates, peer-mesh roster updates, and the fallback chunk-relay envelopes used when no direct data channel is available. The client is intentionally separate from `PeerManager` so the wire codec, reconnect policy, and Zod validation of inbound traffic can be reasoned about in isolation — peer state lives elsewhere and consumes typed `SignalingMessage` events from this surface. A dedicated client also keeps the package universal friendly: the only runtime browser dependency is `WebSocket`, and the constructor accepts a custom `webSocketCtor` so a server-tier transport (or a deterministic test double implementing the `WebSocket` shape) can be plugged in without touching production code paths. ## Features - JSON-over-WebSocket wire format with mandatory Zod validation on every inbound frame via `SignalingMessageSchema`. - Strongly-typed event surface: `open`, `close`, `message`, `error`, `reconnect:attempt`, `reconnect:success`, `reconnect:exhausted`. - Bounded-backoff reconnect with caller-supplied schedule and hard cap on total attempts. After exhaustion the client emits `DriveWebRtcError(SIGNALING_UNREACHABLE)`. - `AbortSignal`-threaded `connect` — pre-aborted signals reject synchronously and mid-flight aborts close the socket cleanly. - Outbound send throws `SIGNALING_UNREACHABLE` when the socket is not in the OPEN state (no silent drops). - OpenTelemetry surface: reconnect-duration histogram and direction/outcome-tagged messages counter. ## Usage: Basic Example ```ts import { SignalingClient, type SignalingMessage, } from "@suaveplan/drive-webrtc"; const client = new SignalingClient({ url: "wss://signaling.example.com/peer", peerId: crypto.randomUUID(), }); client.on("open", () => { client.send({ type: "register", payload: { peerId: client.peerId, peerName: "alice", chunks: [], }, }); }); client.on("message", (msg: SignalingMessage) => { if (msg.type === "registered") { console.log("registered with peers:", msg.payload.peers.length); } }); client.on("error", (err) => { console.error(err.code, err.message); }); await client.connect(); ``` ## Usage: Advanced Example The constructor accepts an injectable WebSocket factory and a custom backoff schedule. The example below wires a deterministic clock, hand-tunes the backoff curve, and threads an `AbortSignal` so the caller can cancel a long-running reconnect chain on shutdown. ```ts import { SignalingClient } from "@suaveplan/drive-webrtc"; const controller = new AbortController(); const client = new SignalingClient({ url: "wss://signaling.internal/peer", peerId: "peer-A", webSocketCtor: globalThis.WebSocket, reconnectBackoffMs: [250, 500, 1_000, 2_000], maxReconnectAttempts: 4, clock: { now: () => performance.now() }, }); client.on("reconnect:attempt", ({ attempt, delayMs }) => { console.warn(`reconnect attempt ${attempt} after ${delayMs} ms`); }); client.on("reconnect:exhausted", ({ attempts }) => { console.error(`signaling exhausted after ${attempts} attempts`); }); try { await client.connect(controller.signal); } catch (err) { // pre-aborted signals reject synchronously; mid-flight aborts close // the socket cleanly and reject with SIGNALING_UNREACHABLE } // graceful shutdown — no further reconnect attempts client.close("shutting down"); ``` ## API Reference ### `new SignalingClient(options)` Construct a new client. `options.url` is the signaling endpoint; `peerId` is the local peer identifier. `webSocketCtor` defaults to `globalThis.WebSocket`. `reconnectBackoffMs` defaults to `DEFAULT_RECONNECT_BACKOFF_MS` from the package constants; `maxReconnectAttempts` defaults to the backoff length. `clock` is optional and only affects the reconnect-duration histogram metric. ### `connect(signal?: AbortSignal): Promise` Opens the WebSocket and resolves once `onopen` fires. Repeated calls while connecting / reconnecting / open are no-ops and return a resolved promise. Pre-aborted signals reject with `SIGNALING_UNREACHABLE`; mid-flight aborts close the socket cleanly and reject. ### `send(message: SignalingMessage): void` Serialises `message` with `JSON.stringify` and writes to the socket. Throws `DriveWebRtcError(SIGNALING_UNREACHABLE)` when the socket is not open. No Zod re-validation on send — the type system enforces the shape at the call site. ### `close(reason?: string): void` Closes the socket cleanly (code 1000) and disables future reconnect. Idempotent. ### `state` Read-only getter returning one of `"idle" | "connecting" | "open" | "reconnecting" | "closed"`. ## Implementation Notes - Reconnect is only scheduled after a successful `open`. If the very first connect attempt fails (unclean close or constructor throw), `connect` rejects and the client transitions to `closed`. Callers wire their own outer retry / circuit-breaker if they need re-attempts before any successful open. - Inbound `JSON.parse` failure, non-string payloads, and Zod validation failures all surface as `SIGNALING_MESSAGE_INVALID` via the `error` event. The connection is NOT closed on malformed messages — this is defensive behaviour against noisy or partially-upgraded peers. - `reconnect:attempt` fires before the backoff delay; `reconnect:success` fires from `onopen` after the socket transitions OPEN. The histogram records wall-clock elapsed time from the start of reconnect (first attempt scheduled) to the successful open. - The class extends `TypedEventEmitter` from the same package so listener registration mirrors every other event surface in `@suaveplan/drive-webrtc`.