From cef3d653a7cff942d2abf6f952fb97517247fb1e Mon Sep 17 00:00:00 2001 From: Zixuan Chen Date: Mon, 28 Sep 2026 04:37:30 +0800 Subject: [PATCH] fix(loro.js): keep a checkout of the current latest version attached Rust's checkout returns early when the target equals the state's frontiers, re-attaching when they are the latest frontiers, and also when the shrunk target equals them. loro.js always detached, so `doc.checkout(doc.frontiers())` stopped later imports from reaching the state and made loro.js peers diverge from loro-crdt ones that ran the same calls. The old detached-import test relied on this; it now uses detach(). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../loro-js-checkout-current-attached.md | 5 ++ loro-js/src/runtime/document.ts | 22 +++++++++ loro-js/tests/runtime.test.ts | 49 ++++++++++++++++++- 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 .changeset/loro-js-checkout-current-attached.md diff --git a/.changeset/loro-js-checkout-current-attached.md b/.changeset/loro-js-checkout-current-attached.md new file mode 100644 index 000000000..cac9db071 --- /dev/null +++ b/.changeset/loro-js-checkout-current-attached.md @@ -0,0 +1,5 @@ +--- +"loro.js": patch +--- + +`checkout(frontiers)` on a `loro.js` document now behaves like `loro-crdt` when `frontiers` is the current version: nothing changes, and the document stays (or becomes) attached when that version is the latest one. Before, `doc.checkout(doc.frontiers())` detached the document, so later imports were not applied to its state. Checking out the latest version from an older one still leaves the document detached, as in Rust. diff --git a/loro-js/src/runtime/document.ts b/loro-js/src/runtime/document.ts index 57f9a8a4d..3b1b4a863 100644 --- a/loro-js/src/runtime/document.ts +++ b/loro-js/src/runtime/document.ts @@ -1715,9 +1715,25 @@ export class LoroDoc = Record `${id.counter}@${id.peer}`)); + return right.every((id) => keys.has(`${id.counter}@${id.peer}`)); +} diff --git a/loro-js/tests/runtime.test.ts b/loro-js/tests/runtime.test.ts index 616eedf6c..74243908f 100644 --- a/loro-js/tests/runtime.test.ts +++ b/loro-js/tests/runtime.test.ts @@ -349,8 +349,7 @@ describe("loro-wasm-compatible runtime", () => { const doc = new LoroDoc(); doc.setPeerId(2); doc.import(base); - const checkedOut = doc.frontiers(); - doc.checkout(checkedOut); + doc.detach(); source.getText("text").push(" latest"); doc.import(source.export({ mode: "update", from: doc.oplogVersion() })); @@ -361,6 +360,52 @@ describe("loro-wasm-compatible runtime", () => { expect(doc.getText("text").toString()).toBe("base latest"); }); + // Rust's checkout returns early when the target is the current version and + // re-attaches when that version is the latest one. + test("keeps a checkout of the current version attached like loro-crdt", () => { + const doc = new LoroDoc(); + doc.setPeerId(1); + doc.getText("text").insert(0, "x"); + doc.commit(); + doc.getText("text").insert(1, "y"); + doc.commit(); + const batches: LoroEventBatch[] = []; + doc.subscribe((batch) => batches.push(batch)); + + doc.checkout(doc.frontiers()); + expect(doc.isDetached()).toBe(false); + expect(batches).toHaveLength(0); + const remote = new LoroDoc(); + remote.setPeerId(2); + remote.getText("text").insert(0, "B"); + remote.commit(); + doc.import(remote.export({ mode: "update" })); + expect(doc.getText("text").toString()).toBe("xyB"); + + // A dominated extra frontier still names the current version. + doc.checkout([...doc.frontiers(), { peer: "1", counter: 0 }]); + expect(doc.isDetached()).toBe(false); + + doc.checkout([{ peer: "1", counter: 0 }]); + doc.checkout([{ peer: "1", counter: 0 }]); + expect(doc.isDetached()).toBe(true); + expect(doc.getText("text").toString()).toBe("x"); + // Checking out the latest version from an older one stays detached. + doc.checkout(doc.oplogFrontiers()); + expect(doc.isDetached()).toBe(true); + expect(doc.getText("text").toString()).toBe("xyB"); + + // detach() at the latest version, then checkout of it, re-attaches. + doc.checkoutToLatest(); + doc.detach(); + doc.checkout(doc.frontiers()); + expect(doc.isDetached()).toBe(false); + doc.getText("text").insert(0, "!"); + doc.commit(); + expect(doc.getText("text").toString()).toBe("!xyB"); + expect(doc.version().compare(doc.oplogVersion())).toBe(0); + }); + test("holds causally incomplete updates pending until dependencies arrive", () => { const source = new LoroDoc(); source.setPeerId(1);