2026-09-21 16:01:48 -07:00
|
|
|
import { portableReflectionType } from "../execution-world/reflection.js";
|
2026-09-20 00:14:08 -07:00
|
|
|
import type { BindingSchema } from "./index.js";
|
|
|
|
|
import type {
|
|
|
|
|
ValueType,
|
|
|
|
|
ObjectExpectation,
|
|
|
|
|
PackageExport,
|
|
|
|
|
DependencyPort,
|
|
|
|
|
ExecutionContract,
|
|
|
|
|
} from "../capability-model/types.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) {
|
2026-09-20 04:04:39 -07:00
|
|
|
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" } },
|
|
|
|
|
},
|
|
|
|
|
};
|
2026-09-20 00:14:08 -07:00
|
|
|
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)",
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-09-20 01:49:56 -07:00
|
|
|
export function tsType(type: ValueType): string {
|
2026-09-20 00:14:08 -07:00
|
|
|
portableValueSchema(type);
|
|
|
|
|
switch (type.kind) {
|
2026-09-20 04:04:39 -07:00
|
|
|
case "change":
|
|
|
|
|
return `PreparedChange<${tsType(type.value)}>`;
|
2026-09-20 00:14:08 -07:00
|
|
|
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;
|
2026-09-21 16:01:48 -07:00
|
|
|
action: { kind: string; primitive?: string; operationId?: string; selector?: string };
|
2026-09-20 00:14:08 -07:00
|
|
|
input: ValueType;
|
|
|
|
|
output: ValueType;
|
|
|
|
|
keyed: boolean;
|
|
|
|
|
}
|
2026-09-20 20:20:47 -07:00
|
|
|
function methods(
|
|
|
|
|
schema: BindingSchema,
|
|
|
|
|
entry: PackageExport,
|
|
|
|
|
port: DependencyPort,
|
|
|
|
|
target: string,
|
|
|
|
|
remote = false,
|
|
|
|
|
): Method[] {
|
2026-09-20 00:14:08 -07:00
|
|
|
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 = () => {
|
2026-09-20 20:20:47 -07:00
|
|
|
if (remote)
|
|
|
|
|
throw new PortableBindingError(path, "Remote writes and creation require a named transaction capability");
|
2026-09-20 00:14:08 -07:00
|
|
|
if (entry.execution?.kind !== "transaction")
|
|
|
|
|
throw new PortableBindingError(path, "Snapshot context cannot expose mutation ports");
|
|
|
|
|
};
|
|
|
|
|
switch (req.kind) {
|
2026-09-21 16:01:48 -07:00
|
|
|
case "reflection":
|
|
|
|
|
if (remote)
|
|
|
|
|
throw new PortableBindingError(
|
|
|
|
|
path,
|
|
|
|
|
"Use an ordinary portable snapshot capability to read installation metadata",
|
|
|
|
|
);
|
|
|
|
|
return req.selectors.map((selector) =>
|
|
|
|
|
method(selector, { kind: "reflection", selector }, unit, portableReflectionType(selector)),
|
|
|
|
|
);
|
2026-09-20 00:14:08 -07:00
|
|
|
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);
|
|
|
|
|
}
|
2026-09-20 04:04:39 -07:00
|
|
|
if (primitive === "edit") {
|
|
|
|
|
writable();
|
|
|
|
|
return method("edit", { kind: "state", primitive }, { kind: "change", value: req.valueType }, unit);
|
|
|
|
|
}
|
2026-09-20 00:14:08 -07:00
|
|
|
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 (
|
2026-09-20 20:20:47 -07:00
|
|
|
(execution.kind === "remote" ? !remote : !execution.targets.includes(target)) ||
|
2026-09-20 00:14:08 -07:00
|
|
|
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`,
|
|
|
|
|
);
|
2026-09-20 20:20:47 -07:00
|
|
|
if (execution.kind === "transaction" && !remote) writable();
|
2026-09-21 18:31:43 -07:00
|
|
|
if (req.receiver === "input" && remote)
|
|
|
|
|
throw new PortableBindingError(path, "Reference-selected dependencies require portable execution");
|
2026-09-20 00:14:08 -07:00
|
|
|
return method(
|
|
|
|
|
`${member.displayName}.${op.displayName}`,
|
|
|
|
|
{ kind: "interface", operationId: op.id },
|
2026-09-20 22:05:59 -07:00
|
|
|
req.receiver === "input"
|
|
|
|
|
? {
|
|
|
|
|
kind: "record",
|
|
|
|
|
fields: {
|
|
|
|
|
receiver: {
|
|
|
|
|
kind: "object-ref",
|
|
|
|
|
expectation: { kind: "interface", interfaceRevisionId: req.interfaceRevisionId },
|
|
|
|
|
},
|
|
|
|
|
input: op.inputType,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: op.inputType,
|
2026-09-20 00:14:08 -07:00
|
|
|
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 {
|
2026-09-20 20:20:47 -07:00
|
|
|
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";
|
2026-09-20 00:14:08 -07:00
|
|
|
const pkg = schema.packages.find((p) => p.revisionId === revision);
|
|
|
|
|
if (!pkg) throw new PortableBindingError(revision, "Missing package");
|
2026-09-20 20:20:47 -07:00
|
|
|
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),
|
2026-09-20 00:14:08 -07:00
|
|
|
);
|
|
|
|
|
const names = new Set<string>();
|
|
|
|
|
const contexts: string[] = [],
|
|
|
|
|
implementations: string[] = [],
|
|
|
|
|
adapters: string[] = [],
|
|
|
|
|
descriptors: unknown[] = [];
|
|
|
|
|
for (const entry of entries) {
|
2026-09-20 20:20:47 -07:00
|
|
|
const effect = entry.execution?.kind ?? "remote";
|
2026-09-20 19:42:24 -07:00
|
|
|
// 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");
|
2026-09-20 00:14:08 -07:00
|
|
|
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);
|
2026-09-20 20:20:47 -07:00
|
|
|
const selected = methods(schema, entry, port, options.target, remote);
|
2026-09-20 00:14:08 -07:00
|
|
|
if (new Set(selected.map((m) => m.name)).size !== selected.length)
|
|
|
|
|
throw new PortableBindingError(port.id, "Duplicate method name");
|
|
|
|
|
all.push(...selected);
|
|
|
|
|
facets.push(
|
2026-09-20 20:20:47 -07:00
|
|
|
`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("; ")} }`,
|
2026-09-20 00:14:08 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
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(
|
2026-09-20 20:20:47 -07:00
|
|
|
`readonly ${q(entry.displayName)}: { readonly effect: ${q(effect)}; ${remote ? "readonly signal: AbortSignal;" : ""} readonly input: ${tsType(entry.inputType)}; readonly receiver: ${receiverType}; readonly ports: { ${facets.join("; ")} } }`,
|
2026-09-20 00:14:08 -07:00
|
|
|
);
|
|
|
|
|
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,
|
2026-09-20 20:20:47 -07:00
|
|
|
effect,
|
2026-09-20 00:14:08 -07:00
|
|
|
receiver,
|
|
|
|
|
input: portableValueSchema(entry.inputType),
|
|
|
|
|
output: portableValueSchema(entry.outputType),
|
|
|
|
|
methods: all.map((m) => ({ ...m, input: portableValueSchema(m.input), output: portableValueSchema(m.output) })),
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-09-21 16:01:48 -07:00
|
|
|
const groups = new Map<string, typeof entries>();
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
if (!entry.application) continue;
|
|
|
|
|
const template = pkg.genericExports?.find((item) => item.id === entry.application!.exportId);
|
|
|
|
|
if (!template) throw new PortableBindingError(entry.id, "Missing generic export template");
|
|
|
|
|
groups.set(template.displayName, [...(groups.get(template.displayName) ?? []), entry]);
|
|
|
|
|
}
|
|
|
|
|
const generic = [...groups].map(([name, applications]) => {
|
|
|
|
|
contexts.push(
|
|
|
|
|
`readonly ${q(name)}: ${applications.map((entry) => `Contexts[${q(entry.displayName)}]`).join(" | ")}`,
|
|
|
|
|
);
|
|
|
|
|
const type = applications
|
|
|
|
|
.map(
|
|
|
|
|
(entry) =>
|
|
|
|
|
`((context: Contexts[${q(entry.displayName)}]) => ${tsType(entry.outputType)} | Promise<${tsType(entry.outputType)}>)`,
|
|
|
|
|
)
|
|
|
|
|
.join(" & ");
|
|
|
|
|
const handlers = applications
|
|
|
|
|
.map((entry) => `${q(entry.displayName)}: (context: Contexts[${q(entry.displayName)}]) => handler(context)`)
|
|
|
|
|
.join(",");
|
|
|
|
|
return `${q(name)}: (handler: ${type}) => ({${handlers}})`;
|
|
|
|
|
});
|
|
|
|
|
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 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 const generic = { ${generic.join(",\n")} };\nexport function createRegistry(handlers: ${mode}Implementation) { const installed = { ...handlers }; return define${mode}Registry(declarations, { ${adapters.join(",\n")} }); }\n`;
|
2026-09-20 00:14:08 -07:00
|
|
|
}
|