Preserve nested CRDT recovery and serialize compound editor submissions
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user