59735c5e38
Replace opaque props with checked generic presentation contracts and lazy typed interface references. Generate readonly/writable field APIs and component checks. Preserve CRDT editing through explicit resolved getter/setter contracts, binding- fenced delta RPCs, native watches and replica-aware field adapters. Custom setters retain semantic writes; storage snapshots never grant write authority. Cover concurrent edits, lost acknowledgements, readonly contracts and authorization. Add receiver-free static factory dispatch, state-field binding shorthand, and conformance-based creation. Migrate TODO, editable scaffolds and authoring guides. Verify language/codegen, SDK, RPC, browser lifecycle, local scaffolds, production browser bundling and CRDT persistence with temporary PostgreSQL.
171 lines
8.2 KiB
TypeScript
171 lines
8.2 KiB
TypeScript
import type { GenericPackageExport, GenericDependencyPort } from "../capability-model/generic-packages.js";
|
|
import type { InterfaceRevision, ValueType } from "../capability-model/types.js";
|
|
import type {
|
|
ValueTypeExpression,
|
|
ObjectTypeExpression,
|
|
TypeArgumentExpression,
|
|
TypeParameter,
|
|
ValueAliasDefinition,
|
|
} from "../capability-model/generics.js";
|
|
|
|
/** Universal source contracts. Only closed exports get executable codecs. */
|
|
export const genericImplementationType = (
|
|
definition: GenericPackageExport,
|
|
interfaces: InterfaceRevision[],
|
|
concrete: (type: ValueType) => string,
|
|
): string => {
|
|
const names = new Map(definition.parameters.map((parameter, index) => [parameter.id, `T${index}`]));
|
|
type Scope = { arguments: Map<string, string>; aliases: ValueAliasDefinition[] };
|
|
const rootScope = (): Scope => ({ arguments: new Map(), aliases: definition.aliases });
|
|
const object = (entries: [string, string][]) =>
|
|
`{${entries.map(([key, value]) => `${JSON.stringify(key)}:${value}`).join(";")}}`;
|
|
const target = (type: ObjectTypeExpression, scope: Scope): string => {
|
|
if (type.kind === "parameter") {
|
|
const bound = scope.arguments.get(type.parameterId);
|
|
if (bound) return bound;
|
|
const name = names.get(type.parameterId);
|
|
if (!name) throw new Error(`Unbound object parameter ${type.parameterId}`);
|
|
return name;
|
|
}
|
|
if (type.kind === "atom") return JSON.stringify(`atom:${type.atomId}`);
|
|
if (type.kind === "interface") return JSON.stringify(`interface:${type.interfaceRevisionId}`);
|
|
if (type.kind === "application")
|
|
return `QxApplied<${JSON.stringify(type.application.definitionId)}, [${type.application.arguments.map((arg) => argument(arg, scope)).join(",")}]>`;
|
|
throw new Error("Generic package Self must be expressed as an explicit object parameter");
|
|
};
|
|
const argument = (arg: TypeArgumentExpression, scope: Scope): string =>
|
|
arg.kind === "value" ? value(arg.type, scope) : target(arg.target, scope);
|
|
const bind = (parameters: TypeParameter[], args: TypeArgumentExpression[], scope: Scope): Scope => {
|
|
if (parameters.length !== args.length) throw new Error("Wrong generic arity during code generation");
|
|
const result = new Map(scope.arguments);
|
|
// Render arguments in their original lexical scope before introducing binders.
|
|
const rendered = args.map((arg) => argument(arg, scope));
|
|
parameters.forEach((parameter, index) => result.set(parameter.id, rendered[index]));
|
|
return { ...scope, arguments: result };
|
|
};
|
|
const value = (type: ValueTypeExpression, scope: Scope, depth = 0): string => {
|
|
if (depth > 128) throw new Error("Generic type expansion exceeds depth limit");
|
|
switch (type.kind) {
|
|
case "parameter": {
|
|
const bound = scope.arguments.get(type.parameterId);
|
|
if (bound) return bound;
|
|
const name = names.get(type.parameterId);
|
|
if (!name) throw new Error(`Unbound value parameter ${type.parameterId}`);
|
|
return name;
|
|
}
|
|
case "object-ref":
|
|
return `QxObjectRef<${target(type.expectation, scope)}>`;
|
|
case "record":
|
|
return object(Object.entries(type.fields).map(([name, field]) => [name, value(field, scope, depth + 1)]));
|
|
case "optional":
|
|
return `(${value(type.value, scope, depth + 1)} | null)`;
|
|
case "list":
|
|
return `Array<${value(type.value, scope, depth + 1)}>`;
|
|
case "alias": {
|
|
const alias = scope.aliases.find((entry) => entry.id === type.definitionId);
|
|
if (!alias) throw new Error(`Missing alias ${type.definitionId}`);
|
|
return value(alias.body, bind(alias.parameters, type.arguments, scope), depth + 1);
|
|
}
|
|
default:
|
|
return concrete(type);
|
|
}
|
|
};
|
|
const params = (type: ValueTypeExpression, scope: Scope) =>
|
|
type.kind === "builtin" && type.name === "unit" ? "" : `input:${value(type, scope)}`;
|
|
const port = (entry: GenericDependencyPort): string => {
|
|
const requirement = entry.requirement,
|
|
scope = rootScope();
|
|
switch (requirement.kind) {
|
|
case "state":
|
|
return object([
|
|
...requirement.primitives.map((primitive): [string, string] => {
|
|
if (primitive === "read") return ["get", `()=>Promise<${value(requirement.valueType, scope)}>`];
|
|
if (primitive === "write") return ["set", `(value:${value(requirement.valueType, scope)})=>Promise<void>`];
|
|
throw new Error(`Unsupported generic state primitive ${primitive}`);
|
|
}),
|
|
...(requirement.primitives.includes("read")
|
|
? [["live", "()=>Promise<QxLiveValue>"] as [string, string]]
|
|
: []),
|
|
]);
|
|
case "edge": {
|
|
const ref = `QxObjectRef<${target(requirement.target, scope)}>`;
|
|
const methods = requirement.primitives.map((primitive): [string, string] => {
|
|
if (primitive === "resolve") return ["resolve", `()=>Promise<Array<${ref}>>`];
|
|
if (primitive === "connect" || primitive === "disconnect")
|
|
return [primitive, `(target:${ref})=>Promise<void>`];
|
|
throw new Error(`Unsupported generic edge primitive ${primitive}`);
|
|
});
|
|
if (requirement.primitives.includes("resolve"))
|
|
methods.push(["collection", `()=>Promise<RelationshipCollection<${ref}>>`]);
|
|
if (
|
|
["resolve", "connect", "disconnect"].every((primitive) =>
|
|
requirement.primitives.includes(primitive as "resolve"),
|
|
)
|
|
)
|
|
methods.push([
|
|
"replace",
|
|
`(entries:RelationshipEntry<${ref}>[],expectedRevision:bigint)=>Promise<RelationshipCollection<${ref}>>`,
|
|
]);
|
|
return object(methods);
|
|
}
|
|
case "constructor":
|
|
return object([
|
|
[
|
|
"construct",
|
|
`(${params(requirement.inputType, scope)})=>Promise<QxObjectRef<${target(requirement.target, scope)}>>`,
|
|
],
|
|
]);
|
|
case "interface": {
|
|
const contract = interfaces.find((entry) => entry.revisionId === requirement.application.definitionId);
|
|
if (!contract) throw new Error(`Missing generic port contract ${requirement.application.definitionId}`);
|
|
const local = {
|
|
...bind(contract.template?.parameters ?? [], requirement.application.arguments, scope),
|
|
aliases: contract.template?.aliases ?? [],
|
|
};
|
|
const operations = (contract.template?.members ?? contract.members).flatMap((member) =>
|
|
member.operations
|
|
.filter((op) => op.mode === "call" && op.scope !== "class")
|
|
.map((op) => ({ ...op, name: `${member.displayName}.${op.displayName}` })),
|
|
);
|
|
return object([
|
|
["objectId", `QxObjectRef<${target({ kind: "application", application: requirement.application }, scope)}>`],
|
|
["live", object(operations.map((op) => [op.name, `(${params(op.inputType, local)})=>Promise<QxLiveValue>`]))],
|
|
...operations.map(
|
|
(op) =>
|
|
[op.name, `(${params(op.inputType, local)})=>Promise<${value(op.outputType, local)}>`] as [
|
|
string,
|
|
string,
|
|
],
|
|
),
|
|
]);
|
|
}
|
|
}
|
|
};
|
|
const declarations = definition.parameters
|
|
.map((parameter) => `${names.get(parameter.id)}${parameter.kind === "object" ? " extends string" : ""}`)
|
|
.join(",");
|
|
const receiver = definition.receiverRequirement;
|
|
const context = object([
|
|
...(definition.kind === "function"
|
|
? []
|
|
: [
|
|
[
|
|
"objectId",
|
|
`QxObjectRef<${receiver.kind === "target" ? target(receiver.target, rootScope()) : "string"}>`,
|
|
] as [string, string],
|
|
]),
|
|
["input", value(definition.inputType, rootScope())],
|
|
["ports", object(definition.dependencyPorts.map((entry) => [entry.displayName, port(entry)]))],
|
|
]);
|
|
const contextWithLifecycle =
|
|
definition.kind === "function"
|
|
? `${context} & {signal?: AbortSignal}`
|
|
: `${context} & QxContextLifecycle<${context} & {signal?: AbortSignal}>`;
|
|
const result = value(definition.eventType ?? definition.outputType, rootScope());
|
|
const handler = `<${declarations}>(context:${contextWithLifecycle})=>${result}|Promise<${result}>`;
|
|
const derived = `{kind:"derived";get:${handler}}`;
|
|
return definition.eventType
|
|
? derived
|
|
: `(${handler})${definition.kind === "operation" && definition.mode === "call" ? ` | ${derived}` : ""}`;
|
|
};
|