From d8fdf6a27500c2f25d439f18bb9fa8aad5b01fd0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 10 Jul 2026 12:38:47 +0100 Subject: [PATCH] Add generic CultMesh provider client --- README.md | 6 + src/cultmesh-provider-client.mjs | 199 +++++++++++++++++++++++++ src/index.d.ts | 13 ++ src/index.mjs | 1 + test/cultmesh-provider-client.test.mjs | 54 +++++++ 5 files changed, 273 insertions(+) create mode 100644 src/cultmesh-provider-client.mjs create mode 100644 test/cultmesh-provider-client.test.mjs diff --git a/README.md b/README.md index bc2e969..574373f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ Providers supply advertisements, surface and embedded-document reads, command submission, asset resolution, and any product daemon lifecycle. EveElectron does not import provider state or plugin semantic implementations. +`EveCultMeshProviderClient` takes an Odin-resolved peer target and advertisement +record reference, then follows only provider-published references for surfaces, +commands, receipts, and embedded documents. CultMesh and MessagePack are +constructor dependencies supplied from the application's installed CultLib +packages; the runtime owns protocol flow without pinning a sibling checkout. + ```powershell npm install npm test diff --git a/src/cultmesh-provider-client.mjs b/src/cultmesh-provider-client.mjs new file mode 100644 index 0000000..11e3380 --- /dev/null +++ b/src/cultmesh-provider-client.mjs @@ -0,0 +1,199 @@ +const providerSchema = "gamecult.eve.provider_advertisement.v1"; +const commandSchema = "gamecult.eve.command.v1"; +const connectionId = 0x45564545; + +export class EveCultMeshProviderClient { + #peer = null; + + constructor(target, dependencies, options = {}) { + this.target = normalizeTarget(target); + this.mesh = requireFunctionSet(dependencies?.CultMesh, [ + "createAuthorityLeaseCatalog", + "createPeerCatalog", + "createRudpPeerForAuthorizedPeer", + "documentFromPublication", + "verse", + ], "CultMesh"); + if (typeof dependencies?.encode !== "function") throw new Error("A MessagePack encode function is required."); + this.encode = dependencies.encode; + this.runtimeId = options.runtimeId || "eve-electron"; + this.timeoutMs = options.timeoutMs ?? 5000; + this.context = this.mesh.verse("eve.remote", this.runtimeId).context; + } + + async close() { + this.#peer?.close(); + this.#peer = null; + } + + async providerAdvertisement() { + const value = await this.#document(providerSchema, this.target.advertisementRecordRef, "provider"); + const advertisement = normalizeProviderAdvertisement(value); + if (this.target.providerId && advertisement.providerId !== this.target.providerId) { + throw new Error(`Resolved provider ${advertisement.providerId} does not match ${this.target.providerId}.`); + } + return advertisement; + } + + async surface(surfaceId = "") { + const advertisement = await this.providerAdvertisement(); + const advertised = selectSurface(advertisement, surfaceId); + const document = normalizeSurfaceDocument(await this.#document(advertised.schema, advertised.recordRef, "surface")); + if (document.surface.id !== advertised.surfaceId) { + throw new Error(`Surface record ${advertised.recordRef} published ${document.surface.id}, expected ${advertised.surfaceId}.`); + } + return document; + } + + async document({ schemaId, recordRef }) { + if (!schemaId || !recordRef) throw new Error("Embedded documents require schemaId and recordRef."); + return this.#document(schemaId, recordRef, "embedded-document"); + } + + async submitCommand(request) { + const advertisement = await this.providerAdvertisement(); + const surface = selectSurface(advertisement, request?.surfaceId); + const interaction = surface.worldInteraction; + if (!interaction?.commandRecordRef) throw new Error(`Surface ${surface.surfaceId} advertises no command record reference.`); + const command = String(request?.command || "").trim(); + if (!command) throw new Error("Command is required."); + const issuedAtUtc = request.issuedAtUtc || new Date().toISOString(); + const commandId = request.commandId || `${this.runtimeId}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`; + const payload = [commandSchema, commandId, advertisement.providerId, surface.surfaceId, command, issuedAtUtc, request.clientId || this.runtimeId, objectValue(request.payload)]; + const peer = await this.#getPeer(); + peer.send({ + schemaVersion: "cultnet.document_put_raw.v0", + messageId: commandId, + document: { + schemaId: commandSchema, + recordKey: childRecordRef(interaction.commandRecordRef, commandId), + storedAt: issuedAtUtc, + payloadEncoding: "messagepack", + payload: this.encode(payload), + sourceRuntimeId: this.runtimeId, + sourceRole: "eve-runtime", + tags: ["eve", "surface-command"], + }, + }); + return { + commandId, + providerId: advertisement.providerId, + surfaceId: surface.surfaceId, + state: "submitted", + receiptSchema: interaction.receiptSchema, + receiptRecordRef: interaction.receiptRecordRef + ? childRecordRef(interaction.receiptRecordRef, commandId) + : "", + }; + } + + async receipt(submission) { + if (!submission?.receiptSchema || !submission?.receiptRecordRef) { + throw new Error("The command submission has no advertised receipt location."); + } + return this.#document(submission.receiptSchema, submission.receiptRecordRef, "receipt"); + } + + async #document(schemaId, recordRef, kind) { + const handle = this.mesh.documentFromPublication( + { kind: "peer-snapshot", peer: () => this.#getPeer(), endpoint: this.#endpoint() }, + schemaId, + recordRef, + { + documentId: recordRef, + routeHint: this.context.routeHint, + sourceId: recordRef, + timeoutMs: this.timeoutMs, + pollMs: 100, + messageIdPrefix: `${this.runtimeId}:${kind}`, + }, + ); + return handle.latest(this.context); + } + + async #getPeer() { + if (this.#peer) return this.#peer; + const peers = this.mesh.createPeerCatalog(); + const leases = this.mesh.createAuthorityLeaseCatalog(); + const leaseId = `${this.target.peerId}:authority`; + peers.upsert({ peerId: this.target.peerId, verseId: this.target.verseId, endpoints: this.target.endpoints, roles: [this.target.role], authorityLeaseId: leaseId }); + leases.upsert({ leaseId, verseId: this.target.verseId, peerId: this.target.peerId, roles: [this.target.role], validFrom: new Date(Date.now() - 60000), expiresAt: new Date(Date.now() + 60000) }); + this.#peer = await this.mesh.createRudpPeerForAuthorizedPeer(this.runtimeId, connectionId, peers, leases, this.target.verseId, this.target.role, { connectTimeoutMs: this.timeoutMs, maxFragmentBytes: 1200, maxPendingReliablePackets: 512 }); + this.#peer.on?.("close", () => { this.#peer = null; }); + return this.#peer; + } + + #endpoint() { + const endpoint = this.target.endpoints.find(value => value.toLowerCase().startsWith("rudp://")); + if (!endpoint) throw new Error(`Provider ${this.target.providerId || this.target.peerId} has no Odin-resolved RUDP endpoint.`); + return endpoint; + } +} + +export function normalizeProviderAdvertisement(value) { + const providerId = stringField(value, "providerId", 1); + if (!providerId) throw new Error("Provider advertisement is missing providerId."); + const surfaces = arrayField(value, "surfaces", 11).map(normalizeAdvertisedSurface).filter(surface => surface.surfaceId); + return { + schema: stringField(value, "schema", 0), providerId, + serviceId: stringField(value, "serviceId", 2), verseId: stringField(value, "verseId", 3), + title: stringField(value, "title", 4), kind: stringField(value, "kind", 5), + cultMeshAddress: stringField(value, "cultMeshAddress", 6), surfaces, + }; +} + +export function normalizeSurfaceDocument(value) { + const portable = stringField(value, "schema", 1) === "gamecult.eve.surface.v1"; + const surface = field(value, "surface", portable ? 7 : 5); + const id = stringField(surface, "id", 0); + if (!id) throw new Error("Eve surface document is missing surface.id."); + return { + schema: "gamecult.eve.surface.v1", + providerId: stringField(value, "providerId", portable ? 2 : 0), + providerKind: stringField(value, "providerKind", portable ? 3 : 1), + title: stringField(value, "title", portable ? 4 : 2), + version: numberField(value, "version", portable ? 5 : 3), + updatedAtUtc: stringField(value, "updatedAtUtc", portable ? 6 : 4), + surface: { id, root: field(surface, "root", 1), styles: arrayField(surface, "styles", 2) }, + commands: arrayField(value, "commands", portable ? 8 : 6), + }; +} + +function normalizeAdvertisedSurface(value) { + const interaction = field(value, "worldInteraction", 6); + return { + surfaceId: stringField(value, "surfaceId", 0), schema: stringField(value, "schema", 1), + recordRef: stringField(value, "recordRef", 2), transport: stringField(value, "transport", 3), + status: stringField(value, "status", 4), surfaceKind: stringField(value, "surfaceKind", 5), + worldInteraction: interaction ? { + projectionKind: stringField(interaction, "projectionKind", 0), + stateSchemas: arrayField(interaction, "stateSchemas", 1), + commandBoundary: stringField(interaction, "commandBoundary", 2), + commandRecordRef: stringField(interaction, "commandRecordRef", 3), + receiptSchema: stringField(interaction, "receiptSchema", 4), + receiptRecordRef: stringField(interaction, "receiptRecordRef", 5), + assetManifestRecordRef: stringField(interaction, "assetManifestRecordRef", 6), + loweringTargets: arrayField(interaction, "loweringTargets", 7), ownership: stringField(interaction, "ownership", 8), + } : null, + }; +} + +function selectSurface(advertisement, surfaceId) { + const selected = surfaceId ? advertisement.surfaces.find(value => value.surfaceId === surfaceId) : advertisement.surfaces[0]; + if (!selected) throw new Error(`Provider ${advertisement.providerId} does not advertise surface ${surfaceId || "(first)"}.`); + if (selected.transport !== "cultmesh-record" || !selected.recordRef) throw new Error(`Surface ${selected.surfaceId} is not a readable CultMesh record.`); + return selected; +} + +function normalizeTarget(value) { + if (!value?.advertisementRecordRef) throw new Error("An Odin-resolved advertisementRecordRef is required."); + return { providerId: String(value.providerId || ""), advertisementRecordRef: String(value.advertisementRecordRef), peerId: String(value.peerId || "provider"), verseId: String(value.verseId || "provider"), role: String(value.role || "provider"), endpoints: Array.isArray(value.endpoints) ? value.endpoints.map(String) : [] }; +} + +function childRecordRef(parent, child) { return `${String(parent).replace(/\/$/u, "")}/${encodeURIComponent(child)}`; } +function field(value, name, slot) { return value && typeof value === "object" ? (value[name] ?? value[slot]) : undefined; } +function stringField(value, name, slot) { const result = field(value, name, slot); return typeof result === "string" ? result : ""; } +function numberField(value, name, slot) { const result = field(value, name, slot); return typeof result === "number" && Number.isFinite(result) ? result : 0; } +function arrayField(value, name, slot) { const result = field(value, name, slot); return Array.isArray(result) ? result : []; } +function objectValue(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } +function requireFunctionSet(value, names, label) { if (!value) throw new Error(`${label} is required.`); for (const name of names) if (typeof value[name] !== "function") throw new Error(`${label}.${name} is required.`); return value; } diff --git a/src/index.d.ts b/src/index.d.ts index ca52db2..ba42331 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,5 +1,18 @@ import type { BrowserWindow, BrowserWindowConstructorOptions, IpcMain, Shell } from "electron"; +export declare class EveCultMeshProviderClient { + constructor(target: object, dependencies: { CultMesh: object; encode: (value: unknown) => Uint8Array }, options?: { runtimeId?: string; timeoutMs?: number }); + close(): Promise; + providerAdvertisement(): Promise; + surface(surfaceId?: string): Promise; + document(request: { schemaId: string; recordRef: string }): Promise; + submitCommand(request: object): Promise; + receipt(submission: object): Promise; +} + +export declare function normalizeProviderAdvertisement(value: unknown): object; +export declare function normalizeSurfaceDocument(value: unknown): object; + export interface EveElectronWindowOptions extends BrowserWindowConstructorOptions { browserWindow?: BrowserWindowConstructorOptions; openExternal?: boolean; diff --git a/src/index.mjs b/src/index.mjs index c751d41..96e90c3 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1,2 +1,3 @@ export * from "./eve-electron-shell.mjs"; +export * from "./cultmesh-provider-client.mjs"; export * from "./window-host.mjs"; diff --git a/test/cultmesh-provider-client.test.mjs b/test/cultmesh-provider-client.test.mjs new file mode 100644 index 0000000..019fc4c --- /dev/null +++ b/test/cultmesh-provider-client.test.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { EveCultMeshProviderClient } from "../src/cultmesh-provider-client.mjs"; + +const advertisement = [ + "gamecult.eve.provider_advertisement.v1", "test-provider", "test.service", "test.verse", + "Test Provider", "game.runtime", "cultmesh://test/provider", "2026-07-10T00:00:00Z", + ["live", "2026-07-10T00:00:00Z", 15000], [], [], [[ + "world", "gamecult.eve.surface.v1", "records/world", "cultmesh-record", "available", "interactive-world", + ["provider-world", [], "commands", "records/commands", "gamecult.eve.command_receipt.v1", "records/receipts", "records/assets", ["electron-shell"], "provider owns truth"], + ]], [], +]; +const surface = ["surface-state", "gamecult.eve.surface.v1", "test-provider", "daemon", "World", 3, "now", ["world", { id: "root", kind: "world.scene", children: [] }, []], []]; + +function harness() { + const sent = []; + const records = new Map([["records/provider", advertisement], ["records/world", surface], ["records/receipts/cmd-1", { state: "accepted" }]]); + const peer = { send: message => sent.push(message), close() {}, on() {} }; + const mesh = { + createPeerCatalog: () => ({ upsert() {} }), createAuthorityLeaseCatalog: () => ({ upsert() {} }), + createRudpPeerForAuthorizedPeer: async () => peer, + documentFromPublication: (_source, _schema, recordRef) => ({ latest: async () => { + if (!records.has(recordRef)) throw new Error(`missing ${recordRef}`); + return records.get(recordRef); + } }), + verse: () => ({ context: { routeHint: { transport: "network" } } }), + }; + const client = new EveCultMeshProviderClient({ + providerId: "test-provider", advertisementRecordRef: "records/provider", peerId: "test-peer", + verseId: "test.verse", role: "provider", endpoints: ["rudp://127.0.0.1:7777"], + }, { CultMesh: mesh, encode: value => Uint8Array.from([value.length]) }, { runtimeId: "test-runtime" }); + return { client, sent }; +} + +test("follows provider-advertised record references", async () => { + const { client } = harness(); + const provider = await client.providerAdvertisement(); + assert.equal(provider.surfaces[0].recordRef, "records/world"); + assert.equal((await client.surface("world")).surface.id, "world"); +}); + +test("submits at the advertised command boundary and reads the advertised receipt", async () => { + const { client, sent } = harness(); + const submission = await client.submitCommand({ surfaceId: "world", command: "move", commandId: "cmd-1", payload: { x: 1 } }); + assert.equal(sent[0].document.recordKey, "records/commands/cmd-1"); + assert.equal(sent[0].document.sourceRole, "eve-runtime"); + assert.equal(submission.receiptRecordRef, "records/receipts/cmd-1"); + assert.deepEqual(await client.receipt(submission), { state: "accepted" }); +}); + +test("refuses unadvertised surfaces", async () => { + const { client } = harness(); + await assert.rejects(() => client.surface("missing"), /does not advertise/); +});