Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
# @suaveplan/drive-mesh - P2P mesh orchestrator for the drive chunks plugin. Peer registry, availability gossip, parallel-pull scheduler with racing window, server fallback, and bad-sig denylist. Consumes ChunkSource (drive-plugin-chunks) and DriveTransport (drive-transport-*).
## Summary
P2P mesh orchestrator over the drive chunks plugin. Consumes
`ChunkSource` (from `@suaveplan/drive-plugin-chunks`) and one or more
`DriveTransport` implementations (HTTPS, WebRTC, libp2p, in-memory).
Provides parallel chunk pull with a first-byte racing window, peer
scoring, bad-signature denylist, availability bitset gossip, and
server fallback.
The package is universal: zero browser- or React-specific code.
It composes on the server (Node) for vault / CDN sources, in the
browser for distributed clients, and inside test harnesses with
in-memory transports.
<!-- begin:auto-docs -->
## Documentation tree
- [error-codes](./src/error-codes/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
- [error-codes](./src/error-codes/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
- [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
- [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
- [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
- [gossip-engine](./src/gossip/gossip-engine/gossip-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_ - [typed-event-emitter](./src/internal/typed-event-emitter/typed-event-emitter.md) — <!-- begin:auto-docs --> _(no submodules)_- [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_ - [drive-mesh](./src/mesh/drive-mesh/drive-mesh.md) — <!-- begin:auto-docs --> _(no submodules)_- [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ - [peer-registry](./src/peer-registry/peer-registry/peer-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_ - [parallel-puller](./src/scheduler/parallel-puller/parallel-puller.md) — <!-- begin:auto-docs --> _(no submodules)_- [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_ - [server-fallback](./src/server-fallback/server-fallback/server-fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the frozen error-code registry for `@suaveplan/drive-mesh` via `defineCodes("DRIVE_MESH", …)` from `@suaveplan/…
## Quick start
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
const mesh = new DriveMesh({
chunks, // ChunkSource from chunksPlugin
transports: [httpsTransport], // any DriveTransport-compatible impl
serverPubKey, // server's ed25519 public key
serverFallback: { url: "https://cdn.example.com/chunks", transport: "https" },
});
await mesh.start();
const chunkBytes = await mesh.requestChunk(fileIdHex, 0);
await mesh.stop();
```
## Layering
```text
┌──────────────────────────────────────────────────────────────┐
│ application (vault / drive) │
├──────────────────────────────────────────────────────────────┤
│ @suaveplan/drive-mesh (this package) │
│ peer registry · gossip · racing puller · server fallback │
├──────────────────────────────────────────────────────────────┤
│ @suaveplan/drive-mesh-protocol (envelopes + bitsets) │
├──────────────────────────────────────────────────────────────┤
│ @suaveplan/drive-plugin-chunks (sig-verify, manifest) │
├──────────────────────────────────────────────────────────────┤
│ DriveTransport implementations (HTTPS / WebRTC / libp2p) │
└──────────────────────────────────────────────────────────────┘
```
`DriveMesh` is the only public class most callers construct. The four
subsystems (`PeerRegistry`, `GossipEngine`, `ParallelPuller`,
`ServerFallback`) are exported under subpaths so advanced consumers
can compose them directly.
## Subpath table
| Subpath | What |
| ---------------------------------------- | ----------------------------------- |
| `@suaveplan/drive-mesh` | Root barrel — `DriveMesh` + types. |
| `@suaveplan/drive-mesh/mesh/drive-mesh` | `DriveMesh` class. |
| `@suaveplan/drive-mesh/peer-registry/peer-registry` | `PeerRegistry`. |
| `@suaveplan/drive-mesh/gossip/gossip-engine` | `GossipEngine`. |
| `@suaveplan/drive-mesh/scheduler/parallel-puller` | `ParallelPuller`. |
| `@suaveplan/drive-mesh/server-fallback/server-fallback` | `ServerFallback`. |
| `@suaveplan/drive-mesh/types` | All public types. |
| `@suaveplan/drive-mesh/schemas` | Zod schemas. |
| `@suaveplan/drive-mesh/errors` | `DRIVE_MESH_CODES`, `DriveMeshError`. |
## Trust model
The server's ed25519 public key (`serverPubKey`) is the only root of
trust. Every chunk returned by a peer or the fallback URL must pass
`verifyChunkSig` against this key before drive-mesh resolves its
`requestChunk` promise. A peer that ever returns a chunk with an
invalid signature is moved to a denylist — `denylistMode: "session"`
(default) clears the list on `mesh.stop()`; `"persistent"` survives
stops.
Mesh never holds file decryption keys. Plaintext only ever materialises
inside the chunks plugin's decrypt pipeline, which is keyed by the
recipient's per-vault key.
## Scoring formula
`PeerRegistry` ranks peers per `(fileId, chunkId)` using a weighted
sum:
```text
score = 2 · (100 / (rttMs + 1))
+ 3 · successRate
− 5 · errorRate
```
- `rttMs` is an EMA (α = 0.3) of observed chunk-fetch wall time.
- `successRate` and `errorRate` are over the lifetime of the registry
entry. Peers with no observations start at `successRate = 0.5`
(neutral) and `errorRate = 0`.
- Bad-sig peers are pinned at `-Infinity` so they never appear in
`rank()` output once denied.
## OpenTelemetry metrics
The mesh emits the following events; consumers wire them to whichever
OTel meter they prefer. Drive-mesh stays metric-emitter-agnostic so it
does not pin a meter provider on universal consumers.
| Event | Suggested metric | Type |
| ----------------- | -------------------------------------------------- | ----------- |
| `chunk:served` | `drive.mesh.chunks_served_total{outcome="peer"}` | counter |
| `chunk:fallback` | `drive.mesh.chunks_served_total{outcome="fallback"}` | counter |
| `chunk:failed` | `drive.mesh.chunks_served_total{outcome="fail"}` | counter |
| `chunk:served` | `drive.mesh.chunk_fetch_ms{outcome="peer"}` | histogram |
| `chunk:fallback` | `drive.mesh.chunk_fetch_ms{outcome="fallback"}` | histogram |
| `peer:joined` | `drive.mesh.peers_admitted_total` | counter |
| `peer:denied` | `drive.mesh.peers_denied_total{reason}` | counter |
| `gossip:sent` | `drive.mesh.gossip_size_bytes` | histogram |
## Error codes
| Code | When |
| ----------------------------- | ----------------------------------------------- |
| `DRIVE_MESH.INVALID_OPTIONS` | Construction or `fileId` shape rejected. |
| `DRIVE_MESH.NOT_STARTED` | `requestChunk` before `start()`. |
| `DRIVE_MESH.CHUNK_UNAVAILABLE`| No candidate peers and no fallback configured. |
| `DRIVE_MESH.SIG_VERIFY_FAILED`| Winning peer returned a chunk with bad sig. |
| `DRIVE_MESH.ALL_PEERS_FAILED` | Every candidate failed; no fallback configured. |
| `DRIVE_MESH.FALLBACK_FAILED` | Peers + fallback both failed. |
| `DRIVE_MESH.INVALID_BITSET` | `advertise` got bad `totalChunks`. |
| `DRIVE_MESH.TRANSPORT_FAILED` | Single-attempt transport failure (internal). |
| `DRIVE_MESH.PEER_DENIED` | Peer on denylist (manual operations). |
| `DRIVE_MESH.ABORTED` | Caller-supplied AbortSignal aborted. |
## License
MIT © Suaveplan

View File

@@ -0,0 +1,116 @@
{
"name": "@suaveplan/drive-mesh",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"browser": "./dist/index.js",
"description": "P2P mesh orchestrator for the drive chunks plugin. Peer registry, availability gossip, parallel-pull scheduler with racing window, server fallback, and bad-sig denylist. Consumes ChunkSource (drive-plugin-chunks) and DriveTransport (drive-transport-*).",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./mesh/drive-mesh": {
"import": {
"types": "./dist/mesh/drive-mesh/drive-mesh.d.ts",
"default": "./dist/mesh/drive-mesh/drive-mesh.js"
}
},
"./peer-registry/peer-registry": {
"import": {
"types": "./dist/peer-registry/peer-registry/peer-registry.d.ts",
"default": "./dist/peer-registry/peer-registry/peer-registry.js"
}
},
"./gossip/gossip-engine": {
"import": {
"types": "./dist/gossip/gossip-engine/gossip-engine.d.ts",
"default": "./dist/gossip/gossip-engine/gossip-engine.js"
}
},
"./scheduler/parallel-puller": {
"import": {
"types": "./dist/scheduler/parallel-puller/parallel-puller.d.ts",
"default": "./dist/scheduler/parallel-puller/parallel-puller.js"
}
},
"./server-fallback/server-fallback": {
"import": {
"types": "./dist/server-fallback/server-fallback/server-fallback.d.ts",
"default": "./dist/server-fallback/server-fallback/server-fallback.js"
}
},
"./types": {
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
}
},
"./schemas": {
"import": {
"types": "./dist/schemas/index.d.ts",
"default": "./dist/schemas/index.js"
}
},
"./errors": {
"import": {
"types": "./dist/error-codes/index.d.ts",
"default": "./dist/error-codes/index.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/ tests/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/ tests/",
"format": "bunx biome format --write --max-diagnostics 500 src/ tests/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/ tests/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@opentelemetry/api": "catalog:observability",
"@suaveplan/crypto-curve": "workspace:^",
"@suaveplan/drive-mesh-protocol": "workspace:^",
"@suaveplan/drive-plugin-chunks": "workspace:^",
"@suaveplan/error": "workspace:^",
"@suaveplan/events": "workspace:^",
"@suaveplan/types": "workspace:^",
"tslib": "catalog:build",
"zod": "catalog:forms"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"fast-check": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": [
"dist",
"src",
"README.md"
]
}

View File

@@ -0,0 +1,93 @@
# `error-codes.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the frozen error-code registry for `@suaveplan/drive-mesh` via
`defineCodes("DRIVE_MESH", …)` from `@suaveplan/error`. Every failure
that crosses a public API boundary in the mesh — peer denial, signature
verification failure, transport breakage, fallback exhaustion — surfaces
as a `DriveMeshError` whose `code` is one of the literals in
`DRIVE_MESH_CODES`. Callers pattern-match on `error.code` rather than
parsing free-form messages, which keeps log-shipping, alerting, and
retry policies stable across releases.
## Features
- Namespaced codes (`DRIVE_MESH.CHUNK_UNAVAILABLE`, etc).
- Frozen at module load — additions go through OpenSpec.
- `DriveMeshError` widens the protected base constructor to public so
feature modules can throw directly without subclassing further.
- Codes cover the full lifecycle: peer selection, signature verify,
transport failure, server fallback, configuration validation.
## Usage: Basic Example
```ts
import { DRIVE_MESH_CODES, DriveMeshError } from "@suaveplan/drive-mesh/errors";
throw new DriveMeshError(
DRIVE_MESH_CODES.SIG_VERIFY_FAILED,
"chunk signature did not validate against server pubkey"
);
```
## Usage: Advanced Example
```ts
import {
DRIVE_MESH_CODES,
DriveMeshError,
} from "@suaveplan/drive-mesh/errors";
try {
await mesh.requestChunk(fileId, chunkId);
} catch (err) {
if (err instanceof DriveMeshError) {
switch (err.code) {
case DRIVE_MESH_CODES.ALL_PEERS_FAILED:
// every peer attempted; falling back to server
break;
case DRIVE_MESH_CODES.SIG_VERIFY_FAILED:
// the responsible peer is now denylisted
break;
case DRIVE_MESH_CODES.ABORTED:
// caller aborted via AbortSignal
break;
default:
throw err;
}
} else {
throw err;
}
}
```
## API Reference
- `DRIVE_MESH_CODES` — `Readonly<Record<Reason, "DRIVE_MESH.<Reason>">>`
containing: `CHUNK_UNAVAILABLE`, `ALL_PEERS_FAILED`,
`SIG_VERIFY_FAILED`, `TRANSPORT_FAILED`, `FALLBACK_FAILED`,
`INVALID_BITSET`, `PEER_DENIED`, `INVALID_OPTIONS`, `ABORTED`,
`NOT_STARTED`.
- `DriveMeshErrorCode` — type alias for the union of values above.
- `DriveMeshError(code, message, options?)` — concrete error class
extending `SuaveplanError<DriveMeshErrorCode>`; supports the standard
`cause` chain via `SuaveplanErrorOptions`.
## Implementation Notes
The registry is closed: each new failure mode must be added via an
OpenSpec change and corresponding spec requirement. Because
`defineCodes` returns a frozen record, accidental in-process mutation
fails fast in development.
`DriveMeshError`'s constructor is a thin pass-through that widens the
base's `protected` constructor to `public`. The class exists so
`instanceof` checks distinguish mesh errors from generic
`SuaveplanError`s thrown by sibling packages (e.g.
`ChunksError`/`DmpError`). Causes propagated from those packages remain
inspectable through the standard `error.cause` chain.

View File

@@ -0,0 +1,58 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
DRIVE_MESH_CODES,
DriveMeshError,
type DriveMeshErrorCode,
} from "./error-codes.js";
describe("DRIVE_MESH_CODES", () => {
it("contains every expected code", { timeout: 5000 }, () => {
const expected = [
"DRIVE_MESH.CHUNK_UNAVAILABLE",
"DRIVE_MESH.ALL_PEERS_FAILED",
"DRIVE_MESH.SIG_VERIFY_FAILED",
"DRIVE_MESH.TRANSPORT_FAILED",
"DRIVE_MESH.FALLBACK_FAILED",
"DRIVE_MESH.INVALID_BITSET",
"DRIVE_MESH.PEER_DENIED",
"DRIVE_MESH.INVALID_OPTIONS",
"DRIVE_MESH.ABORTED",
"DRIVE_MESH.NOT_STARTED",
];
expect(new Set(Object.values(DRIVE_MESH_CODES))).toEqual(
new Set(expected)
);
});
it("is frozen", { timeout: 5000 }, () => {
expect(Object.isFrozen(DRIVE_MESH_CODES)).toBe(true);
});
});
describe("DriveMeshError", () => {
it("carries code, message, and domain", { timeout: 5000 }, () => {
const err = new DriveMeshError(
DRIVE_MESH_CODES.ABORTED,
"aborted by caller"
);
expect(err).toBeInstanceOf(DriveMeshError);
expect(err.code).toBe(DRIVE_MESH_CODES.ABORTED);
expect(err.message).toBe("aborted by caller");
expect(err.domain).toBe("DRIVE_MESH");
});
it("preserves cause chain", { timeout: 5000 }, () => {
const cause = new Error("underlying");
const err = new DriveMeshError(
DRIVE_MESH_CODES.SIG_VERIFY_FAILED,
"bad",
{ cause }
);
expect(err.cause).toBe(cause);
});
it("typed code union resolves at compile time", { timeout: 5000 }, () => {
const code: DriveMeshErrorCode = DRIVE_MESH_CODES.NOT_STARTED;
expect(code).toBe("DRIVE_MESH.NOT_STARTED");
});
});

View File

@@ -0,0 +1,45 @@
/**
* Error codes for `@suaveplan/drive-mesh`.
*
* Domain-prefixed via `defineCodes("DRIVE_MESH", …)` so callers can
* pattern-match `error.code` without parsing strings.
*/
import type { SuaveplanErrorOptions } from "@suaveplan/error";
import { defineCodes, SuaveplanError } from "@suaveplan/error";
export const DRIVE_MESH_CODES = defineCodes("DRIVE_MESH", [
"CHUNK_UNAVAILABLE",
"ALL_PEERS_FAILED",
"SIG_VERIFY_FAILED",
"TRANSPORT_FAILED",
"FALLBACK_FAILED",
"INVALID_BITSET",
"PEER_DENIED",
"INVALID_OPTIONS",
"ABORTED",
"NOT_STARTED",
] as const);
export type DriveMeshErrorCode =
(typeof DRIVE_MESH_CODES)[keyof typeof DRIVE_MESH_CODES];
/**
* Concrete error class for `@suaveplan/drive-mesh`. The constructor
* normalises the `domain` discriminator field at every instantiation
* so `instanceof DriveMeshError` cannot collapse against an unrelated
* `SuaveplanError` subclass. The discriminator also gives tests an
* easy way to assert the namespace without parsing the code string.
*/
export class DriveMeshError extends SuaveplanError<DriveMeshErrorCode> {
public readonly domain: "DRIVE_MESH";
public constructor(
code: DriveMeshErrorCode,
message: string,
options?: SuaveplanErrorOptions
) {
super(code, message, options);
this.domain = "DRIVE_MESH";
}
}

View File

@@ -0,0 +1,5 @@
export type { DriveMeshErrorCode } from "./error-codes.js";
export {
DRIVE_MESH_CODES,
DriveMeshError,
} from "./error-codes.js";

View File

@@ -0,0 +1,116 @@
# `gossip-engine.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`GossipEngine` runs the bitset-gossip loop drive-mesh uses to keep
peers informed of local chunk availability. Two responsibilities:
(1) periodically push local availability to every connected peer,
and (2) accept inbound advertisements and forward them to the
`PeerRegistry`.
The engine is transport-agnostic — it iterates the configured
`DriveTransport[]` and uses each transport's `advertise` /
`onAdvertisement` surface. Gossip is intentionally best-effort: a
failed `advertise` call is swallowed and retried on the next tick.
Chunk-fetch failures, by contrast, drive the scoring + denylist
inside the registry.
## Features
- Periodic outbound tick (default 5_000 ms, configurable).
- Inbound handler wired automatically on `start`.
- `tick()` exposed for deterministic testing.
- Cleanup on `stop` removes every subscription and clears the timer.
- Pluggable `advertSource` so the mesh decides what bytes go on the
wire (the chunks plugin's `hasChunks` result + framing).
## Usage: Basic Example
```ts
import { GossipEngine } from "@suaveplan/drive-mesh/gossip/gossip-engine";
const gossip = new GossipEngine({
registry,
transports: [httpsTransport],
advertSource: fileId => buildAdvert(fileId),
trackedFiles: () => chunks.knownFiles(),
});
gossip.start();
// ...
gossip.stop();
```
## Usage: Advanced Example
```ts
import { GossipEngine } from "@suaveplan/drive-mesh/gossip/gossip-engine";
import { encodeFramedBitset } from "@suaveplan/drive-mesh-protocol";
const gossip = new GossipEngine({
registry,
transports: [webrtcTransport, httpsTransport],
intervalMs: 1_000,
advertSource: async fileId => {
const bits = await chunks.hasChunks(fromHex(fileId));
const totalChunks = bits.length;
const raw = boolsToBytes(bits);
return {
fileId,
totalChunks,
bitset: encodeFramedBitset(raw, totalChunks),
};
},
trackedFiles: () => activeFiles,
onAdvertiseSent: (peerId, advert) =>
telemetry.emit("gossip:sent", { peerId, fileId: advert.fileId }),
onAdvertiseReceived: (peerId, advert) =>
telemetry.emit("gossip:received", { peerId, fileId: advert.fileId }),
});
gossip.start();
// Force-trigger a tick for an integration test:
await gossip.tick();
```
## API Reference
- `class GossipEngine`:
- `constructor(options: GossipEngineOptions)`.
- `start(): void` — wires inbound subscriptions + schedules the
outbound tick. Idempotent.
- `stop(): void` — cancels the tick and detaches subscriptions.
Idempotent.
- `tick(): Promise<void>` — run a single outbound pass; intended
for tests.
- `GossipEngineOptions`:
- `registry` — `PeerRegistry` instance.
- `transports` — `readonly DriveTransport[]`.
- `advertSource(fileId)` — returns the local advertisement or
`undefined` when nothing to advertise.
- `trackedFiles()` — generator of `fileId` hex strings.
- `intervalMs?` — outbound tick period; default `5_000`.
- `clock?` — defaults to `Date.now`-based clock.
- `onAdvertiseSent?(peerId, advert)` — observability hook.
- `onAdvertiseReceived?(peerId, advert)` — observability hook.
## Implementation Notes
Outbound fan-out iterates every peer reported by every transport.
Duplicate peers across transports receive duplicate advertisements,
which is fine: receivers idempotently overwrite the bitset and the
registry's `recordAvailability` is set-shaped.
The scheduling primitive is `setTimeout` rather than `setInterval` so
overlapping ticks cannot occur — the next tick is only scheduled
after the previous tick's promise resolves. `unref` is applied where
available (Node) so the event loop can drain in tests; browsers do
not implement `unref`, which is fine because they manage timers
through page lifecycle instead.
Inbound advertisements flow through `addOrUpdate` (updates
`lastSeen`) and `recordAvailability` (updates the bitset) so a peer
sending its first advertisement is admitted to the registry without
a separate `peer:joined` event being required.

View File

@@ -0,0 +1,226 @@
import { encodeFramedBitset } from "@suaveplan/drive-mesh-protocol";
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
Broker,
FakeDriveTransport,
} from "../../../tests/helpers/fake-transport.js";
import { PeerRegistry } from "../../peer-registry/peer-registry/index.js";
import type { ChunkAdvertisement } from "../../types/index.js";
import { GossipEngine } from "./gossip-engine.js";
function bitset(bits: readonly number[], total: number): Uint8Array {
const raw = new Uint8Array(Math.ceil(total / 8));
for (const b of bits) raw[b >> 3] |= 1 << (b & 7);
return encodeFramedBitset(raw, total);
}
const FILE_ID = "a".repeat(64);
describe("GossipEngine", () => {
it("starts and stops idempotently", { timeout: 5000 }, () => {
const broker = new Broker();
const transport = new FakeDriveTransport({
broker,
localPeerId: "self",
});
const registry = new PeerRegistry();
const engine = new GossipEngine({
registry,
transports: [transport],
advertSource: () => undefined,
trackedFiles: () => [],
intervalMs: 60_000,
});
engine.start();
engine.start(); // idempotent
engine.stop();
engine.stop(); // idempotent
transport.dispose();
});
it("inbound advertisement updates registry + fires hook", {
timeout: 5000,
}, async () => {
const broker = new Broker();
const transport = new FakeDriveTransport({
broker,
localPeerId: "self",
});
const registry = new PeerRegistry();
const received: ChunkAdvertisement[] = [];
const engine = new GossipEngine({
registry,
transports: [transport],
advertSource: () => undefined,
trackedFiles: () => [],
intervalMs: 60_000,
onAdvertiseReceived: (_peerId, advert) => received.push(advert),
});
engine.start();
const advert: ChunkAdvertisement = {
fileId: FILE_ID,
bitset: bitset([0, 1], 8),
totalChunks: 8,
};
for (const handler of transport.handlers) handler("peerB", advert);
expect(received).toHaveLength(1);
expect(registry.get("peerB")?.availability.get(FILE_ID)).toEqual(
advert.bitset
);
engine.stop();
transport.dispose();
await Promise.resolve();
});
it("tick pushes outbound advertisements over every transport", {
timeout: 5000,
}, async () => {
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "self" });
const remote = new FakeDriveTransport({ broker, localPeerId: "peerR" });
local.addKnownPeer("peerR");
const registry = new PeerRegistry();
const seenOnRemote: ChunkAdvertisement[] = [];
remote.onAdvertisement((_p, advert) => seenOnRemote.push(advert));
const sent: ChunkAdvertisement[] = [];
const engine = new GossipEngine({
registry,
transports: [local],
advertSource: () => ({
fileId: FILE_ID,
bitset: bitset([0], 8),
totalChunks: 8,
}),
trackedFiles: () => [FILE_ID],
intervalMs: 60_000,
onAdvertiseSent: (_p, advert) => sent.push(advert),
});
engine.start();
await engine.tick();
expect(sent.length).toBeGreaterThan(0);
expect(seenOnRemote.length).toBeGreaterThan(0);
engine.stop();
local.dispose();
remote.dispose();
});
it("tick skips files whose advertSource returns undefined", {
timeout: 5000,
}, async () => {
const broker = new Broker();
const transport = new FakeDriveTransport({
broker,
localPeerId: "self",
});
transport.addKnownPeer("peerR");
const sent: ChunkAdvertisement[] = [];
const engine = new GossipEngine({
registry: new PeerRegistry(),
transports: [transport],
advertSource: () => undefined,
trackedFiles: () => [FILE_ID],
intervalMs: 60_000,
onAdvertiseSent: (_p, advert) => sent.push(advert),
});
engine.start();
await engine.tick();
expect(sent).toEqual([]);
engine.stop();
transport.dispose();
});
it("swallows per-peer advertise failures", { timeout: 5000 }, async () => {
const broker = new Broker();
const transport = new FakeDriveTransport({
broker,
localPeerId: "self",
});
transport.addKnownPeer("peerR");
// Replace advertise with a throwing function
const original = transport.advertise.bind(transport);
transport.advertise = async (): Promise<void> => {
throw new Error("nope");
};
const engine = new GossipEngine({
registry: new PeerRegistry(),
transports: [transport],
advertSource: () => ({
fileId: FILE_ID,
bitset: bitset([0], 8),
totalChunks: 8,
}),
trackedFiles: () => [FILE_ID],
intervalMs: 60_000,
});
engine.start();
await expect(engine.tick()).resolves.toBeUndefined();
// Restore for cleanup
transport.advertise = original;
engine.stop();
transport.dispose();
});
it("#schedule early-exits when stopped between ticks", {
timeout: 5000,
}, async () => {
const broker = new Broker();
const transport = new FakeDriveTransport({
broker,
localPeerId: "self",
});
transport.addKnownPeer("peerR");
const engine = new GossipEngine({
registry: new PeerRegistry(),
transports: [transport],
advertSource: async (fileId) => {
// small await to ensure the in-flight tick straddles the
// stop() call
await new Promise<void>((r) => setTimeout(r, 5));
return {
fileId,
bitset: bitset([0], 8),
totalChunks: 8,
};
},
trackedFiles: () => [FILE_ID],
intervalMs: 1,
});
engine.start();
// Wait for the first tick to begin
await new Promise<void>((r) => setTimeout(r, 2));
engine.stop();
// Wait for the in-flight tick to settle (calls #schedule which
// should early-return because #running is now false)
await new Promise<void>((r) => setTimeout(r, 25));
transport.dispose();
});
it("schedules tick when started without explicit tick call", {
timeout: 5000,
}, async () => {
const broker = new Broker();
const transport = new FakeDriveTransport({
broker,
localPeerId: "self",
});
transport.addKnownPeer("peerR");
const sent: ChunkAdvertisement[] = [];
const engine = new GossipEngine({
registry: new PeerRegistry(),
transports: [transport],
advertSource: () => ({
fileId: FILE_ID,
bitset: bitset([0], 8),
totalChunks: 8,
}),
trackedFiles: () => [FILE_ID],
intervalMs: 5,
onAdvertiseSent: (_p, advert) => sent.push(advert),
});
engine.start();
await new Promise<void>((resolve) => setTimeout(resolve, 25));
engine.stop();
transport.dispose();
expect(sent.length).toBeGreaterThanOrEqual(1);
});
});

View File

@@ -0,0 +1,139 @@
/**
* `GossipEngine` — periodic advertisement of local availability +
* inbound-advertisement intake.
*
* Outbound: every `intervalMs` (default 5_000) the engine asks the
* configured `peerListing` for a snapshot, then for every known
* `fileId` it pushes a `(fileId, framedBitset)` advertisement to every
* peer over every transport.
*
* Inbound: every transport's `onAdvertisement` is wired to the
* registry so file availability is tracked without separate fan-in.
*/
import type { Clock, PeerId } from "@suaveplan/types";
import type { PeerRegistry } from "../../peer-registry/peer-registry/index.js";
import type {
AdvertisementHandler,
ChunkAdvertisement,
DriveTransport,
Unsubscribe,
} from "../../types/index.js";
export type AdvertSink = (
fileId: string
) => Promise<ChunkAdvertisement | undefined> | ChunkAdvertisement | undefined;
export interface GossipEngineOptions {
readonly registry: PeerRegistry;
readonly transports: readonly DriveTransport[];
/** Returns the current local advertisement for a fileId. */
readonly advertSource: AdvertSink;
/** Files the engine should periodically advertise. */
readonly trackedFiles: () => Iterable<string>;
/** Period in milliseconds. Defaults to 5_000. */
readonly intervalMs?: number;
readonly clock?: Clock;
/** Called whenever an outbound advertisement is sent. */
readonly onAdvertiseSent?: (
peerId: PeerId,
advert: ChunkAdvertisement
) => void;
/** Called whenever an inbound advertisement is received. */
readonly onAdvertiseReceived?: (
peerId: PeerId,
advert: ChunkAdvertisement
) => void;
}
export class GossipEngine {
readonly #registry: PeerRegistry;
readonly #transports: readonly DriveTransport[];
readonly #advertSource: AdvertSink;
readonly #trackedFiles: () => Iterable<string>;
readonly #intervalMs: number;
readonly #clock: Clock;
readonly #onSent: GossipEngineOptions["onAdvertiseSent"];
readonly #onRecv: GossipEngineOptions["onAdvertiseReceived"];
readonly #subscriptions: Unsubscribe[] = [];
#timer: ReturnType<typeof setTimeout> | null = null;
#running = false;
public constructor(options: GossipEngineOptions) {
this.#registry = options.registry;
this.#transports = options.transports;
this.#advertSource = options.advertSource;
this.#trackedFiles = options.trackedFiles;
this.#intervalMs = options.intervalMs ?? 5_000;
this.#clock = options.clock ?? (() => Date.now());
this.#onSent = options.onAdvertiseSent;
this.#onRecv = options.onAdvertiseReceived;
}
/** Begin listening for inbound advertisements + schedule outbound. */
public start(): void {
if (this.#running) return;
this.#running = true;
for (const transport of this.#transports) {
const handler: AdvertisementHandler = (peerId, advert) => {
this.#handleInbound(peerId, advert);
};
this.#subscriptions.push(transport.onAdvertisement(handler));
}
this.#schedule();
}
/** Stop the periodic timer and detach inbound subscriptions. */
public stop(): void {
if (!this.#running) return;
this.#running = false;
// `start()` always schedules a timer before returning, so the
// handle is guaranteed to be set whenever `#running === true`.
const timer = this.#timer as ReturnType<typeof setTimeout>;
clearTimeout(timer);
this.#timer = null;
for (const unsub of this.#subscriptions) unsub();
this.#subscriptions.length = 0;
}
/** Run a single advertisement pass — exposed for tests. */
public async tick(): Promise<void> {
for (const fileId of this.#trackedFiles()) {
const advert = await this.#advertSource(fileId);
if (advert === undefined) continue;
await this.#broadcast(advert);
}
}
async #broadcast(advert: ChunkAdvertisement): Promise<void> {
for (const transport of this.#transports) {
for (const peerId of transport.peers()) {
try {
await transport.advertise(peerId, advert);
this.#onSent?.(peerId, advert);
} catch {
// gossip is best-effort; failures bubble to the peer
// registry only when a chunk request actually fails.
}
}
}
}
#handleInbound(peerId: PeerId, advert: ChunkAdvertisement): void {
this.#registry.addOrUpdate(peerId, { lastSeen: this.#clock() });
this.#registry.recordAvailability(peerId, advert.fileId, advert.bitset);
this.#onRecv?.(peerId, advert);
}
#schedule(): void {
if (!this.#running) return;
this.#timer = setTimeout(() => {
void this.tick().finally(() => this.#schedule());
}, this.#intervalMs);
// Keep the Node / Bun event loop drainable in tests. In
// browser environments setTimeout returns a numeric handle
// with no `unref`; we tolerate both shapes by calling through
// optional access.
(this.#timer as unknown as { unref?: () => void }).unref?.();
}
}

View File

@@ -0,0 +1,5 @@
export {
type AdvertSink,
GossipEngine,
type GossipEngineOptions,
} from "./gossip-engine.js";

View File

@@ -0,0 +1,52 @@
// Side-effect import ensures DRIVE_MESH_CODES is registered for
// subpath consumers that import only from a feature path.
import "./error-codes/index.js";
export {
DRIVE_MESH_CODES,
DriveMeshError,
type DriveMeshErrorCode,
} from "./error-codes/index.js";
export type {
AdvertSink,
GossipEngineOptions,
} from "./gossip/gossip-engine/index.js";
export { GossipEngine } from "./gossip/gossip-engine/index.js";
export { DriveMesh } from "./mesh/drive-mesh/index.js";
export type { PeerRegistryOptions } from "./peer-registry/peer-registry/index.js";
export { PeerRegistry } from "./peer-registry/peer-registry/index.js";
export type { ParallelPullerOptions } from "./scheduler/parallel-puller/index.js";
export { ParallelPuller } from "./scheduler/parallel-puller/index.js";
export {
AdvertisementSchema,
DriveMeshOptionsSchema,
ServerFallbackConfigSchema,
} from "./schemas/index.js";
export type {
FetchFn,
ServerFallbackOptions,
} from "./server-fallback/server-fallback/index.js";
export { ServerFallback } from "./server-fallback/server-fallback/index.js";
export type {
AdvertisementHandler,
ChunkAdvertisement,
ChunkFailedEvent,
ChunkFallbackEvent,
ChunkRequestedEvent,
ChunkServedEvent,
DenylistMode,
DriveMeshEventMap,
DriveMeshOptions,
DriveTransport,
GossipReceivedEvent,
GossipSentEvent,
PeerDenialReason,
PeerDeniedEvent,
PeerInfo,
PeerJoinedEvent,
PeerLeftEvent,
PeerScoredEvent,
PeerUpdate,
ServerFallbackConfig,
Unsubscribe,
} from "./types/index.js";

View File

@@ -0,0 +1,4 @@
export {
type EventArgsMap,
TypedEventEmitter,
} from "./typed-event-emitter.js";

View File

@@ -0,0 +1,74 @@
# `typed-event-emitter.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Internal helper wrapping `EventBus` from `@suaveplan/events` with a
tuple-shaped event map. The public mesh class extends this so call
sites can write `mesh.emit("chunk:served", payload)` rather than
wrapping every argument in an object literal.
The implementation is intentionally private to `@suaveplan/drive-mesh`
— the public API exposes only `on`, `off`, and `once`. Re-implementing
locally (rather than importing from `@suaveplan/drive-webrtc`) keeps
the universal dependency closure clean: drive-mesh must not pull
in WebRTC code.
## Features
- Strict typing of event names → argument tuples.
- Synchronous `emit` matching Node's `EventEmitter` semantics.
- `once` auto-unsubscribes after delivery.
- `removeAllListeners` clears every subscription across every event.
## Usage: Basic Example
```ts
import { TypedEventEmitter } from "./typed-event-emitter.js";
type Events = { hello: [string] };
const ee = new TypedEventEmitter<Events>();
ee.on("hello", name => console.log("hi", name));
ee.emit("hello", "world");
```
## Usage: Advanced Example
```ts
import { TypedEventEmitter } from "./typed-event-emitter.js";
type Events = {
"chunk:served": [{ readonly chunkId: number; readonly bytes: number }];
"chunk:failed": [{ readonly chunkId: number; readonly reason: string }];
};
const ee = new TypedEventEmitter<Events>();
const off = ee.on("chunk:served", e => console.log(e.chunkId, e.bytes));
ee.once("chunk:failed", e => console.error(e.reason));
ee.emit("chunk:served", { chunkId: 0, bytes: 1024 });
off();
ee.removeAllListeners();
```
## API Reference
- `EventArgsMap = Record<string, unknown[]>` — event-map shape.
- `class TypedEventEmitter<TMap>`:
- `on(event, handler): Unsubscribe`.
- `once(event, handler): Unsubscribe` — auto-unsubscribes on first
delivery.
- `off(event, handler): void`.
- `emit(event, ...args): boolean` — `true` iff at least one
subscriber received the event.
- `removeAllListeners(): void`.
## Implementation Notes
`EventBus` keeps payloads as a single value per event, so the wrapper
stores `args` (the tuple) as that value and spreads it back through
`handler(...args)` on dispatch. The auxiliary `#wrapped` map tracks
the unsubscribe closures so `off(event, handler)` removes the
correct registration even though the underlying bus only sees the
wrapping closure.

View File

@@ -0,0 +1,221 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { TypedEventEmitter } from "./typed-event-emitter.js";
type Events = {
a: [number];
b: [string, number];
};
describe("TypedEventEmitter", () => {
it("delivers emitted args to subscribers", { timeout: 5000 }, () => {
const ee = new TypedEventEmitter<Events>();
let last = 0;
const off = ee.on("a", (n) => {
last = n;
});
const delivered = ee.emit("a", 42);
expect(delivered).toBe(true);
expect(last).toBe(42);
off();
ee.emit("a", 999);
expect(last).toBe(42);
});
it("emit returns false when no subscribers", { timeout: 5000 }, () => {
const ee = new TypedEventEmitter<Events>();
expect(ee.emit("a", 1)).toBe(false);
});
it("once auto-unsubscribes", { timeout: 5000 }, () => {
const ee = new TypedEventEmitter<Events>();
let count = 0;
ee.once("a", () => {
count += 1;
});
ee.emit("a", 1);
ee.emit("a", 2);
expect(count).toBe(1);
});
it("once handle can be cancelled before delivery", {
timeout: 5000,
}, () => {
const ee = new TypedEventEmitter<Events>();
let count = 0;
const handler = (): void => {
count += 1;
};
const off = ee.once("a", handler);
off();
ee.emit("a", 1);
expect(count).toBe(0);
});
it("off removes the registered handler", { timeout: 5000 }, () => {
const ee = new TypedEventEmitter<Events>();
let count = 0;
const handler = (): void => {
count += 1;
};
ee.on("a", handler);
ee.off("a", handler);
ee.emit("a", 1);
expect(count).toBe(0);
});
it("off is a no-op for an unknown event", { timeout: 5000 }, () => {
const ee = new TypedEventEmitter<Events>();
expect(() =>
ee.off("a", () => {
/* noop */
})
).not.toThrow();
});
it("off is a no-op for an unknown handler", { timeout: 5000 }, () => {
const ee = new TypedEventEmitter<Events>();
ee.on("a", () => {
/* noop */
});
expect(() =>
ee.off("a", () => {
/* noop */
})
).not.toThrow();
});
it("supports multi-arg payloads", { timeout: 5000 }, () => {
const ee = new TypedEventEmitter<Events>();
let captured: [string, number] | null = null;
ee.on("b", (s, n) => {
captured = [s, n];
});
ee.emit("b", "x", 7);
expect(captured).toEqual(["x", 7]);
});
it("off removes the last handler and drops the inner map", {
timeout: 5000,
}, () => {
const ee = new TypedEventEmitter<Events>();
const h = (): void => {
/* noop */
};
ee.on("a", h);
ee.off("a", h);
// off again is a safe no-op (the inner map was deleted)
expect(() => ee.off("a", h)).not.toThrow();
});
it("on-returned unsubscribe drops the inner map when last", {
timeout: 5000,
}, () => {
const ee = new TypedEventEmitter<Events>();
const off1 = ee.on("a", () => undefined);
off1();
// re-registering after the inner map was dropped works
const off2 = ee.on("a", () => undefined);
off2();
});
it("once after delivery cleans up bookkeeping", { timeout: 5000 }, () => {
const ee = new TypedEventEmitter<Events>();
ee.once("a", () => undefined);
ee.emit("a", 1);
// emit again is a safe no-op
expect(ee.emit("a", 1)).toBe(false);
});
it("unsubscribe twice tolerates the second call", { timeout: 5000 }, () => {
const ee = new TypedEventEmitter<Events>();
const off = ee.on("a", () => undefined);
off();
off();
// emit must not throw despite second-call cleanup hitting empty maps
expect(ee.emit("a", 1)).toBe(false);
});
it("once-returned unsubscribe after delivery tolerates the call", {
timeout: 5000,
}, () => {
const ee = new TypedEventEmitter<Events>();
const off = ee.once("a", () => undefined);
ee.emit("a", 1);
off();
});
it("off keeps inner map when other handlers remain", {
timeout: 5000,
}, () => {
const ee = new TypedEventEmitter<Events>();
let a = 0;
let b = 0;
const h1 = (n: number): void => {
a += n;
};
const h2 = (n: number): void => {
b += n;
};
ee.on("a", h1);
ee.on("a", h2);
ee.off("a", h1);
ee.emit("a", 1);
expect(a).toBe(0);
expect(b).toBe(1);
// h2 still registered — off again on h1 is a no-op
ee.off("a", h1);
ee.emit("a", 1);
expect(b).toBe(2);
});
it("on reuses the inner map for a second handler on the same event", {
timeout: 5000,
}, () => {
const ee = new TypedEventEmitter<Events>();
let total = 0;
ee.on("a", (n) => {
total += n;
});
// Second registration hits the `map !== undefined` branch in #record.
ee.on("a", (n) => {
total += n * 10;
});
ee.emit("a", 1);
expect(total).toBe(11);
});
it("once cleanup with another handler still registered preserves the inner map", {
timeout: 5000,
}, () => {
const ee = new TypedEventEmitter<Events>();
let total = 0;
ee.on("a", (n) => {
total += n;
});
ee.once("a", (n) => {
total += n * 100;
});
ee.emit("a", 1);
expect(total).toBe(101);
// The `on` handler survives — #forget kept the inner map.
ee.emit("a", 1);
expect(total).toBe(102);
});
it("removeAllListeners clears every subscription", {
timeout: 5000,
}, () => {
const ee = new TypedEventEmitter<Events>();
let count = 0;
ee.on("a", () => {
count += 1;
});
ee.on("b", () => {
count += 1;
});
ee.removeAllListeners();
ee.emit("a", 1);
ee.emit("b", "x", 2);
expect(count).toBe(0);
});
});

View File

@@ -0,0 +1,96 @@
/**
* Internal typed event emitter.
*
* Thin wrapper around `EventBus` from `@suaveplan/events`, exposing a
* tuple-shaped event map and synchronous `emit`. Kept internal to the
* package — consumers interact via `DriveMesh`'s public `on`/`off`/
* `once` surface.
*/
import type { Unsubscribe } from "@suaveplan/events";
import { EventBus } from "@suaveplan/events";
export type EventArgsMap = Record<string, unknown[]>;
type Handler<TArgs extends unknown[]> = (...args: TArgs) => void;
export class TypedEventEmitter<TMap extends EventArgsMap> {
readonly #bus = new EventBus<{ [K in keyof TMap]: TMap[K] }>();
readonly #wrapped = new Map<
keyof TMap,
Map<Handler<TMap[keyof TMap]>, Unsubscribe>
>();
public on<K extends keyof TMap>(
event: K,
handler: Handler<TMap[K]>
): Unsubscribe {
const wrapped = (args: TMap[K]): void => {
handler(...args);
};
const unsub = this.#bus.on(event, wrapped);
this.#record(event, handler, unsub);
return () => {
unsub();
this.#forget(event, handler);
};
}
public once<K extends keyof TMap>(
event: K,
handler: Handler<TMap[K]>
): Unsubscribe {
const unsub = this.#bus.once(event, (args: TMap[K]) => {
this.#forget(event, handler);
handler(...args);
});
this.#record(event, handler, unsub);
return () => {
unsub();
this.#forget(event, handler);
};
}
public off<K extends keyof TMap>(
event: K,
handler: Handler<TMap[K]>
): void {
const map = this.#wrapped.get(event);
if (map === undefined) return;
const unsub = map.get(handler as Handler<TMap[keyof TMap]>);
if (unsub === undefined) return;
unsub();
map.delete(handler as Handler<TMap[keyof TMap]>);
if (map.size === 0) this.#wrapped.delete(event);
}
public emit<K extends keyof TMap>(event: K, ...args: TMap[K]): boolean {
const result = this.#bus.emit(event, args);
return result.delivered > 0;
}
public removeAllListeners(): void {
this.#bus.clear();
this.#wrapped.clear();
}
#record<K extends keyof TMap>(
event: K,
handler: Handler<TMap[K]>,
unsub: Unsubscribe
): void {
let map = this.#wrapped.get(event);
if (map === undefined) {
map = new Map();
this.#wrapped.set(event, map);
}
map.set(handler as Handler<TMap[keyof TMap]>, unsub);
}
#forget<K extends keyof TMap>(event: K, handler: Handler<TMap[K]>): void {
const map = this.#wrapped.get(event);
if (map === undefined) return;
map.delete(handler as Handler<TMap[keyof TMap]>);
if (map.size === 0) this.#wrapped.delete(event);
}
}

View File

@@ -0,0 +1,144 @@
# `drive-mesh.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`DriveMesh` is the public face of `@suaveplan/drive-mesh`. It binds
the four supporting subsystems — `PeerRegistry`, `GossipEngine`,
`ParallelPuller`, and (optionally) `ServerFallback` — into a single
class with explicit lifecycle and a single chunk-fetch entry point.
The class is the only mesh component most consumers ever construct.
It validates options at the Zod boundary, wires telemetry hooks
between subsystems, and surfaces a typed event stream callers can
subscribe to. The chunks plugin's signature contract is upheld
end-to-end: every byte returned from `requestChunk` has passed
`verifyChunkSig` against the configured `serverPubKey`.
## Features
- Strict option validation at construction (`INVALID_OPTIONS`).
- Idempotent `start()` / `stop()` lifecycle.
- `requestChunk` racing-window scheduler with automatic fallback.
- `advertise` / `unadvertise` to publish local availability bitsets.
- Typed event emitter (`chunk:*`, `peer:*`, `gossip:*`).
- Read-only access to the registry (`mesh.registry`) for
`NetworkBus` consumers that wire `peer:joined`/`peer:left` events.
## Usage: Basic Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
const mesh = new DriveMesh({
chunks,
transports: [httpsTransport],
serverPubKey,
});
await mesh.start();
const bytes = await mesh.requestChunk(fileIdHex, 0);
await mesh.stop();
```
## Usage: Advanced Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
import { encodeFramedBitset } from "@suaveplan/drive-mesh-protocol";
const mesh = new DriveMesh({
chunks,
transports: [webrtcTransport, httpsTransport],
serverPubKey,
serverFallback: { url: "https://cdn.example.com/chunks", transport: "https" },
maxParallelRequests: 16,
racingWindowMs: 150,
denylistMode: "persistent",
});
// Mirror NetworkBus events into the registry overlay.
networkBus.on("peer:joined", ({ peerId, transports }) =>
mesh.registry.addOrUpdate(peerId, { transports })
);
networkBus.on("peer:left", ({ peerId }) => mesh.registry.forget(peerId));
await mesh.start();
mesh.on("chunk:served", e =>
telemetry.counter("drive.mesh.chunks_served_total", 1, { outcome: "peer" })
);
mesh.on("chunk:fallback", e =>
telemetry.counter("drive.mesh.chunks_served_total", 1, {
outcome: "fallback",
})
);
mesh.on("peer:denied", e =>
telemetry.counter("drive.mesh.peers_denied_total", 1, { reason: e.reason })
);
const bits = await chunks.hasChunks(fileIdBytes);
const raw = boolsToBytes(bits);
mesh.advertise(fileIdHex, encodeFramedBitset(raw, bits.length), bits.length);
const bytes = await mesh.requestChunk(fileIdHex, 0);
await mesh.stop();
```
## API Reference
- `class DriveMesh extends TypedEventEmitter<DriveMeshEventMap>`:
- `constructor(options: DriveMeshOptions)` — Zod-validated.
- `start(signal?): Promise<void>` — idempotent.
- `stop(reason?): Promise<void>` — idempotent; clears registry per
`denylistMode`.
- `requestChunk(fileId, chunkId, signal?): Promise<Uint8Array>` —
racing-window peer pull → server fallback → throw.
- `advertise(fileId, bitset, totalChunks): void`.
- `unadvertise(fileId): void`.
- `peers(): Iterable<PeerInfo>`.
- `registry: PeerRegistry` (read-only getter).
- `tickGossip(): Promise<void>` — single gossip pass for tests.
- Inherited from `TypedEventEmitter`:
- `on(event, handler): Unsubscribe`.
- `once(event, handler): Unsubscribe`.
- `off(event, handler): void`.
- `removeAllListeners(): void`.
- Throws:
- `DRIVE_MESH.INVALID_OPTIONS` — bad construction args / bad
`fileId`.
- `DRIVE_MESH.NOT_STARTED` — `requestChunk` before `start()`.
- `DRIVE_MESH.CHUNK_UNAVAILABLE` — no candidates and no fallback.
- `DRIVE_MESH.SIG_VERIFY_FAILED` — winning peer was Byzantine.
- `DRIVE_MESH.ALL_PEERS_FAILED` — every candidate failed and no
fallback configured.
- `DRIVE_MESH.FALLBACK_FAILED` — peers + fallback both failed.
- `DRIVE_MESH.ABORTED` — caller-supplied signal aborted.
- `DRIVE_MESH.INVALID_BITSET` — `advertise` got bad totalChunks.
## Implementation Notes
`DriveMesh` does NOT register itself as a `NetworkBus` consumer
internally — it exposes `mesh.registry` so the bridge from
`network-plugin-registry`'s `peer:joined` / `peer:left` events lives
in application glue. This keeps the universal package free of a
dependency on `@suaveplan/network` / `@suaveplan/network-plugin-*`,
matching the layering pinned in `drive-network-integration`.
`requestChunk` deliberately does not retry on peers after a bad-sig
event from the winning attempt: the denylist update means the next
call will not even consider that peer. Re-trying inside a single
request would risk amplifying a Byzantine peer's influence over the
score formula. Instead, the fallback path runs.
Bad-sig events flow `puller → registry.recordBadSig → onBadSig
callback → mesh.emit("peer:denied")`. The registry update happens
synchronously inside the puller; the event is fired afterwards so
subscribers see the state-after-update.
`exactOptionalPropertyTypes: true` (per the repo's `tsconfig.base`)
forces the explicit `...(x !== undefined ? { x } : {})` spread shape
when forwarding optionals down into subsystem options — never
`{ x: maybeUndefined }` because that materialises an `undefined`
property the inner schema would reject.

View File

@@ -0,0 +1,625 @@
import { encodeFramedBitset } from "@suaveplan/drive-mesh-protocol";
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
Broker,
FakeDriveTransport,
} from "../../../tests/helpers/fake-transport.js";
import {
buildChunkBytes,
makeKeypair,
TEST_FILE_ID_HEX,
} from "../../../tests/helpers/signed-chunk.js";
import {
DRIVE_MESH_CODES,
DriveMeshError,
} from "../../error-codes/error-codes.js";
import { DriveMesh } from "./drive-mesh.js";
function bitset(bits: readonly number[], total: number): Uint8Array {
const raw = new Uint8Array(Math.ceil(total / 8));
for (const b of bits) raw[b >> 3] |= 1 << (b & 7);
return encodeFramedBitset(raw, total);
}
function arrayBuffer(bytes: Uint8Array): ArrayBuffer {
return bytes.buffer.slice(
bytes.byteOffset,
bytes.byteOffset + bytes.byteLength
) as ArrayBuffer;
}
const FILE_ID = TEST_FILE_ID_HEX;
const dummyChunks = {
/* stub — DriveMesh does not read chunks-plugin methods directly */
} as unknown as Parameters<typeof DriveMesh>[0]["chunks"];
describe("DriveMesh", () => {
it("rejects malformed options", { timeout: 5000 }, () => {
expect(
() =>
new DriveMesh({
chunks: dummyChunks,
transports: [],
serverPubKey: new Uint8Array(32),
})
).toThrow(DriveMeshError);
});
it("requestChunk before start throws NOT_STARTED", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(3);
const broker = new Broker();
const transport = new FakeDriveTransport({ broker, localPeerId: "x" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [transport],
serverPubKey: kp.pubKey,
});
await expect(mesh.requestChunk(FILE_ID, 0)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.NOT_STARTED,
});
transport.dispose();
});
it("rejects malformed fileId", { timeout: 5000 }, async () => {
const kp = makeKeypair(3);
const broker = new Broker();
const transport = new FakeDriveTransport({ broker, localPeerId: "x" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [transport],
serverPubKey: kp.pubKey,
});
await mesh.start();
await expect(mesh.requestChunk("nope", 0)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.INVALID_OPTIONS,
});
await mesh.stop();
transport.dispose();
});
it("start with pre-aborted signal throws ABORTED", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(3);
const broker = new Broker();
const transport = new FakeDriveTransport({ broker, localPeerId: "x" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [transport],
serverPubKey: kp.pubKey,
});
const ac = new AbortController();
ac.abort();
await expect(mesh.start(ac.signal)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.ABORTED,
});
transport.dispose();
});
it("start and stop are idempotent", { timeout: 5000 }, async () => {
const kp = makeKeypair(3);
const broker = new Broker();
const transport = new FakeDriveTransport({ broker, localPeerId: "x" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [transport],
serverPubKey: kp.pubKey,
});
await mesh.start();
await mesh.start();
await mesh.stop();
await mesh.stop("twice");
transport.dispose();
});
it("end-to-end: peer serves verified chunk", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(4);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
local.addKnownPeer("peerA");
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
});
await mesh.start();
// Stage a real signed chunk
const bytes = buildChunkBytes({
serverPrivKey: kp.privKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
broker.serve("peerA", FILE_ID, 0, { bytes });
// Wire the registry overlay directly (network-plugin-registry simulation)
mesh.registry.addOrUpdate("peerA", { transports: ["fake"] });
mesh.registry.recordAvailability("peerA", FILE_ID, bitset([0], 8));
const served: { peerId: string }[] = [];
mesh.on("chunk:served", (e) => served.push({ peerId: e.peerId }));
const out = await mesh.requestChunk(FILE_ID, 0);
expect(out).toEqual(bytes);
expect(served).toHaveLength(1);
await mesh.stop();
local.dispose();
});
it("falls back to server when every peer fails", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(5);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
local.addKnownPeer("peerA");
const fallbackBytes = buildChunkBytes({
serverPrivKey: kp.privKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
serverFallback: { url: "https://cdn", transport: "fake" },
fetchFn: async () =>
new Response(arrayBuffer(fallbackBytes), { status: 200 }),
});
await mesh.start();
// peerA "knows" the chunk but errors on request
broker.serve("peerA", FILE_ID, 0, { throwReason: "down" });
mesh.registry.addOrUpdate("peerA", { transports: ["fake"] });
mesh.registry.recordAvailability("peerA", FILE_ID, bitset([0], 8));
const fallbacks: number[] = [];
mesh.on("chunk:fallback", (e) => fallbacks.push(e.chunkId));
const out = await mesh.requestChunk(FILE_ID, 0);
expect(out).toEqual(fallbackBytes);
expect(fallbacks).toEqual([0]);
await mesh.stop();
local.dispose();
});
it("falls back when no peers advertise the chunk", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(6);
const fallbackBytes = buildChunkBytes({
serverPrivKey: kp.privKey,
fileIdHex: FILE_ID,
chunkId: 1,
});
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
serverFallback: { url: "https://cdn", transport: "fake" },
fetchFn: async () =>
new Response(arrayBuffer(fallbackBytes), { status: 200 }),
});
await mesh.start();
const out = await mesh.requestChunk(FILE_ID, 1);
expect(out).toEqual(fallbackBytes);
await mesh.stop();
local.dispose();
});
it("propagates ABORTED from puller without invoking fallback", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(6);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
local.addKnownPeer("peerA");
let fallbackHit = 0;
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
serverFallback: { url: "https://cdn", transport: "fake" },
fetchFn: async () => {
fallbackHit += 1;
return new Response(new Uint8Array(0), { status: 200 });
},
});
await mesh.start();
broker.serve("peerA", FILE_ID, 0, {
delayMs: 1000,
bytes: new Uint8Array(0),
});
mesh.registry.addOrUpdate("peerA", { transports: ["fake"] });
mesh.registry.recordAvailability("peerA", FILE_ID, bitset([0], 8));
const ac = new AbortController();
const promise = mesh.requestChunk(FILE_ID, 0, ac.signal);
setTimeout(() => ac.abort(), 5);
await expect(promise).rejects.toBeInstanceOf(DriveMeshError);
expect(fallbackHit).toBe(0);
await mesh.stop();
local.dispose();
});
it("emits chunk:failed when fallback also fails", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(7);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
serverFallback: { url: "https://cdn", transport: "fake" },
fetchFn: async () => new Response("nope", { status: 500 }),
});
await mesh.start();
const failures: string[] = [];
mesh.on("chunk:failed", (e) => failures.push(e.reason));
await expect(mesh.requestChunk(FILE_ID, 0)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.FALLBACK_FAILED,
});
expect(failures.length).toBeGreaterThan(0);
await mesh.stop();
local.dispose();
});
it("throws original error when no fallback configured", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(8);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
});
await mesh.start();
await expect(mesh.requestChunk(FILE_ID, 0)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.CHUNK_UNAVAILABLE,
});
await mesh.stop();
local.dispose();
});
it("re-throws non-DriveMeshError exceptions from puller", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(9);
const broker = new Broker();
const transport: FakeDriveTransport = new FakeDriveTransport({
broker,
localPeerId: "local",
});
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [transport],
serverPubKey: kp.pubKey,
});
await mesh.start();
// Force a non-DriveMeshError from rank() by patching registry
// (uses public surface to set up situation)
mesh.registry.addOrUpdate("peerA", { transports: ["fake"] });
mesh.registry.recordAvailability("peerA", FILE_ID, bitset([0], 8));
// Replace registry.rank to throw a generic error
const original = mesh.registry.rank.bind(mesh.registry);
mesh.registry.rank = (): never => {
throw new Error("synthetic");
};
await expect(mesh.requestChunk(FILE_ID, 0)).rejects.toThrow(
"synthetic"
);
mesh.registry.rank = original;
await mesh.stop();
transport.dispose();
});
it("advertise rejects invalid totalChunks", { timeout: 5000 }, () => {
const kp = makeKeypair(10);
const broker = new Broker();
const transport = new FakeDriveTransport({ broker, localPeerId: "x" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [transport],
serverPubKey: kp.pubKey,
});
expect(() => mesh.advertise(FILE_ID, new Uint8Array(2), 0)).toThrow(
DriveMeshError
);
transport.dispose();
});
it("advertise stores entries that gossip consumes", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(11);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
local.addKnownPeer("peerR");
const remote = new FakeDriveTransport({ broker, localPeerId: "peerR" });
const seen: string[] = [];
remote.onAdvertisement((_p, a) => seen.push(a.fileId));
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
});
await mesh.start();
mesh.advertise(FILE_ID, bitset([0, 1], 8), 8);
await mesh.tickGossip();
expect(seen).toContain(FILE_ID);
mesh.unadvertise(FILE_ID);
// ensure unadvertise removes (next tick emits nothing for that file)
seen.length = 0;
await mesh.tickGossip();
expect(seen).toEqual([]);
await mesh.stop();
local.dispose();
remote.dispose();
});
it("peers() snapshot lists registry contents", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(12);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
});
await mesh.start();
mesh.registry.addOrUpdate("peerA");
const ids = [...mesh.peers()].map((p) => p.peerId);
expect(ids).toContain("peerA");
await mesh.stop();
local.dispose();
});
it("emits peer:scored when chunk:served fires", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(13);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
local.addKnownPeer("peerA");
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
});
await mesh.start();
const bytes = buildChunkBytes({
serverPrivKey: kp.privKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
broker.serve("peerA", FILE_ID, 0, { bytes });
mesh.registry.addOrUpdate("peerA", { transports: ["fake"] });
mesh.registry.recordAvailability("peerA", FILE_ID, bitset([0], 8));
const scores: number[] = [];
mesh.on("peer:scored", (e) => scores.push(e.score));
await mesh.requestChunk(FILE_ID, 0);
expect(scores.length).toBeGreaterThan(0);
await mesh.stop();
local.dispose();
});
it("emits peer:denied on bad sig (integration)", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(14);
const wrong = makeKeypair(15);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
local.addKnownPeer("peerA");
const fallbackBytes = buildChunkBytes({
serverPrivKey: kp.privKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
serverFallback: { url: "https://cdn", transport: "fake" },
fetchFn: async () =>
new Response(arrayBuffer(fallbackBytes), { status: 200 }),
});
await mesh.start();
const badBytes = buildChunkBytes({
serverPrivKey: wrong.privKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
broker.serve("peerA", FILE_ID, 0, { bytes: badBytes });
mesh.registry.addOrUpdate("peerA", { transports: ["fake"] });
mesh.registry.recordAvailability("peerA", FILE_ID, bitset([0], 8));
const denied: string[] = [];
mesh.on("peer:denied", (e) => denied.push(e.peerId));
const out = await mesh.requestChunk(FILE_ID, 0);
expect(out).toEqual(fallbackBytes);
expect(denied).toContain("peerA");
await mesh.stop();
local.dispose();
});
it("propagates non-DriveMeshError from fallback (re-wraps as FALLBACK_FAILED)", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(16);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
serverFallback: { url: "https://cdn", transport: "fake" },
fetchFn: async () => {
throw "non-error";
},
maxParallelRequests: 1,
});
await mesh.start();
await expect(mesh.requestChunk(FILE_ID, 0)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.FALLBACK_FAILED,
});
await mesh.stop();
local.dispose();
});
it("emits gossip:received when a peer advertises to us", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(20);
const broker = new Broker();
const localTransport = new FakeDriveTransport({
broker,
localPeerId: "local",
id: "fake",
});
const remoteTransport = new FakeDriveTransport({
broker,
localPeerId: "peerR",
id: "fake",
});
remoteTransport.addKnownPeer("local");
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [localTransport],
serverPubKey: kp.pubKey,
});
await mesh.start();
const received: string[] = [];
mesh.on("gossip:received", (e) => received.push(e.fileId));
// Simulate remote pushing an advertisement back to us
await remoteTransport.advertise("local", {
fileId: FILE_ID,
bitset: bitset([0], 8),
totalChunks: 8,
});
expect(received).toContain(FILE_ID);
await mesh.stop();
localTransport.dispose();
remoteTransport.dispose();
});
it("propagates fallback ABORTED back to caller", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(21);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
serverFallback: { url: "https://cdn", transport: "fake" },
fetchFn: async (_url, init) => {
// honour the abort signal so fallback throws ABORTED
if (init?.signal?.aborted === true) {
throw new Error("aborted");
}
await new Promise<void>((resolve, reject) => {
const onAbort = (): void => reject(new Error("aborted"));
init?.signal?.addEventListener("abort", onAbort, {
once: true,
});
setTimeout(() => {
init?.signal?.removeEventListener("abort", onAbort);
resolve();
}, 5_000);
});
return new Response(new Uint8Array(0));
},
});
await mesh.start();
const ac = new AbortController();
const promise = mesh.requestChunk(FILE_ID, 0, ac.signal);
setTimeout(() => ac.abort(), 10);
await expect(promise).rejects.toBeInstanceOf(DriveMeshError);
await mesh.stop();
local.dispose();
});
it("threads clock, maxParallelRequests, racingWindowMs, and fetchFn through to sub-components", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(50);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
local.addKnownPeer("peerA");
const bytes = buildChunkBytes({
serverPrivKey: kp.privKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
let clockReads = 0;
let fetchCalls = 0;
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
serverFallback: { url: "https://cdn", transport: "fake" },
fetchFn: async () => {
fetchCalls += 1;
return new Response(arrayBuffer(bytes), { status: 200 });
},
clock: () => {
clockReads += 1;
return 1_700_000_000_000 + clockReads;
},
maxParallelRequests: 4,
racingWindowMs: 10,
});
await mesh.start();
broker.serve("peerA", FILE_ID, 0, { bytes });
mesh.registry.addOrUpdate("peerA", { transports: ["fake"] });
mesh.registry.recordAvailability("peerA", FILE_ID, bitset([0], 8));
const out = await mesh.requestChunk(FILE_ID, 0);
expect(out).toEqual(bytes);
// Puller's onSuccess path reads the clock at least twice
// (start + end timestamps via `#clock()` in `#tryPeer`).
expect(clockReads).toBeGreaterThan(0);
expect(fetchCalls).toBe(0);
await mesh.stop();
local.dispose();
});
it("constructs a fallback without explicit fetchFn (delegating to globalThis.fetch)", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(52);
const broker = new Broker();
const local = new FakeDriveTransport({ broker, localPeerId: "local" });
// Construct with serverFallback but no fetchFn. We never call
// requestChunk so globalThis.fetch is never invoked; the test
// covers the constructor spread branch.
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [local],
serverPubKey: kp.pubKey,
serverFallback: { url: "https://cdn", transport: "fake" },
});
await mesh.start();
await mesh.stop();
local.dispose();
});
it("stop without start transitions to stopped", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(17);
const broker = new Broker();
const transport = new FakeDriveTransport({ broker, localPeerId: "x" });
const mesh = new DriveMesh({
chunks: dummyChunks,
transports: [transport],
serverPubKey: kp.pubKey,
});
await mesh.stop();
transport.dispose();
});
});

