Skip to content

Instantly share code, notes, and snippets.

@stephen
Last active July 13, 2023 18:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stephen/5aeac63971520dca4473b5b15f2c42e6 to your computer and use it in GitHub Desktop.
Save stephen/5aeac63971520dca4473b5b15f2c42e6 to your computer and use it in GitHub Desktop.
repro a text mangling bug in automerge
const odiff = require("odiff");
/**
* changeText uses odiff to write a minimal string diff to an automerge.Text.
*/
const changeText = (base, newValue) => {
const diff = odiff([...base.toString()], [...newValue]);
for (const patch of diff) {
switch (patch.type) {
case "add":
base?.insertAt(patch.index, ...patch.vals);
break;
case "rm":
base?.deleteAt(patch.index, patch.num);
break;
case "set":
base?.set(patch.path[0], patch.val);
break;
default:
throw new Error(`unexpected unset patch in text diff: ${patch}`);
}
}
};
const Automerge = require("@automerge/automerge");
// Make a doc and make some changes...
let original = Automerge.from({ t: new Automerge.Text("") });
original = Automerge.change(original, (doc) =>
changeText(doc.t, `Billy Wald "You’re great"`)
);
original = Automerge.change(original, (doc) => changeText(doc.t, `Unknown`));
original = Automerge.change(original, (doc) =>
changeText(doc.t, `billy culinary dropout`)
);
// Case 1: original output (ok)
// Output:
// original: billy culinary dropout
console.log("original:", original.t.toString());
// Case 2: using applyChanges (fails)
const changes = Automerge.getAllChanges(original);
const [replayed] = Automerge.applyChanges(Automerge.init(), changes);
// Output:
// replayed: billy culiknary dropowt
console.log("replayed using applyChanges:", replayed.t.toString());
// Case 3: using loadIncremental (fails)
let incrementalReplay = Automerge.init();
for (const change of changes) {
incrementalReplay = Automerge.loadIncremental(incrementalReplay, change);
}
console.log("replayed using loadIncremental:", incrementalReplay.t.toString());
// Case 4: using sync (fails)
let msg;
let peerASync = Automerge.initSyncState();
let peerBSync = Automerge.initSyncState();
let peerA = Automerge.clone(original);
let peerB = Automerge.init();
do {
if (msg) {
[peerA, peerASync, msg] = Automerge.receiveSyncMessage(
peerA,
peerASync,
msg
);
}
[peerASync, msg] = Automerge.generateSyncMessage(peerA, peerASync);
if (!msg) {
break;
}
[peerB, peerBSync] = Automerge.receiveSyncMessage(peerB, peerBSync, msg);
[peerBSync, msg] = Automerge.generateSyncMessage(peerB, peerBSync);
} while (msg);
console.log("replayed using sync:", peerB.t.toString());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment