Generate typed asynchronous service capability bindings

This commit is contained in:
Timothy J. Aveni
2026-09-20 20:35:16 -07:00
parent 21502ed023
commit c4ae58831d
13 changed files with 192 additions and 21 deletions
+37 -11
View File
@@ -104,7 +104,13 @@ interface Method {
output: ValueType;
keyed: boolean;
}
function methods(schema: BindingSchema, entry: PackageExport, port: DependencyPort, target: string): Method[] {
function methods(
schema: BindingSchema,
entry: PackageExport,
port: DependencyPort,
target: string,
remote = false,
): Method[] {
const req = port.requirement;
const path = `${entry.displayName}.ports.${port.displayName}`;
const method = (
@@ -115,6 +121,8 @@ function methods(schema: BindingSchema, entry: PackageExport, port: DependencyPo
keyed = false,
): Method => ({ name, portId: port.id, portName: port.displayName, action, input, output, keyed });
const writable = () => {
if (remote)
throw new PortableBindingError(path, "Remote writes and creation require a named transaction capability");
if (entry.execution?.kind !== "transaction")
throw new PortableBindingError(path, "Snapshot context cannot expose mutation ports");
};
@@ -166,8 +174,7 @@ function methods(schema: BindingSchema, entry: PackageExport, port: DependencyPo
.map(({ member, op }) => {
const execution: ExecutionContract = op.execution ?? { kind: "remote" };
if (
execution.kind === "remote" ||
!execution.targets.includes(target) ||
(execution.kind === "remote" ? !remote : !execution.targets.includes(target)) ||
op.mode !== "call" ||
op.scope === "class"
)
@@ -175,7 +182,7 @@ function methods(schema: BindingSchema, entry: PackageExport, port: DependencyPo
`${path}.${member.displayName}.${op.displayName}`,
`Selected operation is not an object call available in portable target ${target}; select a portable subset or move this handler to remote execution`,
);
if (execution.kind === "transaction") writable();
if (execution.kind === "transaction" && !remote) writable();
return method(
`${member.displayName}.${op.displayName}`,
{ kind: "interface", operationId: op.id },
@@ -199,10 +206,28 @@ export function generatePortableTypeScriptBindings(
revision: string,
options: PortableBindingOptions,
): string {
return generateBindings(schema, revision, options, false);
}
export function generateRemoteTypeScriptBindings(
schema: BindingSchema,
revision: string,
options: { runtimeModule?: string } = {},
): string {
return generateBindings(schema, revision, { ...options, target: "server" }, true);
}
function generateBindings(
schema: BindingSchema,
revision: string,
options: PortableBindingOptions,
remote: boolean,
): string {
const mode = remote ? "Remote" : "Portable";
const pkg = schema.packages.find((p) => p.revisionId === revision);
if (!pkg) throw new PortableBindingError(revision, "Missing package");
const entries = pkg.exports.filter(
(e) => e.execution && e.execution.kind !== "remote" && e.execution.targets.includes(options.target),
const entries = pkg.exports.filter((e) =>
remote
? !e.execution || e.execution.kind === "remote"
: e.execution && e.execution.kind !== "remote" && e.execution.targets.includes(options.target),
);
const names = new Set<string>();
const contexts: string[] = [],
@@ -210,6 +235,7 @@ export function generatePortableTypeScriptBindings(
adapters: string[] = [],
descriptors: unknown[] = [];
for (const entry of entries) {
const effect = entry.execution?.kind ?? "remote";
// The checked binding schema already contains concrete generic applications.
// Each has its own identity, value codecs and specialized dependency ports.
if (entry.kind === "constructor")
@@ -223,12 +249,12 @@ export function generatePortableTypeScriptBindings(
if (portNames.has(port.displayName))
throw new PortableBindingError(entry.id, `Duplicate port name ${port.displayName}`);
portNames.add(port.displayName);
const selected = methods(schema, entry, port, options.target);
const selected = methods(schema, entry, port, options.target, remote);
if (new Set(selected.map((m) => m.name)).size !== selected.length)
throw new PortableBindingError(port.id, "Duplicate method name");
all.push(...selected);
facets.push(
`readonly ${q(port.displayName)}: { ${selected.map((m) => `readonly ${q(m.name)}: (${[...(isUnit(m.input) ? [] : [`input: ${tsType(m.input)}`]), ...(m.keyed ? ["options: MutationKey"] : [])].join(", ")}) => Promise<${tsType(m.output)}>`).join("; ")} }`,
`readonly ${q(port.displayName)}: { ${selected.map((m) => `readonly ${q(m.name)}: (${[...(isUnit(m.input) ? [] : [`input: ${tsType(m.input)}`]), ...(m.keyed ? [remote ? "options: ServerMutationOptions" : "options: MutationKey"] : [])].join(", ")}) => Promise<${tsType(m.output)}>`).join("; ")} }`,
);
}
const receiver =
@@ -244,7 +270,7 @@ export function generatePortableTypeScriptBindings(
? "ExactReference"
: `ManagedReference<"atom", ${q(receiver.id)}>`;
contexts.push(
`readonly ${q(entry.displayName)}: { readonly effect: ${q(entry.execution!.kind)}; readonly input: ${tsType(entry.inputType)}; readonly receiver: ${receiverType}; readonly ports: { ${facets.join("; ")} } }`,
`readonly ${q(entry.displayName)}: { readonly effect: ${q(effect)}; ${remote ? "readonly signal: AbortSignal;" : ""} readonly input: ${tsType(entry.inputType)}; readonly receiver: ${receiverType}; readonly ports: { ${facets.join("; ")} } }`,
);
const output = tsType(entry.outputType);
implementations.push(
@@ -256,12 +282,12 @@ export function generatePortableTypeScriptBindings(
descriptors.push({
id: entry.id,
name: entry.displayName,
effect: entry.execution!.kind,
effect,
receiver,
input: portableValueSchema(entry.inputType),
output: portableValueSchema(entry.outputType),
methods: all.map((m) => ({ ...m, input: portableValueSchema(m.input), output: portableValueSchema(m.output) })),
});
}
return `// Generated portable bindings. Regenerate from the checked capability schema; do not edit.\nimport { definePortableRegistry, type PortableExport, type PreparedChange, type ManagedReference, type ExactReference, type MutationKey } from ${q(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const bindingDigest = ${q(hexDigest(schema))};\nexport const packageRevision = ${q(revision)};\nexport const target = ${q(options.target)};\nexport type Contexts = { ${contexts.join(";\n")} };\nexport type PortableImplementation = { ${implementations.join(";\n")} };\nexport const declarations = ${JSON.stringify(descriptors, null, 2)} as const satisfies readonly PortableExport[];\nexport function createRegistry(handlers: PortableImplementation) { const installed = { ...handlers }; return definePortableRegistry(declarations, { ${adapters.join(",\n")} }); }\n`;
return `// Generated ${remote ? "remote" : "portable"} bindings. Regenerate from the checked capability schema; do not edit.\nimport { define${mode}Registry, type ${mode}Export, type PreparedChange, type ManagedReference, type ExactReference, type ${remote ? "ServerMutationOptions" : "MutationKey"} } from ${q(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const bindingDigest = ${q(hexDigest(schema))};\nexport const packageRevision = ${q(revision)};\nexport const target = ${q(options.target)};\nexport type Contexts = { ${contexts.join(";\n")} };\nexport type ${mode}Implementation = { ${implementations.join(";\n")} };\nexport const declarations = ${JSON.stringify(descriptors, null, 2)} as const satisfies readonly ${mode}Export[];\nexport function createRegistry(handlers: ${mode}Implementation) { const installed = { ...handlers }; return define${mode}Registry(declarations, { ${adapters.join(",\n")} }); }\n`;
}