View File

@@ -0,0 +1,278 @@
/**
* `DriveMesh` — orchestrator binding peer registry, gossip engine,
* parallel puller, and server fallback into a single public surface.
*
* Lifecycle:
* - `new DriveMesh(options)` validates options at the Zod boundary.
* - `await mesh.start(signal?)` opens transports, wires gossip.
* - `await mesh.requestChunk(fileId, chunkId, signal?)` returns
* verified bytes from peer-or-fallback.
* - `await mesh.stop(reason?)` cancels gossip, releases transports,
* clears registry per `denylistMode`. Idempotent.
*/
import {
DRIVE_MESH_CODES,
DriveMeshError,
} from "../../error-codes/error-codes.js";
import { GossipEngine } from "../../gossip/gossip-engine/index.js";
import { TypedEventEmitter } from "../../internal/typed-event-emitter/index.js";
import { PeerRegistry } from "../../peer-registry/peer-registry/index.js";
import { ParallelPuller } from "../../scheduler/parallel-puller/index.js";
import { DriveMeshOptionsSchema } from "../../schemas/index.js";
import { ServerFallback } from "../../server-fallback/server-fallback/index.js";
import type {
DriveMeshEventMap,
DriveMeshOptions,
PeerInfo,
} from "../../types/index.js";
const HEX_FILE_ID_RE = /^[0-9a-f]{64}$/;
export class DriveMesh extends TypedEventEmitter<DriveMeshEventMap> {
readonly #registry: PeerRegistry;
readonly #puller: ParallelPuller;
readonly #fallback: ServerFallback | undefined;
readonly #gossip: GossipEngine;
readonly #advertisements = new Map<
string,
{
readonly fileId: string;
readonly bitset: Uint8Array;
readonly totalChunks: number;
}
>();
#state: "idle" | "running" | "stopped" = "idle";
public constructor(options: DriveMeshOptions) {
super();
const parsed = DriveMeshOptionsSchema.safeParse(options);
if (!parsed.success) {
throw new DriveMeshError(
DRIVE_MESH_CODES.INVALID_OPTIONS,
`invalid DriveMeshOptions: ${parsed.error.message}`
);
}
const denylistMode = options.denylistMode ?? "session";
const clock = options.clock;
this.#registry = new PeerRegistry({
...(clock !== undefined ? { clock } : {}),
denylistMode,
});
this.#puller = new ParallelPuller({
registry: this.#registry,
transports: options.transports,
serverPubKey: options.serverPubKey,
...(options.maxParallelRequests !== undefined
? { maxParallelRequests: options.maxParallelRequests }
: {}),
...(options.racingWindowMs !== undefined
? { racingWindowMs: options.racingWindowMs }
: {}),
...(clock !== undefined ? { clock } : {}),
onAttempt: (peerId, fileId, chunkId) =>
this.emit("chunk:requested", { fileId, chunkId, peerId }),
onSuccess: (peerId, fileId, chunkId, bytes, elapsedMs) => {
this.emit("chunk:served", {
fileId,
chunkId,
peerId,
bytes,
elapsedMs,
});
// `onSuccess` is invoked synchronously after
// `registry.recordSuccess(peerId)`, so the peer is
// guaranteed to exist in the registry; the non-null
// assertion is safe by construction.
const info = this.#registry.get(peerId) as PeerInfo;
this.emit("peer:scored", {
peerId,
score: info.score,
rttMs: info.rttMs,
});
},
onFailure: (_peerId, fileId, chunkId, reason) =>
this.emit("chunk:failed", { fileId, chunkId, reason }),
onBadSig: (peerId) =>
this.emit("peer:denied", { peerId, reason: "bad_sig" }),
});
if (options.serverFallback !== undefined) {
this.#fallback = new ServerFallback({
baseUrl: options.serverFallback.url,
serverPubKey: options.serverPubKey,
...(options.fetchFn !== undefined
? { fetchFn: options.fetchFn }
: {}),
...(clock !== undefined ? { clock } : {}),
});
} else {
this.#fallback = undefined;
}
this.#gossip = new GossipEngine({
registry: this.#registry,
transports: options.transports,
advertSource: (fileId) => this.#advertisements.get(fileId),
trackedFiles: () => this.#advertisements.keys(),
...(clock !== undefined ? { clock } : {}),
onAdvertiseSent: (peerId, advert) =>
this.emit("gossip:sent", {
peerId,
fileId: advert.fileId,
sizeBytes: advert.bitset.byteLength,
}),
onAdvertiseReceived: (peerId, advert) =>
this.emit("gossip:received", {
peerId,
fileId: advert.fileId,
sizeBytes: advert.bitset.byteLength,
}),
});
}
/** Begin gossip + listen for chunk requests. */
public async start(signal?: AbortSignal): Promise<void> {
if (signal?.aborted) {
throw new DriveMeshError(
DRIVE_MESH_CODES.ABORTED,
"start aborted before begin"
);
}
if (this.#state === "running") return;
this.#state = "running";
this.#gossip.start();
}
/** Stop gossip; clear registry; dispose transports. Idempotent. */
public async stop(reason?: string): Promise<void> {
if (this.#state !== "running") {
this.#state = "stopped";
return;
}
this.#state = "stopped";
this.#gossip.stop();
this.#registry.forgetAll();
void reason;
}
/**
* Request a chunk: tries peers in score order with a racing
* window; falls back to the configured server URL when every
* peer fails. Surfaces the verified bytes.
*/
public async requestChunk(
fileId: string,
chunkId: number,
signal?: AbortSignal
): Promise<Uint8Array> {
this.#assertRunning();
this.#assertFileIdHex(fileId);
try {
return await this.#puller.pull(fileId, chunkId, signal);
} catch (err) {
if (!(err instanceof DriveMeshError)) throw err;
if (err.code === DRIVE_MESH_CODES.ABORTED) throw err;
return await this.#tryFallback(fileId, chunkId, err, signal);
}
}
/** Advertise local availability of (fileId, bitset). */
public advertise(
fileId: string,
bitset: Uint8Array,
totalChunks: number
): void {
this.#assertFileIdHex(fileId);
if (!Number.isInteger(totalChunks) || totalChunks <= 0) {
throw new DriveMeshError(
DRIVE_MESH_CODES.INVALID_BITSET,
"totalChunks must be a positive integer"
);
}
this.#advertisements.set(fileId, { fileId, bitset, totalChunks });
}
/** Forget a previously-pushed local advertisement. */
public unadvertise(fileId: string): void {
this.#advertisements.delete(fileId);
}
/** Snapshot of every live peer (excludes denylisted). */
public peers(): Iterable<PeerInfo> {
return [...this.#registry.peers()];
}
/** Direct access to the registry — used by integrations that subscribe to `NetworkBus`. */
public get registry(): PeerRegistry {
return this.#registry;
}
/** Trigger an immediate gossip pass — exposed for tests. */
public async tickGossip(): Promise<void> {
await this.#gossip.tick();
}
async #tryFallback(
fileId: string,
chunkId: number,
originalErr: DriveMeshError,
signal: AbortSignal | undefined
): Promise<Uint8Array> {
if (this.#fallback === undefined) {
throw originalErr;
}
const start = Date.now();
try {
const bytes = await this.#fallback.fetchChunk(
fileId,
chunkId,
signal
);
this.emit("chunk:fallback", {
fileId,
chunkId,
bytes: bytes.byteLength,
elapsedMs: Date.now() - start,
});
return bytes;
} catch (fallbackErr) {
if (
fallbackErr instanceof DriveMeshError &&
fallbackErr.code === DRIVE_MESH_CODES.ABORTED
) {
throw fallbackErr;
}
// `ServerFallback.fetchChunk` always rejects with a
// `DriveMeshError` (subclass of Error); narrow without
// an `instanceof` branch.
const cause = fallbackErr as Error;
this.emit("chunk:failed", {
fileId,
chunkId,
reason: cause.message,
});
throw new DriveMeshError(
DRIVE_MESH_CODES.FALLBACK_FAILED,
`peer + fallback both failed for ${fileId}/${chunkId}`,
{ cause }
);
}
}
#assertRunning(): void {
if (this.#state !== "running") {
throw new DriveMeshError(
DRIVE_MESH_CODES.NOT_STARTED,
`mesh in state '${this.#state}'; call start() first`
);
}
}
#assertFileIdHex(fileId: string): void {
if (!HEX_FILE_ID_RE.test(fileId)) {
throw new DriveMeshError(
DRIVE_MESH_CODES.INVALID_OPTIONS,
`fileId must be 64 lowercase hex chars: '${fileId}'`
);
}
}
}

