CultLib/packages/cultnet-ts/test/replication-codec.test.ts

104 lines
5.2 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import test from "node:test";
import { z } from "zod";
import { CultCache, codecs, defineDocumentType, documentPayloadBytes, SingleFileMessagePackBackingStore } from "@gamecult/cultcache-ts";
import { CultNetDocumentRegistry, defineCultNetDocumentBinding } from "../src";
const { float32, float64, int32, int64, slots, string } = codecs;
// tests/vectors/numeric-leaves/mixed-record.msgpack is the bytes the C# reference writes for Mixed { "n1", 7, 8, 1.5f, 2.5 }
// (NumericLeafVectorTests). A payload equal to it is a payload the reference reads.
function referenceMixedRecord(): Uint8Array {
for (let dir = __dirname; ; dir = resolve(dir, "..")) {
const file = join(dir, "tests", "vectors", "numeric-leaves", "mixed-record.msgpack");
if (existsSync(file)) return new Uint8Array(readFileSync(file));
if (resolve(dir, "..") === dir) throw new Error("tests/vectors/numeric-leaves not found above the test.");
}
}
const mixedCodec = slots<{ id: string; i: number; l: number | bigint; f: number; d: number }>({
0: ["id", string],
1: ["i", int32],
2: ["l", int64],
3: ["f", float32],
4: ["d", float64],
});
const mixedDefinition = defineDocumentType({
type: "tests.mixed_record",
schemaId: "tests.mixed_record.v1",
schemaName: "tests.mixed_record",
schemaVersion: "tests.mixed_record.v1",
schema: z.object({ id: z.string(), i: z.number(), l: z.union([z.number(), z.bigint()]), f: z.number(), d: z.number() }),
codec: mixedCodec,
name: "id",
});
const value = { id: "n1", i: 7, l: 8, f: 1.5, d: 2.5 };
const hex = (data: Uint8Array) => Buffer.from(data).toString("hex");
function registry() {
return new CultNetDocumentRegistry([defineCultNetDocumentBinding({ definition: mixedDefinition })]);
}
test("a codec document replicates as the bytes the C# reference writes, and a snapshot of it applies on another cache", async () => {
const dir = mkdtempSync(join(tmpdir(), "cultnet-codec-"));
try {
const reference = referenceMixedRecord();
const documents = registry();
const origin = CultCache.builder().withDocumentType(mixedDefinition).withGenericStore(new SingleFileMessagePackBackingStore(join(dir, "origin.cc"))).build();
const target = CultCache.builder().withDocumentType(mixedDefinition).withGenericStore(new SingleFileMessagePackBackingStore(join(dir, "target.cc"))).build();
const put = documents.createRawDocumentPutMessage(documents.get("tests.mixed_record")!, "put-1", "n1", value);
assert.equal(hex(documentPayloadBytes(put.document.payload)), hex(reference), "a raw put carries the reference's record, not a map");
await documents.applyRawDocumentPutMessage(origin, put);
assert.equal(hex(documentPayloadBytes(origin.getRequiredEnvelope(mixedDefinition, "n1").payload)), hex(reference));
const raw = documents.createRawSnapshotResponse(origin, "snapshot-raw");
assert.equal(hex(documentPayloadBytes(raw.documents[0].payload)), hex(reference));
await documents.applyRawSnapshotResponse(target, raw);
assert.deepEqual(target.getRequired(mixedDefinition, "n1"), value);
assert.equal(hex(documentPayloadBytes(target.getRequiredEnvelope(mixedDefinition, "n1").payload)), hex(reference));
// The value-level snapshot decodes the stored record through the codec, so a peer applying it parses a record.
const values = documents.createSnapshotResponse(origin, "snapshot-values");
assert.deepEqual(values.documents[0].payload, value);
const other = CultCache.builder().withDocumentType(mixedDefinition).withGenericStore(new SingleFileMessagePackBackingStore(join(dir, "other.cc"))).build();
await documents.applySnapshotResponse(other, values);
assert.equal(hex(documentPayloadBytes(other.getRequiredEnvelope(mixedDefinition, "n1").payload)), hex(reference));
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("a definition that carries a codec and a formatter is refused where replication reads it", () => {
const both = {
...mixedDefinition,
formatter: { encode: () => new Uint8Array(), decode: () => value },
} as unknown as typeof mixedDefinition;
const documents = new CultNetDocumentRegistry([defineCultNetDocumentBinding({ definition: both })]);
assert.throws(
() => documents.createRawDocumentPutMessage(documents.get("tests.mixed_record")!, "put-2", "n1", value),
{ message: 'CultCache document type "tests.mixed_record" declares both a codec and a formatter.' },
);
});
test("a document with neither a codec nor a formatter replicates as a map, as before", () => {
const plain = defineDocumentType({
type: "tests.plain_record",
schemaId: "tests.plain_record.v1",
schemaName: "tests.plain_record",
schemaVersion: "tests.plain_record.v1",
schema: z.object({ id: z.string() }),
name: "id",
});
const documents = new CultNetDocumentRegistry([defineCultNetDocumentBinding({ definition: plain })]);
const put = documents.createRawDocumentPutMessage(documents.get("tests.plain_record")!, "put-3", "n1", { id: "n1" });
assert.equal(hex(documentPayloadBytes(put.document.payload)), "81a26964a26e31");
});