first commit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# advertise
|
||||
|
||||
## Purpose
|
||||
|
||||
Encodes and decodes `cd:advertise` envelope payloads — the gossip messages a peer emits to announce which chunks it currently holds for a given file. Enforces DMP-08 (presence bitset's `bitLength` MUST equal `totalChunks`; padding bits MUST be zero), DMP-09 (generation field is unsigned 32-bit), and DMP-21 (presence MUST be framed via `@suaveplan/bitpacker-compressed`).
|
||||
|
||||
## Features
|
||||
|
||||
- `encodeAdvertisePayload(payload)` — Zod-validates, cross-checks the presence bitset's embedded `bitLength` against `totalChunks`, base64-wraps the bitset, and emits UTF-8 JSON bytes.
|
||||
- `decodeAdvertisePayload(bytes)` — inverse, with the same DMP-08 cross-check on the receive side so receivers never trust a malformed advertisement.
|
||||
- Both functions throw `DmpError` with `DMP.BITSET_LENGTH_MISMATCH`, `DMP.BITSET_PADDING_NONZERO`, `DMP.CODEC_MISMATCH`, or `DMP.ENVELOPE_MALFORMED` depending on the failure class.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
chunkIdsToRawBitset,
|
||||
decodeAdvertisePayload,
|
||||
encodeAdvertisePayload,
|
||||
encodeFramedBitset,
|
||||
} from "@suaveplan/drive-mesh-protocol";
|
||||
|
||||
const totalChunks = 64;
|
||||
const raw = chunkIdsToRawBitset([0, 5, 17, 63], totalChunks);
|
||||
const presence = encodeFramedBitset(raw, totalChunks);
|
||||
|
||||
const wire = encodeAdvertisePayload({
|
||||
fileId: "f".repeat(64),
|
||||
presence,
|
||||
totalChunks,
|
||||
generation: 7,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const back = decodeAdvertisePayload(wire);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
chunkIdsToRawBitset,
|
||||
decodeAdvertisePayload,
|
||||
encodeAdvertisePayload,
|
||||
encodeFramedBitset,
|
||||
GenerationTracker,
|
||||
} from "@suaveplan/drive-mesh-protocol";
|
||||
|
||||
const tracker = new GenerationTracker();
|
||||
|
||||
function handleIncomingAdvertise(bytes: Uint8Array, peerId: string): void {
|
||||
const adv = decodeAdvertisePayload(bytes);
|
||||
const r = tracker.observe(peerId, adv.fileId, adv.generation);
|
||||
if (!r.accept) return; // DMP-09 stale-drop
|
||||
// Update peer-index for (peerId, adv.fileId) using adv.presence.
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encodeAdvertisePayload(payload: ChunkAdvertisePayload): Uint8Array`.
|
||||
- `decodeAdvertisePayload(bytes: Uint8Array): ChunkAdvertisePayload`.
|
||||
- Errors: `DMP.BITSET_LENGTH_MISMATCH`, `DMP.BITSET_PADDING_NONZERO`, `DMP.CODEC_MISMATCH`, `DMP.ENVELOPE_MALFORMED`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The encode-side cross-check between `bitLength` (recovered from the framed presence bitset) and `totalChunks` is intentional defence-in-depth: it stops a bug in the issuer from emitting an advertisement whose receivers would later reject. Generations are validated up to `0xFFFF_FFFF` at the schema layer; wrap-detection logic lives in `generation/monotonic.ts`. The `unwrapWire` helper passes through bare nulls / non-objects so the schema is the single source of "this isn't a valid envelope" rejection — every malformed input ends up at the schema gate, not at a JS-level type error.
|
||||
Reference in New Issue
Block a user