CultLib/samples/eve-browser-network/browser/main.ts
Meta Vens 0408768d35 Give the counter sample integer MessagePack keys
CounterState in samples/eve-browser-network used string [Key]s, which the
registry rejects at registration, so the provider threw on open. Keys are now
0 counterId, 1 count, 2 receipts. The payload becomes a MessagePack array, so
the browser lowerer reads count positionally instead of by field name. The
getting-started chapter carried the same string-key shape and is corrected to
match.

BenchItem needed nothing: a5d1be6 already marked it [MessagePackObject]; the
earlier scan rejection came from a stale benchmark build. After rebuilding,
both types register.

No .cc stores exist under samples.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 00:27:19 +02:00

110 lines
4.2 KiB
TypeScript

import { parseEveCommandReceipt, parseEveProviderAdvertisement, parseEveSurfaceDocument } from "@gamecult/eve-contracts";
import { renderEveSurface } from "@gamecult/eve-browser-lowering";
import {
CultMeshBrowserClient,
CultMeshBrowserOdinRendezvous,
decodeCultNetOperationPayload,
decodeCultNetPayload,
} from "cultmesh-browser";
// CounterState is an integer-keyed MessagePack object: [counterId, count, receipts].
const counterCount = (record: Parameters<typeof decodeCultNetPayload>[0]) =>
decodeCultNetPayload<[string, number, unknown]>(record)[1];
declare global {
interface Window {
__sampleReady?: boolean;
__sampleCount?: number;
__sampleReceipt?: { receiptId: string; count: number };
__sampleCommandId?: string;
__sampleConnectionStates?: string[];
__sampleError?: string;
}
}
const params = new URLSearchParams(location.search);
const odinEndpoint = params.get("odin");
const token = params.get("token") || "sample-session";
if (!odinEndpoint) throw new Error("The sample requires an Odin endpoint query parameter.");
document.cookie = `cultnet_session=${encodeURIComponent(token)}; Path=/; SameSite=Strict`;
const host = document.querySelector<HTMLElement>("#surface")!;
const receiptOutput = document.querySelector<HTMLOutputElement>("#receipt")!;
try {
const mesh = await CultMeshBrowserClient.connect({
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
runtimeId: "sample.chromium",
trust: { mode: "local-development" },
rendezvous: new CultMeshBrowserOdinRendezvous({
endpoints: [odinEndpoint],
runtimeId: "sample.chromium.discovery",
transportVersion: "cultmesh.v0",
}),
});
window.__sampleConnectionStates = [];
mesh.watchState(state => window.__sampleConnectionStates!.push(state));
window.__sampleCommandId = "browser-click-1";
const counter = await mesh.leaseRawDocument({
schemaId: "sample.counter_state.v1",
recordKey: "counter:main",
subscriptionId: "browser-counter",
});
const surface = await mesh.leaseRawDocument({
schemaId: "gamecult.eve.surface.v1",
recordKey: "sample.counter",
subscriptionId: "browser-surface",
});
// The provider owns the declaration of where this surface's commands go and
// what comes back; the page reads it rather than restating it.
const advertisement = await mesh.leaseRawDocument({
schemaId: "gamecult.eve.provider_advertisement.v1",
recordKey: "sample.counter-ui",
subscriptionId: "browser-provider",
});
const provider = parseEveProviderAdvertisement(decodeCultNetPayload(advertisement.current!));
const currentSurface = parseEveSurfaceDocument(decodeCultNetPayload(surface.current!));
renderEveSurface(currentSurface, host, {
activeSurfaceId: "sample.counter",
clientId: "sample.chromium",
provider,
commandSink: async intent => {
const response = await mesh.invoke({
serviceId: "sample.counter",
operation: intent.operation.operationId,
payloadSchema: "sample.increment.v1",
payload: { amount: 1 },
idempotencyKey: window.__sampleCommandId,
});
const receipt = parseEveCommandReceipt(decodeCultNetOperationPayload(response)) as {
receiptId: string;
count: number;
};
window.__sampleReceipt = receipt;
receiptOutput.textContent = receipt.receiptId;
},
stateBindingResolver: async binding => {
if (binding.pointerId !== "sample.counter.count") return undefined;
return {
latest: async () => counterCount(counter.current!),
watch: callback => counter.watch(record => {
if (!record) return;
const count = counterCount(record);
window.__sampleCount = count;
callback(count);
}),
};
},
});
window.__sampleCount = counterCount(counter.current!);
window.__sampleReady = true;
} catch (error) {
const details = error instanceof AggregateError
? error.errors.map(inner => inner instanceof Error ? inner.stack || inner.message : String(inner)).join("\n---\n")
: "";
window.__sampleError = (error instanceof Error ? error.stack || error.message : String(error))
+ (details ? `\n${details}` : "");
host.textContent = window.__sampleError;
throw error;
}