2026-10-11 09:18:18 +02:00
|
|
|
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";
|
2026-10-11 19:45:59 +02:00
|
|
|
import {
|
2026-10-12 00:01:59 +02:00
|
|
|
CultCachePayloadDepthError,
|
|
|
|
|
MAX_PAYLOAD_DEPTH,
|
|
|
|
|
SingleFileMessagePackBackingStore,
|
2026-10-11 19:45:59 +02:00
|
|
|
codecs,
|
|
|
|
|
decodeDocumentPayload,
|
|
|
|
|
defineDocumentType,
|
|
|
|
|
documentPayloadBytes,
|
2026-10-12 00:01:59 +02:00
|
|
|
documentPayloadFromBytes,
|
2026-10-11 19:45:59 +02:00
|
|
|
encodeDocumentPayload,
|
|
|
|
|
} from "@gamecult/cultcache-ts";
|
2026-10-11 09:18:18 +02:00
|
|
|
import {
|
|
|
|
|
CultNetDocumentRegistry,
|
|
|
|
|
defineCultNetDocumentBinding,
|
|
|
|
|
type CultNetPeer,
|
|
|
|
|
} from "cultnet-ts";
|
2026-10-14 04:31:43 +02:00
|
|
|
import { CultMesh, cultMeshDocumentFromSingleFile, type CultMeshRudpDocumentPut } from "../src/index";
|
2026-10-11 09:18:18 +02:00
|
|
|
|
|
|
|
|
// 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-"));
|
2026-10-11 09:54:10 +02:00
|
|
|
const source = await CultMesh.startNode(join(dir, "source.ccmp"), { documents: [codecNote, plainNote] });
|
2026-10-11 09:18:18 +02:00
|
|
|
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 });
|
2026-10-11 09:54:10 +02:00
|
|
|
const plainBinding = defineCultNetDocumentBinding({ definition: plainNote });
|
2026-10-11 09:18:18 +02:00
|
|
|
const receiptBinding = defineCultNetDocumentBinding({ definition: codecReceipt });
|
2026-10-11 10:25:29 +02:00
|
|
|
const puts: unknown[] = [];
|
2026-10-11 09:18:18 +02:00
|
|
|
const server = CultMesh.createRudpDocumentServer("cultmesh-ts-codec-server", connectionId, {
|
2026-10-11 09:54:10 +02:00
|
|
|
documents: new CultNetDocumentRegistry([commandBinding, plainBinding, receiptBinding]),
|
2026-10-11 09:18:18 +02:00
|
|
|
getCache: () => source.cache,
|
|
|
|
|
bindHost: "127.0.0.1", bindPort: 0, resendDelayMs: 25, resendPollMs: 5,
|
2026-10-11 10:25:29 +02:00
|
|
|
onDocumentPutRaw: async (document) => (puts.push(document.payload), {
|
2026-10-11 09:18:18 +02:00
|
|
|
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" });
|
2026-10-14 04:31:43 +02:00
|
|
|
// A peer snapshot handle reads the same way.
|
2026-10-11 09:38:56 +02:00
|
|
|
assert.deepEqual(await CultMesh.documentFromPeerSnapshot(peer, codecNote, "n1", { timeoutMs: 1_000 }).latest(), { noteId: "n1", body: "synced positional" });
|
2026-10-11 09:18:18 +02:00
|
|
|
|
|
|
|
|
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" });
|
2026-10-11 10:25:29 +02:00
|
|
|
// 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" }]);
|
2026-10-11 09:18:18 +02:00
|
|
|
} finally {
|
|
|
|
|
peer?.close();
|
|
|
|
|
server.close();
|
|
|
|
|
}
|
|
|
|
|
});
|
One typed owner of document-payload reads and writes: DocumentPayload is opaque, the definition is required
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:33:42 +02:00
|
|
|
|
2026-10-11 19:45:59 +02:00
|
|
|
test("a RUDP put naming a schema the server has no definition for reaches the handler as the untouched opaque payload", async () => {
|
One typed owner of document-payload reads and writes: DocumentPayload is opaque, the definition is required
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:33:42 +02:00
|
|
|
const connectionId = 0x10203070;
|
|
|
|
|
const stray = defineDocumentType({
|
2026-10-11 19:45:59 +02:00
|
|
|
type: "cultmesh.stray_note",
|
|
|
|
|
schemaId: "cultmesh.stray_note.v0",
|
One typed owner of document-payload reads and writes: DocumentPayload is opaque, the definition is required
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:33:42 +02:00
|
|
|
schema: z.object({ noteId: z.string() }),
|
|
|
|
|
name: "noteId",
|
2026-10-11 19:45:59 +02:00
|
|
|
codec: codecs.slots<{ noteId: string }>({ 0: ["noteId", codecs.string] }, "Tests.StrayNote"),
|
One typed owner of document-payload reads and writes: DocumentPayload is opaque, the definition is required
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:33:42 +02:00
|
|
|
});
|
2026-10-11 19:45:59 +02:00
|
|
|
const strayBinding = defineCultNetDocumentBinding({ definition: stray });
|
One typed owner of document-payload reads and writes: DocumentPayload is opaque, the definition is required
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:33:42 +02:00
|
|
|
const known = defineCultNetDocumentBinding({ definition: plainNote });
|
|
|
|
|
const errors: Error[] = [];
|
2026-10-11 20:37:29 +02:00
|
|
|
const puts: CultMeshRudpDocumentPut[] = [];
|
One typed owner of document-payload reads and writes: DocumentPayload is opaque, the definition is required
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:33:42 +02:00
|
|
|
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),
|
2026-10-11 20:37:29 +02:00
|
|
|
onDocumentPutRaw: async (document) => { puts.push(document); },
|
One typed owner of document-payload reads and writes: DocumentPayload is opaque, the definition is required
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:33:42 +02:00
|
|
|
});
|
|
|
|
|
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,
|
|
|
|
|
});
|
2026-10-11 19:45:59 +02:00
|
|
|
peer.send(new CultNetDocumentRegistry([strayBinding]).createRawDocumentPutMessage(strayBinding, "unregistered-1", "k", { noteId: "k" }));
|
One typed owner of document-payload reads and writes: DocumentPayload is opaque, the definition is required
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:33:42 +02:00
|
|
|
const startedAt = Date.now();
|
2026-10-11 19:45:59 +02:00
|
|
|
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.
|
2026-10-11 20:37:29 +02:00
|
|
|
const put = puts[0]!;
|
|
|
|
|
assert.ok(!put.decoded, "the registry holds no definition for this schema");
|
|
|
|
|
const payload = put.payload;
|
2026-10-11 19:45:59 +02:00
|
|
|
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" });
|
One typed owner of document-payload reads and writes: DocumentPayload is opaque, the definition is required
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:33:42 +02:00
|
|
|
} finally {
|
|
|
|
|
peer?.close();
|
|
|
|
|
server.close();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-10-11 20:37:29 +02:00
|
|
|
|
2026-10-12 00:01:59 +02:00
|
|
|
// 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);
|
|
|
|
|
});
|