View File

@@ -0,0 +1 @@
export { DriveMesh } from "./drive-mesh.js";

View File

@@ -0,0 +1,4 @@
export {
PeerRegistry,
type PeerRegistryOptions,
} from "./peer-registry.js";

View File

@@ -0,0 +1,112 @@
# `peer-registry.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`PeerRegistry` is the in-memory overlay drive-mesh maintains on top of
the live peer set provided by `network-plugin-registry`. The registry
adds three concerns the network layer does not own: per-`fileId`
availability bitsets, an EMA-smoothed RTT tracker, and a composite
score the parallel puller uses to rank candidates.
The registry never originates peer-identity events. Peers enter via
`addOrUpdate` (called by the mesh in response to bus events) and exit
via `forget` (called when the network registry emits `peer:left`). The
denylist is owned by the registry because bad signatures are a
file-layer concern.
## Features
- Per-file availability bitsets stored as framed bytes (output of
`encodeFramedBitset` from `@suaveplan/drive-mesh-protocol`).
- EMA latency tracker (α = 0.3).
- Composite score: `2 * (100 / (rtt+1)) + 3 * successRate - 5 *
errorRate`. Weights documented in the README.
- Bad-sig denylist with two modes (`"session"` clears on stop;
`"persistent"` survives).
- `rank(fileId, chunkId)` returns scored candidates in
descending-score order, filtered to peers that claim availability of
the chunk.
## Usage: Basic Example
```ts
import { PeerRegistry } from "@suaveplan/drive-mesh/peer-registry/peer-registry";
const registry = new PeerRegistry();
registry.addOrUpdate("peerA", { transports: ["https"], rttMs: 80 });
registry.recordAvailability("peerA", "abcd…", framedBitset);
const candidates = registry.rank("abcd…", 7);
```
## Usage: Advanced Example
```ts
import { PeerRegistry } from "@suaveplan/drive-mesh/peer-registry/peer-registry";
const registry = new PeerRegistry({
clock: { now: () => fakeClock.now() },
denylistMode: "persistent",
});
registry.addOrUpdate("peerA", { transports: ["webrtc", "https"] });
registry.recordRtt("peerA", 50);
registry.recordSuccess("peerA");
registry.recordSuccess("peerA");
registry.addOrUpdate("peerB", { transports: ["https"] });
registry.recordRtt("peerB", 400);
registry.recordFailure("peerB");
const top = registry.rank("file-id-hex", -1);
// peerA outranks peerB because of lower RTT and higher success rate.
registry.recordBadSig("peerB");
console.log(registry.isDenied("peerB")); // true
console.log(registry.denialReason("peerB")); // "bad_sig"
```
## API Reference
- `class PeerRegistry`:
- `constructor(options?: { clock?; denylistMode? })`.
- `addOrUpdate(peerId, update?): PeerInfo` — idempotent insertion /
merge.
- `forget(peerId): void`.
- `forgetAll(): void` — clears the registry; clears the denylist
iff `denylistMode === "session"`.
- `recordAvailability(peerId, fileId, framedBitset): void`.
- `recordRtt(peerId, rttMs): void` — α=0.3 EMA.
- `recordSuccess(peerId): void`.
- `recordFailure(peerId): void`.
- `recordBadSig(peerId): void` — moves to denylist
(`reason="bad_sig"`).
- `deny(peerId, reason): void` — denylist with explicit reason.
- `removeFromDenylist(peerId): void`.
- `isDenied(peerId): boolean`.
- `denialReason(peerId): PeerDenialReason | undefined`.
- `peers(): IterableIterator<PeerInfo>`.
- `rank(fileId, chunkId): PeerInfo[]` — descending-score order.
- `get(peerId): PeerInfo | undefined`.
## Implementation Notes
`addOrUpdate` returns the post-merge snapshot rather than `void` so
the mesh can immediately emit `peer:joined`/`peer:scored` events with
the canonical shape without re-querying. Snapshots are fresh objects
on every call so callers may not assume reference equality.
`rank(fileId, -1)` is a sentinel meaning "any peer with any
availability for the file" — the gossip engine uses it to fan-out
advertisements without requiring a specific chunk. The negative path
is the only branch that skips the bitset bit-check, which keeps the
hot-path tight.
The score formula is intentionally simple: weighted-sum of latency,
success rate, and error rate. The weights are tuned so that a peer
with double the RTT of another remains preferred if it has a higher
success rate, which matches the drive-webrtc-era tuning. Bad-sig
peers are pinned at `-Infinity` so they sort to the bottom of every
ranking without special-casing the consumer.

