diff --git a/src/execution-world/client-contracts.ts b/src/execution-world/client-contracts.ts index 8a9cd17..78ce3ce 100644 --- a/src/execution-world/client-contracts.ts +++ b/src/execution-world/client-contracts.ts @@ -112,7 +112,7 @@ export function generateInterfaceClientBindings( .filter((op) => op.mode === "call") .map( (op) => - `${JSON.stringify(op.displayName)}: defineInterfaceOperation<${tsType(op.inputType)}, ${tsType(op.outputType)}>(${JSON.stringify(publicOperationContract(iface, op))})`, + `${JSON.stringify(op.displayName)}: defineInterfaceOperation<${tsType(op.inputType)}, ${tsType(op.outputType)}, ${JSON.stringify(publicOperationContract(iface, op).execution)}>(${JSON.stringify(publicOperationContract(iface, op))})`, ); return `${JSON.stringify(member.displayName)}: {${operations.join(",\n")}}`; }); @@ -120,3 +120,14 @@ export function generateInterfaceClientBindings( }); return `// Generated from imported interface contracts. Regenerate; do not edit.\nimport { defineInterfaceOperation, type PreparedChange, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const interfaces = {${entries.join(",\n")}} as const;\n`; } + +export function generatePackageQueryClientBindings( + pkg: PackageRevision, + runtimeModule = "@quixos/camino-replica-core", +): string { + const queries = (pkg.checkedQueries ?? []).map( + (query) => + `${JSON.stringify(query.declaration.displayName)}: definePackageQuery<${tsType(query.variables)}, ${tsType(query.output)}>(${JSON.stringify(publicQueryContract(pkg, query.declaration.id))})`, + ); + return `// Generated from checked package query contracts. Regenerate; do not edit.\nimport { definePackageQuery, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const queries = {${queries.join(",\n")}} as const;\n`; +} diff --git a/src/execution-world/installation.ts b/src/execution-world/installation.ts index 3d595a8..6c43ef7 100644 --- a/src/execution-world/installation.ts +++ b/src/execution-world/installation.ts @@ -9,6 +9,7 @@ import { type PublicOperationContract, type PublicQueryContract, } from "./client-contracts.js"; +import { resultInterfaces } from "./projections.js"; /** Activation is assigned by graph authority, never by a package artifact. */ export interface ActiveExecutionIdentity { @@ -23,6 +24,7 @@ export interface ExecutionGroupIdentity { readonly executionDigest: string; } export interface ClientContractCatalog { + readonly resultInterfaces: Readonly>; readonly operations: readonly PublicOperationContract[]; readonly queries: readonly PublicQueryContract[]; readonly components: readonly InstalledComponent[]; @@ -58,6 +60,9 @@ export function clientContractCatalog( ), ); return { + resultInterfaces: Object.fromEntries( + plan.definition.operations.map((operation) => [operation.key, resultInterfaces(plan, operation.key)]), + ), operations, queries: packages.flatMap((pkg) => (pkg.checkedQueries ?? []).map((q) => publicQueryContract(pkg, q.declaration.id)), diff --git a/test/client-contracts.test.ts b/test/client-contracts.test.ts index 5adda40..2af458b 100644 --- a/test/client-contracts.test.ts +++ b/test/client-contracts.test.ts @@ -66,7 +66,7 @@ test("one imported operation contract links to two atom implementations without assert.equal(linkInterfaceOperation(plan, "one", contract).atomId, "one"); assert.equal(linkInterfaceOperation(plan, "two", contract).atomId, "two"); const generated = generateInterfaceClientBindings({ Data: iface }); - assert.ok(generated.includes("defineInterfaceOperation")); + assert.ok(generated.includes("defineInterfaceOperation linkInterfaceOperation(plan, "absent", contract), /UNINSTALLED_OPERATION/); diff --git a/test/emit-managed.ts b/test/emit-managed.ts index 5688fde..69b31c8 100644 --- a/test/emit-managed.ts +++ b/test/emit-managed.ts @@ -4,6 +4,11 @@ import { mkdir, writeFile } from "node:fs/promises"; import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js"; import { compileExecutionPlan } from "../src/execution-world/plan.js"; import { compileKernelProgram } from "../src/execution-world/kernel.js"; +import { compileQuery } from "../src/query/compile.js"; +import { + generateInterfaceClientBindings, + generatePackageQueryClientBindings, +} from "../src/execution-world/client-contracts.js"; import { compileQueryProgram } from "../src/execution-world/queries.js"; const source = { repository: "https://example.test/device-owner.git", commit: "a".repeat(40) }; const factory = compileCapabilityResourceSource( @@ -16,21 +21,83 @@ interface EntryFactory id "interface:entry-factory" revision "interface:entry-fa { source }, ); assert.ok(factory.ok && factory.resource.kind === "interface", JSON.stringify(factory.diagnostics)); -for (const variant of ["base", "added", "gateway", "gateway-next"]) { - const gateway = variant.startsWith("gateway"), - commit = { base: "a", added: "b", gateway: "c", "gateway-next": "d" }[variant]!.repeat(40); +const data = compileCapabilityResourceSource( + `interface Data id "interface:data" revision "interface:data@1" { + queryable value label id "label" : string { + get id "label:get" execution snapshot targets [browser, server]; + set id "label:set" execution transaction targets [browser, server]; + } +}`, + { source }, +); +const constructors = compileCapabilityResourceSource( + `external atom Entry id "atom:entry"; external atom Independent id "atom:independent"; +interface Factories id "interface:factories" revision "interface:factories@1" { + static operation createOne id "create-one" : string -> atom-ref { call id "create-one:call" execution transaction targets [browser, server]; } + static operation createTwo id "create-two" : unit -> atom-ref { call id "create-two:call" execution transaction targets [browser, server]; } +}`, + { source }, +); +assert.ok(data.ok && data.resource.kind === "interface", JSON.stringify(data.diagnostics)); +assert.ok(constructors.ok && constructors.resource.kind === "interface", JSON.stringify(constructors.diagnostics)); +const dataInterface = data.resource.revision, + factoriesInterface = constructors.resource.revision; +for (const variant of ["base", "added", "gateway", "gateway-next", "session"]) { + const session = variant === "session", + gateway = session || variant.startsWith("gateway"), + commit = { base: "a", added: "b", gateway: "c", "gateway-next": "d", session: "e" }[variant]!.repeat(40); const pkg = compileCapabilityResourceSource( `${gateway ? 'external atom Entry id "atom:entry";' : ""} +${session ? 'external atom Independent id "atom:independent"; import interface Data;' : ""} package Owner id "package:owner" revision "package:owner@1" { function identity id "identity" : int32 -> record { pid: int32; child: int32; } execution remote; ${gateway ? 'function create id "create" : unit -> atom-ref execution transaction targets [server] requires { creation entry id "entry:birth" : Entry input unit; };' : ""} + ${ + session + ? `function createOne id "create-one" : string -> atom-ref execution transaction targets [browser, server] requires { creation entry id "one:birth" : Entry input unit; }; + function createTwo id "create-two" : unit -> atom-ref execution transaction targets [browser, server] requires { creation entry id "two:birth" : Independent input unit; }; + query Inputs id "inputs" root workspace Data document "inputs.graphql" operation "Inputs" { replica; } + query View id "view" root workspace Data document "view.graphql" operation "View" { local from Inputs; watch; } + query EditorInputs id "editor-inputs" root Data document "editor.graphql" operation "Editor" { replica; }` + : "" + } }`, - { source }, + { + source, + ...(session + ? { environment: { interfaces: new Map([["Data", dataInterface]]), interfaceClosure: [dataInterface] } } + : {}), + }, ); assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics)); + if (session) + pkg.resource.revision.checkedQueries = await Promise.all( + pkg.resource.revision.queries!.map((query) => + compileQuery( + query, + [dataInterface], + async (name) => + ({ + "inputs.graphql": + "query Inputs($label: String!) { root { objects(all: 100, where: {label: {eq: $label}}) { entries { key node { _qx {ref} label } } } } }", + "view.graphql": + "query View { root { availableObjects(all: 100) { entries { key node { _qx {ref} label } } } } }", + "editor.graphql": "query Editor { root { _qx {ref} label } }", + })[name]!, + ), + ), + ); const result = compileCapabilitySource( `workspace Managed id "workspace:managed-process" revision "workspace:managed-process@${variant}" commit "${commit}" { import package Owner; + ${ + session + ? `import interface Data; import interface Factories; + conform Entry as Factories id "owner:factories" { bind createOne.call to package Owner.createOne with { entry to creation Entry with {}; }; bind createTwo.call to package Owner.createTwo with { entry to creation Independent with {}; }; } + ${["Entry", "Independent"].map((atom) => `conform ${atom} as Data id "owner:${atom}:data" { private state ${atom}Label id "label:${atom}" on ${atom} : string policy register default "${atom}"; bind label.get to state ${atom}Label.read; bind label.set to state ${atom}Label.write; }`).join("\n")} + ` + : "" + } ${variant !== "base" ? 'atom Independent id "atom:independent";' : ""} ${ gateway @@ -47,8 +114,16 @@ package Owner id "package:owner" revision "package:owner@1" { packageClosure: [pkg.resource.revision], ...(gateway ? { - interfaces: new Map([["EntryFactory", factory.resource.revision]]), - interfaceClosure: [factory.resource.revision], + interfaces: new Map([ + ["EntryFactory", factory.resource.revision], + ...(session + ? ([ + ["Data", dataInterface], + ["Factories", factoriesInterface], + ] as const) + : []), + ]), + interfaceClosure: [factory.resource.revision, ...(session ? [dataInterface, factoriesInterface] : [])], } : {}), }, @@ -57,6 +132,13 @@ package Owner id "package:owner" revision "package:owner@1" { const plan = compileExecutionPlan(result.workspace), root = `dist/managed/${variant}`; await mkdir(root, { recursive: true }); + if (session) { + await writeFile( + `${root}/client.ts`, + generateInterfaceClientBindings({ Data: dataInterface, Factories: factoriesInterface }), + ); + await writeFile(`${root}/queries.ts`, generatePackageQueryClientBindings(pkg.resource.revision)); + } for (const [file, value] of Object.entries({ "candidate.json": result.workspace, "entry.json": gateway