Add interface-oriented client sessions and headless capability access (WI-07)

This commit is contained in:
Timothy J. Aveni
2026-09-21 14:45:46 -07:00
parent d159d9b281
commit a80982a780
4 changed files with 106 additions and 8 deletions
+12 -1
View File
@@ -112,7 +112,7 @@ export function generateInterfaceClientBindings(
.filter((op) => op.mode === "call") .filter((op) => op.mode === "call")
.map( .map(
(op) => (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")}}`; 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`; 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`;
}
+5
View File
@@ -9,6 +9,7 @@ import {
type PublicOperationContract, type PublicOperationContract,
type PublicQueryContract, type PublicQueryContract,
} from "./client-contracts.js"; } from "./client-contracts.js";
import { resultInterfaces } from "./projections.js";
/** Activation is assigned by graph authority, never by a package artifact. */ /** Activation is assigned by graph authority, never by a package artifact. */
export interface ActiveExecutionIdentity { export interface ActiveExecutionIdentity {
@@ -23,6 +24,7 @@ export interface ExecutionGroupIdentity {
readonly executionDigest: string; readonly executionDigest: string;
} }
export interface ClientContractCatalog { export interface ClientContractCatalog {
readonly resultInterfaces: Readonly<Record<string, readonly string[]>>;
readonly operations: readonly PublicOperationContract[]; readonly operations: readonly PublicOperationContract[];
readonly queries: readonly PublicQueryContract[]; readonly queries: readonly PublicQueryContract[];
readonly components: readonly InstalledComponent[]; readonly components: readonly InstalledComponent[];
@@ -58,6 +60,9 @@ export function clientContractCatalog(
), ),
); );
return { return {
resultInterfaces: Object.fromEntries(
plan.definition.operations.map((operation) => [operation.key, resultInterfaces(plan, operation.key)]),
),
operations, operations,
queries: packages.flatMap((pkg) => queries: packages.flatMap((pkg) =>
(pkg.checkedQueries ?? []).map((q) => publicQueryContract(pkg, q.declaration.id)), (pkg.checkedQueries ?? []).map((q) => publicQueryContract(pkg, q.declaration.id)),
+1 -1
View File
@@ -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, "one", contract).atomId, "one");
assert.equal(linkInterfaceOperation(plan, "two", contract).atomId, "two"); assert.equal(linkInterfaceOperation(plan, "two", contract).atomId, "two");
const generated = generateInterfaceClientBindings({ Data: iface }); const generated = generateInterfaceClientBindings({ Data: iface });
assert.ok(generated.includes("defineInterfaceOperation<null, string>")); assert.ok(generated.includes("defineInterfaceOperation<null, string,"));
for (const privateValue of [plan.digest, "One:label", '"atomId"', '"binding"']) for (const privateValue of [plan.digest, "One:label", '"atomId"', '"binding"'])
assert.equal(generated.includes(privateValue), false); assert.equal(generated.includes(privateValue), false);
assert.throws(() => linkInterfaceOperation(plan, "absent", contract), /UNINSTALLED_OPERATION/); assert.throws(() => linkInterfaceOperation(plan, "absent", contract), /UNINSTALLED_OPERATION/);
+88 -6
View File
@@ -4,6 +4,11 @@ import { mkdir, writeFile } from "node:fs/promises";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js"; import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js";
import { compileExecutionPlan } from "../src/execution-world/plan.js"; import { compileExecutionPlan } from "../src/execution-world/plan.js";
import { compileKernelProgram } from "../src/execution-world/kernel.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"; import { compileQueryProgram } from "../src/execution-world/queries.js";
const source = { repository: "https://example.test/device-owner.git", commit: "a".repeat(40) }; const source = { repository: "https://example.test/device-owner.git", commit: "a".repeat(40) };
const factory = compileCapabilityResourceSource( const factory = compileCapabilityResourceSource(
@@ -16,21 +21,83 @@ interface EntryFactory id "interface:entry-factory" revision "interface:entry-fa
{ source }, { source },
); );
assert.ok(factory.ok && factory.resource.kind === "interface", JSON.stringify(factory.diagnostics)); assert.ok(factory.ok && factory.resource.kind === "interface", JSON.stringify(factory.diagnostics));
for (const variant of ["base", "added", "gateway", "gateway-next"]) { const data = compileCapabilityResourceSource(
const gateway = variant.startsWith("gateway"), `interface Data id "interface:data" revision "interface:data@1" {
commit = { base: "a", added: "b", gateway: "c", "gateway-next": "d" }[variant]!.repeat(40); 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<Entry> { call id "create-one:call" execution transaction targets [browser, server]; }
static operation createTwo id "create-two" : unit -> atom-ref<Independent> { 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( const pkg = compileCapabilityResourceSource(
`${gateway ? 'external atom Entry id "atom:entry";' : ""} `${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" { package Owner id "package:owner" revision "package:owner@1" {
function identity id "identity" : int32 -> record { pid: int32; child: int32; } execution remote; function identity id "identity" : int32 -> record { pid: int32; child: int32; } execution remote;
${gateway ? 'function create id "create" : unit -> atom-ref<Entry> execution transaction targets [server] requires { creation entry id "entry:birth" : Entry input unit; };' : ""} ${gateway ? 'function create id "create" : unit -> atom-ref<Entry> execution transaction targets [server] requires { creation entry id "entry:birth" : Entry input unit; };' : ""}
${
session
? `function createOne id "create-one" : string -> atom-ref<Entry> execution transaction targets [browser, server] requires { creation entry id "one:birth" : Entry input unit; };
function createTwo id "create-two" : unit -> atom-ref<Independent> 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)); 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( const result = compileCapabilitySource(
`workspace Managed id "workspace:managed-process" revision "workspace:managed-process@${variant}" commit "${commit}" { `workspace Managed id "workspace:managed-process" revision "workspace:managed-process@${variant}" commit "${commit}" {
import package Owner; 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";' : ""} ${variant !== "base" ? 'atom Independent id "atom:independent";' : ""}
${ ${
gateway gateway
@@ -47,8 +114,16 @@ package Owner id "package:owner" revision "package:owner@1" {
packageClosure: [pkg.resource.revision], packageClosure: [pkg.resource.revision],
...(gateway ...(gateway
? { ? {
interfaces: new Map([["EntryFactory", factory.resource.revision]]), interfaces: new Map([
interfaceClosure: [factory.resource.revision], ["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), const plan = compileExecutionPlan(result.workspace),
root = `dist/managed/${variant}`; root = `dist/managed/${variant}`;
await mkdir(root, { recursive: true }); 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({ for (const [file, value] of Object.entries({
"candidate.json": result.workspace, "candidate.json": result.workspace,
"entry.json": gateway "entry.json": gateway