Files
suaveplan/packages/networking/drive/drive-mesh-protocol/src/codec/response.ts
2026-05-27 23:47:53 +01:00

110 lines
3.4 KiB
TypeScript

/**
* Codec for `cd:response` envelopes (DMP-11, DMP-12, DMP-21).
*
* Response payloads carry an array of `ChunkBytes` ordered by `chunkId`
* ascending plus a piggyback presence bitset for the responder's full
* inventory. Encoder and decoder both reject out-of-order chunks with
* `DMP.UNORDERED_RESPONSE`. Both fields cross JSON as base64 strings;
* the presence bitset additionally goes through the
* `bitpacker-compressed` framing magic check (DMP-21).
*/
import { DMP_CODES, DmpError } from "../error-codes.js";
import { ChunkResponsePayloadSchema } from "../schemas/envelope.schema.js";
import type {
ChunkBytes,
ChunkResponsePayload,
} from "../types/protocol.types.js";
import { decodeBitset } from "./bitset.js";
import { fromBase64, parseStrict, toBase64 } from "./common.js";
interface WireChunk {
readonly chunkId: number;
readonly bytes: { readonly _b: string };
}
interface WirePayload {
readonly fileId: string;
readonly chunks: readonly WireChunk[];
readonly presence: { readonly _b: string };
readonly generation: number;
}
export function encodeResponsePayload(
payload: ChunkResponsePayload
): Uint8Array {
const validated = parseStrict<ChunkResponsePayload>(
ChunkResponsePayloadSchema,
payload,
DMP_CODES.ENVELOPE_MALFORMED,
"response payload"
);
assertOrdered(validated.chunks);
// DMP-21: presence MUST already be bitpacker-compressed-framed.
decodeBitset(validated.presence);
const wire: WirePayload = {
fileId: validated.fileId,
chunks: validated.chunks.map((c) => ({
chunkId: c.chunkId,
bytes: { _b: toBase64(c.bytes) },
})),
presence: { _b: toBase64(validated.presence) },
generation: validated.generation,
};
return new TextEncoder().encode(JSON.stringify(wire));
}
export function decodeResponsePayload(bytes: Uint8Array): ChunkResponsePayload {
let parsed: unknown;
try {
parsed = JSON.parse(new TextDecoder().decode(bytes));
} catch (err) {
throw new DmpError(
DMP_CODES.ENVELOPE_MALFORMED,
`response payload is not valid JSON: ${String(err)}`
);
}
const validated = parseStrict<ChunkResponsePayload>(
ChunkResponsePayloadSchema,
unwrapWire(parsed),
DMP_CODES.ENVELOPE_MALFORMED,
"response payload"
);
assertOrdered(validated.chunks);
decodeBitset(validated.presence);
return validated;
}
function assertOrdered(chunks: readonly ChunkBytes[]): void {
for (let i = 1; i < chunks.length; i++) {
const prev = chunks[i - 1] as ChunkBytes;
const cur = chunks[i] as ChunkBytes;
if (cur.chunkId <= prev.chunkId) {
throw new DmpError(
DMP_CODES.UNORDERED_RESPONSE,
`chunk ${cur.chunkId} appears at index ${i} after ${prev.chunkId} — chunks must be strictly ascending`
);
}
}
}
function unwrapWire(parsed: unknown): unknown {
if (
parsed === null ||
typeof parsed !== "object" ||
!("chunks" in parsed) ||
!("presence" in parsed)
) {
return parsed;
}
const wire = parsed as WirePayload;
return {
...wire,
chunks: wire.chunks.map((c) => ({
chunkId: c.chunkId,
bytes: fromBase64(c.bytes._b),
})),
presence: fromBase64(wire.presence._b),
};
}