first commit
This commit is contained in:
116
docs/networking/drive/drive-webrtc/src/client/ice/ice.md
Normal file
116
docs/networking/drive/drive-webrtc/src/client/ice/ice.md
Normal 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.
|
||||
Reference in New Issue
Block a user