Main's codec-document tests and the RUDP document-handle test read a record stored under codec_note, plain_note or a foreign id through a handle for an alias schema, and one store test picked among foreign-id records by whether they parse. retire-schema-alias makes that read refuse; C2a's own tests (a handle reads only records under an id its definition declares) pin the rule. The codec reads and syncs under the definition's own id stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
10 KiB
TypeScript
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 "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);
|
|
});
|