Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
Meta Vens
8c018dc9a1 Add reproducible TypeScript dependency lock 2026-07-19 15:56:22 +01:00
Meta Vens
9295278531 Return RUDP application receipts 2026-07-13 17:30:08 +01:00
3 changed files with 408 additions and 2 deletions

317
package-lock.json generated Normal file
View file

@ -0,0 +1,317 @@
{
"name": "cultlib",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cultlib",
"workspaces": [
"packages/cultcache-ts",
"packages/cultnet-ts",
"packages/cultmesh-ts"
]
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "11.9.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.15",
"js-yaml": "^4.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/philsturgeon"
}
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"dev": true,
"license": "MIT"
},
"node_modules/@msgpack/msgpack": {
"version": "3.1.3",
"license": "ISC",
"engines": {
"node": ">= 18"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.24",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.13.3",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/ajv": {
"version": "8.20.0",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"dev": true,
"license": "Python-2.0"
},
"node_modules/cultcache-ts": {
"resolved": "packages/cultcache-ts",
"link": true
},
"node_modules/cultmesh-ts": {
"resolved": "packages/cultmesh-ts",
"link": true
},
"node_modules/cultnet-ts": {
"resolved": "packages/cultnet-ts",
"link": true
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.3",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fdir": {
"version": "6.5.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-yaml": {
"version": "4.3.0",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-schema-to-typescript": {
"version": "15.0.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.5.5",
"@types/json-schema": "^7.0.15",
"@types/lodash": "^4.17.7",
"is-glob": "^4.0.3",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"prettier": "^3.2.5",
"tinyglobby": "^0.2.9"
},
"bin": {
"json2ts": "dist/src/cli.js"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.18.1",
"dev": true,
"license": "MIT"
},
"node_modules/minimist": {
"version": "1.2.8",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/picomatch": {
"version": "4.0.5",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/prettier": {
"version": "3.9.5",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"dev": true,
"license": "MIT"
},
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"packages/cultcache-ts": {
"version": "0.13.5",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^3.1.3",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^24.0.0",
"typescript": "^5.9.0"
}
},
"packages/cultmesh-ts": {
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^3.1.2",
"cultcache-ts": "file:../cultcache-ts",
"cultnet-ts": "file:../cultnet-ts"
},
"devDependencies": {
"@types/node": "^24.0.0",
"typescript": "^5.9.0",
"zod": "^3.25.76"
}
},
"packages/cultnet-ts": {
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^3.1.3",
"ajv": "^8.17.1",
"cultcache-ts": "file:../cultcache-ts"
},
"devDependencies": {
"@types/node": "^24.0.0",
"json-schema-to-typescript": "^15.0.4",
"typescript": "^5.9.0",
"zod": "^3.25.76"
}
}
}
}

View file

