Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c018dc9a1 | ||
|
|
9295278531 |
3 changed files with 408 additions and 2 deletions
317
package-lock.json
generated
Normal file
317
package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue