Files
suaveplan/openspec/specs/drive-webrtc/spec.md
2026-05-27 23:29:53 +01:00

7.3 KiB

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