Preserve nested CRDT recovery and serialize compound editor submissions

This commit is contained in:
Timothy J. Aveni
2026-09-20 23:26:13 -07:00
parent 2056c1e352
commit 47a3fe9eae
5 changed files with 37 additions and 1 deletions
+10
View File
@@ -8,6 +8,14 @@ import type {
} from "../capability-model/types.js"; } from "../capability-model/types.js";
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js"; import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { portableValueSchema } from "../bindings/portable.js"; import { portableValueSchema } from "../bindings/portable.js";
function preparedInputPaths(type: ValueType, path: (string | null)[] = []): (string | null)[][] {
if (type.kind === "change") return [path];
if (type.kind === "optional") return preparedInputPaths(type.value, path);
if (type.kind === "list") return preparedInputPaths(type.value, [...path, null]);
if (type.kind === "record")
return Object.entries(type.fields).flatMap(([key, value]) => preparedInputPaths(value, [...path, key]));
return [];
}
/** Small data-only interpreter input; no compiler, parser, Node module or SQL dependency reaches the browser. */ /** Small data-only interpreter input; no compiler, parser, Node module or SQL dependency reaches the browser. */
export function compileKernelProgram(plan: ExecutionPlan): unknown { export function compileKernelProgram(plan: ExecutionPlan): unknown {
if (hexDigest(plan.definition) !== plan.digest) if (hexDigest(plan.definition) !== plan.digest)
@@ -172,6 +180,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
: {}), : {}),
}; };
} }
const inputChanges = preparedInputPaths(operation.input);
const input = schema(operation.input), const input = schema(operation.input),
output = schema(operation.output); output = schema(operation.output);
return { return {
@@ -183,6 +192,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
effect: operation.execution.kind, effect: operation.execution.kind,
targets: operation.execution.kind === "remote" ? [] : operation.execution.targets, targets: operation.execution.kind === "remote" ? [] : operation.execution.targets,
input, input,
...(inputChanges.length ? { inputChanges } : {}),
output, output,
unsupported: unsupported ?? (input && output ? null : "UNSUPPORTED_OPERATION_CODEC"), unsupported: unsupported ?? (input && output ? null : "UNSUPPORTED_OPERATION_CODEC"),
binding: target, binding: target,
+11 -1
View File
@@ -9,10 +9,20 @@ const source = { repository: "https://example.test/change.git", commit: "a".repe
test("compound captured changes survive planning, transport lowering and typed edit ports", async () => { test("compound captured changes survive planning, transport lowering and typed edit ports", async () => {
const plan = await compoundCrdtPlan(); const plan = await compoundCrdtPlan();
const kernel = compileKernelProgram(plan) as { const kernel = compileKernelProgram(plan) as {
operations: { operation: string; input: { fields: { change: { kind: string } } } }[]; operations: {
operation: string;
inputChanges?: (string | null)[][];
input: { fields: { change: { kind: string } } };
}[];
}; };
const operation = kernel.operations.find((op) => op.operation === "compound:update:call")!; const operation = kernel.operations.find((op) => op.operation === "compound:update:call")!;
assert.equal(operation.input.fields.change.kind, "record"); assert.equal(operation.input.fields.change.kind, "record");
const paths = kernel.operations as unknown as { operation: string; inputChanges?: (string | null)[][] }[];
assert.deepEqual(paths.find((op) => op.operation === "compound:update:call")!.inputChanges, [["change"]]);
assert.deepEqual(paths.find((op) => op.operation === "compound:batch:call")!.inputChanges, [
["edits", null, "change"],
]);
assert.deepEqual(paths.find((op) => op.operation === "compound:content:edit")!.inputChanges, [[]]);
const code = generatePortableTypeScriptBindings(plan.definition.bindingSchema, "compound:runtime@1", { const code = generatePortableTypeScriptBindings(plan.definition.bindingSchema, "compound:runtime@1", {
target: "browser", target: "browser",
}); });
+9
View File
@@ -2,6 +2,15 @@ external atom Note id "compound:note";
type Content = record { text: string; tags: list<string>; }; type Content = record { text: string; tags: list<string>; };
type Edit = record { change: change<Content>; resolved: bool; }; type Edit = record { change: change<Content>; resolved: bool; };
package EditRuntime id "compound:runtime" revision "compound:runtime@1" { package EditRuntime id "compound:runtime" revision "compound:runtime@1" {
operation batch id "compound:runtime:batch" : record { edits: list<Edit>; } -> unit
mode call execution transaction targets [browser, server] receiver atom Note requires {
state content id "compound:batch:content" : Content [edit];
state resolved id "compound:batch:resolved" : bool [write];
} accept using checkBatch;
operation checkBatch id "compound:runtime:check-batch" : record { edits: list<Edit>; } -> unit
mode call execution snapshot targets [server] receiver atom Note requires {
state allowed id "compound:batch:allowed" : bool [read];
};
operation update id "compound:runtime:update" : Edit -> unit operation update id "compound:runtime:update" : Edit -> unit
mode call execution transaction targets [browser, server] receiver atom Note requires { mode call execution transaction targets [browser, server] receiver atom Note requires {
state content id "compound:edit:content" : Content [edit]; state content id "compound:edit:content" : Content [edit];
+3
View File
@@ -1,6 +1,9 @@
type Content = record { text: string; tags: list<string>; }; type Content = record { text: string; tags: list<string>; };
type Edit = record { change: change<Content>; resolved: bool; }; type Edit = record { change: change<Content>; resolved: bool; };
interface NoteData id "compound:data" revision "compound:data@1" { interface NoteData id "compound:data" revision "compound:data@1" {
operation batch id "compound:batch" : record { edits: list<Edit>; } -> unit {
call id "compound:batch:call" execution transaction targets [browser, server];
}
operation update id "compound:update" : Edit -> unit { operation update id "compound:update" : Edit -> unit {
call id "compound:update:call" execution transaction targets [browser, server]; call id "compound:update:call" execution transaction targets [browser, server];
} }
+4
View File
@@ -14,6 +14,10 @@ workspace Compound id "workspace:compound" revision "workspace:compound@1" commi
bind resolved.get to state Resolved.read; bind resolved.get to state Resolved.read;
bind title.get to state Title.read; bind title.get to state Title.read;
bind title.set to state Title.write; bind title.set to state Title.write;
bind batch.call to package EditRuntime.batch with {
content to state Content;
resolved to state Resolved;
} accept with { allowed to state Allowed; };
bind update.call to package EditRuntime.update with { bind update.call to package EditRuntime.update with {
content to state Content; content to state Content;
resolved to state Resolved; resolved to state Resolved;