Project source-free capability contracts and keep exact source provenance in package assembly. Reuse compiled groups across unrelated graph and dependency implementation edits. Keep React public inputs separate from workspace query lowering and retain explicit type-companion dependencies. Allow independent execution-package checks, scope resource compilation to declared dependencies, and memoize source closure traversal. Document generic-specialization limits and measured Reader rebuild behavior. Verified protocol, artifact, package, React, and managed activation checks; retained-Git independent/workspace artifact equality and incompatible type dependency rejection; nix fmt -- --check and lint. One-off Reader measurements recompiled 1 of 13 groups for a child edit and 0 for a workspace QX default edit.
366 lines
16 KiB
TypeScript
366 lines
16 KiB
TypeScript
import { portableReflectionType } from "../execution-world/reflection.js";
|
|
import type { BindingContractSchema } 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) {
|
|
case "variant":
|
|
return {
|
|
kind: "variant",
|
|
id: type.id,
|
|
cases: Object.fromEntries(
|
|
Object.entries(type.cases).map(([label, entry]) => [
|
|
label,
|
|
{ id: entry.id, value: portableValueSchema(entry.value) },
|
|
]),
|
|
),
|
|
};
|
|
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 "variant":
|
|
return `(${Object.entries(type.cases)
|
|
.map(([label, entry]) => `{ readonly kind: ${q(label)}; readonly value: ${tsType(entry.value)} }`)
|
|
.join(" | ")})`;
|
|
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; selector?: string };
|
|
input: ValueType;
|
|
output: ValueType;
|
|
keyed: boolean;
|
|
explicitInput?: boolean;
|
|
}
|
|
function methods(
|
|
schema: BindingContractSchema,
|
|
entry: PackageExport,
|
|
port: DependencyPort,
|
|
target: string,
|
|
remote = false,
|
|
): 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 (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");
|
|
};
|
|
switch (req.kind) {
|
|
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)),
|
|
);
|
|
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 mapKey: ValueType | undefined = req.keyType
|
|
? { kind: "scalar", name: req.keyType === "boolean" ? "bool" : req.keyType }
|
|
: undefined;
|
|
const item: ValueType = { kind: "record", fields: { entry: string, target: ref, ...(mapKey ? { mapKey } : {}) } };
|
|
const connectInput: ValueType = mapKey ? { kind: "record", fields: { mapKey, target: ref } } : 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 }, connectInput, 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" && !remote) ||
|
|
op.mode !== "call" ||
|
|
(op.scope === "class" && (remote || req.receiver === "input"))
|
|
)
|
|
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" && !remote) writable();
|
|
if (req.receiver === "input" && remote)
|
|
throw new PortableBindingError(path, "Reference-selected dependencies require portable execution");
|
|
return {
|
|
...method(
|
|
`${member.displayName}.${op.displayName}`,
|
|
{ kind: "interface", operationId: op.id },
|
|
req.receiver === "input"
|
|
? {
|
|
kind: "record",
|
|
fields: {
|
|
receiver: {
|
|
kind: "object-ref",
|
|
expectation: { kind: "interface", interfaceRevisionId: req.interfaceRevisionId },
|
|
},
|
|
input: op.inputType,
|
|
},
|
|
}
|
|
: op.inputType,
|
|
op.outputType,
|
|
execution.kind === "transaction",
|
|
),
|
|
...(op.scope === "class" ? { explicitInput: true } : {}),
|
|
};
|
|
});
|
|
}
|
|
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: BindingContractSchema,
|
|
revision: string,
|
|
options: PortableBindingOptions,
|
|
): string {
|
|
return generateBindings(schema, revision, options, false);
|
|
}
|
|
export function generateRemoteTypeScriptBindings(
|
|
schema: BindingContractSchema,
|
|
revision: string,
|
|
options: { runtimeModule?: string } = {},
|
|
): string {
|
|
return generateBindings(schema, revision, { ...options, target: "server" }, true);
|
|
}
|
|
function generateBindings(
|
|
schema: BindingContractSchema,
|
|
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) =>
|
|
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[] = [],
|
|
implementations: string[] = [],
|
|
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")
|
|
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, 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) && !m.explicitInput ? [] : [`input: ${tsType(m.input)}`]), ...(m.keyed ? [remote ? "options: ServerMutationOptions" : "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(effect)}; ${remote ? "readonly signal: AbortSignal;" : ""} 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,
|
|
receiver,
|
|
input: portableValueSchema(entry.inputType),
|
|
output: portableValueSchema(entry.outputType),
|
|
methods: all.map((m) => ({ ...m, input: portableValueSchema(m.input), output: portableValueSchema(m.output) })),
|
|
});
|
|
}
|
|
const groups = new Map<string, typeof entries>();
|
|
for (const template of pkg.genericExports ?? []) {
|
|
if (template.execution?.kind !== "remote" && template.execution?.targets.includes(options.target))
|
|
groups.set(template.displayName, []);
|
|
}
|
|
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]) => {
|
|
if (!applications.length) return `${q(name)}: (_handler: unknown) => ({})`;
|
|
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`;
|
|
}
|