Initial Commit
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
# SignalingClient
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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<void>`
|
||||
|
||||
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`.
|
||||
Reference in New Issue
Block a user