CultLib/packages/cultmesh-ts/test/codec-documents.test.ts
Libby 286a9af389
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m49s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 36s
npm: scope cultnet-ts, cultmesh-ts and cultmesh-browser under @gamecult
Unscoped npm names are unclaimed and open to dependency confusion; cultcache-ts was
already @gamecult/cultcache-ts. package.json names, intra-repo dependencies, package-lock,
import specifiers (src, tests, samples, docs/start, first-call docs test), the workspace
list in run-typescript-workspaces, the verify-eve module map and package READMEs now use
the scoped names. Directory names, workspace paths in CI and release tag prefixes are
unchanged. Historical cut docs and past CHANGELOG entries keep the old names.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 16:25:10 +00:00

209 lines
10 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { z } from "zod";
import {
CultCachePayloadDepthError,
MAX_PAYLOAD_DEPTH,
SingleFileMessagePackBackingStore,
codecs,
decodeDocumentPayload,
defineDocumentType,
documentPayloadBytes,
documentPayloadFromBytes,
encodeDocumentPayload,
} from "@gamecult/cultcache-ts";
import {
CultNetDocumentRegistry,
defineCultNetDocumentBinding,
type CultNetPeer,
} from "@gamecult/cultnet-ts";
import { CultMesh, cultMeshDocumentFromSingleFile, type CultMeshRudpDocumentPut } from "../src/index";
// A document written in the reference's positional shape, and a plain map document beside it. Every mesh reader of a
// payload reads it the way the cache does, so both read through the mesh.
const codecNote = defineDocumentType({
type: "cultmesh.codec_note",
schemaId: "cultmesh.codec_note.v0",
schema: z.object({ noteId: z.string(), body: z.string() }),
name: "noteId",
codec: codecs.slots<{ noteId: string; body: string }>({
0: ["noteId", codecs.string],
1: ["body", codecs.string],
}, "Tests.CodecNote"),
});
const codecReceipt = defineDocumentType({
type: "cultmesh.codec_receipt",
schemaId: "cultmesh.codec_receipt.v0",
schema: z.object({ commandId: z.string(), body: z.string() }),
name: "commandId",
codec: codecs.slots<{ commandId: string; body: string }>({
0: ["commandId", codecs.string],
1: ["body", codecs.string],
}, "Tests.CodecReceipt"),
});
const plainNote = defineDocumentType({
type: "cultmesh.plain_note",
schemaId: "cultmesh.plain_note.v0",
schema: z.object({ noteId: z.string(), body: z.string() }),
name: "noteId",
});
test("the mesh reads a stored codec document as the cache does, and a plain document unchanged", async () => {
const dir = await mkdtemp(join(tmpdir(), "cultmesh-ts-codec-store-"));
const codecPath = join(dir, "codec.ccmp");
const plainPath = join(dir, "plain.ccmp");
const codecNode = await CultMesh.startNode(codecPath, { documents: [codecNote] });
await codecNode.put(codecNote, "n1", { noteId: "n1", body: "positional" });
const plainNode = await CultMesh.startNode(plainPath, { documents: [plainNote] });
await plainNode.put(plainNote, "n2", { noteId: "n2", body: "map" });
assert.deepEqual(await cultMeshDocumentFromSingleFile(codecPath, codecNote).latest(), { noteId: "n1", body: "positional" });
assert.deepEqual(await cultMeshDocumentFromSingleFile(plainPath, plainNote).latest(), { noteId: "n2", body: "map" });
});
test("the mesh syncs a codec document from a peer snapshot and returns a codec receipt", async () => {
const connectionId = 0x10203060;
const dir = await mkdtemp(join(tmpdir(), "cultmesh-ts-codec-sync-"));
const source = await CultMesh.startNode(join(dir, "source.ccmp"), { documents: [codecNote, plainNote] });
await source.put(codecNote, "n1", { noteId: "n1", body: "synced positional" });
const target = await CultMesh.startNode(join(dir, "target.ccmp"), { documents: [codecNote] });
const commandBinding = defineCultNetDocumentBinding({ definition: codecNote });
const plainBinding = defineCultNetDocumentBinding({ definition: plainNote });
const receiptBinding = defineCultNetDocumentBinding({ definition: codecReceipt });
const puts: unknown[] = [];
const server = CultMesh.createRudpDocumentServer("cultmesh-ts-codec-server", connectionId, {
documents: new CultNetDocumentRegistry([commandBinding, plainBinding, receiptBinding]),
getCache: () => source.cache,
bindHost: "127.0.0.1", bindPort: 0, resendDelayMs: 25, resendPollMs: 5,
onDocumentPutRaw: async (document) => (puts.push(document.payload), {
binding: receiptBinding,
recordKey: document.recordKey,
value: { commandId: document.recordKey, body: "applied" },
}),
});
let peer: CultNetPeer | undefined;
try {
await server.start();
peer = await CultMesh.createRudpPeer("cultmesh-ts-codec-client", connectionId, `rudp://127.0.0.1:${server.bind.port}`, {
resendDelayMs: 25, resendPollMs: 5, connectTimeoutMs: 1_000,
});
const synced = await target.syncDocumentFromPeerSnapshot(peer, codecNote, "n1", { timeoutMs: 1_000 });
assert.deepEqual(synced, { noteId: "n1", body: "synced positional" });
assert.deepEqual(target.getRequired(codecNote, "n1"), { noteId: "n1", body: "synced positional" });
// A peer snapshot handle reads the same way.
assert.deepEqual(await CultMesh.documentFromPeerSnapshot(peer, codecNote, "n1", { timeoutMs: 1_000 }).latest(), { noteId: "n1", body: "synced positional" });
const receipt = await CultMesh.publishRudpDocumentAndWaitForReceipt(
"cultmesh-ts-codec-receipt", connectionId, `rudp://127.0.0.1:${server.bind.port}`,
commandBinding, "n2", { noteId: "n2", body: "command" }, receiptBinding,
{ messageId: "n2", receiptTimeoutMs: 1_000, resendPollMs: 5 },
);
assert.deepEqual(receipt, { commandId: "n2", body: "applied" });
// The handler is handed the document, not its wire shape: the server reads a put through the registered definition.
await CultMesh.publishRudpDocumentAndWaitForReceipt(
"cultmesh-ts-plain-receipt", connectionId, `rudp://127.0.0.1:${server.bind.port}`,
plainBinding, "n4", { noteId: "n4", body: "plain command" }, receiptBinding,
{ messageId: "n4", receiptTimeoutMs: 1_000, resendPollMs: 5 },
);
assert.deepEqual(puts, [{ noteId: "n2", body: "command" }, { noteId: "n4", body: "plain command" }]);
} finally {
peer?.close();
server.close();
}
});
test("a RUDP put naming a schema the server has no definition for reaches the handler as the untouched opaque payload", async () => {
const connectionId = 0x10203070;
const stray = defineDocumentType({
type: "cultmesh.stray_note",
schemaId: "cultmesh.stray_note.v0",
schema: z.object({ noteId: z.string() }),
name: "noteId",
codec: codecs.slots<{ noteId: string }>({ 0: ["noteId", codecs.string] }, "Tests.StrayNote"),
});
const strayBinding = defineCultNetDocumentBinding({ definition: stray });
const known = defineCultNetDocumentBinding({ definition: plainNote });
const errors: Error[] = [];
const puts: CultMeshRudpDocumentPut[] = [];
const server = CultMesh.createRudpDocumentServer("cultmesh-ts-unregistered-server", connectionId, {
documents: new CultNetDocumentRegistry([known]),
bindHost: "127.0.0.1", bindPort: 0, resendDelayMs: 25, resendPollMs: 5,
onError: (error) => errors.push(error),
onDocumentPutRaw: async (document) => { puts.push(document); },
});
let peer: CultNetPeer | undefined;
try {
await server.start();
peer = await CultMesh.createRudpPeer("cultmesh-ts-unregistered-client", connectionId, `rudp://127.0.0.1:${server.bind.port}`, {
resendDelayMs: 25, resendPollMs: 5, connectTimeoutMs: 1_000,
});
peer.send(new CultNetDocumentRegistry([strayBinding]).createRawDocumentPutMessage(strayBinding, "unregistered-1", "k", { noteId: "k" }));
const startedAt = Date.now();
while (puts.length === 0 && Date.now() - startedAt < 2_000) await new Promise((resolve) => setTimeout(resolve, 5));
assert.deepEqual(errors, []);
assert.equal(puts.length, 1);
// Not read as plain msgpack: the bytes are the codec's slot record, handed over opaque, and the definition that reads
// them is the handler's to hold.
const put = puts[0]!;
assert.ok(!put.decoded, "the registry holds no definition for this schema");
const payload = put.payload;
assert.equal(Buffer.from(documentPayloadBytes(payload)).toString("hex"), "91a16b", "a one-slot codec record, not a map");
assert.deepEqual(Buffer.from(documentPayloadBytes(payload)), Buffer.from(documentPayloadBytes(encodeDocumentPayload(stray, { noteId: "k" }))));
assert.deepEqual(decodeDocumentPayload(stray, payload), { noteId: "k" });
} finally {
peer?.close();
server.close();
}
});
// A payload nested past the limit: MAX_PAYLOAD_DEPTH + 1 one-element arrays around a nil.
const pastLimitPayload = () => documentPayloadFromBytes(Uint8Array.from([...new Array(MAX_PAYLOAD_DEPTH + 1).fill(0x91), 0xc0]));
test("a payload past the nesting limit is refused at the network doors: a RUDP put and a single-file read", async () => {
const connectionId = 0x10203080;
const plainBinding = defineCultNetDocumentBinding({ definition: plainNote });
const registry = new CultNetDocumentRegistry([plainBinding]);
const errors: unknown[] = [];
const puts: CultMeshRudpDocumentPut[] = [];
const server = CultMesh.createRudpDocumentServer("cultmesh-ts-depth-server", connectionId, {
documents: registry,
bindHost: "127.0.0.1", bindPort: 0, resendDelayMs: 25, resendPollMs: 5,
onError: (error) => errors.push(error),
onDocumentPutRaw: async (document) => { puts.push(document); },
});
let peer: CultNetPeer | undefined;
try {
await server.start();
peer = await CultMesh.createRudpPeer("cultmesh-ts-depth-client", connectionId, `rudp://127.0.0.1:${server.bind.port}`, {
resendDelayMs: 25, resendPollMs: 5, connectTimeoutMs: 1_000,
});
peer.send(registry.createRawDocumentPutMessageFromEnvelope("deep-1", {
key: "k", type: plainNote.type, schemaId: plainNote.schemaId, payload: pastLimitPayload(), storedAt: new Date().toISOString(),
}));
const startedAt = Date.now();
while (errors.length === 0 && Date.now() - startedAt < 2_000) await new Promise((resolve) => setTimeout(resolve, 5));
assert.equal(errors.length, 1);
assert.ok(errors[0] instanceof CultCachePayloadDepthError, "the put is refused by the payload owner");
assert.deepEqual(puts, [], "the handler never saw the put");
// The server is still serving: an ordinary put goes through.
peer.send(registry.createRawDocumentPutMessage(plainBinding, "ok-1", "k2", { noteId: "k2", body: "fine" }));
while (puts.length === 0 && Date.now() - startedAt < 4_000) await new Promise((resolve) => setTimeout(resolve, 5));
assert.equal(puts.length, 1);
} finally {
peer?.close();
server.close();
}
const dir = await mkdtemp(join(tmpdir(), "cultmesh-ts-depth-store-"));
const path = join(dir, "deep.ccmp");
await new SingleFileMessagePackBackingStore(path).push({
key: "k", type: plainNote.type, schemaId: plainNote.schemaId, payload: pastLimitPayload(), storedAt: new Date().toISOString(),
});
await assert.rejects(() => cultMeshDocumentFromSingleFile(path, plainNote).latest(), CultCachePayloadDepthError);
});