View File

@@ -0,0 +1,275 @@
import { encodeFramedBitset } from "@suaveplan/drive-mesh-protocol";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { PeerRegistry } from "./peer-registry.js";
function bitsetOf(
chunkIds: readonly number[],
totalChunks: number
): Uint8Array {
const raw = new Uint8Array(Math.ceil(totalChunks / 8));
for (const id of chunkIds) raw[id >> 3] |= 1 << (id & 7);
return encodeFramedBitset(raw, totalChunks);
}
describe("PeerRegistry", () => {
it("admits and snapshots a new peer", { timeout: 5000 }, () => {
const r = new PeerRegistry();
const info = r.addOrUpdate("peerA", { transports: ["https"] });
expect(info.peerId).toBe("peerA");
expect(info.transports).toEqual(["https"]);
expect(info.rttMs).toBe(null);
});
it("merges updates onto an existing peer", { timeout: 5000 }, () => {
const r = new PeerRegistry();
r.addOrUpdate("peerA", { transports: ["https"] });
const info = r.addOrUpdate("peerA", {
transports: ["https", "webrtc"],
rttMs: 50,
lastSeen: 1_700_000_000_000,
});
expect(info.transports).toEqual(["https", "webrtc"]);
expect(info.rttMs).toBe(50);
expect(info.lastSeen).toBe(1_700_000_000_000);
});
it("addOrUpdate with no fields keeps an existing peer alive", {
timeout: 5000,
}, () => {
let now = 1000;
const r = new PeerRegistry({ clock: () => now });
r.addOrUpdate("peerA", { transports: ["https"] });
now = 2000;
const info = r.addOrUpdate("peerA");
expect(info.lastSeen).toBe(2000);
});
it("uses defaults for clock and denylistMode", { timeout: 5000 }, () => {
const r = new PeerRegistry();
const info = r.addOrUpdate("peerA");
expect(info.lastSeen).toBeGreaterThan(0);
// session denylist clears on forgetAll:
r.recordBadSig("peerA");
r.forgetAll();
expect(r.isDenied("peerA")).toBe(false);
});
it("forget drops the peer but preserves denylist", {
timeout: 5000,
}, () => {
const r = new PeerRegistry({ denylistMode: "persistent" });
r.addOrUpdate("peerA");
r.recordBadSig("peerA");
r.forget("peerA");
expect(r.isDenied("peerA")).toBe(true);
});
it("forgetAll preserves persistent denylist", { timeout: 5000 }, () => {
const r = new PeerRegistry({ denylistMode: "persistent" });
r.addOrUpdate("peerA");
r.recordBadSig("peerA");
r.forgetAll();
expect(r.isDenied("peerA")).toBe(true);
});
it("recordAvailability is no-op for unknown peers", {
timeout: 5000,
}, () => {
const r = new PeerRegistry();
const bs = bitsetOf([0], 8);
r.recordAvailability("peerX", "f1", bs);
expect(r.get("peerX")).toBeUndefined();
});
it("recordAvailability stores the bitset on a known peer", {
timeout: 5000,
}, () => {
let now = 100;
const r = new PeerRegistry({ clock: () => now });
r.addOrUpdate("peerA");
now = 200;
const bs = bitsetOf([3], 8);
r.recordAvailability("peerA", "f1", bs);
const info = r.get("peerA");
expect(info?.availability.get("f1")).toEqual(bs);
expect(info?.lastSeen).toBe(200);
});
it("recordRtt initialises and EMA-smooths", { timeout: 5000 }, () => {
const r = new PeerRegistry();
r.recordRtt("peerX", 50);
expect(r.get("peerX")).toBeUndefined(); // no-op for unknown peer
r.addOrUpdate("peerA");
r.recordRtt("peerA", 100);
expect(r.get("peerA")?.rttMs).toBe(100);
r.recordRtt("peerA", 200);
// ema with α=0.3: 0.3*200 + 0.7*100 = 130
expect(r.get("peerA")?.rttMs).toBeCloseTo(130, 5);
});
it("recordSuccess / recordFailure update score", { timeout: 5000 }, () => {
const r = new PeerRegistry();
r.recordSuccess("ghost");
r.recordFailure("ghost");
r.addOrUpdate("peerA");
r.recordSuccess("peerA");
r.recordSuccess("peerA");
r.addOrUpdate("peerB");
r.recordFailure("peerB");
const a = r.get("peerA");
const b = r.get("peerB");
expect(a?.score).toBeGreaterThan(b?.score ?? 0);
});
it("recordBadSig moves to denylist and pins score", {
timeout: 5000,
}, () => {
const r = new PeerRegistry();
r.addOrUpdate("peerA");
r.recordBadSig("peerA");
expect(r.isDenied("peerA")).toBe(true);
expect(r.denialReason("peerA")).toBe("bad_sig");
expect(r.get("peerA")?.score).toBe(Number.NEGATIVE_INFINITY);
});
it("recordBadSig on unknown peer still records denylist", {
timeout: 5000,
}, () => {
const r = new PeerRegistry();
r.recordBadSig("peerGhost");
expect(r.isDenied("peerGhost")).toBe(true);
});
it("deny accepts an explicit reason", { timeout: 5000 }, () => {
const r = new PeerRegistry();
r.addOrUpdate("peerA");
r.deny("peerA", "manual");
expect(r.denialReason("peerA")).toBe("manual");
expect(r.get("peerA")?.score).toBe(Number.NEGATIVE_INFINITY);
});
it("deny on unknown peer is recorded but has no score effect", {
timeout: 5000,
}, () => {
const r = new PeerRegistry();
r.deny("peerGhost", "manual");
expect(r.isDenied("peerGhost")).toBe(true);
});
it("removeFromDenylist restores score", { timeout: 5000 }, () => {
const r = new PeerRegistry({ denylistMode: "persistent" });
r.addOrUpdate("peerA");
r.recordBadSig("peerA");
r.removeFromDenylist("peerA");
expect(r.isDenied("peerA")).toBe(false);
expect(r.get("peerA")?.score).not.toBe(Number.NEGATIVE_INFINITY);
});
it("removeFromDenylist tolerates unknown peers", { timeout: 5000 }, () => {
const r = new PeerRegistry();
expect(() => r.removeFromDenylist("ghost")).not.toThrow();
});
it("addOrUpdate on denylisted peer returns a denied snapshot", {
timeout: 5000,
}, () => {
const r = new PeerRegistry({ denylistMode: "persistent" });
r.recordBadSig("peerA");
const info = r.addOrUpdate("peerA", { transports: ["x"] });
expect(info.score).toBe(Number.NEGATIVE_INFINITY);
expect(info.transports).toEqual([]);
});
it("peers excludes denylisted entries", { timeout: 5000 }, () => {
const r = new PeerRegistry({ denylistMode: "persistent" });
r.addOrUpdate("peerA");
r.addOrUpdate("peerB");
r.recordBadSig("peerB");
const ids = [...r.peers()].map((p) => p.peerId).sort();
expect(ids).toEqual(["peerA"]);
});
it("get returns denied snapshot for denied peer", { timeout: 5000 }, () => {
const r = new PeerRegistry({ denylistMode: "persistent" });
r.addOrUpdate("peerA");
r.recordBadSig("peerA");
const info = r.get("peerA");
expect(info?.score).toBe(Number.NEGATIVE_INFINITY);
});
it("rank filters by chunk availability and orders by score", {
timeout: 5000,
}, () => {
const r = new PeerRegistry();
r.addOrUpdate("peerA");
r.addOrUpdate("peerB");
r.addOrUpdate("peerC");
r.recordRtt("peerA", 30);
r.recordSuccess("peerA");
r.recordRtt("peerB", 200);
r.recordFailure("peerB");
r.recordAvailability("peerA", "f1", bitsetOf([0, 1, 2], 8));
r.recordAvailability("peerB", "f1", bitsetOf([0, 1], 8));
r.recordAvailability("peerC", "f1", bitsetOf([3], 8));
// chunkId=0: A and B but not C
const c0 = r.rank("f1", 0).map((p) => p.peerId);
expect(c0).toEqual(["peerA", "peerB"]);
// chunkId=3: only C
const c3 = r.rank("f1", 3).map((p) => p.peerId);
expect(c3).toEqual(["peerC"]);
// chunkId=-1: all peers with any availability
const all = r
.rank("f1", -1)
.map((p) => p.peerId)
.sort();
expect(all).toEqual(["peerA", "peerB", "peerC"]);
});
it("rank skips denylisted peers", { timeout: 5000 }, () => {
const r = new PeerRegistry({ denylistMode: "persistent" });
r.addOrUpdate("peerA");
r.addOrUpdate("peerB");
r.recordAvailability("peerA", "f1", bitsetOf([0], 8));
r.recordAvailability("peerB", "f1", bitsetOf([0], 8));
r.recordBadSig("peerA");
const ranked = r.rank("f1", 0).map((p) => p.peerId);
expect(ranked).toEqual(["peerB"]);
});
it("rank ignores peers without availability for the file", {
timeout: 5000,
}, () => {
const r = new PeerRegistry();
r.addOrUpdate("peerA");
const ranked = r.rank("f1", 0);
expect(ranked).toEqual([]);
});
it("rank rejects out-of-range chunkId", { timeout: 5000 }, () => {
const r = new PeerRegistry();
r.addOrUpdate("peerA");
r.recordAvailability("peerA", "f1", bitsetOf([0], 8));
// bitLength=8, chunkId=8 -> out of range
expect(r.rank("f1", 8)).toEqual([]);
});
it("rank handles truncated bitset bytes safely", { timeout: 5000 }, () => {
const r = new PeerRegistry();
r.addOrUpdate("peerA");
// Hand-craft a framed bitset whose body byte index is OOB.
const framed = new Uint8Array(12); // header only, no body
new DataView(framed.buffer).setUint32(4, 8, false);
r.recordAvailability("peerA", "f1", framed);
// hasChunkInFramedBitset returns false because body byte is undefined
expect(r.rank("f1", 0)).toEqual([]);
});
it("rank rejects under-sized framed bitset header", {
timeout: 5000,
}, () => {
const r = new PeerRegistry();
r.addOrUpdate("peerA");
r.recordAvailability("peerA", "f1", new Uint8Array(4));
expect(r.rank("f1", 0)).toEqual([]);
});
});

