|
|
|
@@ -0,0 +1,68 @@
|
|
|
|
|
import { hexDigest, type ExecutionPlan } from "../execution-world/plan.js";
|
|
|
|
|
import { resultInterfaces } from "../execution-world/projections.js";
|
|
|
|
|
import { portableValueSchema, tsType, PortableBindingError } from "./portable.js";
|
|
|
|
|
/** Closed-world app descriptors. Handler bindings and application acquisition share the same public identities. */
|
|
|
|
|
export function generatePortableClientBindings(
|
|
|
|
|
plan: ExecutionPlan,
|
|
|
|
|
options: { target: string; runtimeModule?: string },
|
|
|
|
|
): string {
|
|
|
|
|
if (hexDigest(plan.definition) !== plan.digest) throw new PortableBindingError("client", "Invalid plan digest");
|
|
|
|
|
const workspace = plan.definition.workspace,
|
|
|
|
|
atoms = new Map<string, string[]>(),
|
|
|
|
|
unavailable: { key: string; reason: string }[] = [];
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
for (const operation of plan.definition.operations) {
|
|
|
|
|
if (operation.execution.kind === "remote" || !operation.execution.targets.includes(options.target)) {
|
|
|
|
|
unavailable.push({ key: operation.key, reason: "OPERATION_NOT_PORTABLE" });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let input: string, output: string;
|
|
|
|
|
try {
|
|
|
|
|
input = tsType(operation.input);
|
|
|
|
|
output = tsType(operation.output);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (!(error instanceof PortableBindingError)) throw error;
|
|
|
|
|
unavailable.push({ key: operation.key, reason: "UNSUPPORTED_OPERATION_CODEC" });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const atom = workspace.atoms.find((a) => a.id === operation.atomId)!;
|
|
|
|
|
const iface = workspace.interfaceImports.find((i) => i.revisionId === operation.interfaceRevisionId)!;
|
|
|
|
|
const member = iface.members.find((m) => m.operations.some((o) => o.id === operation.operationId))!;
|
|
|
|
|
const facet = member.operations.find((o) => o.id === operation.operationId)!;
|
|
|
|
|
const name = `${iface.displayName}.${member.displayName}.${facet.displayName}`;
|
|
|
|
|
const alias = JSON.stringify([atom.displayName, name]);
|
|
|
|
|
if (seen.has(alias)) throw new PortableBindingError(alias, "Ambiguous application descriptor alias");
|
|
|
|
|
seen.add(alias);
|
|
|
|
|
const roots = resultInterfaces(plan, operation.key);
|
|
|
|
|
const descriptor = {
|
|
|
|
|
key: operation.key,
|
|
|
|
|
plan: plan.digest,
|
|
|
|
|
effect: operation.execution.kind,
|
|
|
|
|
input: portableValueSchema(operation.input),
|
|
|
|
|
output: portableValueSchema(operation.output),
|
|
|
|
|
resultInterfaces: roots,
|
|
|
|
|
};
|
|
|
|
|
const list = atoms.get(atom.displayName) ?? [];
|
|
|
|
|
list.push(
|
|
|
|
|
`${JSON.stringify(name)}: defineOperationDescriptor<${input}, ${output}, ${roots.map((r) => JSON.stringify(r)).join(" | ") || "never"}, ${JSON.stringify(operation.execution.kind)}>(${JSON.stringify(descriptor)})`,
|
|
|
|
|
);
|
|
|
|
|
atoms.set(atom.displayName, list);
|
|
|
|
|
}
|
|
|
|
|
const queries = new Map<string, string[]>();
|
|
|
|
|
for (const query of plan.definition.queries) {
|
|
|
|
|
const declaration = query.query.checked.declaration;
|
|
|
|
|
if (declaration.placement?.kind !== "replica") continue;
|
|
|
|
|
const pkg = workspace.packageImports.find((p) => p.revisionId === query.query.packageRevisionId)!;
|
|
|
|
|
const alias = JSON.stringify([pkg.displayName, declaration.displayName]);
|
|
|
|
|
if (seen.has(`query:${alias}`)) throw new PortableBindingError(alias, "Ambiguous query alias");
|
|
|
|
|
seen.add(`query:${alias}`);
|
|
|
|
|
const list = queries.get(pkg.displayName) ?? [];
|
|
|
|
|
list.push(
|
|
|
|
|
`${JSON.stringify(declaration.displayName)}: { id: ${JSON.stringify(query.id)}, plan: ${JSON.stringify(plan.digest)}, root: ${JSON.stringify(declaration.root.interface)}, rootKind: ${JSON.stringify(declaration.root.kind)} } as const satisfies ReplicaProjection<${JSON.stringify(declaration.root.interface)}, ${JSON.stringify(declaration.root.kind)}>`,
|
|
|
|
|
);
|
|
|
|
|
queries.set(pkg.displayName, list);
|
|
|
|
|
}
|
|
|
|
|
const groups = (entries: Map<string, string[]>) =>
|
|
|
|
|
[...entries].map(([name, values]) => `${JSON.stringify(name)}: {${values.join(",\n")}}`).join(",\n");
|
|
|
|
|
return `// Generated application descriptors. Regenerate; do not edit.\nimport { defineOperationDescriptor, type ManagedReference, type ReplicaProjection } from ${JSON.stringify(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const planDigest = ${JSON.stringify(plan.digest)};\nexport const operations = {${groups(atoms)}} as const;\nexport const queries = {${groups(queries)}} as const;\nexport const unavailable = ${JSON.stringify(unavailable)} as const;\n`;
|
|
|
|
|
}
|