@ -4088,11 +4088,25 @@ export interface CultMeshRudpDocumentPut {
};
}
export interface CultMeshRudpDocumentReceipt<TDefinition extends AnyCultCacheDocumentDefinition = AnyCultCacheDocumentDefinition> {
binding: CultNetDocumentBinding<TDefinition>;
recordKey: string;
value: CultCacheDocumentValue<TDefinition>;
messageId?: string;
sourceRuntimeId?: string;
sourceAgentId?: string;
sourceRole?: string;
tags?: string[];
}
export interface CultMeshRudpDocumentServerOptions extends CultMeshRudpSocketOptions {
documents: CultNetDocumentRegistry;
getCache?: () => Promise<CultCache> | CultCache;
onError?: (error: Error) => void;
onDocumentPutRaw?: (document: CultMeshRudpDocumentPut) => void | Promise<void>;
onDocumentPutRaw?: (document: CultMeshRudpDocumentPut) =>
| CultMeshRudpDocumentReceipt
| void
| Promise<CultMeshRudpDocumentReceipt | void>;
wireContract?: CultNetWireContract;
sessionTimeoutMs?: number;
}
@ -4112,6 +4126,10 @@ export interface CultMeshRudpDocumentPublishOptions extends CultMeshRudpPeerOpti
flushTimeoutMs?: number;
}
export interface CultMeshRudpDocumentReceiptOptions extends CultMeshRudpDocumentPublishOptions {
receiptTimeoutMs?: number;
}
export interface CultMeshRudpEndpoint {
host: string;
port: number;
@ -5638,7 +5656,18 @@ export class CultMesh {
return;
case "cultnet.document_put_raw.v0":
if (options.onDocumentPutRaw) {
await options.onDocumentPutRaw(normalizeRudpDocumentPut(message, record.remote));
const receipt = await options.onDocumentPutRaw(normalizeRudpDocumentPut(message, record.remote));
if (receipt) {
sendSchemaMessage(record, options.documents.createRawDocumentPutMessage(
receipt.binding, receipt.messageId ?? message.messageId, receipt.recordKey, receipt.value,
{
sourceRuntimeId: receipt.sourceRuntimeId ?? runtimeId,
sourceAgentId: receipt.sourceAgentId,
sourceRole: receipt.sourceRole ?? "provider",
tags: receipt.tags ?? ["receipt"],
},
));
}
}
return;
default:
@ -5753,6 +5782,36 @@ export class CultMesh {
}
}
public static async publishRudpDocumentAndWaitForReceipt<TDefinition extends AnyCultCacheDocumentDefinition, TReceiptDefinition extends AnyCultCacheDocumentDefinition>(
runtimeId: string, connectionId: number, endpoint: string | CultMeshRudpEndpoint,
binding: CultNetDocumentBinding<TDefinition>, recordKey: string, value: CultCacheDocumentValue<TDefinition>,
receiptBinding: CultNetDocumentBinding<TReceiptDefinition>, options: CultMeshRudpDocumentReceiptOptions = {},
): Promise<CultCacheDocumentValue<TReceiptDefinition>> {
requireNonEmpty(runtimeId, "runtimeId");
requireNonEmpty(recordKey, "recordKey");
const messageId = options.messageId ?? `${runtimeId}:${binding.definition.type}:${recordKey}`;
const peer = await CultMesh.createRudpPeer(runtimeId, connectionId, endpoint, options);
try {
const receipt = new Promise<CultCacheDocumentValue<TReceiptDefinition>>((resolve, reject) => {
const timeout = setTimeout(() => { cleanup(); reject(new Error(`Timed out waiting for RUDP application receipt for ${recordKey}.`)); }, options.receiptTimeoutMs ?? 5_000);
const onMessage = (message: CultNetMessage): void => {
if (message.schemaVersion !== "cultnet.document_put_raw.v0" || message.messageId !== messageId || message.document.schemaId !== receiptBinding.definition.schemaId) return;
try {
const decoded = decode(new Uint8Array(message.document.payload));
cleanup();
resolve(receiptBinding.definition.schema.parse(decoded) as CultCacheDocumentValue<TReceiptDefinition>);
} catch (error) { cleanup(); reject(error); }
};
const cleanup = (): void => { clearTimeout(timeout); peer.off("message", onMessage); };
peer.on("message", onMessage);
});
peer.send(new CultNetDocumentRegistry([binding]).createRawDocumentPutMessage(binding, messageId, recordKey, value, {
sourceRuntimeId: options.sourceRuntimeId ?? runtimeId, sourceAgentId: options.sourceAgentId, sourceRole: options.sourceRole, tags: options.tags,
}));
return await receipt;
} finally { peer.close(); }
}
public static async createRudpServer(
runtimeId: string,
connectionId: number,

View file

@ -34,6 +34,13 @@ const noteAliasDocument = defineDocumentType({
name: "noteId",
});
const noteReceiptDocument = defineDocumentType({
type: "cultmesh.note_receipt",
schemaId: "cultmesh.note_receipt.v0",
schema: z.object({ commandId: z.string(), state: z.literal("applied"), body: z.string() }),
name: "commandId",
});
const foreignNoteDocument = defineDocumentType({
type: "runtime.generated.cultmesh.note",
schemaId: "runtime.generated.cultmesh.note.ui.42",
@ -2568,6 +2575,29 @@ test("CultMesh TS publishes one registered document to a RUDP catalog", async ()
}
});
test("CultMesh TS returns a correlated provider receipt after a RUDP document mutation", async () => {
const connectionId = 0x10203050;
const commandBinding = defineCultNetDocumentBinding({ definition: noteDocument });
const receiptBinding = defineCultNetDocumentBinding({ definition: noteReceiptDocument });
const server = CultMesh.createRudpDocumentServer("cultmesh-ts-receipt-server", connectionId, {
documents: new CultNetDocumentRegistry([commandBinding, receiptBinding]), bindHost: "127.0.0.1", bindPort: 0,
resendDelayMs: 25, resendPollMs: 5,
onDocumentPutRaw: async (document) => ({
binding: receiptBinding, recordKey: document.recordKey,
value: { commandId: document.recordKey, state: "applied", body: (document.payload as { body: string }).body },
}),
});
try {
await server.start();
const receipt = await CultMesh.publishRudpDocumentAndWaitForReceipt(
"cultmesh-ts-receipt-client", connectionId, `rudp://127.0.0.1:${server.bind.port}`,
commandBinding, "command:note", { noteId: "command:note", body: "provider applied this" }, receiptBinding,
{ messageId: "command:note", receiptTimeoutMs: 1_000, resendPollMs: 5 },
);
assert.deepEqual(receipt, { commandId: "command:note", state: "applied", body: "provider applied this" });
} finally { server.close(); }
});
test("CultMesh TS reads remote RUDP snapshots through document handles", async () => {
const connectionId = 0x10203048;
const node = await CultMesh.startNode(