first commit
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# `WebRTCDrivePlugin`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`WebRTCDrivePlugin` is the top-level orchestrator that wires every
|
||||
`@suaveplan/drive-webrtc` building block — signaling client, ICE-aware
|
||||
WebRTC client, peer manager, bandwidth tracker, data-channel manager,
|
||||
chunk-integrity manager, chunk-discovery manager, and chunk source —
|
||||
behind one cohesive `start()` / `stop()` / `stats()` surface. It is the
|
||||
package's primary public entry point and the only class consumers must
|
||||
instantiate to participate in a P2P mesh.
|
||||
|
||||
The plugin reacts to signalling traffic, drives offer/answer exchange
|
||||
through the `WebRTCClient`, opens data channels when ICE reaches
|
||||
`connected`, evicts peers when ICE reports `failed`, and forwards
|
||||
locally available chunks to peers that ask for them via an injectable
|
||||
`localChunkProvider` callback.
|
||||
|
||||
## Features
|
||||
|
||||
- Idempotent `start()` / `stop()` lifecycle.
|
||||
- Real Ed25519 signature verification through `ChunkIntegrityManager`.
|
||||
- Automatic peer admission + eviction on ICE state transitions.
|
||||
- Pluggable topology strategy (`FullMesh` default,
|
||||
`SuperPeerStrategy`, `HubAndSpokeStrategy` available).
|
||||
- OTel surface — `drive.webrtc.peers_connected_total`,
|
||||
`drive.webrtc.chunks_served_total{outcome}`, `drive.webrtc.peer_rtt_ms`.
|
||||
- AbortSignal threaded into `start()` so cooperative connect cancellation
|
||||
works through every layer.
|
||||
- Optional `localChunkProvider` callback for serving inbound
|
||||
`request-chunk` data-channel frames.
|
||||
- Defaults are browser-tier (`globalThis.WebSocket`,
|
||||
`globalThis.RTCPeerConnection`); tests inject structural doubles via
|
||||
`webSocketCtor` / `rtcPeerConnectionCtor`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WebRTCDrivePlugin } from "@suaveplan/drive-webrtc";
|
||||
|
||||
const plugin = new WebRTCDrivePlugin({
|
||||
peerId: crypto.randomUUID(),
|
||||
config: {
|
||||
signaling: { url: "wss://signal.example.com/p2p" },
|
||||
},
|
||||
signalingUrl: "wss://signal.example.com/p2p",
|
||||
});
|
||||
|
||||
await plugin.start();
|
||||
|
||||
// Drive integration: hand `plugin.chunkSource` to the Drive instance so
|
||||
// it can fetch chunks over the P2P mesh.
|
||||
const bytes = await plugin.chunkSource.fetchChunk("chunk:42");
|
||||
|
||||
await plugin.stop();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
HubAndSpokeStrategy,
|
||||
WebRTCDrivePlugin,
|
||||
} from "@suaveplan/drive-webrtc";
|
||||
|
||||
const plugin = new WebRTCDrivePlugin({
|
||||
peerId: "node-a",
|
||||
config: {
|
||||
signaling: {
|
||||
url: "wss://signal.example.com/p2p",
|
||||
reconnect: true,
|
||||
reconnectDelay: 2_000,
|
||||
},
|
||||
p2p: {
|
||||
priority: 50,
|
||||
iceServers: [
|
||||
{ urls: ["stun:stun.example.com:3478"] },
|
||||
{
|
||||
urls: ["turn:turn.example.com:3478?transport=udp"],
|
||||
username: "user",
|
||||
credential: "secret",
|
||||
},
|
||||
],
|
||||
maxConcurrentDownloads: 8,
|
||||
},
|
||||
},
|
||||
signalingUrl: "wss://signal.example.com/p2p?peerId=node-a",
|
||||
topology: new HubAndSpokeStrategy({ hubPeerIds: ["hub-a"] }),
|
||||
maxPeers: 32,
|
||||
defaultPublicKey: publisherPubKey,
|
||||
localChunkProvider: async (chunkId) => {
|
||||
return cache.get(chunkId) ?? null;
|
||||
},
|
||||
});
|
||||
|
||||
plugin.on("webrtc:connected", ({ peerId }) => {
|
||||
log.info({ peerId }, "p2p peer is up");
|
||||
});
|
||||
|
||||
await plugin.start(abortController.signal);
|
||||
|
||||
const bytes = await plugin.chunkSource.fetchChunk("chunk:42", {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
const stats = plugin.stats();
|
||||
metrics.gauge("p2p.active_peers", stats.activePeers);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new WebRTCDrivePlugin(options: WebRTCDrivePluginOptions)`
|
||||
|
||||
Constructs the orchestrator and every collaborator. Does not initiate
|
||||
any network traffic until `start()` is called.
|
||||
|
||||
### `WebRTCDrivePluginOptions`
|
||||
|
||||
- `peerId: string` — local peer id (UUID v4 recommended).
|
||||
- `config: WebRTCPluginConfig` — validated by `resolveConfig`.
|
||||
- `signalingUrl: string` — WebSocket URL for the signalling server.
|
||||
- `webSocketCtor?: typeof WebSocket` — optional WebSocket constructor
|
||||
override. Defaults to `globalThis.WebSocket`.
|
||||
- `rtcPeerConnectionCtor?: RTCPeerConnectionCtor` — optional override.
|
||||
- `topology?: TopologyStrategy` — defaults to `FullMeshStrategy`.
|
||||
- `maxPeers?: number` — forwarded to `PeerManager`.
|
||||
- `defaultPublicKey?: Uint8Array` — Ed25519 public key for chunk
|
||||
signature verification.
|
||||
- `clock?: { now(): number }` — test-injectable wall clock.
|
||||
- `localChunkProvider?: LocalChunkProvider` — callback that returns the
|
||||
chunk bytes a local node owns, or `null`.
|
||||
- `defaultLabel?: string` — data channel label. Defaults to
|
||||
`"drive-chunks"`.
|
||||
|
||||
### `plugin.start(signal?: AbortSignal): Promise<void>`
|
||||
|
||||
Connect to signalling, send `register`, install all event wiring.
|
||||
Idempotent: a second call is a no-op while the plugin is running.
|
||||
Throws `DriveWebRtcError(SIGNALING_UNREACHABLE)` if invoked after
|
||||
`stop()`.
|
||||
|
||||
### `plugin.stop(reason?: string): Promise<void>`
|
||||
|
||||
Tear everything down: unsubscribe handlers, close the discovery
|
||||
manager, close all data channels, close every peer connection, close
|
||||
the signalling socket. Idempotent and safe to call before `start()`.
|
||||
|
||||
### `plugin.stats(): WebRTCStats`
|
||||
|
||||
Snapshot of plugin-level statistics. Fills `activePeers` (peers in
|
||||
`connected` state) and `totalPeers`; remaining counters stay at the
|
||||
`createDefaultStats()` baseline pending the broader stats integration.
|
||||
|
||||
### Public collaborators
|
||||
|
||||
`client`, `signaling`, `peers`, `bandwidth`, `dataChannels`,
|
||||
`integrity`, `discovery`, `chunkSource`. Each is the real instance and
|
||||
each surfaces its own event emitter — consumers can subscribe directly
|
||||
when they need finer-grained events than `WebRTCEventMap`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The plugin extends `TypedEventEmitter<WebRTCEventMap>` so it can
|
||||
surface a stable public event surface independent of the
|
||||
collaborator-specific event maps below it.
|
||||
- Drive contract deviation: the upstream `@suaveplan/drive` exposes
|
||||
`DrivePlugin<THandle>` with `install(drive)` / `dispose?()`. The
|
||||
spec for this orchestrator requires a richer `start()` / `stop()` /
|
||||
`stats()` API; the two surfaces are not directly compatible, so this
|
||||
class implements the spec surface and `name: "drive-webrtc"` rather
|
||||
than the structural `DrivePlugin` shape. Drive integration can
|
||||
trivially wrap an instance in `{ name, install(){...}, dispose() }`
|
||||
when needed.
|
||||
- ICE state mapping collapses `connected` and `completed` onto the
|
||||
4-value `PeerConnectionState`; all other states route to
|
||||
`connecting`, `disconnected`, or `failed`.
|
||||
- Local-chunk serving uses a minimal JSON envelope
|
||||
(`{type:"chunk-data", payload:{requestId, chunkId, data}}`) compatible
|
||||
with the discovery manager's wire format. Sub-chunked uploads remain
|
||||
the responsibility of the discovery / sub-chunk subsystem.
|
||||
- Every cross-component event subscription is recorded in a single
|
||||
`unsubscribers` list so `stop()` can flush them in O(n).
|
||||
Reference in New Issue
Block a user