164 lines
7.3 KiB
Markdown
164 lines
7.3 KiB
Markdown
# @suaveplan/drive-webrtc — WebRTC P2P Mesh Capability
|
|
|
|
Browser-tier WebRTC peer-to-peer mesh plugin for `@suaveplan/drive`: signalling
|
|
client, RTC client (offer/answer/ICE/reconnect), peer manager with bandwidth
|
|
tracking + topology strategies, data-channel pool, chunk discovery + integrity
|
|
verification, sub-chunk splitter/reassembler, LRU cache, rate limiter, security
|
|
validators, and a `WebRTCDrivePlugin` orchestrator that wires them all together.
|
|
|
|
## Requirements
|
|
|
|
### Requirement: Signalling client surface
|
|
|
|
The package SHALL expose `SignalingClient` over a WebSocket transport:
|
|
|
|
- Injectable `webSocketCtor` (defaults to `globalThis.WebSocket`) for testability.
|
|
- Zod-validated inbound messages via `SignalingMessageSchema`; malformed
|
|
payloads MUST emit `"error"` (not close the socket) with
|
|
`DriveWebRtcError(SIGNALING_MESSAGE_INVALID)`.
|
|
- Reconnect with configurable backoff array + `maxReconnectAttempts`; emits
|
|
`"reconnect:attempt"`, `"reconnect:success"`, `"reconnect:exhausted"` events.
|
|
- `send` while not open MUST throw `DriveWebRtcError(SIGNALING_UNREACHABLE)`.
|
|
- AbortSignal threaded through `connect`.
|
|
|
|
#### Scenario: Pre-aborted connect rejects
|
|
|
|
- **WHEN** `client.connect(signal)` is called with an already-aborted signal
|
|
- **THEN** the returned promise MUST reject with `DriveWebRtcError`.
|
|
|
|
### Requirement: WebRTC client + ICE-restart policy
|
|
|
|
The package SHALL expose `WebRTCClient` and `resolveConfig`:
|
|
|
|
- `resolveConfig(input)` Zod-validates via `WebRTCPluginConfigSchema`; on
|
|
failure throws `DriveWebRtcError(CONFIG_INVALID, { cause: zodError })`.
|
|
- `WebRTCClient` lazy-creates one `RTCPeerConnection` per `remotePeerId` and
|
|
caches in an internal map.
|
|
- On `iceConnectionState === "failed"`, automatically calls `pc.restartIce()`
|
|
up to `maxIceRestartAttempts` (default 3) and emits `"ice:restart"` with
|
|
`{ peerId, attempt, timestamp }`.
|
|
- After exhaustion, no further restart attempts fire; caller decides to evict.
|
|
- `createOffer` / `handleOffer` / `handleAnswer` / `addIceCandidate` translate
|
|
DOMExceptions into `DriveWebRtcError(PEER_HANDSHAKE_FAILED | ICE_FAILED)`
|
|
with original error in `cause`.
|
|
|
|
### Requirement: Real signature verification
|
|
|
|
`ChunkIntegrityManager.verifySignature` MUST perform real Ed25519 verification
|
|
via `@suaveplan/crypto-curve` `ed25519.verify(signature, message, publicKey)`
|
|
over canonical `hash || utf8(chunkId)`. NO placeholder, NO "forward-looking"
|
|
wording allowed in implementation or docs.
|
|
|
|
#### Scenario: Tampered signature rejected
|
|
|
|
- **WHEN** `verifySignature` is called with a signature whose any byte has been
|
|
flipped
|
|
- **THEN** it MUST return `{ ok: false, reason: "bad-signature", code:
|
|
"CHUNK_INTEGRITY_FAILED" }`.
|
|
|
|
### Requirement: Hash verification
|
|
|
|
`computeChunkHash` MUST compute SHA-256 over the chunk bytes; `verifyChunkIntegrity`
|
|
MUST recompute the hash and compare constant-time via `constantTimeEqual` from
|
|
`@suaveplan/crypto-utils`.
|
|
|
|
### Requirement: Sub-chunk splitter + reassembler
|
|
|
|
The package SHALL expose `splitIntoSubChunks`, `sendSubChunks`, and
|
|
`SubChunkReassembler`:
|
|
|
|
- Default `maxBytes = 16_000` (DataChannel safe payload).
|
|
- Returns parts tagged `{ chunkId, partId, index, total, bytes }`.
|
|
- `sendSubChunks(parts, callback, { signal?, delayMs? })` is transport-agnostic,
|
|
AbortSignal-aware on both pre-iteration and inter-part delays.
|
|
- `SubChunkReassembler.addPart` returns a discriminated union (`incomplete` /
|
|
`complete` / `duplicate`).
|
|
- `assertComplete(chunkId)` throws `DriveWebRtcError(SUB_CHUNK_MISSING)` when
|
|
any index is absent.
|
|
- `cleanup()` purges expired in-progress assemblies past `timeoutMs`;
|
|
`assertComplete` then throws `SUB_CHUNK_TIMEOUT`.
|
|
|
|
### Requirement: Peer manager + topology strategies
|
|
|
|
The package SHALL expose `PeerManager`, `PeerBandwidthTracker`, and three
|
|
`TopologyStrategy` implementations: `FullMeshStrategy`, `SuperPeerStrategy`,
|
|
`HubAndSpokeStrategy`. Each strategy implements `selectConnections` and
|
|
`admitOrEvict` for capacity-bound peer admission.
|
|
|
|
`PeerBandwidthTracker` MUST compose `score = 0.5*(1-bytesInLoad) + 0.3*(1-requestsInLoad) + 0.2*rttFactor` and expose a snapshot for ranking. Score
|
|
MUST default to `1.0` when no samples have been recorded.
|
|
|
|
### Requirement: Data channel manager
|
|
|
|
`DataChannelManager` SHALL pool `RTCDataChannel`s per `(peerId, label)`,
|
|
back-pressure via `bufferedAmount`/`bufferedamountlow`, and route messages by
|
|
label. `send` MUST throw `DriveWebRtcError(DATA_CHANNEL_CLOSED)` when the
|
|
channel is not open. `open(peerId)` on an unknown peer throws
|
|
`DriveWebRtcError(PEER_NOT_FOUND)`.
|
|
|
|
### Requirement: Chunk discovery with peer fallback
|
|
|
|
`ChunkDiscoveryManager.request(chunkId)` SHALL:
|
|
|
|
1. De-duplicate concurrent requests for the same `chunkId`.
|
|
2. Rank candidate peers by `PeerBandwidthTracker` score.
|
|
3. Iterate candidates in order, sending `request-chunk` and awaiting reassembly
|
|
within `requestTimeoutMs`.
|
|
4. Verify via `ChunkIntegrityManager.verify(chunk, publicKey?)`.
|
|
5. On any candidate failure (timeout / disconnect / integrity), emit
|
|
`"request:fail"` and move to next candidate.
|
|
6. After all candidates exhausted, emit `"request:exhausted"` and throw
|
|
`DriveWebRtcError(CHUNK_NOT_AVAILABLE)`.
|
|
|
|
### Requirement: WebRTCDrivePlugin orchestration
|
|
|
|
The package SHALL expose `WebRTCDrivePlugin` with `name: "drive-webrtc"` and
|
|
lifecycle methods `start(signal?)` / `stop(reason?)` / `stats()`. The plugin
|
|
MUST wire `SignalingClient`, `WebRTCClient`, `PeerManager`, `DataChannelManager`,
|
|
`ChunkIntegrityManager`, `ChunkDiscoveryManager`, and `WebRTCChunkSource`
|
|
internally and forward their events through a unified `WebRTCEventMap`.
|
|
|
|
#### Scenario: stop is idempotent
|
|
|
|
- **WHEN** `plugin.stop()` is invoked twice in succession
|
|
- **THEN** the second invocation MUST NOT throw and MUST NOT re-close already-
|
|
closed transports.
|
|
|
|
### Requirement: Subpath exports + tree-shaking
|
|
|
|
`package.json` SHALL expose 11 subpath exports covering `./client/webrtc-client`,
|
|
`./chunk-source/chunk-source`, `./plugin/webrtc-plugin`,
|
|
`./signaling/signaling-client`, `./peer-manager/peer-manager`,
|
|
`./data-channel/data-channel-manager`, `./rate-limiter/rate-limiter`,
|
|
`./lru-cache/lru-cache`, `./integrity/chunk-integrity`,
|
|
`./security/security-validator`, `./sub-chunk/sub-chunk-splitter`. Leaf
|
|
subpaths MUST NOT drag in unrelated subsystems (verified via per-subpath bundle-
|
|
size snapshot tests).
|
|
|
|
### Requirement: Universal-tier-clean tests
|
|
|
|
Tests MUST NOT use `vi.stubGlobal`, `vi.mock`, or any runtime monkey-patch.
|
|
WebSocket/RTCPeerConnection paths are exercised via injected structural
|
|
real implementations (`FakeWebSocket`, `FakeRTCPeerConnection`,
|
|
`FakeRTCDataChannel`) using the `webSocketCtor` / `rtcPeerConnectionCtor`
|
|
options. All test imports come from `@suaveplan/testing/runner`.
|
|
|
|
### Requirement: OTel observability
|
|
|
|
The package SHALL register the following instruments under tracer
|
|
`@suaveplan/drive-webrtc`:
|
|
|
|
- Counter `drive.webrtc.peers_connected_total`
|
|
- Counter `drive.webrtc.chunks_served_total{outcome}`
|
|
- Counter `drive.webrtc.chunks_rejected_total{reason}`
|
|
- Counter `drive.webrtc.signaling_messages_total{direction,outcome}`
|
|
- Histogram `drive.webrtc.peer_rtt_ms`
|
|
- Histogram `drive.webrtc.chunk_transfer_ms`
|
|
- Histogram `drive.webrtc.signaling_reconnect_ms`
|
|
|
|
### Requirement: Coverage + gate
|
|
|
|
`bunx turbo lint typecheck test:coverage --filter=@suaveplan/drive-webrtc` MUST
|
|
be green and istanbul coverage MUST be 100% on every touched module before any
|
|
release.
|