View File

@@ -0,0 +1,267 @@
/**
* `PeerRegistry` — file-availability + scoring overlay on the live
* peer set.
*
* Identity events (`peer:joined` / `peer:left`) come from
* `network-plugin-registry` via the `NetworkBus`. The registry adds
* three concerns on top: per-`fileId` availability bitsets, an EMA
* latency tracker, and a composite peer score used by the parallel
* puller to pick candidates.
*
* Denylist semantics:
* - A peer that ever delivers a chunk whose server signature fails
* verification is added to the denylist with `score = -Infinity`.
* - In `"session"` mode (default) the denylist resets on
* `forgetAll()` (called by `mesh.stop()`).
* - In `"persistent"` mode the denylist survives stops; only an
* explicit `removeFromDenylist()` clears it.
*/
import type { Clock, PeerId } from "@suaveplan/types";
import type {
DenylistMode,
PeerDenialReason,
PeerInfo,
PeerUpdate,
} from "../../types/index.js";
const RTT_EMA_ALPHA = 0.3;
interface MutablePeer {
transports: Set<string>;
rttMs: number | null;
lastSeen: number;
success: number;
failure: number;
score: number;
availability: Map<string, Uint8Array>;
}
export interface PeerRegistryOptions {
readonly clock?: Clock;
readonly denylistMode?: DenylistMode;
}
const DEFAULT_CLOCK: Clock = () => Date.now();
export class PeerRegistry {
readonly #peers = new Map<PeerId, MutablePeer>();
readonly #denylist = new Map<PeerId, PeerDenialReason>();
readonly #clock: Clock;
readonly #denylistMode: DenylistMode;
public constructor(options: PeerRegistryOptions = {}) {
this.#clock = options.clock ?? DEFAULT_CLOCK;
this.#denylistMode = options.denylistMode ?? "session";
}
/** Add a peer or merge updates onto an existing entry. */
public addOrUpdate(peerId: PeerId, update: PeerUpdate = {}): PeerInfo {
if (this.#denylist.has(peerId)) {
return this.#snapshotDenied(peerId);
}
const existing = this.#peers.get(peerId);
const now = update.lastSeen ?? this.#clock();
if (existing === undefined) {
const created: MutablePeer = {
transports: new Set(update.transports ?? []),
rttMs: update.rttMs ?? null,
lastSeen: now,
success: 0,
failure: 0,
score: 0,
availability: new Map(),
};
this.#peers.set(peerId, created);
return this.#snapshot(peerId, created);
}
if (update.transports !== undefined) {
existing.transports = new Set(update.transports);
}
if (update.rttMs !== undefined) {
existing.rttMs = update.rttMs;
}
existing.lastSeen = now;
existing.score = this.#computeScore(existing);
return this.#snapshot(peerId, existing);
}
/** Drop the peer entirely. Denylist state is preserved. */
public forget(peerId: PeerId): void {
this.#peers.delete(peerId);
}
/** Drop every peer; clears the denylist only in session mode. */
public forgetAll(): void {
this.#peers.clear();
if (this.#denylistMode === "session") {
this.#denylist.clear();
}
}
/** Record (or update) the bitset of chunks `peerId` holds for `fileId`. */
public recordAvailability(
peerId: PeerId,
fileId: string,
framedBitset: Uint8Array
): void {
const peer = this.#peers.get(peerId);
if (peer === undefined) return;
peer.availability.set(fileId, framedBitset);
peer.lastSeen = this.#clock();
}
/** Update the EMA RTT after a successful or failed chunk fetch. */
public recordRtt(peerId: PeerId, rttMs: number): void {
const peer = this.#peers.get(peerId);
if (peer === undefined) return;
peer.rttMs =
peer.rttMs === null
? rttMs
: RTT_EMA_ALPHA * rttMs + (1 - RTT_EMA_ALPHA) * peer.rttMs;
peer.score = this.#computeScore(peer);
}
/** Record a successful chunk delivery. */
public recordSuccess(peerId: PeerId): void {
const peer = this.#peers.get(peerId);
if (peer === undefined) return;
peer.success += 1;
peer.score = this.#computeScore(peer);
}
/** Record a non-signature failure (timeout, transport error). */
public recordFailure(peerId: PeerId): void {
const peer = this.#peers.get(peerId);
if (peer === undefined) return;
peer.failure += 1;
peer.score = this.#computeScore(peer);
}
/**
* Add the peer to the denylist with `score = -Infinity`. Idempotent.
*/
public recordBadSig(peerId: PeerId): void {
this.#denylist.set(peerId, "bad_sig");
const peer = this.#peers.get(peerId);
if (peer !== undefined) peer.score = Number.NEGATIVE_INFINITY;
}
/** Add a peer to the denylist for a non-signature reason. */
public deny(peerId: PeerId, reason: PeerDenialReason): void {
this.#denylist.set(peerId, reason);
const peer = this.#peers.get(peerId);
if (peer !== undefined) peer.score = Number.NEGATIVE_INFINITY;
}
/** Remove the peer from the denylist. */
public removeFromDenylist(peerId: PeerId): void {
this.#denylist.delete(peerId);
const peer = this.#peers.get(peerId);
if (peer !== undefined) peer.score = this.#computeScore(peer);
}
/** Is the peer currently denylisted? */
public isDenied(peerId: PeerId): boolean {
return this.#denylist.has(peerId);
}
/** Reason the peer was denylisted (or `undefined`). */
public denialReason(peerId: PeerId): PeerDenialReason | undefined {
return this.#denylist.get(peerId);
}
/** Snapshot of every live (non-denied) peer. */
public peers(): IterableIterator<PeerInfo> {
const out: PeerInfo[] = [];
for (const [peerId, peer] of this.#peers) {
if (this.#denylist.has(peerId)) continue;
out.push(this.#snapshot(peerId, peer));
}
return out[Symbol.iterator]();
}
/**
* Rank peers known to hold `chunkId` of `fileId`, best score first.
* A `chunkId` of `-1` ranks every peer with any availability for
* the file (used by the gossip engine for fan-out).
*/
public rank(fileId: string, chunkId: number): PeerInfo[] {
const out: { info: PeerInfo; score: number }[] = [];
for (const [peerId, peer] of this.#peers) {
if (this.#denylist.has(peerId)) continue;
const bitset = peer.availability.get(fileId);
if (bitset === undefined) continue;
if (chunkId >= 0 && !hasChunkInFramedBitset(bitset, chunkId)) {
continue;
}
out.push({ info: this.#snapshot(peerId, peer), score: peer.score });
}
out.sort((a, b) => b.score - a.score);
return out.map((e) => e.info);
}
/** Lookup a single peer snapshot. */
public get(peerId: PeerId): PeerInfo | undefined {
const peer = this.#peers.get(peerId);
if (peer === undefined) return undefined;
if (this.#denylist.has(peerId)) return this.#snapshotDenied(peerId);
return this.#snapshot(peerId, peer);
}
#snapshot(peerId: PeerId, peer: MutablePeer): PeerInfo {
return {
peerId,
transports: [...peer.transports],
rttMs: peer.rttMs,
lastSeen: peer.lastSeen,
score: peer.score,
availability: new Map(peer.availability),
};
}
#snapshotDenied(peerId: PeerId): PeerInfo {
return {
peerId,
transports: [],
rttMs: null,
lastSeen: this.#clock(),
score: Number.NEGATIVE_INFINITY,
availability: new Map(),
};
}
#computeScore(peer: MutablePeer): number {
const attempts = peer.success + peer.failure;
const successRate = attempts === 0 ? 0.5 : peer.success / attempts;
const errorRate = attempts === 0 ? 0 : peer.failure / attempts;
const latencyTerm = peer.rttMs === null ? 1 : 100 / (peer.rttMs + 1);
// weights documented in README
return 2 * latencyTerm + 3 * successRate - 5 * errorRate;
}
}
/**
* Inspect bit `chunkId` of a framed bitset.
*
* Framed bitsets are produced by `encodeFramedBitset` from
* `@suaveplan/drive-mesh-protocol`: 12 header bytes (magic + bitlen +
* bytelen) followed by the raw bitmap. We bounds-check the header and
* extract the bit by direct index.
*/
function hasChunkInFramedBitset(framed: Uint8Array, chunkId: number): boolean {
const HEADER = 12;
if (framed.length < HEADER) return false;
const view = new DataView(
framed.buffer,
framed.byteOffset,
framed.byteLength
);
const bitLength = view.getUint32(4, false);
if (chunkId < 0 || chunkId >= bitLength) return false;
const byteIndex = HEADER + (chunkId >> 3);
const bitMask = 1 << (chunkId & 7);
const byte = framed[byteIndex];
if (byte === undefined) return false;
return (byte & bitMask) !== 0;
}

View File

@@ -0,0 +1,4 @@
export {
ParallelPuller,
type ParallelPullerOptions,
} from "./parallel-puller.js";

View File

@@ -0,0 +1,126 @@
# `parallel-puller.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ParallelPuller` is the chunk-fetch scheduler that turns "I want chunk
N of file F" into a verified `Uint8Array`. The puller asks the
`PeerRegistry` for ranked candidates, issues a request to the best
peer, waits `racingWindowMs`, then issues parallel requests to the
runners-up. The first response whose server signature verifies wins;
the other in-flight attempts are aborted via a shared
`AbortController`.
The puller is the mesh's primary trust enforcement point. Every byte
returned from a transport flows through `verifyChunkSig` before the
puller acknowledges success. A signature failure moves the offending
peer to the registry's denylist with `score = -Infinity` and surfaces
`DriveMeshError(SIG_VERIFY_FAILED)` — the orchestrator can choose to
fall back to the server rather than retry on peers.
## Features
- Top-K candidate selection (default 3).
- Racing-window staggered issuance (default 250 ms).
- Shared `AbortController` short-circuits losing attempts.
- Mandatory `verifyChunkSig` gate against the configured
`serverPubKey`.
- Bad-sig denylisting via `registry.recordBadSig`.
- Non-signature failures update success / failure counters so
ranking adapts.
- `maxParallelRequests` global cap.
## Usage: Basic Example
```ts
import { ParallelPuller } from "@suaveplan/drive-mesh/scheduler/parallel-puller";
const puller = new ParallelPuller({
registry,
transports: [httpsTransport],
serverPubKey,
});
const bytes = await puller.pull("file-id-hex", 0);
```
## Usage: Advanced Example
```ts
import { ParallelPuller } from "@suaveplan/drive-mesh/scheduler/parallel-puller";
const puller = new ParallelPuller({
registry,
transports: [webrtcTransport, httpsTransport],
serverPubKey,
racingWindowMs: 150,
topK: 4,
maxParallelRequests: 16,
clock: fakeClock,
onAttempt: (peerId, fileId, chunkId) =>
log.debug("attempt", peerId, fileId, chunkId),
onSuccess: (peerId, _f, _c, bytes, elapsedMs) =>
log.info("served", peerId, bytes, elapsedMs),
onBadSig: peerId => log.warn("denylisting", peerId),
});
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
try {
const bytes = await puller.pull(fileId, chunkId, controller.signal);
} catch (err) {
if (err instanceof DriveMeshError && err.code === DRIVE_MESH_CODES.ABORTED) {
// caller-driven cancellation
}
}
```
## API Reference
- `class ParallelPuller`:
- `constructor(options: ParallelPullerOptions)`.
- `pull(fileId, chunkId, signal?): Promise<Uint8Array>`.
- `inflight: number` (read-only) — in-flight request count.
- `ParallelPullerOptions`:
- `registry`, `transports`, `serverPubKey` (required).
- `maxParallelRequests?` (default `8`).
- `racingWindowMs?` (default `250`).
- `topK?` (default `3`).
- `clock?`.
- `onAttempt?(peerId, fileId, chunkId)`.
- `onSuccess?(peerId, fileId, chunkId, bytes, elapsedMs)`.
- `onFailure?(peerId, fileId, chunkId, reason)`.
- `onBadSig?(peerId, fileId, chunkId)`.
- Throws:
- `DRIVE_MESH.CHUNK_UNAVAILABLE` — no candidate peers.
- `DRIVE_MESH.SIG_VERIFY_FAILED` — winning peer returned bad sig.
- `DRIVE_MESH.TRANSPORT_FAILED` — single-attempt transport failure.
- `DRIVE_MESH.ALL_PEERS_FAILED` — every candidate failed.
- `DRIVE_MESH.ABORTED` — caller-provided signal aborted.
## Implementation Notes
`firstSettled` is hand-rolled rather than using `Promise.any`:
`Promise.any` resolves on first success but does NOT abort losing
attempts, leaving them in flight wastefully. The shared
`AbortController` is the cancellation primitive — `controller.abort()`
fires both when a winner appears and when the outer caller aborts.
`SIG_VERIFY_FAILED` short-circuits the "all peers failed" wrapping:
the orchestrator distinguishes "every peer failed transport" (try
server fallback) from "winning peer was Byzantine" (denylist + retry
on a different peer). Re-wrapping the bad-sig error inside
`ALL_PEERS_FAILED` would lose that distinction.
`maxParallelRequests` is enforced by a yielding micro-loop in
`#awaitSlot`. The loop is bounded by the number of concurrent `pull`
calls — drive-mesh issues `pull` from a small, controlled set of
async contexts so the loop never spins indefinitely. Under heavier
fan-in the orchestrator should queue requests in front of the puller
rather than issuing every chunk concurrently.
Bad-sig denylist is permanent within the registry's denylist scope:
`session` (default) clears on `mesh.stop()`, `persistent` survives.
The puller does not concern itself with scope — the registry is the
authority.

View File

@@ -0,0 +1,406 @@
import { encodeFramedBitset } from "@suaveplan/drive-mesh-protocol";
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
Broker,
FakeDriveTransport,
} from "../../../tests/helpers/fake-transport.js";
import {
buildChunkBytes,
makeKeypair,
TEST_FILE_ID_HEX,
} from "../../../tests/helpers/signed-chunk.js";
import {
DRIVE_MESH_CODES,
DriveMeshError,
} from "../../error-codes/error-codes.js";
import { PeerRegistry } from "../../peer-registry/peer-registry/index.js";
import { ParallelPuller } from "./parallel-puller.js";
function bitset(bits: readonly number[], total: number): Uint8Array {
const raw = new Uint8Array(Math.ceil(total / 8));
for (const b of bits) raw[b >> 3] |= 1 << (b & 7);
return encodeFramedBitset(raw, total);
}
const FILE_ID = TEST_FILE_ID_HEX;
function setupTwoPeers(): {
broker: Broker;
transport: FakeDriveTransport;
registry: PeerRegistry;
serverPubKey: Uint8Array;
serverPrivKey: Uint8Array;
} {
const kp = makeKeypair(7);
const broker = new Broker();
const transport = new FakeDriveTransport({ broker, localPeerId: "local" });
transport.addKnownPeer("peerA");
transport.addKnownPeer("peerB");
const registry = new PeerRegistry();
registry.addOrUpdate("peerA", { transports: ["fake"] });
registry.addOrUpdate("peerB", { transports: ["fake"] });
registry.recordAvailability("peerA", FILE_ID, bitset([0, 1, 2], 8));
registry.recordAvailability("peerB", FILE_ID, bitset([0, 1, 2], 8));
return {
broker,
transport,
registry,
serverPubKey: kp.pubKey,
serverPrivKey: kp.privKey,
};
}
describe("ParallelPuller", () => {
it("throws CHUNK_UNAVAILABLE when no peers know the chunk", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(1);
const broker = new Broker();
const transport = new FakeDriveTransport({ broker, localPeerId: "x" });
const registry = new PeerRegistry();
const puller = new ParallelPuller({
registry,
transports: [transport],
serverPubKey: kp.pubKey,
});
await expect(puller.pull(FILE_ID, 0)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.CHUNK_UNAVAILABLE,
});
});
it("rejects immediately when signal is already aborted", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(1);
const puller = new ParallelPuller({
registry: new PeerRegistry(),
transports: [],
serverPubKey: kp.pubKey,
});
const ac = new AbortController();
ac.abort();
await expect(puller.pull(FILE_ID, 0, ac.signal)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.ABORTED,
});
});
it("returns verified bytes from the top peer", {
timeout: 5000,
}, async () => {
const ctx = setupTwoPeers();
const bytes = buildChunkBytes({
serverPrivKey: ctx.serverPrivKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
ctx.broker.serve("peerA", FILE_ID, 0, { bytes });
ctx.registry.recordRtt("peerA", 10);
ctx.registry.recordSuccess("peerA");
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
});
const out = await puller.pull(FILE_ID, 0);
expect(out).toEqual(bytes);
});
it("falls through to second peer when first throws", {
timeout: 5000,
}, async () => {
const ctx = setupTwoPeers();
const bytes = buildChunkBytes({
serverPrivKey: ctx.serverPrivKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
// peerA broken; peerB serves
ctx.broker.serve("peerA", FILE_ID, 0, {
throwReason: "transport down",
});
ctx.broker.serve("peerB", FILE_ID, 0, { bytes, delayMs: 5 });
ctx.registry.recordRtt("peerA", 10);
ctx.registry.recordSuccess("peerA");
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
racingWindowMs: 1,
});
const out = await puller.pull(FILE_ID, 0);
expect(out).toEqual(bytes);
});
it("issues parallel attempts after racing window", {
timeout: 5000,
}, async () => {
const ctx = setupTwoPeers();
const bytes = buildChunkBytes({
serverPrivKey: ctx.serverPrivKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
// peerA delays 100 ms, peerB delays 5 ms -> after racing window, B wins
ctx.broker.serve("peerA", FILE_ID, 0, { bytes, delayMs: 100 });
ctx.broker.serve("peerB", FILE_ID, 0, { bytes, delayMs: 5 });
ctx.registry.recordRtt("peerA", 10);
ctx.registry.recordSuccess("peerA");
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
racingWindowMs: 10,
});
const out = await puller.pull(FILE_ID, 0);
expect(out).toEqual(bytes);
});
it("surfaces SIG_VERIFY_FAILED and denylists the peer", {
timeout: 5000,
}, async () => {
const ctx = setupTwoPeers();
// Build bytes signed by a *different* key so verify fails
const wrong = makeKeypair(99);
const badBytes = buildChunkBytes({
serverPrivKey: wrong.privKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
ctx.broker.serve("peerA", FILE_ID, 0, { bytes: badBytes });
// Drop peerB so peerA is the only candidate
ctx.registry.forget("peerB");
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
});
await expect(puller.pull(FILE_ID, 0)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.SIG_VERIFY_FAILED,
});
expect(ctx.registry.isDenied("peerA")).toBe(true);
});
it("emits ALL_PEERS_FAILED when every candidate throws", {
timeout: 5000,
}, async () => {
const ctx = setupTwoPeers();
ctx.broker.serve("peerA", FILE_ID, 0, { throwReason: "x" });
ctx.broker.serve("peerB", FILE_ID, 0, { throwReason: "y" });
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
racingWindowMs: 1,
topK: 2,
});
await expect(puller.pull(FILE_ID, 0)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.ALL_PEERS_FAILED,
});
});
it("emits TRANSPORT_FAILED when no transport claims the peer", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(2);
const broker = new Broker();
const transport = new FakeDriveTransport({ broker, localPeerId: "x" });
// do NOT add peerA to transport.knownPeers
const registry = new PeerRegistry();
registry.addOrUpdate("peerA");
registry.recordAvailability("peerA", FILE_ID, bitset([0], 8));
const puller = new ParallelPuller({
registry,
transports: [transport],
serverPubKey: kp.pubKey,
});
await expect(puller.pull(FILE_ID, 0)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.ALL_PEERS_FAILED,
});
});
it("fires observability hooks on success", { timeout: 5000 }, async () => {
const ctx = setupTwoPeers();
const bytes = buildChunkBytes({
serverPrivKey: ctx.serverPrivKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
ctx.broker.serve("peerA", FILE_ID, 0, { bytes });
ctx.registry.recordRtt("peerA", 10);
ctx.registry.recordSuccess("peerA");
let attempts = 0;
let successes = 0;
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
onAttempt: () => {
attempts += 1;
},
onSuccess: () => {
successes += 1;
},
onFailure: () => undefined,
onBadSig: () => undefined,
});
await puller.pull(FILE_ID, 0);
expect(attempts).toBeGreaterThanOrEqual(1);
expect(successes).toBe(1);
});
it("inflight gauge updates around pull", { timeout: 5000 }, async () => {
const ctx = setupTwoPeers();
const bytes = buildChunkBytes({
serverPrivKey: ctx.serverPrivKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
ctx.broker.serve("peerA", FILE_ID, 0, { bytes, delayMs: 30 });
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
});
const promise = puller.pull(FILE_ID, 0);
// allow microtask to enter pull
await Promise.resolve();
await Promise.resolve();
expect(puller.inflight).toBeGreaterThanOrEqual(1);
await promise;
expect(puller.inflight).toBe(0);
});
it("respects maxParallelRequests (busy-waits when slot is full)", {
timeout: 5000,
}, async () => {
const ctx = setupTwoPeers();
const bytes0 = buildChunkBytes({
serverPrivKey: ctx.serverPrivKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
const bytes1 = buildChunkBytes({
serverPrivKey: ctx.serverPrivKey,
fileIdHex: FILE_ID,
chunkId: 1,
});
// hold the first chunk long enough that p2 hits the busy-wait
// loop body inside `#awaitSlot`.
ctx.broker.serve("peerA", FILE_ID, 0, { bytes: bytes0, delayMs: 60 });
ctx.broker.serve("peerA", FILE_ID, 1, { bytes: bytes1, delayMs: 5 });
ctx.registry.recordAvailability(
"peerA",
FILE_ID,
(() => {
const raw = new Uint8Array(1);
raw[0] = 0b111;
return encodeFramedBitset(raw, 8);
})()
);
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
maxParallelRequests: 1,
racingWindowMs: 1,
topK: 1,
});
const p1 = puller.pull(FILE_ID, 0);
// Force p2 to enter #awaitSlot AFTER p1 has incremented inflight.
await Promise.resolve();
const p2 = puller.pull(FILE_ID, 1);
const [a, b] = await Promise.all([p1, p2]);
expect(a).toEqual(bytes0);
expect(b).toEqual(bytes1);
});
it("delay short-circuits when racingWindowMs is 0", {
timeout: 5000,
}, async () => {
const ctx = setupTwoPeers();
const bytes = buildChunkBytes({
serverPrivKey: ctx.serverPrivKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
ctx.broker.serve("peerA", FILE_ID, 0, { bytes, delayMs: 50 });
ctx.broker.serve("peerB", FILE_ID, 0, { bytes, delayMs: 5 });
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
racingWindowMs: 0,
topK: 2,
});
const out = await puller.pull(FILE_ID, 0);
expect(out).toEqual(bytes);
});
it("wraps a non-Error throw inside firstSettled", {
timeout: 5000,
}, async () => {
const ctx = setupTwoPeers();
// Drop peerB so only peerA is a candidate
ctx.registry.forget("peerB");
// monkey-patch transport.requestChunk to throw a non-Error
ctx.transport.requestChunk = (async () => {
throw "string-rejection";
}) as typeof ctx.transport.requestChunk;
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
});
await expect(puller.pull(FILE_ID, 0)).rejects.toMatchObject({
code: DRIVE_MESH_CODES.ALL_PEERS_FAILED,
});
});
it("ignores second winner after first attempt settles", {
timeout: 5000,
}, async () => {
const ctx = setupTwoPeers();
const bytes = buildChunkBytes({
serverPrivKey: ctx.serverPrivKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
// Both peers stage successful responses with no delay; with
// racingWindowMs:0 both attempts are issued back-to-back and
// both resolve. The microtask scheduler runs the `then`
// handlers in registration order so the second handler hits
// the `settled` guard.
ctx.broker.serve("peerA", FILE_ID, 0, { bytes });
ctx.broker.serve("peerB", FILE_ID, 0, { bytes });
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
racingWindowMs: 0,
topK: 2,
});
const out = await puller.pull(FILE_ID, 0);
expect(out).toEqual(bytes);
});
it("aborts in-flight attempts when outer signal aborts", {
timeout: 5000,
}, async () => {
const ctx = setupTwoPeers();
const bytes = buildChunkBytes({
serverPrivKey: ctx.serverPrivKey,
fileIdHex: FILE_ID,
chunkId: 0,
});
ctx.broker.serve("peerA", FILE_ID, 0, { bytes, delayMs: 1000 });
const ac = new AbortController();
const puller = new ParallelPuller({
registry: ctx.registry,
transports: [ctx.transport],
serverPubKey: ctx.serverPubKey,
});
const promise = puller.pull(FILE_ID, 0, ac.signal);
setTimeout(() => ac.abort(), 5);
await expect(promise).rejects.toBeInstanceOf(DriveMeshError);
});
});

