Files
suaveplan/openspec/archive/2026-05-12-drive-mesh-port/proposal.md
2026-05-27 23:29:53 +01:00

6.7 KiB

OpenSpec Change: drive-mesh-port

  • Change ID: drive-mesh-port
  • Package: @suaveplan/drive-mesh (new)
  • Status: PROPOSED
  • Date: 2026-04-28

Summary

P2P mesh layer over the chunks plugin. DriveMesh orchestrates peer discovery, availability gossip (compressed bitsets per file), parallel chunk pull (multi-peer racing window), peer scoring, server fallback, and upload backpressure. Mesh consumes the ChunkSource interface returned by @suaveplan/drive-plugin-chunks — it does NOT import drive core. Mesh consumes one or more DriveTransport implementations (HTTPS, WebRTC, libp2p) for the wire.

Why mesh is a separate package

A vault running on a server doesn't need mesh — it just wants drive + storage-s3. A browser session in the CDN scenario needs drive + chunks plugin + mesh + drive-transport-webrtc + drive-transport-https (server fallback). Bundling mesh into drive or chunks would push WebRTC peer code and Bloom-filter logic into every consumer.

Trust model

  • Server is the root of trust. Server signs every chunk and every manifest with its ed25519 key.
  • Peers are caches, never authorities. Mesh treats every byte from a peer as untrusted until the chunks plugin's sig-verify passes.
  • Bad-sig peers are punished. A peer that ever delivers a chunk with an invalid server signature is added to a deny list (configurable: per-session or persistent).
  • Mesh never holds the file decryption key — that's in the manifest, decrypted client-side via the recipient's key.

API surface

const mesh = new DriveMesh({
    chunks,                  // ChunkSource handle from chunksPlugin
    transports: [webrtcTransport, httpsTransport],
    serverPubKey: Uint8Array,
    serverFallback: { url: string, transport: "https" },
    scheduler: { concurrency: 8, perChunkTimeoutMs: 4000, racingWindowMs: 200 },
    peerPolicy: { maxConcurrentUploadsPerPeer: 4, blocklistOnBadSig: true },
    rng: types.Rng,
    clock: types.Clock,
});

await mesh.start();           // open transports, begin advertising
await mesh.fetchFile(fileId); // resolves manifest, schedules missing chunks
await mesh.fetchChunks(fileId, [12, 13, 14]);
mesh.on("chunk:received", ({ fileId, chunkId, fromPeer }) => …);
await mesh.stop();

Sub-systems

1. Peer index (PeerIndex)

Tracks live peers and their advertised availability per file (compressed bitset keyed by fileId), latency EMAs, error rates. This is an index overlay, not a registry. Peer identity and lifecycle events come from network-plugin-registry via the NetworkBus. The index only adds file-specific metadata on top.

// drive-mesh consumes registry events from the network layer
bus.on("peer:joined", ({ peerId, tier, latency }) => {
  driveMesh.peerIndex.add(peerId, { tier, latency });
});
bus.on("peer:left", ({ peerId }) => {
  driveMesh.peerIndex.remove(peerId);
});

Methods: addPeer(peerId, transport, metadata), removePeer, peersFor(fileId, chunkId).

2. Availability gossip (AvailabilityIndex)

  • Each peer maintains a BitSet per known fileId of chunks it has.
  • On chunk acquisition, peer pushes advertise(fileId, BitSet) to known peers via the transport.
  • On peer-join, peer pulls advertisement(fileId) from new neighbours.
  • Bitsets are gzipped on the wire.

3. Parallel-pull scheduler (Scheduler)

  • Takes a list of (fileId, chunkId) requests.
  • For each, picks top-K peers by score from peersFor(fileId, chunkId).
  • Issues request to the best peer; after racingWindowMs, issues a parallel request to the second-best (cancelled when the first arrives).
  • Per-chunk timeout perChunkTimeoutMs triggers retry on next-best peer.
  • Honours concurrency global cap.
  • After all peer attempts exhaust, falls back to serverFallback.url via the HTTPS transport.

4. Peer scoring (PeerScore)

  • score = w_latency / latency_ema + w_success * success_rate - w_errors * error_rate.
  • Updated on every chunk-receive completion or failure.
  • Sig-verify failure → permanent score = -∞ (effectively blocked).

5. Upload backpressure (UploadGovernor)

  • Limits concurrent uploads per peer (maxConcurrentUploadsPerPeer).
  • Honours WebRTC datachannel bufferedAmount watermark; pauses sends when high.
  • Emits upload:throttled events for observability.

6. NetworkBus integration

DriveMesh listens to network-plugin-registry events on the NetworkBus to maintain its PeerIndex without duplicating peer lifecycle tracking:

bus.on("peer:joined", ({ peerId, tier, latency }) => {
  driveMesh.peerIndex.add(peerId, { tier, latency });
});
bus.on("peer:left", ({ peerId }) => {
  driveMesh.peerIndex.remove(peerId);
});
bus.on("peer:updated", ({ peerId, latency }) => {
  driveMesh.peerIndex.updateLatency(peerId, latency);
});

This ensures that drive-mesh and network-plugin-registry share a single source of truth for peer identity. File-specific availability bitsets remain the responsibility of drive-mesh's AvailabilityIndex.

7. Manifest bootstrap

Defaults to manifest-as-chunk (chunks plugin's default). Mesh requests manifest:<fileId> from peers like any other chunk; if unavailable, falls back to serverFallback.url. mesh.fetchFile(fileId) is an end-to-end happy-path that:

  1. Fetch manifest (peer or server).
  2. Verify manifest sig.
  3. Diff manifest's chunk list against chunks.hasChunks(fileId).
  4. Schedule missing chunks via Scheduler.
  5. Resolve when bitset is full.

Error codes

MESH_CODES = defineCodes("MESH", [PEER_NOT_FOUND, NO_PEERS_AVAILABLE, ALL_PEERS_FAILED, SERVER_FALLBACK_FAILED, MANIFEST_FETCH_FAILED, ABORTED, TRANSPORT_ERROR, PEER_BLOCKED] as const). MeshError extends SuaveplanError<MeshErrorCode>.

Dependencies

  • Consumes: @suaveplan/drive-plugin-chunks (interface only — ChunkSource), @suaveplan/drive-transport-https (server fallback), and any other registered DriveTransport. Plus @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/streams, @opentelemetry/api, zod.
  • Consumes (NetworkBus events): @suaveplan/network-plugin-registry (peer:joined, peer:left, peer:updated). Drive-mesh does NOT import the registry package directly; it listens to bus events.
  • Does NOT depend on: @suaveplan/drive core.
  • Blocked on: drive-plugin-chunks-port, at least one drive-transport-*-port (HTTPS minimum for fallback), network-plugin-registry (for peer identity events).

Out of scope

  • Wire-protocol implementations → drive-transport-https-port, drive-transport-webrtc-port.
  • DHT-based peer discovery → future change drive-mesh-discovery-dht.
  • BitSwap-style pairwise want-list reconciliation → optimisation in v2.
  • Sharing primitives (envelope encryption, grant tokens) → @suaveplan/drive-sharing (separate change).