Files
quixos-protocol/src/bindings/client.ts
T

110 lines
5.2 KiB
TypeScript
Raw Normal View History

import type { InterfaceRevision, ValueType } from "../capability-model/types.js";
/** Host clients have no package receiver, but must use the same checked
* interface signatures and argument framing as generated package ports. */
export const generateClientContracts = (
interfaces: InterfaceRevision[],
messages: Record<string, string>,
qualified = false,
) => {
if (interfaces.some((entry) => entry.template))
throw new Error("Host contracts require closed interface applications, not generic definitions");
const type = (value: ValueType): string => {
switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin":
return value.name === "unit" ? "undefined" : "string";
case "scalar":
return {
bool: "boolean",
string: "string",
bytes: "Uint8Array",
int32: "number",
uint32: "number",
double: "number",
int64: "bigint",
uint64: "bigint",
}[value.name];
case "object-ref":
return qualified
? `CapabilityReference<${JSON.stringify(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`
: `{readonly $quixosRef: string}`;
case "change":
throw new Error("CRDT change values require the prepared-change codec and runtime (LF-19)");
case "optional":
return `(${type(value.value)} | null)`;
case "list":
return `Array<${type(value.value)}>`;
case "record":
return `{${Object.entries(value.fields)
.map(([name, field]) => `${JSON.stringify(name)}${field.kind === "optional" ? "?" : ""}: ${type(field)}`)
.join("; ")}}`;
case "message": {
const binding = messages[value.descriptorId];
if (!binding) throw new Error(`Missing host message type ${value.descriptorId}`);
return binding;
}
}
};
const operations = interfaces.flatMap((iface) =>
iface.members.flatMap((member) =>
member.operations
.filter((operation) => operation.mode === "call")
.map((operation) => ({ ...operation, interfaceRevisionId: iface.revisionId })),
),
);
if (qualified) {
const contracts = interfaces.map((iface) => {
const members = iface.members.flatMap((member) =>
member.operations
.filter((op) => op.mode === "call")
.map((op) => ` ${JSON.stringify(op.id)}: {input: ${type(op.inputType)}; output: ${type(op.outputType)}};`),
);
return `${JSON.stringify(iface.revisionId)}: {\n${members.join("\n")}\n}`;
});
if (new Set(interfaces.map((iface) => iface.revisionId)).size !== interfaces.length)
throw new Error("Duplicate closed interface identity");
return (
`// Generated closed capability contracts. Dispatch by interface AND operation.\n` +
`declare const referenceType: unique symbol;\nexport type CapabilityReference<T extends string> = {readonly $quixosRef: string; readonly [referenceType]: T};\n` +
`export type CapabilityContracts = {${contracts.join(";\n")}};\n` +
`export type CapabilityInput<I extends keyof CapabilityContracts, O extends keyof CapabilityContracts[I]> = CapabilityContracts[I][O] extends {input: infer T} ? T : never;\n` +
`export type CapabilityOutput<I extends keyof CapabilityContracts, O extends keyof CapabilityContracts[I]> = CapabilityContracts[I][O] extends {output: infer T} ? T : never;\n` +
`export const capabilityApplications = ${JSON.stringify(Object.fromEntries(interfaces.map((iface) => [iface.revisionId, { definitionId: iface.application?.definitionId ?? iface.revisionId, arguments: iface.application?.arguments ?? [], ...(iface.application?.self ? { self: iface.application.self } : {}) }])))} as const;\n`
);
}
if (new Set(operations.map((entry) => entry.id)).size !== operations.length)
throw new Error(
"Host operation IDs are ambiguous across interfaces; select a closed interface application explicitly",
);
return (
`// Generated from checked QX interfaces. Regenerate with scripts/generate-platform-contracts.mjs.\n` +
`export type PlatformInputs = {\n${operations.map((operation) => ` ${JSON.stringify(operation.id)}: ${type(operation.inputType)};`).join("\n")}\n};\n` +
`export const platformOperations = ${JSON.stringify(
Object.fromEntries(
operations.map((operation) => [
operation.id,
{
interfaceRevisionId: operation.interfaceRevisionId,
input:
operation.inputType.kind === "builtin" && operation.inputType.name === "unit"
? "unit"
: ["record", "message"].includes(operation.inputType.kind)
? "fields"
: "value",
},
]),
),
null,
2,
)} as const;\n`
);
};
/** New consumers use exact closed interface identities; operation IDs alone are not unique. */
export const generateAppliedClientContracts = (
interfaces: InterfaceRevision[],
messages: Record<string, string> = {},
) => generateClientContracts(interfaces, messages, true);