View File

@@ -0,0 +1,367 @@
/**
* `ParallelPuller` — racing chunk fetcher.
*
* Picks the top-K peers from `PeerRegistry.rank()`, issues a request
* to the best peer, and after `racingWindowMs` issues parallel
* requests to the runners-up. The first response that survives
* signature verification wins; the others are cancelled via the
* shared `AbortController`.
*
* Bad-sig responses surface as
* `DriveMeshError(DRIVE_MESH.SIG_VERIFY_FAILED)` and trigger
* `registry.recordBadSig` (which moves the peer to the denylist with
* `score = -Infinity`).
*
* Other failures (transport error, timeout) are recorded via
* `registry.recordFailure` so subsequent ranking deprioritises the
* peer without banning it.
*/
import { verifyChunkSig } from "@suaveplan/drive-plugin-chunks";
import type { Clock, PeerId } from "@suaveplan/types";
import {
DRIVE_MESH_CODES,
DriveMeshError,
} from "../../error-codes/error-codes.js";
import type { PeerRegistry } from "../../peer-registry/peer-registry/index.js";
import type { DriveTransport, PeerInfo } from "../../types/index.js";
export interface ParallelPullerOptions {
readonly registry: PeerRegistry;
readonly transports: readonly DriveTransport[];
readonly serverPubKey: Uint8Array;
/** Default 8. */
readonly maxParallelRequests?: number;
/** Default 250. */
readonly racingWindowMs?: number;
/** Top-K candidates considered per request. Default 3. */
readonly topK?: number;
readonly clock?: Clock;
/** Observability hooks. */
readonly onAttempt?: (
peerId: PeerId,
fileId: string,
chunkId: number
) => void;
readonly onSuccess?: (
peerId: PeerId,
fileId: string,
chunkId: number,
bytes: number,
elapsedMs: number
) => void;
readonly onFailure?: (
peerId: PeerId,
fileId: string,
chunkId: number,
reason: string
) => void;
readonly onBadSig?: (
peerId: PeerId,
fileId: string,
chunkId: number
) => void;
}
interface AttemptResult {
readonly peerId: PeerId;
readonly bytes: Uint8Array;
readonly elapsedMs: number;
}
export class ParallelPuller {
readonly #registry: PeerRegistry;
readonly #transports: readonly DriveTransport[];
readonly #serverPubKey: Uint8Array;
readonly #racingWindowMs: number;
readonly #topK: number;
readonly #maxParallel: number;
readonly #clock: Clock;
readonly #onAttempt: ParallelPullerOptions["onAttempt"];
readonly #onSuccess: ParallelPullerOptions["onSuccess"];
readonly #onFailure: ParallelPullerOptions["onFailure"];
readonly #onBadSig: ParallelPullerOptions["onBadSig"];
#inflight = 0;
public constructor(options: ParallelPullerOptions) {
this.#registry = options.registry;
this.#transports = options.transports;
this.#serverPubKey = options.serverPubKey;
this.#racingWindowMs = options.racingWindowMs ?? 250;
this.#topK = options.topK ?? 3;
this.#maxParallel = options.maxParallelRequests ?? 8;
this.#clock = options.clock ?? (() => Date.now());
this.#onAttempt = options.onAttempt;
this.#onSuccess = options.onSuccess;
this.#onFailure = options.onFailure;
this.#onBadSig = options.onBadSig;
}
/** In-flight request count — useful for backpressure assertions. */
public get inflight(): number {
return this.#inflight;
}
/**
* Try to satisfy a chunk request via peers. Returns the verified
* bytes on success, or throws `DriveMeshError(ALL_PEERS_FAILED)`
* when every candidate is exhausted (the caller may then attempt
* the server fallback).
*/
public async pull(
fileId: string,
chunkId: number,
signal?: AbortSignal
): Promise<Uint8Array> {
if (signal?.aborted) {
throw new DriveMeshError(
DRIVE_MESH_CODES.ABORTED,
"request aborted before start"
);
}
const candidates = this.#registry.rank(fileId, chunkId);
if (candidates.length === 0) {
throw new DriveMeshError(
DRIVE_MESH_CODES.CHUNK_UNAVAILABLE,
`no peers advertise chunk ${chunkId} of ${fileId}`
);
}
await this.#awaitSlot();
this.#inflight += 1;
try {
return await this.#race(fileId, chunkId, candidates, signal);
} finally {
this.#inflight -= 1;
}
}
async #awaitSlot(): Promise<void> {
// Busy-wait via microtask yield until a slot frees. Pull is
// called from a small set of async contexts so this loop has
// a tight upper bound (`maxParallel` concurrent calls).
while (this.#inflight >= this.#maxParallel) {
await new Promise<void>((resolve) => setTimeout(resolve, 0));
}
}
async #race(
fileId: string,
chunkId: number,
candidates: readonly PeerInfo[],
outerSignal: AbortSignal | undefined
): Promise<Uint8Array> {
const topK = candidates.slice(0, this.#topK);
const controller = new AbortController();
const onOuterAbort = (): void => controller.abort();
outerSignal?.addEventListener("abort", onOuterAbort, { once: true });
const attempts: Promise<AttemptResult>[] = [];
const failures: { peerId: PeerId; reason: string }[] = [];
try {
let i = 0;
for (const peer of topK) {
if (i > 0) {
try {
await delay(this.#racingWindowMs, controller.signal);
} catch (delayErr) {
// Outer / inner abort during racing window.
// Fall through to firstSettled with whatever was
// already issued; attempts are guaranteed to be
// .catch'd inside firstSettled.
void delayErr;
break;
}
}
i += 1;
const attempt = this.#tryPeer(
peer.peerId,
fileId,
chunkId,
controller.signal,
failures
);
// Attach a catch-all the same tick the promise is
// created so an early rejection (e.g., synchronous
// transport throw) cannot surface as
// "unhandled-rejection" before `firstSettled` attaches
// its own handler.
attempt.catch(() => undefined);
attempts.push(attempt);
}
return await firstSettled(attempts, controller);
} catch (err) {
outerSignal?.removeEventListener("abort", onOuterAbort);
// `firstSettled` rejects with a DriveMeshError (from `#tryPeer`)
// and `delay` rejects with a DriveMeshError(ABORTED); every
// path into this catch produces an `Error` subclass.
const cause = err as Error;
if (outerSignal?.aborted) {
throw new DriveMeshError(
DRIVE_MESH_CODES.ABORTED,
"request aborted",
{ cause }
);
}
if (
err instanceof DriveMeshError &&
err.code === DRIVE_MESH_CODES.SIG_VERIFY_FAILED
) {
throw err;
}
throw new DriveMeshError(
DRIVE_MESH_CODES.ALL_PEERS_FAILED,
`all ${topK.length} peers failed for ${fileId}/${chunkId}: ` +
failures.map((f) => `${f.peerId}=${f.reason}`).join(", "),
{ cause }
);
} finally {
outerSignal?.removeEventListener("abort", onOuterAbort);
controller.abort();
}
}
async #tryPeer(
peerId: PeerId,
fileId: string,
chunkId: number,
signal: AbortSignal,
failures: { peerId: PeerId; reason: string }[]
): Promise<AttemptResult> {
const transport = this.#pickTransport(peerId);
if (transport === undefined) {
const reason = "no_transport";
failures.push({ peerId, reason });
this.#onFailure?.(peerId, fileId, chunkId, reason);
this.#registry.recordFailure(peerId);
throw new DriveMeshError(
DRIVE_MESH_CODES.TRANSPORT_FAILED,
`no transport for peer ${peerId}`
);
}
this.#onAttempt?.(peerId, fileId, chunkId);
const start = this.#clock();
try {
const bytes = await transport.requestChunk(
peerId,
fileId,
chunkId,
signal
);
try {
verifyChunkSig(bytes, this.#serverPubKey);
} catch (verifyErr) {
this.#registry.recordBadSig(peerId);
this.#onBadSig?.(peerId, fileId, chunkId);
// `verifyChunkSig` throws a `ChunksError` (which
// extends `Error`), so the `cause` cast is always safe.
throw new DriveMeshError(
DRIVE_MESH_CODES.SIG_VERIFY_FAILED,
`bad sig from peer ${peerId}`,
{ cause: verifyErr as Error }
);
}
const elapsedMs = this.#clock() - start;
this.#registry.recordRtt(peerId, elapsedMs);
this.#registry.recordSuccess(peerId);
this.#onSuccess?.(
peerId,
fileId,
chunkId,
bytes.byteLength,
elapsedMs
);
return { peerId, bytes, elapsedMs };
} catch (err) {
if (err instanceof DriveMeshError) throw err;
const reason = err instanceof Error ? err.message : "unknown";
failures.push({ peerId, reason });
this.#registry.recordFailure(peerId);
this.#onFailure?.(peerId, fileId, chunkId, reason);
throw new DriveMeshError(
DRIVE_MESH_CODES.TRANSPORT_FAILED,
`peer ${peerId} failed: ${reason}`,
err instanceof Error ? { cause: err } : undefined
);
}
}
#pickTransport(peerId: PeerId): DriveTransport | undefined {
for (const transport of this.#transports) {
for (const candidate of transport.peers()) {
if (candidate === peerId) return transport;
}
}
return undefined;
}
}
function delay(ms: number, signal: AbortSignal): Promise<void> {
if (ms <= 0) return Promise.resolve();
return new Promise<void>((resolve, reject) => {
const onAbort = (): void => {
clearTimeout(handle);
reject(
new DriveMeshError(DRIVE_MESH_CODES.ABORTED, "delay aborted")
);
};
const handle = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve();
}, ms);
signal.addEventListener("abort", onAbort, { once: true });
});
}
/**
* Resolve with the first attempt that settles with success; reject
* with the most recent error iff every attempt fails. The controller
* is aborted as soon as a winner appears so losing attempts can
* short-circuit their transport call.
*/
async function firstSettled(
attempts: readonly Promise<AttemptResult>[],
controller: AbortController
): Promise<Uint8Array> {
// Callers are responsible for passing a non-empty array; #race
// guards on `topK.length > 0` before invocation, and the early
// `CHUNK_UNAVAILABLE` throw in `pull` covers the empty-candidates
// case.
let remaining = attempts.length;
let settled = false;
return await new Promise<Uint8Array>((resolve, reject) => {
let lastErr: unknown;
for (const attempt of attempts) {
// Attach a catch-all sibling so late rejections (after a
// winner was picked) cannot surface as unhandled-rejection
// diagnostics in the test runner.
attempt.catch(() => undefined);
attempt.then(
(result) => {
if (settled) return;
settled = true;
controller.abort();
resolve(result.bytes);
},
(err) => {
lastErr = err;
remaining -= 1;
if (settled) return;
if (remaining === 0) {
settled = true;
// #tryPeer always rejects with a DriveMeshError,
// so lastErr is always an Error subclass here.
if (
lastErr instanceof DriveMeshError &&
lastErr.code === DRIVE_MESH_CODES.SIG_VERIFY_FAILED
) {
reject(lastErr);
return;
}
reject(lastErr as Error);
}
}
);
}
});
}

View File

@@ -0,0 +1,15 @@
/**
* Zod boundary for advertisement payloads.
*/
import z from "zod";
export const AdvertisementSchema = z
.object({
fileId: z
.string()
.regex(/^[0-9a-f]{64}$/, "fileId must be 64 lowercase hex chars"),
bitset: z.instanceof(Uint8Array),
totalChunks: z.number().int().positive(),
})
.strict();

View File

@@ -0,0 +1,121 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { AdvertisementSchema } from "./advertisement.schemas.js";
import {
DriveMeshOptionsSchema,
ServerFallbackConfigSchema,
} from "./config.schemas.js";
describe("DriveMeshOptionsSchema", () => {
it("accepts a minimal valid options object", { timeout: 5000 }, () => {
const out = DriveMeshOptionsSchema.safeParse({
chunks: {},
transports: [{}],
serverPubKey: new Uint8Array(32),
});
expect(out.success).toBe(true);
});
it("rejects an empty transports array", { timeout: 5000 }, () => {
const out = DriveMeshOptionsSchema.safeParse({
chunks: {},
transports: [],
serverPubKey: new Uint8Array(32),
});
expect(out.success).toBe(false);
});
it("rejects a wrong-length serverPubKey", { timeout: 5000 }, () => {
const out = DriveMeshOptionsSchema.safeParse({
chunks: {},
transports: [{}],
serverPubKey: new Uint8Array(16),
});
expect(out.success).toBe(false);
});
it("rejects unknown keys (strict)", { timeout: 5000 }, () => {
const out = DriveMeshOptionsSchema.safeParse({
chunks: {},
transports: [{}],
serverPubKey: new Uint8Array(32),
extra: "nope",
});
expect(out.success).toBe(false);
});
it("accepts optional fields", { timeout: 5000 }, () => {
const out = DriveMeshOptionsSchema.safeParse({
chunks: {},
transports: [{}],
serverPubKey: new Uint8Array(32),
serverFallback: { url: "https://x", transport: "https" },
maxParallelRequests: 4,
racingWindowMs: 100,
denylistMode: "persistent",
});
expect(out.success).toBe(true);
});
it("rejects denylistMode not in enum", { timeout: 5000 }, () => {
const out = DriveMeshOptionsSchema.safeParse({
chunks: {},
transports: [{}],
serverPubKey: new Uint8Array(32),
denylistMode: "forever",
});
expect(out.success).toBe(false);
});
});
describe("ServerFallbackConfigSchema", () => {
it("requires non-empty url and transport", { timeout: 5000 }, () => {
expect(
ServerFallbackConfigSchema.safeParse({ url: "", transport: "x" })
.success
).toBe(false);
expect(
ServerFallbackConfigSchema.safeParse({
url: "https://x",
transport: "",
}).success
).toBe(false);
expect(
ServerFallbackConfigSchema.safeParse({
url: "https://x",
transport: "https",
}).success
).toBe(true);
});
});
describe("AdvertisementSchema", () => {
it("accepts valid input", { timeout: 5000 }, () => {
expect(
AdvertisementSchema.safeParse({
fileId: "a".repeat(64),
bitset: new Uint8Array(2),
totalChunks: 8,
}).success
).toBe(true);
});
it("rejects malformed fileId", { timeout: 5000 }, () => {
expect(
AdvertisementSchema.safeParse({
fileId: "Z".repeat(64),
bitset: new Uint8Array(2),
totalChunks: 8,
}).success
).toBe(false);
});
it("rejects zero totalChunks", { timeout: 5000 }, () => {
expect(
AdvertisementSchema.safeParse({
fileId: "a".repeat(64),
bitset: new Uint8Array(2),
totalChunks: 0,
}).success
).toBe(false);
});
});

View File

@@ -0,0 +1,38 @@
/**
* Zod boundary for `DriveMeshOptions`.
*
* The schema is strict: unknown keys are rejected so misconfigurations
* are caught before `new DriveMesh(...)` constructs internal state.
*/
import z from "zod";
const ServerFallbackConfigSchema = z
.object({
url: z.string().min(1, "serverFallback.url must be non-empty"),
transport: z
.string()
.min(1, "serverFallback.transport must be non-empty"),
})
.strict();
export const DriveMeshOptionsSchema = z
.object({
chunks: z.unknown(),
transports: z.array(z.unknown()).min(1, "at least one transport"),
serverPubKey: z
.instanceof(Uint8Array)
.refine(
(b) => b.byteLength === 32,
"serverPubKey must be 32 bytes"
),
serverFallback: ServerFallbackConfigSchema.optional(),
maxParallelRequests: z.number().int().positive().optional(),
racingWindowMs: z.number().int().nonnegative().optional(),
denylistMode: z.enum(["session", "persistent"]).optional(),
clock: z.unknown().optional(),
fetchFn: z.unknown().optional(),
})
.strict();
export { ServerFallbackConfigSchema };

View File

@@ -0,0 +1,5 @@
export { AdvertisementSchema } from "./advertisement.schemas.js";
export {
DriveMeshOptionsSchema,
ServerFallbackConfigSchema,
} from "./config.schemas.js";

View File

@@ -0,0 +1,5 @@
export {
type FetchFn,
ServerFallback,
type ServerFallbackOptions,
} from "./server-fallback.js";

View File

@@ -0,0 +1,105 @@
# `server-fallback.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ServerFallback` is the chunk-fetch path drive-mesh uses when every
peer attempt in `ParallelPuller` has failed. The server is the root of
trust, so a fallback request must still produce a chunk that passes
`verifyChunkSig` against the configured `serverPubKey`. The fallback
does not bypass trust — it bypasses peers.
The fallback URL is composed as `<baseUrl>/<fileId>/<chunkId>` and
fetched via the injectable `fetchFn`. Failures are retried with
exponential backoff and equal jitter so a wave of mesh nodes
simultaneously falling back does not stampede the origin.
## Features
- Plain `GET <baseUrl>/<fileId>/<chunkId>` request shape.
- Equal-jitter exponential backoff
(`exp/2 + rand(0, exp/2)`, clamped `[base, max]`).
- Mandatory signature verification against `serverPubKey`.
- AbortSignal threaded through `fetch` and through delay waits.
- Configurable retry count (`maxAttempts`, default 3).
## Usage: Basic Example
```ts
import { ServerFallback } from "@suaveplan/drive-mesh/server-fallback/server-fallback";
const fallback = new ServerFallback({
baseUrl: "https://cdn.example.com/chunks",
serverPubKey,
});
const bytes = await fallback.fetchChunk(fileId, 0);
```
## Usage: Advanced Example
```ts
import { ServerFallback } from "@suaveplan/drive-mesh/server-fallback/server-fallback";
const fallback = new ServerFallback({
baseUrl: "https://cdn.example.com/chunks",
serverPubKey,
fetchFn: customFetch,
maxAttempts: 5,
backoffBaseMs: 100,
backoffMaxMs: 3_000,
clock: fakeClock,
random: deterministicRng,
});
const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
try {
const bytes = await fallback.fetchChunk(fileId, chunkId, controller.signal);
} catch (err) {
if (err instanceof DriveMeshError) {
if (err.code === DRIVE_MESH_CODES.FALLBACK_FAILED) {
// every retry exhausted; surface to the caller
}
}
}
```
## API Reference
- `class ServerFallback`:
- `constructor(options: ServerFallbackOptions)`.
- `fetchChunk(fileId, chunkId, signal?): Promise<Uint8Array>`.
- `now(): number` — current clock reading.
- `ServerFallbackOptions`:
- `baseUrl` (required) — trailing slashes stripped.
- `serverPubKey` (required) — ed25519 32-byte public key.
- `fetchFn?` — defaults to `globalThis.fetch`; throws
`FALLBACK_FAILED` lazily if missing.
- `maxAttempts?` (default `3`).
- `backoffBaseMs?` (default `200`).
- `backoffMaxMs?` (default `5_000`).
- `clock?` — defaults to `Date.now`-based clock.
- `random?` — `() => number` in `[0, 1)`; default `Math.random`.
## Implementation Notes
The path shape (`<base>/<fileId>/<chunkId>`) is intentionally simple
rather than templated — drive-mesh consumers usually front the
fallback URL with a CDN that knows the layout. Customers who want
shaped URLs (e.g., reverse-id sharding per `drive-template`) can
wrap `fetchFn` with their own URL builder.
`verifyChunkSig` is called on every successful response before
returning bytes. A signature failure in the fallback path is a
configuration error, not a transient failure — but for symmetry with
the peer path the error still surfaces as `FALLBACK_FAILED` rather
than `SIG_VERIFY_FAILED`, so that the caller's denylist counter
remains a "peer was Byzantine" counter rather than "server was
Byzantine".
`defaultFetch` returns a lazy `globalThis.fetch` bridge so the
package remains universal: a Node ≥18 / Deno / browser runtime
ships `fetch` natively, and consumers that build to older targets
inject `fetchFn` explicitly.

