Eve refuses to build a command intent for a surface that has not
advertised a command boundary and receipt schema, and the intent it
builds now carries the operation under operation.operationId. The
browser sample did neither: the click was refused inside the page and
the checkpoint saw only a 30 s timeout on the receipt.
The C# provider now publishes its gamecult.eve.provider_advertisement.v1
document with the surface's worldInteraction, and the page leases that
and hands it to the lowering as `provider`. The provider owns the
declaration; the page reads it rather than restating it. The command
sink reads intent.operation.operationId.
The checkpoint now forwards the page's console and pageerror output.
"CultMesh browser operation is required." took one run to find with it
and had been a bare timeout without it.
scripts/verify-eve-getting-started.ps1 -EveRoot ..\Eve (Eve main)
-> Eve getting-started verification passed: clean .NET and TypeScript
artifacts, DOM and headless convergence, real Chromium and C#
clients, Odin discovery, authority-bound routes, receipts,
persistence, and route replacement.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
106 lines
4.1 KiB
TypeScript
106 lines
4.1 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";
|
|
|
|
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 () => decodeCultNetPayload<{ count: number }>(counter.current!).count,
|
|
watch: callback => counter.watch(record => {
|
|
if (!record) return;
|
|
const count = decodeCultNetPayload<{ count: number }>(record).count;
|
|
window.__sampleCount = count;
|
|
callback(count);
|
|
}),
|
|
};
|
|
},
|
|
});
|
|
window.__sampleCount = decodeCultNetPayload<{ count: number }>(counter.current!).count;
|
|
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;
|
|
}
|