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

4.5 KiB

@suaveplan/drive-transport-webrtc — WebRTC DriveTransport

WebRTC datachannel implementation of the DriveTransport interface for browser-to-browser chunk transfer. Composes WebRTCClient + DataChannelManager from @suaveplan/drive-webrtc; consumed by @suaveplan/drive-mesh alongside drive-transport-https as one of the pluggable wire transports.

Requirements

Requirement: DriveTransport contract

The package SHALL expose WebRTCDriveTransport implementing the structural DriveTransport interface:

  • name: "drive-transport-webrtc"
  • requestChunk(peerId, fileId, chunkId, signal?): AsyncIterable<Uint8Array>
  • advertise(peerId, fileId, bitset): Promise<void>
  • onAdvertisement(handler): () => void
  • peers(): Iterable<PeerInfo>
  • dispose(): Promise<void>

The local DriveTransport interface MUST remain structurally compatible with the canonical one defined in @suaveplan/drive-mesh so consumers can swap to the mesh-canonical type without code changes when mesh lands.

Requirement: Wire framing

Each datachannel message SHALL be a length-prefixed binary frame:

[4 B uint32 BE  frameLen]
[1 B uint8     frameType]   // 0x01 chunk-request, 0x02 chunk-response, 0x03 advertisement
[frameLen-1 B  payload]
  • MAX_FRAME_BYTES = 16 MiB; over-cap frames MUST be rejected with DriveTransportWebRtcError(FRAME_TOO_LARGE).
  • Malformed frames (length mismatch, unknown type) MUST raise FRAME_INVALID.
  • Chunk request payload: fileId(32) ‖ chunkId(4 BE uint32).
  • Chunk response payload: raw chunk bytes (no extra length prefix — frame already carries frameLen).
  • Advertisement payload: fileId(32) ‖ gzipped-bitset. Compression uses the platform CompressionStream/DecompressionStream API; the stream ctors are injectable via StreamCtorResolver so tests cover missing- stream branches without vi.stubGlobal.

Requirement: requestChunk semantics

requestChunk(peerId, fileId, chunkId, signal?):

  1. Reuse existing channel via dataChannels.get(peerId, defaultLabel) or open(peerId, defaultLabel). Unknown peer → PEER_NOT_CONNECTED.
  2. Encode + send chunk-request frame.
  3. Wait for the next chunk-response frame from the same (peerId, label) within requestTimeoutMs. AbortSignal cancels the wait + throws ABORTED. Timeout throws REQUEST_TIMEOUT.
  4. Yield the response payload as a single value of the AsyncIterable<Uint8Array>.
  5. Chunk-responses FIFO-match per peer — the wire format has no request-id field. Documented in implementation notes as a known design constraint.

Requirement: advertise + onAdvertisement

advertise(peerId, fileId, bitset):

  • Gzip the bitset via CompressionStream. Compression failure throws BITSET_COMPRESSION_FAILED.
  • Encode advertisement frame, send via DataChannelManager.send.
  • Underlying send failure (channel closed) maps to DATA_CHANNEL_CLOSED.

onAdvertisement(handler) subscribes to dataChannels.on("channel:message"), decodes incoming frames, invokes handler only for frameType === 0x03, returns an unsubscribe function.

Requirement: AbortSignal threading

Every async path (requestChunk, advertise, listener wait) SHALL accept and honour an optional AbortSignal. Pre-aborted signals throw DriveTransportWebRtcError(ABORTED) before any side effect.

Requirement: dispose idempotency

dispose() SHALL unsubscribe from DataChannelManager listeners and release internal state. It MUST be safe to call multiple times. It MUST NOT close the underlying WebRTCClient — client lifetime is managed by the parent (typically WebRTCDrivePlugin or DriveMesh).

Requirement: OTel observability

The package SHALL register the following instruments under tracer @suaveplan/drive-transport-webrtc:

  • Counter drive.transport.webrtc.requests_total{outcome}
  • Counter drive.transport.webrtc.advertisements_total{direction}
  • Histogram drive.transport.webrtc.request_duration_ms
  • Histogram drive.transport.webrtc.frame_size_bytes

Requirement: Tests use injected real fakes

Tests MUST NOT use vi.mock or vi.stubGlobal. The FakeRTCPeerConnection / FakeRTCDataChannel real-implementation pattern from @suaveplan/drive-webrtc MUST be reused (structural EventTarget subclasses with simulate* driver methods). All test imports come from @suaveplan/testing/runner.

Requirement: Coverage + gate

bunx turbo lint typecheck test:coverage --filter=@suaveplan/drive-transport-webrtc MUST be green and istanbul coverage MUST be 100% on every touched module before any release.