View File

@@ -0,0 +1,272 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
buildChunkBytes,
makeKeypair,
TEST_FILE_ID_HEX,
} from "../../../tests/helpers/signed-chunk.js";
import {
DRIVE_MESH_CODES,
DriveMeshError,
} from "../../error-codes/error-codes.js";
import { ServerFallback } from "./server-fallback.js";
function arrayBuffer(bytes: Uint8Array): ArrayBuffer {
return bytes.buffer.slice(
bytes.byteOffset,
bytes.byteOffset + bytes.byteLength
) as ArrayBuffer;
}
describe("ServerFallback", () => {
it("fetches and verifies on success", { timeout: 5000 }, async () => {
const kp = makeKeypair(11);
const bytes = buildChunkBytes({
serverPrivKey: kp.privKey,
fileIdHex: TEST_FILE_ID_HEX,
chunkId: 5,
});
const fallback = new ServerFallback({
baseUrl: "https://cdn.example.com/chunks/",
serverPubKey: kp.pubKey,
fetchFn: async () =>
new Response(arrayBuffer(bytes), { status: 200 }),
});
const out = await fallback.fetchChunk(TEST_FILE_ID_HEX, 5);
expect(out).toEqual(bytes);
});
it("rejects pre-aborted signals", { timeout: 5000 }, async () => {
const kp = makeKeypair(11);
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: kp.pubKey,
fetchFn: async () => new Response(new Uint8Array(0)),
});
const ac = new AbortController();
ac.abort();
await expect(
fallback.fetchChunk(TEST_FILE_ID_HEX, 0, ac.signal)
).rejects.toMatchObject({ code: DRIVE_MESH_CODES.ABORTED });
});
it("retries on non-2xx responses then surfaces FALLBACK_FAILED", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(11);
let calls = 0;
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: kp.pubKey,
fetchFn: async () => {
calls += 1;
return new Response("nope", { status: 500 });
},
maxAttempts: 2,
backoffBaseMs: 1,
backoffMaxMs: 2,
random: () => 0.5,
});
await expect(
fallback.fetchChunk(TEST_FILE_ID_HEX, 0)
).rejects.toMatchObject({ code: DRIVE_MESH_CODES.FALLBACK_FAILED });
expect(calls).toBe(2);
});
it("recovers after a transient failure", { timeout: 5000 }, async () => {
const kp = makeKeypair(11);
const bytes = buildChunkBytes({
serverPrivKey: kp.privKey,
fileIdHex: TEST_FILE_ID_HEX,
chunkId: 0,
});
let calls = 0;
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: kp.pubKey,
fetchFn: async () => {
calls += 1;
if (calls === 1) throw new Error("network");
return new Response(arrayBuffer(bytes), { status: 200 });
},
backoffBaseMs: 1,
random: () => 0,
});
const out = await fallback.fetchChunk(TEST_FILE_ID_HEX, 0);
expect(out).toEqual(bytes);
expect(calls).toBe(2);
});
it("surfaces ABORTED when signal fires mid-wait", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(11);
const ac = new AbortController();
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: kp.pubKey,
fetchFn: async () => {
throw new Error("transient");
},
backoffBaseMs: 50,
backoffMaxMs: 50,
random: () => 1,
});
const promise = fallback.fetchChunk(TEST_FILE_ID_HEX, 0, ac.signal);
setTimeout(() => ac.abort(), 5);
await expect(promise).rejects.toBeInstanceOf(DriveMeshError);
});
it("re-raises ABORTED with non-Error throw", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(23);
const ac = new AbortController();
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: kp.pubKey,
fetchFn: async () => {
ac.abort();
throw "non-error-string";
},
maxAttempts: 1,
});
await expect(
fallback.fetchChunk(TEST_FILE_ID_HEX, 0, ac.signal)
).rejects.toMatchObject({ code: DRIVE_MESH_CODES.ABORTED });
});
it("exhausts attempts when last error is non-Error", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(24);
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: kp.pubKey,
fetchFn: async () => {
throw "string-rejection";
},
maxAttempts: 1,
});
await expect(
fallback.fetchChunk(TEST_FILE_ID_HEX, 0)
).rejects.toMatchObject({ code: DRIVE_MESH_CODES.FALLBACK_FAILED });
});
it("re-raises ABORTED when signal aborts while fetch throws", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(22);
const ac = new AbortController();
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: kp.pubKey,
fetchFn: async () => {
ac.abort();
throw new Error("aborted-during-fetch");
},
maxAttempts: 1,
});
await expect(
fallback.fetchChunk(TEST_FILE_ID_HEX, 0, ac.signal)
).rejects.toMatchObject({ code: DRIVE_MESH_CODES.ABORTED });
});
it("now() reflects clock", { timeout: 5000 }, () => {
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: new Uint8Array(32),
clock: () => 42,
fetchFn: async () => new Response(new Uint8Array(0)),
});
expect(fallback.now()).toBe(42);
});
it("now() falls back to Date.now when no clock provided", {
timeout: 5000,
}, () => {
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: new Uint8Array(32),
fetchFn: async () => new Response(new Uint8Array(0)),
});
const before = Date.now();
const reading = fallback.now();
const after = Date.now();
expect(reading).toBeGreaterThanOrEqual(before);
expect(reading).toBeLessThanOrEqual(after);
});
it("default fetch routes through globalThis.fetch when available", {
timeout: 5000,
}, async () => {
const kp = makeKeypair(30);
const bytes = buildChunkBytes({
serverPrivKey: kp.privKey,
fileIdHex: TEST_FILE_ID_HEX,
chunkId: 0,
});
const originalFetch = (globalThis as { fetch?: typeof fetch }).fetch;
let captured: string | URL | null = null;
(globalThis as { fetch?: unknown }).fetch = async (
input: string | URL
) => {
captured = input;
return new Response(arrayBuffer(bytes), { status: 200 });
};
try {
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: kp.pubKey,
maxAttempts: 1,
});
const out = await fallback.fetchChunk(TEST_FILE_ID_HEX, 0);
expect(out).toEqual(bytes);
expect(captured).not.toBeNull();
} finally {
(globalThis as { fetch?: unknown }).fetch = originalFetch;
}
});
it("default fetch throws FALLBACK_FAILED when globalThis.fetch is missing", {
timeout: 5000,
}, async () => {
const originalFetch = (globalThis as { fetch?: unknown }).fetch;
delete (globalThis as { fetch?: unknown }).fetch;
try {
const fallback = new ServerFallback({
baseUrl: "https://x",
serverPubKey: new Uint8Array(32),
maxAttempts: 1,
});
await expect(
fallback.fetchChunk(TEST_FILE_ID_HEX, 0)
).rejects.toMatchObject({
code: DRIVE_MESH_CODES.FALLBACK_FAILED,
});
} finally {
(globalThis as { fetch?: unknown }).fetch = originalFetch;
}
});
it("strips trailing slashes from baseUrl", { timeout: 5000 }, async () => {
const kp = makeKeypair(11);
const bytes = buildChunkBytes({
serverPrivKey: kp.privKey,
fileIdHex: TEST_FILE_ID_HEX,
chunkId: 1,
});
let seenUrl = "";
const fallback = new ServerFallback({
baseUrl: "https://x.example.com/base///",
serverPubKey: kp.pubKey,
fetchFn: async (url) => {
seenUrl = url.toString();
return new Response(arrayBuffer(bytes), { status: 200 });
},
});
await fallback.fetchChunk(TEST_FILE_ID_HEX, 1);
expect(seenUrl).toBe(
`https://x.example.com/base/${TEST_FILE_ID_HEX}/1`
);
});
});

View File

@@ -0,0 +1,162 @@
/**
* `ServerFallback` — last-resort chunk fetch via the configured
* fallback URL.
*
* The fallback URL is appended with `/<fileId>/<chunkId>` to form the
* concrete request. The response body is read in full and handed to
* `verifyChunkSig` against the same `serverPubKey` peers are validated
* against — the server is the root of trust, so the same gate applies.
*
* Retries use exponential backoff with a small jitter so concurrent
* fallback bursts from multiple consumers do not stampede the origin.
*/
import { verifyChunkSig } from "@suaveplan/drive-plugin-chunks";
import type { Clock, FetchLike } from "@suaveplan/types";
import {
DRIVE_MESH_CODES,
DriveMeshError,
} from "../../error-codes/error-codes.js";
/**
* A `fetch`-shaped function. Backwards-compatibility alias for `FetchLike`
* from `@suaveplan/types` — the canonical workspace contract. Existing
* consumers that import `FetchFn` from this package keep working unchanged.
*/
export type FetchFn = FetchLike;
export interface ServerFallbackOptions {
readonly baseUrl: string;
readonly serverPubKey: Uint8Array;
readonly fetchFn?: FetchFn;
/** Default 3. */
readonly maxAttempts?: number;
/** Base delay between retries. Default 200 ms. */
readonly backoffBaseMs?: number;
/** Maximum delay between retries. Default 5_000 ms. */
readonly backoffMaxMs?: number;
readonly clock?: Clock;
readonly random?: () => number;
}
export class ServerFallback {
readonly #baseUrl: string;
readonly #serverPubKey: Uint8Array;
readonly #fetchFn: FetchFn;
readonly #maxAttempts: number;
readonly #backoffBaseMs: number;
readonly #backoffMaxMs: number;
readonly #clock: Clock;
readonly #random: () => number;
public constructor(options: ServerFallbackOptions) {
this.#baseUrl = options.baseUrl.replace(/\/+$/, "");
this.#serverPubKey = options.serverPubKey;
this.#fetchFn = options.fetchFn ?? defaultFetch();
this.#maxAttempts = options.maxAttempts ?? 3;
this.#backoffBaseMs = options.backoffBaseMs ?? 200;
this.#backoffMaxMs = options.backoffMaxMs ?? 5_000;
this.#clock = options.clock ?? (() => Date.now());
this.#random = options.random ?? Math.random;
}
/**
* Fetch the chunk, verify the signature, and return the bytes.
* Throws `DRIVE_MESH.FALLBACK_FAILED` on transport / verification
* failure after every retry.
*/
public async fetchChunk(
fileId: string,
chunkId: number,
signal?: AbortSignal
): Promise<Uint8Array> {
if (signal?.aborted) {
throw new DriveMeshError(
DRIVE_MESH_CODES.ABORTED,
"fallback aborted before start"
);
}
const url = `${this.#baseUrl}/${fileId}/${chunkId}`;
let lastErr: unknown;
for (let attempt = 0; attempt < this.#maxAttempts; attempt++) {
if (attempt > 0) {
await this.#wait(attempt, signal);
}
try {
const init: RequestInit = signal ? { signal } : {};
const response = await this.#fetchFn(url, init);
if (!response.ok) {
lastErr = new DriveMeshError(
DRIVE_MESH_CODES.FALLBACK_FAILED,
`fallback http ${response.status}`
);
continue;
}
const buf = new Uint8Array(await response.arrayBuffer());
verifyChunkSig(buf, this.#serverPubKey);
return buf;
} catch (err) {
if (signal?.aborted) {
throw new DriveMeshError(
DRIVE_MESH_CODES.ABORTED,
"fallback aborted",
err instanceof Error ? { cause: err } : undefined
);
}
lastErr = err;
}
}
throw new DriveMeshError(
DRIVE_MESH_CODES.FALLBACK_FAILED,
`fallback exhausted after ${this.#maxAttempts} attempts`,
lastErr instanceof Error ? { cause: lastErr } : undefined
);
}
/** Last-recorded clock reading — surfaced so callers can build elapsed-ms metrics. */
public now(): number {
return this.#clock();
}
async #wait(
attempt: number,
signal: AbortSignal | undefined
): Promise<void> {
const exp = Math.min(
this.#backoffMaxMs,
this.#backoffBaseMs * 2 ** (attempt - 1)
);
const half = exp / 2;
const delayMs = Math.floor(half + this.#random() * half);
await new Promise<void>((resolve, reject) => {
const onAbort = (): void => {
clearTimeout(handle);
reject(
new DriveMeshError(
DRIVE_MESH_CODES.ABORTED,
"fallback delay aborted"
)
);
};
const handle = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, delayMs);
signal?.addEventListener("abort", onAbort, { once: true });
});
}
}
function defaultFetch(): FetchFn {
const fn = (globalThis as { fetch?: FetchFn }).fetch;
if (fn !== undefined) {
return (input, init) => fn(input, init);
}
return () =>
Promise.reject(
new DriveMeshError(
DRIVE_MESH_CODES.FALLBACK_FAILED,
"no fetch implementation available; pass options.fetchFn"
)
);
}

View File

@@ -0,0 +1,86 @@
# `config.types.ts`
## Purpose
Defines the `DriveMeshOptions` shape consumed by `new DriveMesh(...)`.
The interface is the TypeScript-side contract; the matching Zod
boundary lives in `./schemas/config.schemas.ts` and is the runtime
validator that rejects malformed input before the mesh's internal
state is constructed.
Splitting the two halves keeps the type surface dependency-free at
compile time and the runtime validation centralised at the package
boundary.
## Features
- Single shape covering scheduler tuning, denylist policy, server
fallback, transports, and the `ChunkSource` handle.
- Every tuning parameter is optional with a documented default.
- `serverPubKey` is mandatory — trust roots are not optional.
- Injectable `clock` and `fetchFn` for deterministic testing.
## Usage: Basic Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
const mesh = new DriveMesh({
chunks,
transports: [httpsTransport],
serverPubKey,
});
```
## Usage: Advanced Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
const mesh = new DriveMesh({
chunks,
transports: [webrtcTransport, httpsTransport],
serverPubKey,
serverFallback: { url: "https://cdn.example.com", transport: "https" },
maxParallelRequests: 16,
racingWindowMs: 150,
denylistMode: "persistent",
clock: fakeClock,
fetchFn: testFetch,
});
await mesh.start();
const chunkBytes = await mesh.requestChunk(fileIdHex, 0);
```
## API Reference
- `DenylistMode = "session" | "persistent"` — controls how long
bad-sig peers stay denylisted. `session` clears on `mesh.stop()`;
`persistent` keeps the denylist across stops.
- `ServerFallbackConfig`:
- `url: string` — absolute URL the fallback path hits.
- `transport: string` — `DriveTransport.id` carrying the fallback.
- `DriveMeshOptions`:
- `chunks: ChunkSource` — handle returned by `chunksPlugin`.
- `transports: readonly DriveTransport[]` — injectable transports.
- `serverPubKey: Uint8Array` — server's ed25519 public key.
- `serverFallback?: ServerFallbackConfig` — optional fallback.
- `maxParallelRequests?: number` — default `8`.
- `racingWindowMs?: number` — default `250`.
- `denylistMode?: DenylistMode` — default `"session"`.
- `clock?: Clock` — defaults to `Date.now`-based clock.
- `fetchFn?: typeof fetch` — defaults to `globalThis.fetch`.
## Implementation Notes
`fetchFn` is typed loosely as `(input, init?) => Promise<Response>`
rather than reusing `typeof fetch`, because `typeof fetch` carries a
DOM-specific overload set that fights the universal package
target. The looser shape still binds `globalThis.fetch` correctly at
runtime.
`serverPubKey` is required — there is no implicit default. Drive-mesh
treats peers as untrusted caches, so a configuration with no root of
trust would silently disable signature verification, which is a footgun
the type system prevents.

View File

@@ -0,0 +1,51 @@
/**
* Public configuration shape for `DriveMesh`.
*
* Aligned with the `drive-mesh-port` OpenSpec proposal but expressed
* as a `readonly` interface rather than a Zod schema — the runtime
* Zod boundary lives in `./schemas/config.schemas.ts`.
*/
import type { ChunkSource } from "@suaveplan/drive-plugin-chunks";
import type { Clock, FetchLike } from "@suaveplan/types";
import type { DriveTransport } from "./drive-transport.types.js";
export type DenylistMode = "session" | "persistent";
export interface ServerFallbackConfig {
/** Absolute URL the server fallback path hits. */
readonly url: string;
/**
* Transport id (matches `DriveTransport.id`) responsible for
* carrying the fallback request. Typically the HTTPS transport.
*/
readonly transport: string;
}
export interface DriveMeshOptions {
/** ChunkSource handle from `chunksPlugin`. */
readonly chunks: ChunkSource;
/** Injectable transports (HTTPS, WebRTC, libp2p, in-memory…). */
readonly transports: readonly DriveTransport[];
/** Server's ed25519 public key — the root of trust. */
readonly serverPubKey: Uint8Array;
/** Optional server fallback. */
readonly serverFallback?: ServerFallbackConfig;
/** Global cap on in-flight chunk requests. Default `8`. */
readonly maxParallelRequests?: number;
/**
* First-byte racing window in milliseconds. After this delay,
* additional candidate peers receive parallel requests. Default
* `250`.
*/
readonly racingWindowMs?: number;
/** Bad-sig denylist scope. Default `"session"`. */
readonly denylistMode?: DenylistMode;
/** Injectable clock — defaults to `Date.now`. */
readonly clock?: Clock;
/**
* Optional fetch implementation for the server fallback path.
* Defaults to `globalThis.fetch`. Tests inject a fake.
*/
readonly fetchFn?: FetchLike;
}

View File

@@ -0,0 +1,118 @@
# `drive-transport.types.ts`
## Purpose
Defines the chunk-shaped `DriveTransport` contract every mesh transport
implementation conforms to. This is a structural ("ducked") interface so
the mesh can compose HTTPS, WebRTC, libp2p, or in-memory transports
without depending on their concrete classes. Drive-mesh consumes the
contract; transport packages implement it.
The contract is intentionally narrow: a transport answers chunk
requests, pushes advertisements, surfaces inbound advertisements via a
handler, and reports the live peer set. Everything else (handshake,
authentication, replay protection) belongs inside the transport and
never crosses this boundary.
## Features
- Structural interface — no inheritance required.
- Async `requestChunk` returns the full chunk bytes (locked v0x0001
format) so the mesh can hand them directly to
`verifyChunkSig` and `parseChunk`.
- `advertise` is `peerId`-targeted — broadcasts are open-coded by the
gossip engine via repeated unicast.
- `onAdvertisement` returns an unsubscribe; multiple subscribers are
allowed.
- Optional `dispose()` honours `DriveMesh.stop()` lifecycle.
## Usage: Basic Example
```ts
import type { DriveTransport } from "@suaveplan/drive-mesh/types";
const transport: DriveTransport = {
id: "https",
async requestChunk(peerId, fileId, chunkId) {
return await myHttpsClient.get(peerId, fileId, chunkId);
},
async advertise(peerId, advert) {
await myHttpsClient.push(peerId, advert);
},
onAdvertisement(handler) {
const unsub = myHttpsClient.subscribe(handler);
return unsub;
},
peers() {
return myHttpsClient.connectedPeers();
},
};
```
## Usage: Advanced Example
```ts
import type { DriveTransport } from "@suaveplan/drive-mesh/types";
import { DriveMesh } from "@suaveplan/drive-mesh";
class WebRtcTransport implements DriveTransport {
public readonly id = "webrtc";
readonly #handlers = new Set<AdvertisementHandler>();
public async requestChunk(peerId, fileId, chunkId, signal) {
const chan = this.channelFor(peerId);
const reply = await chan.request({ fileId, chunkId }, { signal });
return reply.bytes;
}
public async advertise(peerId, advert) {
this.channelFor(peerId).send({ kind: "advert", ...advert });
}
public onAdvertisement(handler) {
this.#handlers.add(handler);
return () => this.#handlers.delete(handler);
}
public peers() {
return this.connectedChannels.keys();
}
public async dispose() {
for (const chan of this.connectedChannels.values()) chan.close();
}
}
const mesh = new DriveMesh({
chunks,
transports: [new WebRtcTransport(), httpsTransport],
serverPubKey,
});
```
## API Reference
- `ChunkAdvertisement { fileId: string; bitset: Uint8Array; totalChunks: number }`
— payload pushed in `advertise`; bitset is framed via
`encodeFramedBitset` from `@suaveplan/drive-mesh-protocol`.
- `AdvertisementHandler = (peerId, advert) => void` — callback invoked
for every inbound advertisement.
- `Unsubscribe = () => void` — handle returned by `onAdvertisement`.
- `DriveTransport`:
- `id: string` — stable identifier used by `serverFallback.transport`.
- `requestChunk(peerId, fileId, chunkId, signal?): Promise<Uint8Array>` —
fetch chunk bytes from one peer.
- `advertise(peerId, advert): Promise<void>` — unicast availability
push.
- `onAdvertisement(handler): Unsubscribe` — subscribe to inbound
advertisements.
- `peers(): Iterable<PeerId>` — live peer snapshot.
- `dispose?(): Promise<void> | void` — optional teardown.
## Implementation Notes
The contract is deliberately bytes-in / bytes-out: the mesh never
inspects payload metadata before signature verification, which keeps
trust boundaries explicit. Signature verification is performed by the
mesh against the configured `serverPubKey` before the chunk is handed
to the chunks plugin.
`peers()` returns an `Iterable<PeerId>` (not an array) so transports
can stream live views without materialising arrays on every gossip
tick. Implementations are free to return a `Set` or `Map.keys()`.

