first commit
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user