Generate typed asynchronous service capability bindings
This commit is contained in:
+20
-9
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { generateTypeScriptBindings, generatePortableTypeScriptBindings } from "./index.js";
|
||||
import {
|
||||
generateTypeScriptBindings,
|
||||
generatePortableTypeScriptBindings,
|
||||
generateRemoteTypeScriptBindings,
|
||||
} from "./index.js";
|
||||
import path from "node:path";
|
||||
import { reactPlatformTypes } from "./react-platform.js";
|
||||
import { generateReactBindings } from "./react.js";
|
||||
@@ -11,16 +15,23 @@ const main = async () => {
|
||||
throw new Error("usage: quixos-codegen-ts SCHEMA.json PACKAGE_REVISION OUTPUT.ts [OPTIONS.json]");
|
||||
const config = options ? JSON.parse(await readFile(options, "utf8")) : {};
|
||||
const contracts = JSON.parse(await readFile(schema, "utf8"));
|
||||
if (config.portable && config.react)
|
||||
throw new Error("Portable handlers and React component bindings are separate artifacts");
|
||||
if ([config.portable, config.remote, config.react].filter(Boolean).length > 1)
|
||||
throw new Error("Portable, remote and React bindings are separate artifacts");
|
||||
if (config.remote !== undefined && config.remote !== true) throw new Error("Remote binding mode must be true");
|
||||
if (config.portable && typeof config.portable.target !== "string")
|
||||
throw new Error("Portable bindings require an explicit target");
|
||||
const generated = config.portable
|
||||
? generatePortableTypeScriptBindings(contracts, revision, {
|
||||
...config.portable,
|
||||
...(config.runtimeModule ? { runtimeModule: config.runtimeModule } : {}),
|
||||
})
|
||||
: generateTypeScriptBindings(contracts, revision, config);
|
||||
const generated = config.remote
|
||||
? generateRemoteTypeScriptBindings(
|
||||
contracts,
|
||||
revision,
|
||||
config.runtimeModule ? { runtimeModule: config.runtimeModule } : {},
|
||||
)
|
||||
: config.portable
|
||||
? generatePortableTypeScriptBindings(contracts, revision, {
|
||||
...config.portable,
|
||||
...(config.runtimeModule ? { runtimeModule: config.runtimeModule } : {}),
|
||||
})
|
||||
: generateTypeScriptBindings(contracts, revision, config);
|
||||
await writeFile(output, generated);
|
||||
if (config.react) {
|
||||
await writeFile(path.join(path.dirname(output), "web-studio-react-runtime.d.ts"), reactPlatformTypes);
|
||||
|
||||
@@ -424,6 +424,7 @@ export const generateTypeScriptBindings = (
|
||||
|
||||
export {
|
||||
generatePortableTypeScriptBindings,
|
||||
generateRemoteTypeScriptBindings,
|
||||
portableValueSchema,
|
||||
PortableBindingError,
|
||||
type PortableBindingOptions,
|
||||
|
||||
+37
-11
@@ -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`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user