diff --git a/samples/eve-browser-network/Program.cs b/samples/eve-browser-network/Program.cs index bfadd7db..68f72e05 100644 --- a/samples/eve-browser-network/Program.cs +++ b/samples/eve-browser-network/Program.cs @@ -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(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"); diff --git a/samples/eve-browser-network/browser/main.ts b/samples/eve-browser-network/browser/main.ts index 8251a0a3..b04ee5c8 100644 --- a/samples/eve-browser-network/browser/main.ts +++ b/samples/eve-browser-network/browser/main.ts @@ -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, diff --git a/scripts/verify-eve-browser-network.mjs b/scripts/verify-eve-browser-network.mjs index 8c865b1d..c1388712 100644 --- a/scripts/verify-eve-browser-network.mjs +++ b/scripts/verify-eve-browser-network.mjs @@ -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);