Let the browser sample read the provider's advertisement
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>
This commit is contained in:
parent
f94f5581b0
commit
89687fceb4
3 changed files with 59 additions and 3 deletions
|
|
@ -14,6 +14,8 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
|
||||
const string counterKey = "counter:main";
|
||||
const string surfaceKey = "sample.counter";
|
||||
const string providerKey = "sample.counter-ui";
|
||||
const string commandBoundary = "sample.counter.commands";
|
||||
var arguments = Args.Parse(args);
|
||||
if (arguments.Mode == "provider")
|
||||
{
|
||||
|
|
@ -89,7 +91,7 @@ static async Task RunOdinAsync(Args arguments)
|
|||
|
||||
static async Task RunProviderAsync(Args arguments)
|
||||
{
|
||||
var documentTypes = new[] { typeof(CounterState), typeof(EveSurfaceDocument) };
|
||||
var documentTypes = new[] { typeof(CounterState), typeof(EveSurfaceDocument), typeof(EveProviderAdvertisementDocument) };
|
||||
var registry = CultMesh.CreateCultCacheDocumentRegistry(documentTypes);
|
||||
var documents = CultMesh.CreateCultNetDocumentRegistry(documentTypes, registry);
|
||||
using var node = await CultMesh.CreateNodeAsync(arguments.StatePath, new CultMeshNodeOptions
|
||||
|
|
@ -111,6 +113,10 @@ static async Task RunProviderAsync(Args arguments)
|
|||
await node.Database.PutAsync(new CultRecordKey(counterKey), CounterState.Initial(counterKey));
|
||||
if (node.Cache.Get<EveSurfaceDocument>(new CultRecordKey(surfaceKey)) == null)
|
||||
await node.Database.PutAsync(new CultRecordKey(surfaceKey), CreateCounterSurface());
|
||||
// The advertisement is how a lowering learns where this surface's commands
|
||||
// go and what comes back. Eve refuses to build a command intent without it,
|
||||
// and the browser must not have to restate it: the provider is the owner.
|
||||
await node.Database.PutAsync(new CultRecordKey(providerKey), CreateProviderAdvertisement(arguments));
|
||||
await node.FlushAsync();
|
||||
|
||||
await using var schemaServer = new CultNetWebSocketSchemaServer();
|
||||
|
|
@ -341,6 +347,43 @@ static void ForceCollection()
|
|||
GC.Collect();
|
||||
}
|
||||
|
||||
static EveProviderAdvertisementDocument CreateProviderAdvertisement(Args arguments)
|
||||
{
|
||||
var worldInteraction = new EveWorldInteractionAdvertisement(
|
||||
projectionKind: "surface-state",
|
||||
stateSchemas: ["sample.counter_state.v1"],
|
||||
commandBoundary: commandBoundary,
|
||||
commandRecordRef: "",
|
||||
receiptSchema: EveCommandReceiptDocument.SchemaId,
|
||||
receiptRecordRef: "",
|
||||
assetManifestRecordRef: "",
|
||||
loweringTargets: ["browser"],
|
||||
ownership: arguments.AuthorityRuntimeId);
|
||||
return new EveProviderAdvertisementDocument(
|
||||
providerId: providerKey,
|
||||
serviceId: arguments.VerseId,
|
||||
verseId: arguments.VerseId,
|
||||
title: "CultMesh browser counter",
|
||||
kind: "sample.daemon",
|
||||
cultMeshAddress: arguments.AuthorityRuntimeId,
|
||||
updatedAtUtc: DateTime.UtcNow.ToString("O"),
|
||||
freshness: new EveProviderFreshness("live", DateTime.UtcNow.ToString("O"), 0),
|
||||
schemas: ["sample.counter_state.v1", EveSurfaceDocument.SchemaId, EveCommandReceiptDocument.SchemaId],
|
||||
witnesses: [],
|
||||
surfaces:
|
||||
[
|
||||
new EveAdvertisedSurface(
|
||||
surfaceKey,
|
||||
EveSurfaceDocument.SchemaId,
|
||||
surfaceKey,
|
||||
"cultmesh",
|
||||
"live",
|
||||
"surface",
|
||||
worldInteraction)
|
||||
],
|
||||
commands: [new EveAdvertisedCommand("sample.counter.increment", surfaceKey, "cultmesh", "Increment")]);
|
||||
}
|
||||
|
||||
static EveSurfaceDocument CreateCounterSurface()
|
||||
{
|
||||
var networkRoute = new CultMeshRouteHint(CultMeshLocalityKind.Network, "cultmesh");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { parseEveCommandReceipt, parseEveSurfaceDocument } from "@gamecult/eve-contracts";
|
||||
import { parseEveCommandReceipt, parseEveProviderAdvertisement, parseEveSurfaceDocument } from "@gamecult/eve-contracts";
|
||||
import { renderEveSurface } from "@gamecult/eve-browser-lowering";
|
||||
import {
|
||||
CultMeshBrowserClient,
|
||||
|
|
@ -52,14 +52,23 @@ try {
|
|||
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.command,
|
||||
operation: intent.operation.operationId,
|
||||
payloadSchema: "sample.increment.v1",
|
||||
payload: { amount: 1 },
|
||||
idempotencyKey: window.__sampleCommandId,
|
||||
|
|
|
|||
|
|
@ -107,6 +107,10 @@ try {
|
|||
|
||||
browser = await chromium.launch({ executablePath: resolveChromiumExecutable(), headless: true });
|
||||
let page = await browser.newPage();
|
||||
// A command the lowering refuses is caught inside the page; without this
|
||||
// the only symptom is a 30 s timeout on the receipt.
|
||||
page.on("console", message => console.log(`[browser:${message.type()}] ${message.text()}`));
|
||||
page.on("pageerror", error => console.log(`[browser:pageerror] ${error.message}`));
|
||||
const url = `http://127.0.0.1:${httpPort}/?odin=${encodeURIComponent(odinEndpoint)}&token=${token}`;
|
||||
await page.goto(url);
|
||||
await page.waitForFunction(() => window.__sampleReady || window.__sampleError);
|
||||
|
|
|
|||
Loading…
Reference in a new issue