Skip to content

Instantly share code, notes, and snippets.

@stephen
Created July 13, 2023 17:50
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/2892b843927a17b9dc78a3751e8dc603 to your computer and use it in GitHub Desktop.
Save stephen/2892b843927a17b9dc78a3751e8dc603 to your computer and use it in GitHub Desktop.
repair a damaged automerge doc
/* eslint-disable no-loop-func */
const Automerge = require("@automerge/automerge");
const assert = require("node:assert");
const fs = require("fs");
function verify(document, unchecked = false) {
return Automerge.load(Automerge.save(document), { unchecked });
}
const brokenDoc = process.argv[2];
const original = Automerge.load(fs.readFileSync(brokenDoc));
const changes = Automerge.getAllChanges(original);
let dummy = Automerge.init();
let repaired = Automerge.init();
for (const [i, change] of changes.entries()) {
console.time(`applied ${i}`);
const decoded = Automerge.decodeChange(change);
[dummy] = Automerge.applyChanges(dummy, [change], {
patchCallback: (patches, { after }) => {
repaired = Automerge.change(
repaired,
{ message: decoded.message, time: decoded.time },
(doc) => {
for (const patch of patches) {
let target = doc;
for (const part of patch.path.slice(0, -1)) {
target = target[part];
}
switch (patch.action) {
case "put":
let dummyValue = after;
for (const part of patch.path) {
dummyValue = dummyValue[part];
}
target[patch.path[patch.path.length - 1]] =
dummyValue instanceof Automerge.Text
? new Automerge.Text(patch.value)
: patch.value;
break;
case "splice":
if (patch.marks) {
throw new Error("marks not implemented");
}
if (target instanceof Automerge.Text) {
target.insertAt(
patch.path[patch.path.length - 1],
patch.value
);
} else if (Array.isArray(target)) {
target.splice(
patch.path[patch.path.length - 1],
0,
patch.value
);
} else {
throw new Error("unknown type to splice");
}
break;
case "insert":
if (patch.conflicts || patch.marks) {
console.error(patch);
throw new Error("not supported");
}
if (target instanceof Automerge.Text) {
target.insertAt(
patch.path[patch.path.length - 1],
...patch.values
);
} else if (Array.isArray(target)) {
target.splice(
patch.path[patch.path.length - 1],
0,
...patch.values
);
} else {
throw new Error("unknown type to splice");
}
break;
case "del":
if (patch.length) {
if (target instanceof Automerge.Text) {
target.deleteAt(
patch.path[patch.path.length - 1],
patch.length
);
} else if (Array.isArray(target)) {
target.splice(
patch.path[patch.path.length - 1],
patch.length
);
} else {
throw new Error("unknown type to splice");
}
} else {
delete target[patch.path[patch.path.length - 1]];
}
break;
case "conflict":
break;
default:
throw new Error("not implemented");
}
}
}
);
},
});
console.timeEnd(`applied ${i}`);
}
verify(repaired);
assert.deepStrictEqual(repaired, original);
fs.writeFileSync("fixed.bin", Automerge.save(repaired));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment