first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
# @suaveplan/network-plugin-content-delivery - Mesh-as-CDN core: ChunkIndex (gossip-propagated holder map), ChunkRequestRouter (latency-aware routing with server fallback), RedundancyManager (N-copy enforcement + re-replication on peer churn), IntegrityVerifier (hash-agnostic verification + sender blacklisting), and ContentDeliveryPlugin facade. Drive-store, packer, prefetch, and bandwidth-throttle integrations live in follow-up changes.
## Summary
Mesh-as-CDN core for the Suaveplan network. `ChunkIndex` maintains a distributed holder map keyed by chunk hash with gossip-style merge over `ChunkAdvertisement` records (peer + timestamp ordering); `ChunkRequestRouter` resolves a chunk request to the lowest-latency holder via injected `LatencyOracle`, falling back to the configured server when no mesh peer holds the chunk; `RedundancyManager` enforces the per-chunk `redundancyTarget` (configurable preset map for `critical` / `shared` / `user-content` / `telemetry` classes), detects peer departures via the bus, and emits `ReplicationTask` orders with deterministic replacement-holder selection; `IntegrityVerifier` runs an injected `HashFn` (SHA-256 by default; BLAKE3 plug-compatible) over received chunk bytes, blacklists senders that ship corrupted chunks for a TTL window, and exposes a clear-text trust map for downstream observability. Wrapped in a single `ContentDeliveryPlugin` satisfying the `NetworkPlugin` lifecycle.
<!-- begin:auto-docs -->
## Documentation tree
- [index](./src/index/chunk-index.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [chunk-index](./src/index/chunk-index.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [integrity](./src/integrity/integrity-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [integrity-verifier](./src/integrity/integrity-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [packer](./src/packer/packer-integration.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [packer-integration](./src/packer/packer-integration.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [plugin](./src/plugin/content-delivery-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [content-delivery-plugin](./src/plugin/content-delivery-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [prefetch](./src/prefetch/prefetch-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [prefetch-engine](./src/prefetch/prefetch-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [redundancy](./src/redundancy/redundancy-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [redundancy-manager](./src/redundancy/redundancy-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [router](./src/router/chunk-request-router.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [chunk-request-router](./src/router/chunk-request-router.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [store](./src/store/chunk-store.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [chunk-store](./src/store/chunk-store.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [throttle](./src/throttle/bandwidth-throttler.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bandwidth-throttler](./src/throttle/bandwidth-throttler.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/error-codes.md) — Frozen `NET_CD` error registry for `@suaveplan/network-plugin-content-delivery`. Every failure path the content-deliver…
<!-- end:auto-docs --> - [chunk-index](./src/index/chunk-index.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity](./src/integrity/integrity-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity-verifier](./src/integrity/integrity-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [packer](./src/packer/packer-integration.md) — <!-- begin:auto-docs --> _(no submodules)_- [packer-integration](./src/packer/packer-integration.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/content-delivery-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [content-delivery-plugin](./src/plugin/content-delivery-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [prefetch](./src/prefetch/prefetch-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [prefetch-engine](./src/prefetch/prefetch-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [redundancy](./src/redundancy/redundancy-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [redundancy-manager](./src/redundancy/redundancy-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/chunk-request-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [chunk-request-router](./src/router/chunk-request-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [store](./src/store/chunk-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [chunk-store](./src/store/chunk-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttle](./src/throttle/bandwidth-throttler.md) — <!-- begin:auto-docs --> _(no submodules)_- [bandwidth-throttler](./src/throttle/bandwidth-throttler.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen `NET_CD` error registry for `@suaveplan/network-plugin-content-delivery`. Every failure path the content-deliver…
- [chunk-index](./src/index/chunk-index.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity](./src/integrity/integrity-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity-verifier](./src/integrity/integrity-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [packer](./src/packer/packer-integration.md) — <!-- begin:auto-docs --> _(no submodules)_- [packer-integration](./src/packer/packer-integration.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/content-delivery-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [content-delivery-plugin](./src/plugin/content-delivery-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [prefetch](./src/prefetch/prefetch-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [prefetch-engine](./src/prefetch/prefetch-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [redundancy](./src/redundancy/redundancy-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [redundancy-manager](./src/redundancy/redundancy-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/chunk-request-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [chunk-request-router](./src/router/chunk-request-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [store](./src/store/chunk-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [chunk-store](./src/store/chunk-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttle](./src/throttle/bandwidth-throttler.md) — <!-- begin:auto-docs --> _(no submodules)_- [bandwidth-throttler](./src/throttle/bandwidth-throttler.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen `NET_CD` error registry for `@suaveplan/network-plugin-content-delivery`. Every failure path the content-deliver…
- [chunk-index](./src/index/chunk-index.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity](./src/integrity/integrity-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity-verifier](./src/integrity/integrity-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [packer](./src/packer/packer-integration.md) — <!-- begin:auto-docs --> _(no submodules)_- [packer-integration](./src/packer/packer-integration.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/content-delivery-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [content-delivery-plugin](./src/plugin/content-delivery-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [prefetch](./src/prefetch/prefetch-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [prefetch-engine](./src/prefetch/prefetch-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [redundancy](./src/redundancy/redundancy-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [redundancy-manager](./src/redundancy/redundancy-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/chunk-request-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [chunk-request-router](./src/router/chunk-request-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [store](./src/store/chunk-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [chunk-store](./src/store/chunk-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttle](./src/throttle/bandwidth-throttler.md) — <!-- begin:auto-docs --> _(no submodules)_- [bandwidth-throttler](./src/throttle/bandwidth-throttler.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen `NET_CD` error registry for `@suaveplan/network-plugin-content-delivery`. Every failure path the content-deliver…
- [chunk-index](./src/index/chunk-index.md) — <!-- begin:auto-docs --> _(no submodules)_- [integrity](./src/integrity/integrity-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_ - [integrity-verifier](./src/integrity/integrity-verifier.md) — <!-- begin:auto-docs --> _(no submodules)_- [packer](./src/packer/packer-integration.md) — <!-- begin:auto-docs --> _(no submodules)_ - [packer-integration](./src/packer/packer-integration.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/content-delivery-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [content-delivery-plugin](./src/plugin/content-delivery-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [prefetch](./src/prefetch/prefetch-engine.md) — <!-- begin:auto-docs --> _(no submodules)_ - [prefetch-engine](./src/prefetch/prefetch-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [redundancy](./src/redundancy/redundancy-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ - [redundancy-manager](./src/redundancy/redundancy-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [router](./src/router/chunk-request-router.md) — <!-- begin:auto-docs --> _(no submodules)_ - [chunk-request-router](./src/router/chunk-request-router.md) — <!-- begin:auto-docs --> _(no submodules)_- [store](./src/store/chunk-store.md) — <!-- begin:auto-docs --> _(no submodules)_ - [chunk-store](./src/store/chunk-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [throttle](./src/throttle/bandwidth-throttler.md) — <!-- begin:auto-docs --> _(no submodules)_ - [bandwidth-throttler](./src/throttle/bandwidth-throttler.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Frozen `NET_CD` error registry for `@suaveplan/network-plugin-content-delivery`. Every failure path the content-deliver…
## Modules
- `index/chunk-index` — distributed holder map.
- `router/chunk-request-router` — latency-aware request routing.
- `redundancy/redundancy-manager` — N-copy enforcement + re-replication.
- `integrity/integrity-verifier` — hash verification + sender blacklisting.
- `packer/packer-integration` — adapts `@suaveplan/packer` output into `ChunkManifest` and broadcasts `cd:manifest`.
- `store/chunk-store` — local chunk persistence over a pluggable `ChunkStoreAdapter` (`@suaveplan/drive` in production) with TTL + LRU eviction; emits `cd:chunk-stored` / `cd:chunk-evicted`.
- `prefetch/prefetch-engine` — proactive fetch path with pluggable `PredictionModel` and session-scoped fan-out; emits `cd:prefetch-issued`.
- `throttle/bandwidth-throttler` — per-peer + global token-bucket throttling tagging every admitted dispatch with `priority: "bulk"`; emits `cd:throttled`.
- `plugin/content-delivery-plugin` — `NetworkPlugin` facade wiring every subsystem.

View File

@@ -0,0 +1,53 @@
# error-codes
## Purpose
Frozen `NET_CD` error registry for `@suaveplan/network-plugin-content-delivery`. Every failure path the content-delivery subsystem can surface — a request for an unknown chunk, no holder available across the mesh, an integrity hash mismatch, an attempt to receive bytes from a blacklisted peer, an invalid redundancy target, a missing index entry, or a router with no fallback configured — is mapped onto one of these stable codes.
The registry is locked at module load time via `defineCodes("NET_CD", [...] as const)` so consumers downstream (logger redaction maps, i18n catalogs, the OpenSpec conformance checker) can rely on the set never silently growing without a code change.
## Features
- Seven codes covering the chunk-discovery path (`CHUNK_NOT_FOUND`, `INDEX_ENTRY_MISSING`), routing failures (`NO_HOLDER_AVAILABLE`, `ROUTER_NO_FALLBACK`), integrity gates (`INTEGRITY_HASH_MISMATCH`, `SENDER_BLACKLISTED`), and redundancy configuration validation (`REDUNDANCY_TARGET_INVALID`).
- Sibling `NetworkCdError` extends `SuaveplanError<NetworkCdErrorCode>` so consumers can write `instanceof NetworkCdError` and access `error.code` with full type narrowing.
- Codes can be referenced via `error.code` matching as well as carried as outcome discriminants on report-style result types — both count as "used" for the conformance check.
## Usage: Basic Example
```ts
import {
NETWORK_CD_CODES,
NetworkCdError,
} from "@suaveplan/network-plugin-content-delivery";
throw new NetworkCdError(
NETWORK_CD_CODES.NO_HOLDER_AVAILABLE,
"cd.no_holder",
{ context: { chunkHash: "aaa" } }
);
```
## Usage: Advanced Example
```ts
import {
NetworkCdError,
type NetworkCdErrorCode,
} from "@suaveplan/network-plugin-content-delivery";
function classify(err: unknown): NetworkCdErrorCode | null {
return err instanceof NetworkCdError ? err.code : null;
}
```
## API Reference
- `NETWORK_CD_CODES` — frozen object with seven keys: `CHUNK_NOT_FOUND`, `NO_HOLDER_AVAILABLE`, `INTEGRITY_HASH_MISMATCH`, `SENDER_BLACKLISTED`, `REDUNDANCY_TARGET_INVALID`, `INDEX_ENTRY_MISSING`, `ROUTER_NO_FALLBACK`. Values are `"NET_CD.<KEY>"` strings.
- `NetworkCdErrorCode` — string-literal union of the registry values.
- `NetworkCdError` — error class extending `SuaveplanError<NetworkCdErrorCode>`. Constructor: `(code, messageKey, options?)`.
## Implementation Notes
- Per `AGENTS.md §2c`, every code in the registry must be referenced from `src/` somewhere — typically as the argument to `new NetworkCdError(...)` (thrown classes) but `INTEGRITY_HASH_MISMATCH` and `SENDER_BLACKLISTED` may be carried on `IntegrityVerifier` outcome unions when the verifier reports rather than throws. Either form satisfies the gate.
- The list grows only by adding to the array literal; there is no programmatic `register()` API by design — the `as const` plus `defineCodes` arrangement is the contract.
- Code messages use dotted i18n keys (e.g. `cd.no_holder`) so the logger's i18n adapter can resolve them at format time without coupling this package to a specific locale catalog.

View File

@@ -0,0 +1,72 @@
# ChunkIndex
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Distributed-by-design index of which peers hold which chunks. Each peer keeps a local copy of the index that it updates by ingesting `ChunkAdvertisement` messages either gossiped over the bus (`cd:advertise`) or piggy-backed on chunk responses. The index is the data structure both `ChunkRequestRouter` and `RedundancyManager` consult to make decisions — the router asks "who has chunk X?" and the redundancy manager asks "is chunk X under-replicated?".
The index uses a per-holder timestamp ledger to merge advertisements deterministically: a later advertisement from the same holder replaces an earlier one (so a peer that re-broadcasts after a restart wins), but an out-of-order advertisement that arrives late is dropped (so the index converges regardless of message ordering).
## Features
- Last-writer-wins gossip merge keyed on `(chunkHash, holderId)` with the holder's `advertisedAt` timestamp as the merge clock.
- `ingestAdvertisement` returns a boolean indicating whether the index was updated, so the caller can avoid re-gossiping no-op advertisements.
- `removeHolder(chunkHash, holderId)` for graceful chunk-level eviction; `removePeer(holderId)` for whole-peer departures (returns the affected chunk hashes so the redundancy manager can fan out replication tasks).
- `setRedundancyTarget(chunkHash, target)` lets a higher-level policy bump or relax replication for individual chunks (e.g. promoting a "shared" chunk to "critical" once it goes mainline).
- Optional `bus` re-broadcasts `cd:advertise` on every accepted advertisement, so observability adapters and downstream subsystems hear about the index change without polling.
- `defaultRedundancyTarget` enforced at construction time (`>= 1`) and at `setRedundancyTarget` call time (`>= 1`); both raise `NET_CD.REDUNDANCY_TARGET_INVALID`.
## Usage: Basic Example
```ts
import { ChunkIndex } from "@suaveplan/network-plugin-content-delivery";
const index = new ChunkIndex({ defaultRedundancyTarget: 3 });
index.ingestAdvertisement({
chunkHash: "aaa",
fileId: "f-1",
chunkIndex: 0,
holderId: "peer-1",
advertisedAt: Date.now(),
});
console.log(index.listHolders("aaa")); // ["peer-1"]
```
## Usage: Advanced Example
```ts
import { ChunkIndex } from "@suaveplan/network-plugin-content-delivery";
const index = new ChunkIndex({
defaultRedundancyTarget: 3,
clock: () => 1_700_000_000_000,
bus: { emit: (topic, payload) => observability.publish(topic, payload) },
});
index.ingestAdvertisement(advA);
index.ingestAdvertisement(advB);
index.setRedundancyTarget("aaa", 5);
const affected = index.removePeer("peer-2");
for (const hash of affected) redundancyManager.evaluate(hash);
```
## API Reference
- `ChunkIndex(config)` — `config: { defaultRedundancyTarget: number; clock?: () => number; bus?: ChunkIndexBus }`. Throws `NET_CD.REDUNDANCY_TARGET_INVALID` if `defaultRedundancyTarget < 1`.
- `ingestAdvertisement(adv)` — returns `true` if the index was updated, `false` if the advertisement was dropped (older than the last-seen for the same holder).
- `removeHolder(chunkHash, holderId)` — returns `true` if the holder was removed.
- `removePeer(holderId)` — returns the list of chunk hashes the peer was holding.
- `setRedundancyTarget(chunkHash, target)` — raises `NET_CD.INDEX_ENTRY_MISSING` if the chunk has no entry; raises `NET_CD.REDUNDANCY_TARGET_INVALID` if `target < 1`.
- `getEntry(chunkHash)` — returns the `ChunkIndexEntry` or `undefined`.
- `requireEntry(chunkHash)` — same as `getEntry` but raises `NET_CD.INDEX_ENTRY_MISSING` instead of returning `undefined`.
- `listHolders(chunkHash)` — `readonly string[]`; empty if the chunk has no entry.
- `size()` — number of distinct chunk hashes in the index.
- `snapshot()` — defensive copy of every entry for serialization / debugging.
## Implementation Notes
- The `holderTimestamps` ledger is parallel to the `entries` map rather than embedded inside `ChunkIndexEntry`, because the entry shape is wire-stable and we don't want serializers to leak per-peer clocks. This keeps the wire-stable shape minimal at the cost of one extra lookup per ingest.
- `removePeer` iterates the entire entries map. In a 100k-chunk mesh this is still micro-second work; if it becomes hot we'll add a reverse `holderId → Set<chunkHash>` index.
- `setRedundancyTarget` is the only mutator that can raise — every other mutator silently coalesces invalid input. This matches the proposal's "best-effort" availability model: a stale or partially-formed advertisement should not break the index.

View File

@@ -0,0 +1,79 @@
# IntegrityVerifier
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Verifies that chunk bytes received from a peer match the expected hash, and blacklists peers that ship corrupted chunks for a TTL window. The verifier is hash-agnostic by design: callers inject a `HashFn` (typically `bytes => sha256(bytes)`, but the same interface accepts BLAKE3, SHA-3, or a hardware-backed verifier) so the package itself depends only on `@suaveplan/error` and `@suaveplan/types`.
The blacklist is the verifier's only piece of state. It expires automatically — a peer that ships one bad chunk gets a temporary cool-down rather than a permanent ban — which matches the proposal's "blacklists the sender temporarily" wording and lets the mesh self-heal across transient failures.
## Features
- Hash-agnostic verification via injectable `HashFn`; the verifier never imports a specific hashing library.
- Hex-encoding by default (matches the rest of the audit / drive chain hashing); callers can override `encodeHash` to use base64 / multibase / etc.
- TTL-based blacklist: a sender that ships a corrupted chunk is blacklisted for `blacklistTtlMs`; subsequent `verify` calls from the same sender raise `NET_CD.SENDER_BLACKLISTED` until the TTL expires.
- `verify(chunk)` returns a discriminated `VerifyOutcome` so non-throwing call sites can branch cleanly; `verifyOrThrow(chunk)` is the throwing variant for "I expect this to be valid" sites.
- `isBlacklisted(peerId)` lazily expires entries on read so the blacklist size stays bounded without a sweep timer.
- `blacklistedPeers()` is a snapshot helper for observability dashboards.
- `clearBlacklist()` for operator-driven recovery (e.g. after a deploy that shipped a bad hash).
- Optional `bus` emits `cd:integrity-violation` per failure for fan-out to alerting.
## Usage: Basic Example
```ts
import { sha256 } from "@suaveplan/crypto-hash";
import { IntegrityVerifier } from "@suaveplan/network-plugin-content-delivery";
const verifier = new IntegrityVerifier({
hash: (bytes) => new Uint8Array(sha256(bytes)),
blacklistTtlMs: 60_000,
});
const outcome = verifier.verify({
chunkHash: "expected-hex",
bytes: receivedBytes,
senderId: "peer-2",
});
if (!outcome.ok) console.warn(`bad chunk from ${outcome.senderId}`);
```
## Usage: Advanced Example
```ts
import { IntegrityVerifier } from "@suaveplan/network-plugin-content-delivery";
const verifier = new IntegrityVerifier({
hash: (bytes) => blake3(bytes),
encodeHash: (bytes) => base64.encode(bytes),
blacklistTtlMs: 5 * 60_000,
clock: () => 1_700_000_000_000,
bus: { emit: (topic, payload) => observability.publish(topic, payload) },
});
bus.on("cd:chunk-received", (chunk) => {
try {
verifier.verifyOrThrow(chunk);
chunkStore.put(chunk);
} catch (err) {
metrics.increment("cd.integrity.failures");
}
});
```
## API Reference
- `IntegrityVerifier(config)` — `config: { hash; encodeHash?; blacklistTtlMs; clock?; bus? }`. A negative `blacklistTtlMs` is clamped to `0` (effectively no blacklist).
- `verify(chunk)` — returns `VerifyOutcome` (`{ ok: true; chunkHash } | { ok: false; chunkHash; observedHash; senderId }`). Raises `NET_CD.SENDER_BLACKLISTED` when the sender is in the blacklist.
- `verifyOrThrow(chunk)` — throws `NET_CD.INTEGRITY_HASH_MISMATCH` on hash mismatch (sender is also blacklisted as a side effect); throws `NET_CD.SENDER_BLACKLISTED` if already blacklisted.
- `isBlacklisted(peerId)` — boolean; lazily expires the entry.
- `blacklistedPeers()` — snapshot of currently-blacklisted peer IDs.
- `clearBlacklist()` — drops every entry.
- `defaultEncode(bytes)` — exported lowercase-hex helper that the verifier uses by default.
## Implementation Notes
- The verifier deliberately couples the blacklist to verification (a bad chunk auto-blacklists the sender) rather than exposing a separate `blacklist(peerId)` mutator; this avoids the foot-gun of "we noticed something off but forgot to actually blacklist."
- `verify` and `verifyOrThrow` share the same code path — the throwing variant just unwraps the outcome — so semantics stay aligned.
- Comparing the encoded hash as a string (rather than as raw bytes) means the encoding scheme determines equality. Mixing two callers with different encoders against the same chunk-hash field will produce false negatives; document `encodeHash` once at the boundary and stick with it.

View File

@@ -0,0 +1,98 @@
# PackerIntegration
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`PackerIntegration` adapts the chunked output of `@suaveplan/packer` into the
wire-stable `ChunkManifest` consumed by the rest of the content-delivery
subsystem. The packer pipeline is owned by a separate package (so any future
change to compression, encryption, or signing flows independently of the
mesh-CDN), and that package's output shape is not locked to `ChunkManifest`.
This integration sits at the seam — it validates the packer payload, builds a
manifest with stable field ordering, and broadcasts it on the
`cd:manifest` bus topic so peers can warm their `ChunkIndex` ahead of the first
peer-to-peer advertisement.
## Features
- Structural `PackerOutputLike` input — no hard runtime dep on `@suaveplan/packer`.
- Defensive validation: empty `fileId`, empty `hashAlgorithm`, empty
`packerVersion`, empty `chunkHashes` array, and non-string entries in the
hash list all surface as `NETWORK_CD_CODES.PACKER_MANIFEST_INVALID` with
rich `context` rather than producing a corrupted manifest.
- `buildManifest()` is pure (return-only); `publish()` builds AND broadcasts on
the optional bus so non-observability consumers can drop the bus and skip
the side effect.
- Defensive copy of `chunkHashes` so the returned manifest is not aliased to
the caller's array.
- Injectable clock for deterministic test wiring even when the field itself is
not embedded today.
## Usage: Basic Example
```ts
import { PackerIntegration } from "@suaveplan/network-plugin-content-delivery";
const packer = new PackerIntegration();
const manifest = packer.buildManifest({
fileId: "f-1",
chunkHashes: ["h0", "h1", "h2"],
hashAlgorithm: "sha256",
packerVersion: "1.0.0",
});
// manifest.totalChunks === 3
```
## Usage: Advanced Example
Wired through a `ContentDeliveryBus` so peers receive the manifest on the wire
the moment the upload completes, and constructed with an injected clock for
deterministic replay in property tests:
```ts
import {
PackerIntegration,
type ChunkManifest,
} from "@suaveplan/network-plugin-content-delivery";
const seen: ChunkManifest[] = [];
const packer = new PackerIntegration({
bus: { emit: (_topic, payload) => seen.push(payload) },
clock: () => 0,
});
const manifest = packer.publish({
fileId: "release.tar",
chunkHashes: ["h0", "h1"],
hashAlgorithm: "blake3",
packerVersion: "2.0.0-rc.4",
});
// seen[0] === manifest, seen[0].hashAlgorithm === "blake3"
```
## API Reference
- `new PackerIntegration(config?: PackerIntegrationConfig)` — construct. All
fields optional.
- `buildManifest(output: PackerOutputLike): ChunkManifest` — validate +
convert. Throws `NetworkCdError(PACKER_MANIFEST_INVALID)` on any
field-level failure.
- `publish(output: PackerOutputLike): ChunkManifest` — `buildManifest()` then
`bus.emit("cd:manifest", manifest)` if a bus was supplied. Returns the same
manifest the bus saw so callers can confirm round-trip equality.
- `PackerOutputLike` — `{ fileId, chunkHashes, hashAlgorithm, packerVersion }`.
- `PackerBus` — `{ emit(topic: "cd:manifest", payload: ChunkManifest): void }`.
## Implementation Notes
- The integration keeps zero hard dep on `@suaveplan/packer`. The packer's
real `PackedDataMetadata` already carries the four fields this contract
requires; consumers either pass the metadata object directly or wrap it.
- Validation is total — any malformed field triggers a typed throw so an
upstream packer regression cannot silently poison the mesh-CDN holder map.
- `hashAlgorithm` is propagated verbatim. The mesh trusts the packer's choice
— the `IntegrityVerifier` is wired with the matching `HashFn` separately.
- The field-order of the returned `ChunkManifest` matches the type definition
(`fileId`, `totalChunks`, `chunkHashes`, `hashAlgorithm`, `packerVersion`)
so JSON-serialised manifests are stable for content-addressed caching.

View File

@@ -0,0 +1,80 @@
# ContentDeliveryPlugin
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Single `NetworkPlugin` facade that wires `ChunkIndex`, `ChunkRequestRouter`, `RedundancyManager`, and `IntegrityVerifier` into a cohesive subsystem and registers them with a `NetworkNode` via the standard plugin lifecycle (`install` / `start` / `stop` / `dispose`). Consumers register exactly one of these per host; everything else is reachable via the public properties on the plugin instance.
The plugin keeps its own state minimal: the lifecycle just remembers the node and flips a started flag. The interesting state — the chunk index, the blacklist, the redundancy classifications — lives in the composed subsystems and survives `start` / `stop` cycles. Only `dispose` clears state, by emptying the integrity blacklist (so a re-installed plugin starts with a fresh slate).
## Features
- One-shot construction wires every subsystem from a single config object, threading the local peer id and clock through to each.
- Public read-only access to `index`, `router`, `redundancy`, `integrity` so consumers can drop directly into any subsystem.
- The router is auto-wired with `isBlacklisted: (peerId) => integrity.isBlacklisted(peerId)` so a blacklisted peer is automatically excluded from holder ranking — no manual wiring at the call site.
- Optional `redundancy` config; defaults to the proposal's reference table (`critical: 5, shared: 3, user-content: 2, telemetry: 1`).
- `dispose` clears the integrity blacklist so a re-installed plugin doesn't carry a stale ban list.
## Usage: Basic Example
```ts
import { sha256 } from "@suaveplan/crypto-hash";
import { NetworkNode } from "@suaveplan/network";
import { ContentDeliveryPlugin } from "@suaveplan/network-plugin-content-delivery";
const node = new NetworkNode({ id: "peer-1" });
const cd = new ContentDeliveryPlugin({
localPeerId: "peer-1",
defaultRedundancyTarget: 3,
latency: webrtc.latencyForPeer,
hash: (bytes) => new Uint8Array(sha256(bytes)),
blacklistTtlMs: 60_000,
candidatePool: () => peerRoster.snapshot(),
});
await node.use(cd).start();
cd.index.ingestAdvertisement(adv);
const decision = cd.router.route("aaa");
```
## Usage: Advanced Example
```ts
import { ContentDeliveryPlugin } from "@suaveplan/network-plugin-content-delivery";
const cd = new ContentDeliveryPlugin({
localPeerId: "peer-1",
defaultRedundancyTarget: 3,
redundancy: {
defaultClass: "shared",
perClass: { critical: 7, shared: 4, "user-content": 2, telemetry: 1 },
},
latency: (peerId) => webrtc.latencyMs(peerId),
fallback: (hash) => server.fetchChunk(hash),
hash: (bytes) => blake3(bytes),
blacklistTtlMs: 5 * 60_000,
candidatePool: () => peerRoster.healthy(),
clock: () => 1_700_000_000_000,
bus: { emit: (topic, payload) => observability.publish(topic, payload) },
});
cd.redundancy.classify("aaa", "critical");
const tasks = cd.redundancy.onPeerDeparture("peer-3");
```
## API Reference
- `ContentDeliveryPlugin(config)` — `config: ContentDeliveryPluginConfig`. See type definition for the full surface.
- `name` — `"content-delivery"` (constant).
- Subsystem properties: `index`, `router`, `redundancy`, `integrity`.
- `install(node)` / `start()` / `stop()` / `dispose()` — `NetworkPlugin` lifecycle.
- `host()` — returns the installed `NetworkNode` or `null`.
- `isStarted()` — boolean lifecycle inspector.
## Implementation Notes
- The plugin uses spread-with-conditional-key (`...(config.x === undefined ? {} : { x: config.x })`) so that under `exactOptionalPropertyTypes` an `undefined` value never reaches a sub-component that expects an _absent_ key.
- The `bus` is forwarded to both the index and the integrity verifier; their event topics are different (`cd:advertise` vs `cd:integrity-violation`) so they don't conflict.
- `dispose` does NOT reset the chunk index — only the integrity blacklist. The chunk index is part of the durable mesh state and would be wrong to drop on a reload.

View File

@@ -0,0 +1,125 @@
# PrefetchEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`PrefetchEngine` is the proactive-fetch path for the mesh-CDN. Whenever a peer
opens or advances a session, the engine consults a pluggable
`PredictionModel` to ask "what chunks is this peer about to need?", filters the
predictions against the local store + known holders + the requester's session
scope, and broadcasts `cd:prefetch-issued` events targeting only peers that
share the requester's session. Without this layer, the mesh services every
chunk request reactively — every chunk waits for the requester's first miss.
With it, hot working-set chunks arrive ahead of demand and the perceived tail
latency of large file transfers collapses to the cost of a single ack
round-trip.
## Features
- Pluggable `PredictionModel` returning an ordered list of chunk hashes (sync
or async).
- Session-scoped fan-out via injected `SessionScopeProvider` — peers outside
the requester's session are never targeted.
- Self-skip: the requester is filtered out of the dispatch even when the
scope provider returns it.
- Local-store dedupe via injected `LocalChunkPredicate` — chunks the local
node already holds are skipped (no useless self-issued requests).
- Index-aware dedupe: chunks no in-scope peer holds yet are reported as
`skippedAlreadyHeld` (the wire request would just bounce).
- Hard `maxBatchSize` cap (default 16) with truncation count returned to the
caller.
- Per-dispatch summary: `{ issued, skippedLocal, skippedAlreadyHeld, truncated,
peersAvailable }` so callers can attach metrics/decision-trace logging.
- Defensive validation: a model that throws or returns a non-array surfaces
as `NETWORK_CD_CODES.PREFETCH_MODEL_FAILED` with the original error chained.
- Empty `cd:prefetch-issued` event volume when scope is empty — never burn
bus capacity on no-op dispatches.
## Usage: Basic Example
```ts
import {
ChunkIndex,
PrefetchEngine,
} from "@suaveplan/network-plugin-content-delivery";
const index = new ChunkIndex({ defaultRedundancyTarget: 3 });
const engine = new PrefetchEngine({
model: () => ["h0", "h1"],
sessionScope: () => ["peer-a", "peer-b"],
index,
});
await engine.dispatch({
sessionId: "s-1",
requesterId: "peer-self",
recentChunkHashes: [],
});
```
## Usage: Advanced Example
Async prediction model, local-store dedupe, custom batch cap, and observability
bus wiring:
```ts
import {
ChunkIndex,
PrefetchEngine,
type PredictionModel,
} from "@suaveplan/network-plugin-content-delivery";
const localCache = new Set<string>(["h0"]);
const model: PredictionModel = async (ctx) => {
return ctx.recentChunkHashes.flatMap((h) => [`${h}-next`, `${h}-after`]);
};
const issued: unknown[] = [];
const engine = new PrefetchEngine({
model,
sessionScope: async (sessionId) => {
// Look up scope from your session manager.
return sessionId === "s-1" ? ["peer-a", "peer-b"] : [];
},
index,
hasLocally: (h) => localCache.has(h),
bus: { emit: (_t, payload) => issued.push(payload) },
maxBatchSize: 8,
clock: () => 0,
});
const summary = await engine.dispatch({
sessionId: "s-1",
requesterId: "peer-self",
recentChunkHashes: ["h0", "h1"],
hint: { route: "warm" },
});
// summary.issued is the subset that was actually dispatched
```
## API Reference
- `new PrefetchEngine(config: PrefetchEngineConfig)` — `model`, `sessionScope`,
`index` are required. Other fields optional.
- `dispatch(context: PrefetchContext): Promise<PrefetchSummary>` — runs the
pipeline. Always returns; never throws on empty scope or empty predictions.
Throws `NetworkCdError(PREFETCH_MODEL_FAILED)` on model failure.
- `PrefetchContext` — `{ sessionId, requesterId, recentChunkHashes, hint? }`.
- `PredictionModel` — `(ctx) => readonly string[] | Promise<readonly string[]>`.
- `SessionScopeProvider` — `(sessionId) => readonly string[] | Promise<...>`.
- `LocalChunkPredicate` — `(chunkHash) => boolean`.
- `PrefetchSummary` — `{ issued, skippedLocal, skippedAlreadyHeld, truncated,
peersAvailable }`.
## Implementation Notes
- The engine never dispatches to the requester itself — `peersFiltered`
removes `context.requesterId` from the scope list before any fan-out.
- Dedupe is order-preserving: the first occurrence of a chunk hash in the
prediction list wins; subsequent duplicates are skipped silently.
- Empty/non-string entries in the prediction list are skipped silently rather
than triggering `PREFETCH_MODEL_FAILED` — the model is allowed to emit
defensive empty placeholders without trapping the engine.
- `truncated` is computed BEFORE dedupe / skip filtering so callers can tell
when the model is over-producing relative to `maxBatchSize`.
- `cd:prefetch-issued` carries `targetPeers` already filtered to in-scope
holders so the transport layer can fan out without re-querying the index.

View File

@@ -0,0 +1,81 @@
# RedundancyManager
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Enforces "every chunk lives on at least N peers" across the mesh. The manager classifies chunks (`critical` / `shared` / `user-content` / `telemetry`) per the proposal's table, watches the index for under-replication, and emits `ReplicationTask` orders that the consuming `drive-mesh` scheduler executes. The manager itself is policy-only: it never fetches bytes, never opens transports, and never picks the _order_ of fetches — it just decides _when_ and _to whom_ a copy should propagate.
The four-class taxonomy is closed by design: adding a new class is a code change, not a configuration change. This keeps the redundancy table auditable in source rather than in dynamic configuration that can drift.
## Features
- `DEFAULT_REDUNDANCY` exports the proposal's reference table (`critical: 5, shared: 3, user-content: 2, telemetry: 1`); consumers can pass it as-is or override per-class.
- `classify(chunkHash, klass)` records the classification AND propagates the implied `redundancyTarget` into the `ChunkIndex`, so the index stays the single source of truth for per-chunk targets.
- `evaluate(chunkHash)` answers "is this chunk under-replicated, and if so where should new copies go?" — returns `null` when the chunk is already at or above target.
- `onPeerDeparture(holderId)` is the single bus-listener entry point: ingests a peer-left event, removes the peer from the index, evaluates every affected chunk, and returns the bundle of replication tasks ready to dispatch.
- `requestManualReplication(chunkHash)` is an operator escape hatch (e.g. a forced re-shard of a stuck chunk) that produces the same `ReplicationTask` shape with `reason: "manual"`.
- `candidatePool` is an injected callback (typically wired to the live peer roster from `network-core`) so the manager picks new holders from the actually-connected mesh, not a stale snapshot.
- Construction-time validation: every `perClass` value must be `>= 1`, otherwise the constructor throws `NET_CD.REDUNDANCY_TARGET_INVALID`.
## Usage: Basic Example
```ts
import {
ChunkIndex,
DEFAULT_REDUNDANCY,
RedundancyManager,
} from "@suaveplan/network-plugin-content-delivery";
const index = new ChunkIndex({ defaultRedundancyTarget: 3 });
const manager = new RedundancyManager({
index,
redundancy: DEFAULT_REDUNDANCY,
candidatePool: () => peerRoster.snapshot(),
localPeerId: "peer-1",
});
manager.classify("aaa", "critical");
const task = manager.evaluate("aaa");
if (task !== null) driveMesh.dispatch(task);
```
## Usage: Advanced Example
```ts
import { RedundancyManager } from "@suaveplan/network-plugin-content-delivery";
const manager = new RedundancyManager({
index,
redundancy: {
defaultClass: "shared",
perClass: { critical: 7, shared: 4, "user-content": 2, telemetry: 1 },
},
candidatePool: () => peerRoster.healthy(),
localPeerId: "peer-1",
});
bus.on("peer:left", (peerId) => {
const tasks = manager.onPeerDeparture(peerId);
for (const task of tasks) driveMesh.dispatch(task);
});
const adminTask = manager.requestManualReplication("aaa");
```
## API Reference
- `RedundancyManager(config)` — `config: { index; redundancy; candidatePool; localPeerId }`. Throws `NET_CD.REDUNDANCY_TARGET_INVALID` if any `perClass` value is `< 1`.
- `classify(chunkHash, klass)` — records the class and sets the index's `redundancyTarget`.
- `targetFor(chunkHash)` — returns the target copies for the chunk's class (or the default class for unclassified chunks).
- `evaluate(chunkHash)` — returns a `ReplicationTask` (`reason: "below-target"`) or `null` if at target.
- `onPeerDeparture(holderId)` — removes the peer from the index AND evaluates every affected chunk; returns the bundle of replication tasks.
- `requestManualReplication(chunkHash)` — same as `evaluate` but with `reason: "manual"`.
- `DEFAULT_REDUNDANCY` — frozen `RedundancyConfig` with the proposal's reference table.
## Implementation Notes
- The exclude set inside `evaluateWithReason` excludes both the current holders AND the local peer; the local peer should not be picked as a "new" holder because it's the one running the manager. This avoids self-replication tasks that downstream schedulers would have to filter out.
- `recommendedCandidates` is sliced to exactly `need = target - currentHolders.length` so the consumer doesn't have to second-guess which candidates were the manager's first choice. If the candidate pool runs short, the slice truncates silently — the proposal calls this "best-effort" availability.
- The `chunkClass` map is in-memory only; persisting class assignments across restarts is the responsibility of the caller (typically by re-applying classifications from a manifest at start-up).

View File

@@ -0,0 +1,74 @@
# ChunkRequestRouter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Decides where to fetch a chunk from. Given a chunk hash, the router asks the `ChunkIndex` who holds it, ranks the holders by injected `LatencyOracle` (typically the per-peer latency table from `network-plugin-webrtc`), and returns a `RouteDecision` carrying the chosen peer. When no mesh peer holds the chunk, the router emits a `"server-fallback"` decision (or throws `NET_CD.NO_HOLDER_AVAILABLE` when no fallback is configured) so the caller can hand off to the central server.
The router is deliberately read-only with respect to the index — it does not register holders, evict peers, or trigger replication. Its sole job is to answer "who should I ask?" deterministically.
## Features
- Latency-aware ranking: sort holders by injected oracle, lowest latency first; ties break by `peerId` lex order so two routers seeing the same input always make the same call.
- Self-exclusion: the local peer is never returned as a holder even if the index has it (a router that routed back to itself would deadlock).
- Blacklist-aware: an injected `isBlacklisted(peerId)` predicate (typically wired to the `IntegrityVerifier`'s blacklist) filters bad senders out of the candidate set without mutating the index.
- Server fallback opt-in: pass `fallback: ServerFallbackFn` to enable; without it, the router throws `NET_CD.CHUNK_NOT_FOUND` when the index has no entry for the chunk at all, or `NET_CD.NO_HOLDER_AVAILABLE` when the entry exists but every holder is filtered out (self / blacklisted / latency-unknown). The two codes let callers tell "we've never heard of this chunk" apart from "we know it but can't reach anyone who has it."
- `fetchViaFallback(hash)` is the explicit fallback path, raising `NET_CD.ROUTER_NO_FALLBACK` if no fallback was configured at construction time so the caller can detect the misconfiguration loudly.
## Usage: Basic Example
```ts
import {
ChunkIndex,
ChunkRequestRouter,
} from "@suaveplan/network-plugin-content-delivery";
const index = new ChunkIndex({ defaultRedundancyTarget: 3 });
index.ingestAdvertisement({ chunkHash: "aaa", fileId: "f-1", chunkIndex: 0, holderId: "peer-2", advertisedAt: Date.now() });
index.ingestAdvertisement({ chunkHash: "aaa", fileId: "f-1", chunkIndex: 0, holderId: "peer-3", advertisedAt: Date.now() });
const router = new ChunkRequestRouter({
index,
latency: (p) => ({ "peer-2": 80, "peer-3": 20 } as Record<string, number>)[p],
localPeerId: "peer-1",
});
const decision = router.route("aaa");
console.log(decision.peerId, decision.latencyMs); // "peer-3", 20
```
## Usage: Advanced Example
```ts
import { ChunkRequestRouter } from "@suaveplan/network-plugin-content-delivery";
const router = new ChunkRequestRouter({
index,
latency: webrtc.latencyForPeer,
fallback: (hash) => server.fetchChunk(hash),
isBlacklisted: integrityVerifier.isBlacklisted,
localPeerId: "peer-1",
});
const decision = router.route(hash);
const bytes = decision.source === "server-fallback"
? await router.fetchViaFallback(hash)
: await transport.requestChunk(decision.peerId!, hash);
```
## API Reference
- `ChunkRequestRouter(config)` — `config: { index; latency; fallback?; localPeerId; isBlacklisted? }`.
- `route(chunkHash)` — returns `RouteDecision`. Raises `NET_CD.CHUNK_NOT_FOUND` when the index has no entry at all and no fallback is configured; raises `NET_CD.NO_HOLDER_AVAILABLE` when the entry exists but no holder is usable and no fallback is configured.
- `fetchViaFallback(chunkHash)` — returns `Promise<Uint8Array>`. Raises `NET_CD.ROUTER_NO_FALLBACK` when no fallback is configured.
- `rankedHolders(chunkHash)` — pure ranking helper for diagnostics; returns the ranked `{ peerId, latencyMs }` list (empty if the chunk has no entry or no usable holder).
- `LatencyOracle` — `(peerId) => number | undefined`; `undefined` means "latency unknown" and that peer is dropped from the candidate list.
- `RouteDecision` — `{ chunkHash; source: "mesh" | "server-fallback"; peerId: string | null; latencyMs: number | null }`.
## Implementation Notes
- Holders without latency data are dropped from the candidate list rather than ranked at the bottom. The reasoning: a peer for whom we have no latency reading is one we have not connected to recently, so trying to fetch from it is at best high-latency and at worst a cold-connect dance.
- The `peerId.localeCompare` tiebreak keeps the router deterministic even when two peers report identical latency, which is important for property-based tests and replay debugging.
- Blacklist filtering happens before latency ranking, not after — a blacklisted peer with the lowest latency would be a footgun if we ranked first then filtered.

View File

@@ -0,0 +1,108 @@
# ChunkStore
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ChunkStore` is the persistence layer of the mesh-CDN. It holds chunk bytes
locally so this peer can serve other peers (without re-fetching from origin)
and so it can resume large transfers across process restarts. Bookkeeping —
LRU ordering, TTL expiry, byte-budget enforcement — is owned by `ChunkStore`
itself; only the byte payload is delegated to a pluggable
`ChunkStoreAdapter` (the production wiring uses `@suaveplan/drive` for
OPFS / IndexedDB / Node filesystem; tests and Worker-less environments use
the bundled `MemoryChunkStoreAdapter`). This split guarantees that an
in-memory store and a Drive-backed store evict identically — bugs in
eviction policy can be reproduced without booting the storage backend.
## Features
- Byte-budget LRU eviction: every `put` that would push usage past `maxBytes`
evicts oldest-touched chunks first until the new chunk fits.
- TTL eviction: chunks older than `maxAgeMs` are evicted on read (in-band) or
via explicit `sweepExpired()`.
- Pluggable `ChunkStoreAdapter` — `put`, `get`, `delete` only. Sync OR async.
- Bus events: `cd:chunk-stored` on every accepted `put`; `cd:chunk-evicted`
on every removal with `reason: "ttl" | "lru" | "manual"` so observability
consumers can attribute capacity pressure correctly.
- Defensive: `put` of a chunk larger than `maxBytes` throws
`STORE_QUOTA_EXCEEDED` rather than evicting everything in a futile attempt.
- Replace-in-place: putting a hash that already exists updates bookkeeping
without double-counting bytes.
- `has()` is age-aware — expired entries report absent without firing the
eviction event (the next `get` performs the actual eviction).
## Usage: Basic Example
```ts
import { ChunkStore } from "@suaveplan/network-plugin-content-delivery";
const store = new ChunkStore({ maxBytes: 1024 * 1024 });
await store.put("h0", new Uint8Array([1, 2, 3]));
const bytes = await store.get("h0");
```
## Usage: Advanced Example
Wired with the Drive-backed adapter, a 5 MiB byte budget, 10-minute TTL, an
injected clock, and a bus that updates the local `ChunkIndex`:
```ts
import {
ChunkIndex,
ChunkStore,
type ChunkStoreAdapter,
} from "@suaveplan/network-plugin-content-delivery";
const driveAdapter: ChunkStoreAdapter = {
put: (h, bytes) => drive.write(`chunks/${h}`, bytes),
get: (h) => drive.read(`chunks/${h}`),
delete: (h) => drive.delete(`chunks/${h}`),
};
const index = new ChunkIndex({ defaultRedundancyTarget: 3 });
const store = new ChunkStore({
maxBytes: 5 * 1024 * 1024,
maxAgeMs: 10 * 60 * 1000,
adapter: driveAdapter,
bus: {
emit: (topic, payload) => {
if (topic === "cd:chunk-stored") {
// index.ingestAdvertisement({...})
}
},
},
});
```
## API Reference
- `new ChunkStore(config: ChunkStoreConfig)` — `maxBytes` required; rest
optional. Throws `STORE_QUOTA_EXCEEDED` on `maxBytes < 1`.
- `put(chunkHash, bytes): Promise<void>` — store chunk; evict oldest until
fits; emit `cd:chunk-stored`.
- `get(chunkHash): Promise<Uint8Array | undefined>` — return bytes or
`undefined`. TTL-expired entries are evicted in-band.
- `has(chunkHash): boolean` — true if present and not expired.
- `evict(chunkHash, reason?): Promise<boolean>` — manual removal; emits
`cd:chunk-evicted`.
- `sweepExpired(): Promise<readonly string[]>` — sweep all TTL-expired
entries; returns the hashes evicted.
- `size(): number` / `bytes(): number` — current bookkeeping.
- `MemoryChunkStoreAdapter` — bundled in-memory adapter for tests and Worker-
less environments.
## Implementation Notes
- The `entries` Map is insertion-ordered by spec. Every `get` that finds a
live entry re-inserts it, so the head of the map is always the LRU
candidate. `evictOldest` pops the head.
- Byte counting is authoritative in-process. Adapter `delete` failures during
LRU eviction are swallowed — the in-memory bookkeeping wins so a transient
storage error cannot wedge the byte budget.
- `STORE_QUOTA_EXCEEDED` is reused for both "single chunk larger than budget"
and "internal eviction failed" — both are quota-class failures the caller
must respond to.
- `cd:chunk-evicted` carries the reason so a single bus subscriber can drive
both a "warm cache pressure" gauge (from `lru` events) and a "data freshness
policy" trace (from `ttl` events) without needing two subscriptions.

View File

@@ -0,0 +1,113 @@
# BandwidthThrottler
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`BandwidthThrottler` regulates the byte-rate of mesh-CDN chunk dispatches so
file traffic cannot starve control-plane messages on the same transport. It
maintains two token buckets — one per peer, one global — and forces every
admitted dispatch to consume bytes from BOTH before it is allowed onto the
wire. Every admitted dispatch is tagged `priority: "bulk"` so the
`network-plugin-transport-manager` queue can keep BULK behind CRITICAL
without the throttler needing to know the transport's queue topology.
Saturation events emit on the bus as `cd:throttled` carrying the precise
delay and the responsible bucket so the operator can answer "are we global-
limited or peer-limited?" without sampling.
## Features
- Per-peer token bucket with configurable rate + burst.
- Global token bucket with configurable rate + burst.
- Exact wait-time calculation: the throttler computes the precise refill
duration required to satisfy both buckets and sleeps that long once,
rather than spinning.
- `admit()` blocks until both buckets are satisfied and returns a
`BulkDispatch` envelope; `tryAdmit()` is the non-blocking probe form.
- `cd:throttled` event with `reason: "per-peer" | "global"` so dashboards can
attribute saturation correctly.
- Defensive: a single chunk request larger than either burst capacity throws
`THROTTLE_RATE_INVALID` rather than blocking forever.
- Injectable `clock` and `sleep` so tests run in zero wall-clock with full
determinism over the wait math.
- `availableForPeer(peerId)` and `globalAvailable()` inspectors for capacity
planning + admission-control hints.
## Usage: Basic Example
```ts
import { BandwidthThrottler } from "@suaveplan/network-plugin-content-delivery";
const throttler = new BandwidthThrottler({
perPeerBytesPerSec: 1024 * 1024, // 1 MiB/s per peer
globalBytesPerSec: 8 * 1024 * 1024, // 8 MiB/s aggregate
});
const dispatch = await throttler.admit("peer-a", "h0", 64 * 1024);
// dispatch.priority === "bulk"
```
## Usage: Advanced Example
Deterministic clock + sleep for property tests, with bus-driven observability
and a tighter burst configuration:
```ts
import {
BandwidthThrottler,
type ThrottledEvent,
} from "@suaveplan/network-plugin-content-delivery";
let now = 0;
const sleeps: number[] = [];
const throttled: ThrottledEvent[] = [];
const throttler = new BandwidthThrottler({
perPeerBytesPerSec: 1000,
perPeerBurstBytes: 1000,
globalBytesPerSec: 5000,
globalBurstBytes: 5000,
clock: () => now,
sleep: async (ms) => {
sleeps.push(ms);
now += ms;
},
bus: { emit: (_t, payload) => throttled.push(payload) },
});
await throttler.admit("peer-a", "h0", 800);
await throttler.admit("peer-a", "h1", 800);
// Second call empties the per-peer bucket: a `cd:throttled` event fires with
// reason "per-peer" and the recorded sleep durations sum to the exact refill.
```
## API Reference
- `new BandwidthThrottler(config: BandwidthThrottlerConfig)` — `perPeerBytesPerSec`
and `globalBytesPerSec` required (both > 0). Burst capacities default to
one second of rate.
- `admit(peerId, chunkHash, bytes): Promise<BulkDispatch>` — blocking admit.
- `tryAdmit(peerId, chunkHash, bytes): BulkDispatch | undefined` — probe.
- `availableForPeer(peerId): number` — min of peer bucket and global bucket.
- `globalAvailable(): number` — current global bucket tokens.
- `BulkDispatch` — `{ priority: "bulk", peerId, chunkHash, bytes, admittedAt,
waitedMs }`.
- `ThrottledEvent` — `{ peerId, chunkHash, bytes, delayMs, reason, observedAt }`.
## Implementation Notes
- The wait-time calculation is exact: the throttler computes
`peerShort / perPeerRate` and `globalShort / globalRate` independently and
sleeps the larger of the two. After the sleep, both buckets are guaranteed
refilled past the threshold, so a single iteration usually suffices.
- The loop exists for defensive symmetry: if the injected sleep returns early
(e.g. a test stub that does not advance the clock the full amount), the
next iteration recomputes the residual short and sleeps again.
- `reason` reflects which bucket dictated the wait — the bucket whose
required wait was longer wins. Equal waits attribute to `per-peer` (the
more actionable signal for an operator).
- Per-peer buckets are created lazily on first `admit`/`tryAdmit` so a peer
that never sends consumes no bookkeeping memory.
- `BulkDispatch.waitedMs` is computed from the clock delta when available
(covers real-clock callers); when the injected clock does not advance
during the sleep (e.g. deterministic test wiring), the throttler falls
back to the sum of requested sleep durations so the value is still
meaningful.

View File

@@ -0,0 +1,75 @@
# chunk.types
## Purpose
Wire-stable shapes for every record that flows through the content-delivery subsystem: the per-file `ChunkManifest` that announces a packed file (small enough to broadcast on the `NetworkBus`), the per-chunk `ChunkAdvertisement` that a peer emits when it stores a new chunk, the merged `ChunkIndexEntry` the local `ChunkIndex` keeps, the `ChunkRequest` envelope a peer sends when it needs bytes, and the `ChunkBytes` envelope the responding peer ships back with its identity attached so `IntegrityVerifier` can blacklist on hash mismatch.
These types are the public contract every subsystem in `@suaveplan/network-plugin-content-delivery` agrees on, so they live in a shared `types/` directory rather than next to any one consumer.
## Features
- `ChunkManifest.hashAlgorithm` is a free-form string (e.g. `"sha-256"` / `"blake3"`) so the verifier can swap algorithms without a wire-format change.
- `ChunkAdvertisement.advertisedAt` is the holder's local timestamp and is the gossip merge key (later advertisements replace earlier ones for the same `(chunkHash, holderId)`).
- `ChunkIndexEntry.holders` is `readonly string[]` so the index can hand out shared references without defensive copies — callers that mutate the array are doing the wrong thing.
- `ChunkRequest.requesterId` lets the responder credit / debit per-peer bandwidth budgets and rate-limit greedy peers.
- `ChunkBytes.senderId` is required so the integrity verifier can blacklist the sender without an out-of-band correlation.
## Usage: Basic Example
```ts
import type {
ChunkManifest,
ChunkAdvertisement,
} from "@suaveplan/network-plugin-content-delivery";
const manifest: ChunkManifest = {
fileId: "f-1",
totalChunks: 4,
chunkHashes: ["aaa", "bbb", "ccc", "ddd"],
hashAlgorithm: "sha-256",
packerVersion: "1.0.0",
};
const adv: ChunkAdvertisement = {
chunkHash: "aaa",
fileId: "f-1",
chunkIndex: 0,
holderId: "peer-1",
advertisedAt: Date.now(),
};
```
## Usage: Advanced Example
```ts
import type {
ChunkBytes,
ChunkRequest,
} from "@suaveplan/network-plugin-content-delivery";
const request: ChunkRequest = {
chunkHash: "aaa",
fileId: "f-1",
chunkIndex: 0,
requesterId: "peer-2",
};
const response: ChunkBytes = {
chunkHash: "aaa",
bytes: new Uint8Array([1, 2, 3]),
senderId: "peer-1",
};
```
## API Reference
- `ChunkManifest` — `{ fileId; totalChunks; chunkHashes; hashAlgorithm; packerVersion }`.
- `ChunkAdvertisement` — `{ chunkHash; fileId; chunkIndex; holderId; advertisedAt }`.
- `ChunkIndexEntry` — `{ chunkHash; fileId; chunkIndex; holders; redundancyTarget; lastUpdated }`.
- `ChunkRequest` — `{ chunkHash; fileId; chunkIndex; requesterId }`.
- `ChunkBytes` — `{ chunkHash; bytes; senderId }`.
## Implementation Notes
- All shapes are pure data — no methods, no behavior. Logic lives in the consuming subsystems (`ChunkIndex`, `RedundancyManager`, etc.).
- `chunkIndex` (the array index inside the manifest) is duplicated on advertisements and entries for fast bookkeeping; consumers should always treat `chunkHash` as the identity key and `chunkIndex` as a positional hint that may go stale across re-packs.

View File

@@ -0,0 +1,58 @@
# redundancy.types
## Purpose
Captures the inputs and outputs of `RedundancyManager`. The four-class taxonomy (`critical` / `shared` / `user-content` / `telemetry`) maps directly to the redundancy table in the proposal: critical executable chunks need 5 copies to survive multiple peer departures, shared assets need 3 copies, user-generated content needs 2, and telemetry needs only 1 because the server has the canonical record.
The `ReplicationTask` shape is what the redundancy manager *emits* when it determines a chunk needs more copies — it does NOT execute the replication itself. The actual fetch/place is the responsibility of `drive-mesh` (per the boundary documented in the proposal). This separation keeps redundancy policy and chunk-fetch scheduling decoupled.
## Features
- `RedundancyConfig.defaultClass` lets callers route untagged chunks (e.g. ad-hoc gossip blobs) to a sensible default rather than failing.
- `RedundancyConfig.perClass` is a `Readonly<Record<...>>` so the configuration is frozen at construction time; mutating it is a runtime contract violation.
- `ReplicationTask.recommendedCandidates` is a hint, not a directive — the consuming `drive-mesh` scheduler may reorder based on local latency / bandwidth state.
- `ReplicationReason` discriminates `"below-target"` (initial under-replication on first advertise), `"holder-departed"` (peer left the mesh and we lost a copy), and `"manual"` (caller-driven re-replication).
## Usage: Basic Example
```ts
import type {
RedundancyConfig,
RedundancyClass,
} from "@suaveplan/network-plugin-content-delivery";
const config: RedundancyConfig = {
defaultClass: "shared",
perClass: { critical: 5, shared: 3, "user-content": 2, telemetry: 1 },
};
const klass: RedundancyClass = "critical";
console.log(config.perClass[klass]); // 5
```
## Usage: Advanced Example
```ts
import type { ReplicationTask } from "@suaveplan/network-plugin-content-delivery";
function applyTask(task: ReplicationTask): void {
if (task.reason === "holder-departed") {
metrics.increment("cd.replication.churn");
}
for (const candidate of task.recommendedCandidates) {
driveMesh.scheduleFetch(task.chunkHash, candidate);
}
}
```
## API Reference
- `RedundancyClass` — `"critical" | "shared" | "user-content" | "telemetry"`.
- `RedundancyConfig` — `{ defaultClass; perClass: Readonly<Record<RedundancyClass, number>> }`.
- `ReplicationTask` — `{ chunkHash; fileId; chunkIndex; currentHolders; targetCount; recommendedCandidates; reason }`.
- `ReplicationReason` — `"below-target" | "holder-departed" | "manual"`.
## Implementation Notes
- The four-class enum is intentionally closed: adding a new class is a code change, not a configuration change. This keeps the redundancy table auditable in source.
- `targetCount` is materialised on the task (rather than re-derived from `RedundancyConfig`) so a downstream consumer can act on the task without re-resolving configuration; this matters when the manager and consumer run in different processes.