first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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`.