Rebase pending Automerge field edits

This commit is contained in:
Timothy J. Aveni
2026-09-03 09:39:22 -07:00
parent a5899e6b57
commit 1a4f3ab8cc
5 changed files with 56 additions and 15 deletions
+2 -1
View File
@@ -18,7 +18,8 @@ export declare const loadAutomergeFieldDoc: (value: CaminoCrdtEnvelope, type: st
export declare const saveAutomergeFieldDoc: (doc: Automerge.Doc<AutomergeFieldDoc>, type: string) => CaminoCrdtEnvelope;
export declare const createAutomergeFieldValue: (value: unknown, type: string) => CaminoCrdtEnvelope;
export declare const isAutomergeFieldDoc: (value: unknown) => value is Automerge.Doc<AutomergeFieldDoc>;
export declare const createAutomergeFieldChanges: (base: Automerge.Doc<AutomergeFieldDoc>, next: Automerge.Doc<AutomergeFieldDoc>, type: string) => CaminoCrdtEnvelope;
export declare const reconcileAutomergeFieldDocs: <T>(base: CaminoAutomergeDoc<T>, desired: CaminoAutomergeDoc<T>) => CaminoAutomergeDoc<T>;
export declare const createAutomergeFieldChanges: (base: Automerge.Doc<AutomergeFieldDoc>, desired: Automerge.Doc<AutomergeFieldDoc>, type: string) => CaminoCrdtEnvelope;
export declare const encodeAutomergeFieldWrite: (value: unknown, type: string) => CaminoCrdtEnvelope;
export declare const normalizeAutomergeFieldValue: (value: unknown, type: string) => CaminoCrdtEnvelope;
export declare const materializeAutomergeFieldValue: (value: CaminoCrdtEnvelope, type: string) => unknown;
+9 -5
View File
@@ -107,11 +107,15 @@ const decodeChanges = (payload) => {
}
return changes;
};
export const createAutomergeFieldChanges = (base, next, type) => ({
$caminoCrdtType: type,
$caminoCrdtEncoding: AUTOMERGE_CHANGES_ENCODING,
$caminoCrdtPayload: bytesToBase64(encodeChanges(Automerge.getChanges(base, next))),
});
export const reconcileAutomergeFieldDocs = (base, desired) => Automerge.merge(Automerge.clone(base), Automerge.clone(desired));
export const createAutomergeFieldChanges = (base, desired, type) => {
const next = reconcileAutomergeFieldDocs(base, desired);
return {
$caminoCrdtType: type,
$caminoCrdtEncoding: AUTOMERGE_CHANGES_ENCODING,
$caminoCrdtPayload: bytesToBase64(encodeChanges(Automerge.getChanges(base, next))),
};
};
// Full snapshots remain an explicit escape hatch for initialization, import,
// and recovery when a client cannot establish a shared Automerge history.
export const encodeAutomergeFieldWrite = (value, type) => {
+1 -1
View File
File diff suppressed because one or more lines are too long
+17 -8
View File
@@ -162,17 +162,26 @@ const decodeChanges = (payload: Uint8Array): Automerge.Change[] => {
return changes;
};
export const reconcileAutomergeFieldDocs = <T>(
base: CaminoAutomergeDoc<T>,
desired: CaminoAutomergeDoc<T>,
): CaminoAutomergeDoc<T> =>
Automerge.merge(Automerge.clone(base), Automerge.clone(desired));
export const createAutomergeFieldChanges = (
base: Automerge.Doc<AutomergeFieldDoc>,
next: Automerge.Doc<AutomergeFieldDoc>,
desired: Automerge.Doc<AutomergeFieldDoc>,
type: string,
): CaminoCrdtEnvelope => ({
$caminoCrdtType: type,
$caminoCrdtEncoding: AUTOMERGE_CHANGES_ENCODING,
$caminoCrdtPayload: bytesToBase64(
encodeChanges(Automerge.getChanges(base, next)),
),
});
): CaminoCrdtEnvelope => {
const next = reconcileAutomergeFieldDocs(base, desired);
return {
$caminoCrdtType: type,
$caminoCrdtEncoding: AUTOMERGE_CHANGES_ENCODING,
$caminoCrdtPayload: bytesToBase64(
encodeChanges(Automerge.getChanges(base, next)),
),
};
};
// Full snapshots remain an explicit escape hatch for initialization, import,
// and recovery when a client cannot establish a shared Automerge history.
+27
View File
@@ -11,6 +11,7 @@ import {
loadAutomergeFieldDoc,
materializeAutomergeFieldValue,
mergeAutomergeFieldValues,
reconcileAutomergeFieldDocs,
saveAutomergeFieldDoc,
} from "../src/crdt-automerge.js";
@@ -105,6 +106,32 @@ test("Automerge change application is idempotent", () => {
assert.deepEqual(twice.materialized, { count: 1 });
});
test("Automerge changes retain remote history while local writes are pending", () => {
const stored = createAutomergeFieldValue({ local: false, remote: false }, type);
const base = loadAutomergeFieldDoc(stored, type);
const local = Automerge.change(
Automerge.clone(base, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
(doc) => {
(doc.value as { local: boolean }).local = true;
},
);
const remote = Automerge.change(
Automerge.clone(base, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
(doc) => {
(doc.value as { remote: boolean }).remote = true;
},
);
const rebased = reconcileAutomergeFieldDocs(remote, local);
const update = createAutomergeFieldChanges(remote, rebased, type);
const result = mergeAutomergeFieldValues({
existing: saveAutomergeFieldDoc(remote, type),
incoming: update,
type,
});
assert.deepEqual(result.materialized, { local: true, remote: true });
});
test("Automerge change batches reject malformed framing", () => {
const stored = createAutomergeFieldValue({ count: 0 }, type);
assert.throws(