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,116 @@
# ICE Handlers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `ice` module owns the two thin pieces of ICE handling that `WebRTCClient`
needs but that don't belong in the orchestrator: listener attachment with
auto-restart policy, and a typed wrapper around `addIceCandidate`. Keeping
this logic in its own module means the restart algorithm is testable in
isolation (against a structural `FakeRTCPeerConnection`) and reusable if
another transport wants to adopt the same bounded-restart semantics.
The restart policy is intentionally bounded — three attempts by default —
because mobile networks routinely cycle `iceConnectionState === "failed"`
during handover. We want to ride those out without flapping forever; once
the budget is exhausted, `onStateChange("failed")` fires and the caller
(typically `WebRTCClient`) decides whether to close the peer or escalate to
the reconnect controller.
## Features
- `attachIceHandlers(pc, peerId, options)` installs both `icecandidate`
and `iceconnectionstatechange` listeners on an `RTCPeerConnection` and
returns an idempotent `dispose()` handle.
- Bounded auto-restart: on `iceConnectionState === "failed"` the helper
calls `pc.restartIce()` and emits `onRestart({ peerId, attempt, timestamp })`
until `maxRestartAttempts` (default 3) is reached.
- `restartIce` throws are swallowed (closed connections re-fire the
terminal state through `onStateChange`).
- `applyIceCandidate(pc, candidate)` wraps `pc.addIceCandidate` and
translates failures into `DriveWebRtcError(ICE_FAILED)` with the
original error preserved via `cause`.
- Injectable clock keeps `timestamp` deterministic in tests.
## Usage: Basic Example
```ts
import { applyIceCandidate, attachIceHandlers } from "@suaveplan/drive-webrtc";
const handle = attachIceHandlers(pc, "peer-A", {
onCandidate: (c) => signaling.send({ type: "ice-candidate", payload: c }),
onStateChange: (s) => console.log("ice state", s),
onRestart: (info) => console.log("restart", info),
});
// Later, when a remote candidate arrives:
await applyIceCandidate(pc, remoteCandidate);
// On peer teardown:
handle.dispose();
```
## Usage: Advanced Example
```ts
import { attachIceHandlers } from "@suaveplan/drive-webrtc";
let nowMs = 0;
const clock = { now: () => nowMs };
const restarts: { attempt: number; timestamp: number }[] = [];
const states: RTCIceConnectionState[] = [];
const handle = attachIceHandlers(fakePc, "peer-X", {
onCandidate: () => {},
onStateChange: (s) => states.push(s),
onRestart: (info) => restarts.push(info),
maxRestartAttempts: 2,
clock,
});
// Simulate three failures: the first two trigger restartIce, the third
// surfaces as a terminal "failed" without restart.
nowMs = 100; fakePc.simulateIceState("failed");
nowMs = 200; fakePc.simulateIceState("failed");
nowMs = 300; fakePc.simulateIceState("failed");
// restarts === [{ attempt: 1, timestamp: 100 }, { attempt: 2, timestamp: 200 }]
// states === ["failed", "failed", "failed"]
// handle.restartAttempts === 2
handle.dispose();
```
## API Reference
### `attachIceHandlers(pc, peerId, options): IceHandlersHandle`
- `pc: RTCPeerConnection`
- `peerId: string`
- `options.onCandidate(candidate: RTCIceCandidate): void`
- `options.onStateChange(state: RTCIceConnectionState): void`
- `options.onRestart(info: IceRestartInfo): void`
- `options.maxRestartAttempts?: number` (default 3)
- `options.clock?: { now: () => number }`
Returns `{ dispose(), restartAttempts }`. `dispose()` is idempotent.
### `applyIceCandidate(pc, candidate): Promise<void>`
Wraps `pc.addIceCandidate(candidate)`. Throws
`DriveWebRtcError(ICE_FAILED)` with `cause` populated on any failure.
## Implementation Notes
- `EventTarget`-style `addEventListener`/`removeEventListener` is used
rather than `oniceconnectionstatechange = ...` assignment so that the
module never clobbers a listener the caller installed for another
purpose.
- `pc.restartIce()` is guarded by `typeof pc.restartIce === "function"`
to remain robust against older `RTCPeerConnection` shims and the
structural test double.
- `dispose()` flips a local flag _before_ removing listeners so an
in-flight event-dispatch loop that re-enters during teardown is a
no-op rather than a double-emit.