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

268 lines
11 KiB
TypeScript

import type { BindingSchema } from "./index.js";
import type {
ValueType,
ObjectExpectation,
PackageExport,
DependencyPort,
ExecutionContract,
} from "../capability-model/types.js";
import { hexDigest } from "../execution-world/plan.js";
export class PortableBindingError extends Error {
constructor(
readonly path: string,
message: string,
) {
super(`${path}: ${message}`);
this.name = "PortableBindingError";
}
}
const q = JSON.stringify;
const unit: ValueType = { kind: "builtin", name: "unit" };
const string: ValueType = { kind: "scalar", name: "string" };
const bool: ValueType = { kind: "scalar", name: "bool" };
const isUnit = (t: ValueType) => t.kind === "builtin" && t.name === "unit";
const expectation = (e: ObjectExpectation) => ({
kind: e.kind,
id: e.kind === "atom" ? e.atomId : e.interfaceRevisionId,
});
/** The protocol IR and replica wire schema intentionally share structure, not reference identity. */
export function portableValueSchema(type: ValueType): unknown {
switch (type.kind) {
case "change":
return {
kind: "record",
fields: {
codec: { kind: "scalar", name: "string" },
profile: { kind: "scalar", name: "string" },
generation: { kind: "scalar", name: "string" },
changes: { kind: "list", value: { kind: "scalar", name: "bytes" } },
},
};
case "scalar":
return type;
case "builtin":
if (type.name === "unit") return type;
break;
case "object-ref":
return { kind: type.kind, expectation: expectation(type.expectation) };
case "optional":
case "list":
return { kind: type.kind, value: portableValueSchema(type.value) };
case "record":
return {
kind: type.kind,
fields: Object.fromEntries(
Object.entries(type.fields).map(([key, value]) => [key, portableValueSchema(value)]),
),
};
}
throw new PortableBindingError(
type.kind,
"Unsupported portable value; use a supported record/reference contract (native edits and generic adapters have separate tasks)",
);
}
export function tsType(type: ValueType): string {
portableValueSchema(type);
switch (type.kind) {
case "change":
return `PreparedChange<${tsType(type.value)}>`;
case "scalar":
return type.name === "bool"
? "boolean"
: type.name === "string"
? "string"
: type.name === "bytes"
? "Uint8Array"
: ["int64", "uint64"].includes(type.name)
? "bigint"
: "number";
case "builtin":
return "null";
case "object-ref": {
const e = expectation(type.expectation);
return `ManagedReference<${q(e.kind)}, ${q(e.id)}>`;
}
case "optional":
return `(${tsType(type.value)} | null)`;
case "list":
return `ReadonlyArray<${tsType(type.value)}>`;
case "record":
return `{ ${Object.entries(type.fields)
.map(([key, value]) => `readonly ${q(key)}: ${tsType(value)}`)
.join("; ")} }`;
default:
throw new PortableBindingError(type.kind, "Unsupported type");
}
}
interface Method {
name: string;
portId: string;
portName: string;
action: { kind: string; primitive?: string; operationId?: string };
input: ValueType;
output: ValueType;
keyed: boolean;
}
function methods(schema: BindingSchema, entry: PackageExport, port: DependencyPort, target: string): Method[] {
const req = port.requirement;
const path = `${entry.displayName}.ports.${port.displayName}`;
const method = (
name: string,
action: Method["action"],
input: ValueType,
output: ValueType,
keyed = false,
): Method => ({ name, portId: port.id, portName: port.displayName, action, input, output, keyed });
const writable = () => {
if (entry.execution?.kind !== "transaction")
throw new PortableBindingError(path, "Snapshot context cannot expose mutation ports");
};
switch (req.kind) {
case "creation":
writable();
return [
method(
"create",
{ kind: "creation" },
req.inputType,
{ kind: "object-ref", expectation: { kind: "atom", atomId: req.atomId } },
true,
),
];
case "state":
return req.primitives.map((primitive) => {
if (primitive === "read") return method("get", { kind: "state", primitive }, unit, req.valueType);
if (primitive === "write") {
writable();
return method("set", { kind: "state", primitive }, req.valueType, unit);
}
if (primitive === "edit") {
writable();
return method("edit", { kind: "state", primitive }, { kind: "change", value: req.valueType }, unit);
}
throw new PortableBindingError(path, `State ${primitive} is not supported by this portable adapter yet`);
});
case "edge": {
const ref: ValueType = { kind: "object-ref", expectation: req.target };
const item: ValueType = { kind: "record", fields: { entry: string, target: ref } };
return req.primitives.map((primitive) => {
if (primitive === "resolve")
return method("resolve", { kind: "edge", primitive }, unit, { kind: "list", value: item });
writable();
if (primitive === "connect") return method("connect", { kind: "edge", primitive }, ref, string, true);
if (primitive === "disconnect") return method("disconnect", { kind: "edge", primitive }, string, bool);
throw new PortableBindingError(path, `Edge ${primitive} is not supported by this portable adapter yet`);
});
}
case "interface": {
const iface = schema.interfaces.find((i) => i.revisionId === req.interfaceRevisionId);
if (!iface) throw new PortableBindingError(path, `Missing interface ${req.interfaceRevisionId}`);
const operations = iface.members.flatMap((member) => member.operations.map((op) => ({ member, op })));
if (req.operations?.some((id) => !operations.some(({ op }) => op.id === id)))
throw new PortableBindingError(path, "Unknown selected interface operation");
return operations
.filter(({ op }) => !req.operations || req.operations.includes(op.id))
.map(({ member, op }) => {
const execution: ExecutionContract = op.execution ?? { kind: "remote" };
if (
execution.kind === "remote" ||
!execution.targets.includes(target) ||
op.mode !== "call" ||
op.scope === "class"
)
throw new PortableBindingError(
`${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();
return method(
`${member.displayName}.${op.displayName}`,
{ kind: "interface", operationId: op.id },
op.inputType,
op.outputType,
execution.kind === "transaction",
);
});
}
default:
throw new PortableBindingError(path, `Unsupported ${req.kind} port in portable bindings`);
}
}
export interface PortableBindingOptions {
target: string;
runtimeModule?: string;
}
/** Generate one target's handlers. The same source signatures are used in browser/server builds. */
export function generatePortableTypeScriptBindings(
schema: BindingSchema,
revision: string,
options: PortableBindingOptions,
): string {
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 names = new Set<string>();
const contexts: string[] = [],
implementations: string[] = [],
adapters: string[] = [],
descriptors: unknown[] = [];
for (const entry of entries) {
// The checked binding schema already contains concrete generic applications.
// Each has its own identity, value codecs and specialized dependency ports.
if (entry.kind === "constructor")
throw new PortableBindingError(entry.id, "Legacy constructors are not supported; declare an ordinary factory");
if (names.has(entry.displayName)) throw new PortableBindingError(entry.id, "Duplicate handler name");
names.add(entry.displayName);
const portNames = new Set<string>();
const all: Method[] = [],
facets: string[] = [];
for (const port of entry.dependencyPorts) {
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);
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("; ")} }`,
);
}
const receiver =
entry.kind === "function"
? null
: entry.receiverRequirement.kind === "exact-atom"
? { kind: "atom", id: entry.receiverRequirement.atomId }
: "object";
const receiverType =
receiver === null
? "null"
: receiver === "object"
? "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("; ")} } }`,
);
const output = tsType(entry.outputType);
implementations.push(
`readonly ${q(entry.displayName)}: (context: Contexts[${q(entry.displayName)}]) => ${output} | Promise<${output}>`,
);
adapters.push(
`${q(entry.id)}: context => installed[${q(entry.displayName)}](context as Contexts[${q(entry.displayName)}])`,
);
descriptors.push({
id: entry.id,
name: entry.displayName,
effect: entry.execution!.kind,
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`;
}