View File

@@ -0,0 +1,71 @@
/**
* Structural contract every drive-mesh peer-to-peer transport
* implementation conforms to.
*
* This is intentionally NOT `TransportLike<MessageEnvelope>` from
* `@suaveplan/types/transport` — the mesh transport surface is
* chunk-shaped (`requestChunk` returns bytes, `advertise` pushes a
* bitset). The HTTPS chunk-fetch path is NOT a `DriveTransport`
* implementation; it is served by the built-in `ServerFallback`
* module (see `./server-fallback/server-fallback`).
*
* Currently the only implementation is
* `@suaveplan/drive-transport-webrtc`.
*/
import type { PeerId } from "@suaveplan/types";
export interface ChunkAdvertisement {
/** Hex-encoded 32-byte file identifier. */
readonly fileId: string;
/** Bitset whose bit `i` is `1` iff the peer has chunk `i`. */
readonly bitset: Uint8Array;
/** Total chunks the file resolves to. */
readonly totalChunks: number;
}
export type AdvertisementHandler = (
peerId: PeerId,
advert: ChunkAdvertisement
) => void;
export type Unsubscribe = () => void;
export interface DriveTransport {
/**
* Stable identifier for this transport — referenced by
* `serverFallback.transport` to pick the right wire for the
* server fallback path.
*/
readonly id: string;
/**
* Request a specific chunk from `peerId`. Implementations MAY
* stream a multi-frame response; the mesh consumes the full
* concatenation as the chunk bytes. Resolves with the chunk's
* locked v0x0001 bytes (metadata ‖ sig ‖ ciphertext).
*/
requestChunk(
peerId: PeerId,
fileId: string,
chunkId: number,
signal?: AbortSignal
): Promise<Uint8Array>;
/**
* Push an availability advertisement to `peerId`. The bitset is
* already framed via `encodeFramedBitset` from
* `@suaveplan/drive-mesh-protocol` so the receiver can decode it
* without negotiation.
*/
advertise(peerId: PeerId, advert: ChunkAdvertisement): Promise<void>;
/** Subscribe to inbound advertisements. */
onAdvertisement(handler: AdvertisementHandler): Unsubscribe;
/** Snapshot of currently-connected peers on this transport. */
peers(): Iterable<PeerId>;
/** Optional disposal hook called by `DriveMesh.stop()`. */
dispose?(): Promise<void> | void;
}

View File

@@ -0,0 +1,105 @@
# `events.types.ts`
## Purpose
Defines the tuple-shaped event map emitted by `DriveMesh`. The map
follows the same `Record<string, unknown[]>` convention as
`@suaveplan/drive-webrtc`'s `TypedEventEmitter`, so call sites read
`mesh.emit("chunk:served", payload)` rather than wrapping arguments in
an object literal on every emission.
Events are organised in three families: chunk lifecycle (`chunk:*`),
peer lifecycle (`peer:*`), and gossip telemetry (`gossip:*`). Each
event payload is fully typed so downstream subscribers get
compile-time validation of the property names they touch.
## Features
- Strict typing — `mesh.on("chunk:served", e => …)` infers
`ChunkServedEvent`.
- Stable shape: every event carries the contextual identifiers
(`fileId`, `chunkId`, `peerId` where applicable) needed to correlate
it with the request that triggered it.
- Latency observability via `elapsedMs` on chunk events.
## Usage: Basic Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
mesh.on("chunk:served", ({ fileId, chunkId, peerId, elapsedMs }) => {
console.log("hit", fileId, chunkId, peerId, elapsedMs);
});
mesh.on("chunk:fallback", ({ fileId, chunkId }) => {
console.log("fallback", fileId, chunkId);
});
```
## Usage: Advanced Example
```ts
import { DriveMesh } from "@suaveplan/drive-mesh";
import type { DriveMeshEventMap } from "@suaveplan/drive-mesh/types";
function wire(mesh: DriveMesh, sink: TelemetrySink) {
const map: { [K in keyof DriveMeshEventMap]: () => void } = {
"chunk:requested": mesh.on("chunk:requested", e =>
sink.counter("drive.mesh.chunk_requested_total", 1, e)
),
"chunk:served": mesh.on("chunk:served", e => {
sink.counter("drive.mesh.chunk_served_total", 1, { outcome: "peer" });
sink.histogram("drive.mesh.chunk_fetch_ms", e.elapsedMs, e);
}),
"chunk:fallback": mesh.on("chunk:fallback", e =>
sink.counter("drive.mesh.chunk_served_total", 1, { outcome: "fallback" })
),
"chunk:failed": mesh.on("chunk:failed", e =>
sink.counter("drive.mesh.chunk_served_total", 1, { outcome: "fail" })
),
"peer:joined": mesh.on("peer:joined", () =>
sink.counter("drive.mesh.peers_admitted_total", 1)
),
"peer:left": mesh.on("peer:left", () => {}),
"peer:denied": mesh.on("peer:denied", e =>
sink.counter("drive.mesh.peers_denied_total", 1, { reason: e.reason })
),
"peer:scored": mesh.on("peer:scored", () => {}),
"gossip:sent": mesh.on("gossip:sent", e =>
sink.histogram("drive.mesh.gossip_size_bytes", e.sizeBytes)
),
"gossip:received": mesh.on("gossip:received", () => {}),
};
return () => Object.values(map).forEach(off => off());
}
```
## API Reference
- `DriveMeshEventMap` — `Record<EventName, [PayloadType]>` consumed by
`TypedEventEmitter`.
- Chunk lifecycle:
- `ChunkRequestedEvent { fileId; chunkId; peerId }`.
- `ChunkServedEvent { fileId; chunkId; peerId; bytes; elapsedMs }`.
- `ChunkFallbackEvent { fileId; chunkId; bytes; elapsedMs }`.
- `ChunkFailedEvent { fileId; chunkId; reason }`.
- Peer lifecycle:
- `PeerJoinedEvent { peerId; transports }`.
- `PeerLeftEvent { peerId }`.
- `PeerDeniedEvent { peerId; reason }`.
- `PeerScoredEvent { peerId; score; rttMs }`.
- Gossip telemetry:
- `GossipSentEvent { peerId; fileId; sizeBytes }`.
- `GossipReceivedEvent { peerId; fileId; sizeBytes }`.
## Implementation Notes
Every payload is a single object rather than a positional tuple of
primitives. This is intentional: object payloads survive future
schema growth without changing the call signature at every subscriber
site.
`elapsedMs` is measured against the injectable `clock` so tests
running under a `FakeClock` get deterministic latency numbers.
`bytes` is always the size of the verified ciphertext, not the wire
payload, so consumers can compare against the file size in the
manifest without double-counting headers.

View File

@@ -0,0 +1,80 @@
/**
* Tuple-shaped event map for `DriveMesh`. Mirrors the convention used
* by `@suaveplan/drive-webrtc` so `mesh.emit("name", payload)` reads
* naturally at call sites.
*/
import type { PeerId } from "@suaveplan/types";
import type { PeerDenialReason } from "./peer.types.js";
export interface ChunkRequestedEvent {
readonly fileId: string;
readonly chunkId: number;
readonly peerId: PeerId;
}
export interface ChunkServedEvent {
readonly fileId: string;
readonly chunkId: number;
readonly peerId: PeerId;
readonly bytes: number;
readonly elapsedMs: number;
}
export interface ChunkFallbackEvent {
readonly fileId: string;
readonly chunkId: number;
readonly bytes: number;
readonly elapsedMs: number;
}
export interface ChunkFailedEvent {
readonly fileId: string;
readonly chunkId: number;
readonly reason: string;
}
export interface PeerJoinedEvent {
readonly peerId: PeerId;
readonly transports: readonly string[];
}
export interface PeerLeftEvent {
readonly peerId: PeerId;
}
export interface PeerDeniedEvent {
readonly peerId: PeerId;
readonly reason: PeerDenialReason;
}
export interface PeerScoredEvent {
readonly peerId: PeerId;
readonly score: number;
readonly rttMs: number | null;
}
export interface GossipSentEvent {
readonly peerId: PeerId;
readonly fileId: string;
readonly sizeBytes: number;
}
export interface GossipReceivedEvent {
readonly peerId: PeerId;
readonly fileId: string;
readonly sizeBytes: number;
}
export type DriveMeshEventMap = {
"chunk:requested": [ChunkRequestedEvent];
"chunk:served": [ChunkServedEvent];
"chunk:fallback": [ChunkFallbackEvent];
"chunk:failed": [ChunkFailedEvent];
"peer:joined": [PeerJoinedEvent];
"peer:left": [PeerLeftEvent];
"peer:denied": [PeerDeniedEvent];
"peer:scored": [PeerScoredEvent];
"gossip:sent": [GossipSentEvent];
"gossip:received": [GossipReceivedEvent];
};

View File

@@ -0,0 +1,25 @@
export type {
DenylistMode,
DriveMeshOptions,
ServerFallbackConfig,
} from "./config.types.js";
export type {
AdvertisementHandler,
ChunkAdvertisement,
DriveTransport,
Unsubscribe,
} from "./drive-transport.types.js";
export type {
ChunkFailedEvent,
ChunkFallbackEvent,
ChunkRequestedEvent,
ChunkServedEvent,
DriveMeshEventMap,
GossipReceivedEvent,
GossipSentEvent,
PeerDeniedEvent,
PeerJoinedEvent,
PeerLeftEvent,
PeerScoredEvent,
} from "./events.types.js";
export type { PeerDenialReason, PeerInfo, PeerUpdate } from "./peer.types.js";

View File

@@ -0,0 +1,108 @@
# `peer.types.ts`
## Purpose
Defines the `PeerInfo` overlay tracked by the drive-mesh peer registry.
Drive-mesh does not own peer identity — that comes from
`network-plugin-registry` via the `NetworkBus` (see
`drive-network-integration` spec). What drive-mesh adds on top is
file-specific state: which chunks of which files a peer claims to have,
and a composite score derived from observed latency / success / error
rates.
The `PeerInfo` shape is intentionally read-only at every nested level
so consumers can hand snapshots to telemetry / dashboards without
worrying about external mutation.
## Features
- File-keyed availability bitsets (one per `fileId`).
- EMA latency tracker surfaced as `rttMs`.
- Composite `score: number`; `-Infinity` is the denylist sentinel.
- Multi-transport tracking — same `peerId` may be reachable via
multiple wires.
## Usage: Basic Example
```ts
import type { PeerInfo } from "@suaveplan/drive-mesh/types";
function logTop(peers: Iterable<PeerInfo>) {
const ranked = [...peers].sort((a, b) => b.score - a.score);
for (const peer of ranked.slice(0, 5)) {
console.log(peer.peerId, peer.score, peer.rttMs);
}
}
```
## Usage: Advanced Example
```ts
import type { PeerInfo, PeerDenialReason } from "@suaveplan/drive-mesh/types";
import { DRIVE_MESH_CODES, DriveMeshError } from "@suaveplan/drive-mesh/errors";
function ensureReachable(peer: PeerInfo): void {
if (peer.score === Number.NEGATIVE_INFINITY) {
throw new DriveMeshError(
DRIVE_MESH_CODES.PEER_DENIED,
`${peer.peerId} denylisted`
);
}
if (peer.transports.length === 0) {
throw new DriveMeshError(
DRIVE_MESH_CODES.TRANSPORT_FAILED,
`${peer.peerId} has no reachable transport`
);
}
}
function fileCoverage(peer: PeerInfo, fileId: string): number {
const bits = peer.availability.get(fileId);
if (bits === undefined) return 0;
let count = 0;
for (const byte of bits) count += popcount(byte);
return count;
}
function popcount(byte: number): number {
let n = byte & 0xff;
let c = 0;
while (n) {
c += n & 1;
n >>>= 1;
}
return c;
}
```
## API Reference
- `PeerDenialReason = "bad_sig" | "manual"` — taxonomy surfaced on
denylist counters.
- `PeerInfo`:
- `peerId: PeerId` — wire identity (hex pubkey).
- `transports: readonly string[]` — transport ids the peer is
reachable on; empty iff every transport has dropped it.
- `rttMs: number | null` — EMA-smoothed RTT in ms; `null` until the
first measurement.
- `lastSeen: number` — unix ms of the most recent observation.
- `score: number` — composite score; `-Infinity` is the denylist
sentinel.
- `availability: ReadonlyMap<string, Uint8Array>` — `fileId` →
framed bitset of held chunks.
- `PeerUpdate { transports?; rttMs?; lastSeen? }` — partial mutation
accepted by `PeerRegistry.addOrUpdate`.
## Implementation Notes
`score` is computed by the peer registry from observations rather than
stored mutably from the outside — the registry is the authority. The
field is exposed read-only on `PeerInfo` so consumers (scheduler,
gossip engine, telemetry) can read a stable snapshot. Mutations happen
inside the registry's `record*` methods.
`availability` is keyed by hex `fileId` rather than raw `Uint8Array`
because `Map`'s equality is reference-based — comparing two byte
arrays with the same content would always miss. The framed bitset is
the value rather than a parsed `BitSet` so the gossip engine can
forward bytes without re-encoding.

View File

@@ -0,0 +1,44 @@
/**
* Public type shapes for the peer-registry overlay.
*
* Identity events come from `network-plugin-registry` via the
* `NetworkBus`; drive-mesh maintains the file-specific availability
* overlay on top.
*/
import type { PeerId } from "@suaveplan/types";
/** Reason a peer is on the denylist. */
export type PeerDenialReason = "bad_sig" | "manual";
export interface PeerInfo {
/** Wire identity of the peer. */
readonly peerId: PeerId;
/**
* Transports through which the peer is reachable. Empty when the
* peer has been forgotten by every transport — the registry retains
* it briefly for late-arriving advertisements.
*/
readonly transports: readonly string[];
/** Last EMA-smoothed round-trip time in milliseconds. */
readonly rttMs: number | null;
/** Unix milliseconds the peer was last observed. */
readonly lastSeen: number;
/**
* Composite score combining latency, success rate, and error rate.
* Higher is better. `-Infinity` indicates denylisted.
*/
readonly score: number;
/**
* File-keyed availability bitsets. Keys are hex `fileId`. Values
* are framed bitsets (output of `encodeFramedBitset`).
*/
readonly availability: ReadonlyMap<string, Uint8Array>;
}
/** Partial update accepted by `PeerRegistry.addOrUpdate`. */
export interface PeerUpdate {
readonly transports?: readonly string[];
readonly rttMs?: number;
readonly lastSeen?: number;
}

View File

@@ -0,0 +1,143 @@
/**
* In-memory `DriveTransport` for tests.
*
* Modelled on the FakeWebSocketBroker pattern from drive-webrtc: a
* single `Broker` object knows about every transport instance, so
* an `advertise` call on transport A reaches every subscriber on
* transport B that is registered for the same broker.
*
* `requestChunk` resolves with the bytes the broker has stored under
* `(peerId, fileId, chunkId)`. Tests stage bytes via `broker.serve(...)`.
*/
import type { PeerId } from "@suaveplan/types";
import type {
AdvertisementHandler,
ChunkAdvertisement,
DriveTransport,
Unsubscribe,
} from "../../src/types/index.js";
export interface ChunkBytesSource {
/** Bytes to return, OR a thrown error / promise rejection. */
readonly bytes?: Uint8Array;
readonly throwReason?: string;
/** Optional delay in ms before resolving. */
readonly delayMs?: number;
}
export class Broker {
public readonly transports = new Set<FakeDriveTransport>();
public readonly chunkStore = new Map<string, ChunkBytesSource>();
public serve(
peerId: PeerId,
fileId: string,
chunkId: number,
source: ChunkBytesSource
): void {
this.chunkStore.set(`${peerId}:${fileId}:${chunkId}`, source);
}
public deliverAdvertisement(
senderId: PeerId,
advert: ChunkAdvertisement
): void {
for (const transport of this.transports) {
for (const handler of transport.handlers) {
if (transport.localPeerId === senderId) continue;
handler(senderId, advert);
}
}
}
}
export class FakeDriveTransport implements DriveTransport {
public readonly id: string;
public readonly localPeerId: PeerId;
public readonly handlers = new Set<AdvertisementHandler>();
public readonly knownPeers = new Set<PeerId>();
readonly #broker: Broker;
public constructor(args: {
readonly broker: Broker;
readonly localPeerId: PeerId;
readonly id?: string;
}) {
this.id = args.id ?? "fake";
this.localPeerId = args.localPeerId;
this.#broker = args.broker;
this.#broker.transports.add(this);
}
public addKnownPeer(peerId: PeerId): void {
this.knownPeers.add(peerId);
}
public async requestChunk(
peerId: PeerId,
fileId: string,
chunkId: number,
signal?: AbortSignal
): Promise<Uint8Array> {
const key = `${peerId}:${fileId}:${chunkId}`;
const source = this.#broker.chunkStore.get(key);
if (source === undefined) {
throw new Error(`no chunk staged at ${key}`);
}
if (source.delayMs !== undefined && source.delayMs > 0) {
await new Promise<void>((resolve, reject) => {
const handle = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, source.delayMs);
const onAbort = (): void => {
clearTimeout(handle);
reject(new Error("aborted"));
};
if (signal?.aborted) {
onAbort();
} else {
signal?.addEventListener("abort", onAbort, { once: true });
}
});
}
if (source.throwReason !== undefined) {
throw new Error(source.throwReason);
}
if (source.bytes === undefined) {
throw new Error(`no bytes staged at ${key}`);
}
return source.bytes;
}
public async advertise(
peerId: PeerId,
advert: ChunkAdvertisement
): Promise<void> {
// Deliver to every other transport's subscribers via the broker.
for (const transport of this.#broker.transports) {
if (transport === this) continue;
if (transport.localPeerId !== peerId) continue;
for (const handler of transport.handlers) {
handler(this.localPeerId, advert);
}
}
}
public onAdvertisement(handler: AdvertisementHandler): Unsubscribe {
this.handlers.add(handler);
return () => {
this.handlers.delete(handler);
};
}
public peers(): Iterable<PeerId> {
return this.knownPeers;
}
public dispose(): void {
this.#broker.transports.delete(this);
this.handlers.clear();
}
}

View File

@@ -0,0 +1,73 @@
/**
* Test helper: build a real, signature-valid chunk byte string.
*
* Uses `@suaveplan/drive-plugin-chunks`' `buildSignedChunk` over a
* synthetic but well-formed `Chunk` so `verifyChunkSig` accepts the
* result. Fixtures stay deterministic — no random bytes anywhere.
*/
import {
getPublicKey,
randomPrivateKey,
} from "@suaveplan/crypto-curve/ed25519";
import { buildSignedChunk } from "@suaveplan/drive-plugin-chunks";
export interface Keypair {
readonly privKey: Uint8Array;
readonly pubKey: Uint8Array;
}
export function makeKeypair(seed = 1): Keypair {
// Deterministic seed via a 32-byte buffer filled with `seed`. Noble
// does not strictly require uniformly-random keys for verifiability,
// only that the corresponding public key is derived from the same
// private key.
const priv =
seed === 0 ? randomPrivateKey() : new Uint8Array(32).fill(seed & 0xff);
const pub = getPublicKey(priv);
return { privKey: priv, pubKey: pub };
}
export function fileIdBytes(hex: string): Uint8Array {
if (hex.length !== 64) {
throw new Error(`fileId hex must be 64 chars, got ${hex.length}`);
}
const out = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
const byteHex = hex.slice(i * 2, i * 2 + 2);
out[i] = Number.parseInt(byteHex, 16);
}
return out;
}
export function buildChunkBytes(args: {
readonly serverPrivKey: Uint8Array;
readonly fileIdHex: string;
readonly chunkId: number;
readonly totalChunks?: number;
readonly payload?: Uint8Array;
}): Uint8Array {
const totalChunks = args.totalChunks ?? 8;
const payload =
args.payload ?? new Uint8Array([args.chunkId & 0xff, 0x42, 0x13, 0x37]);
const chunkSize = Math.max(payload.byteLength, 4);
const chunk = {
metadata: {
version: 0x0001,
fileId: fileIdBytes(args.fileIdHex),
chunkId: args.chunkId,
totalChunks,
fileSize: BigInt(chunkSize * totalChunks),
chunkSize,
payloadLen: payload.byteLength,
flags: 0,
nonce: new Uint8Array(24),
aeadTag: new Uint8Array(16),
},
sig: new Uint8Array(64), // overwritten by buildSignedChunk
ciphertext: payload,
};
return buildSignedChunk(chunk, args.serverPrivKey);
}
export const TEST_FILE_ID_HEX = "a".repeat(64);

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022", "DOM"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,41 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/drive-mesh",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
"@opentelemetry/api",
"@suaveplan/crypto-curve",
"@suaveplan/drive-mesh-protocol",
"@suaveplan/drive-plugin-chunks",
"@suaveplan/error",
"@suaveplan/events",
"@suaveplan/types",
"tslib",
"zod",
/^node:/,
],
},
},
});

View File

@@ -0,0 +1,11 @@
import { createTestConfig } from "@suaveplan/testing/config";
export default createTestConfig({
testTimeout: 30_000,
coverageThresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
});