From 83092edc1cfcf329ec92bbe05e41f1e184ede713 Mon Sep 17 00:00:00 2001 From: Duane Johnson Date: Thu, 22 Jan 2026 17:12:39 -0700 Subject: [PATCH] fix: keep forked result and toJSON in sync during checkout --- .../src/encoding/shallow_snapshot.rs | 2 +- crates/loro-wasm/tests/checkout.test.ts | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/loro-internal/src/encoding/shallow_snapshot.rs b/crates/loro-internal/src/encoding/shallow_snapshot.rs index bb9a6cf52..fb2f44c0e 100644 --- a/crates/loro-internal/src/encoding/shallow_snapshot.rs +++ b/crates/loro-internal/src/encoding/shallow_snapshot.rs @@ -254,7 +254,7 @@ pub(crate) fn encode_snapshot_at( w: &mut W, ) -> Result<(), LoroEncodeError> { let was_detached = doc.is_detached(); - let version_before_start = doc.oplog_frontiers(); + let version_before_start = doc.state_frontiers(); doc._checkout_without_emitting(frontiers, true, false) .unwrap(); let result = 'block: { diff --git a/crates/loro-wasm/tests/checkout.test.ts b/crates/loro-wasm/tests/checkout.test.ts index 78d2a98af..7dc9cd59f 100644 --- a/crates/loro-wasm/tests/checkout.test.ts +++ b/crates/loro-wasm/tests/checkout.test.ts @@ -76,4 +76,42 @@ describe("Checkout", () => { doc.import(docB.export({ mode: "update" })); expect(docB.cmpWithFrontiers(doc.frontiers())).toBe(0); }); + + it("forkAt inside subscription during checkout event should not corrupt state", async () => { + const doc = new LoroDoc(); + doc.setPeerId("1"); + + // Make some changes + doc.getText("text").insert(0, "Hello"); + doc.commit(); + const frontier1 = doc.frontiers(); // [{ peer: "1", counter: 4 }] + + doc.getText("text").insert(5, " World"); + doc.commit(); + + // Verify initial state + expect(doc.toJSON()).toStrictEqual({ text: "Hello World" }); + + // Subscribe and call forkAt inside the callback + let forkResult: any = null; + doc.subscribe((event) => { + if (event.by === "checkout") { + // BUG: This corrupts the checkout state + const fork = doc.forkAt(doc.frontiers()); + forkResult = fork.toJSON(); + } + }); + + // Checkout to earlier state + doc.checkout(frontier1); + + // Wait for events to be processed + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect(doc.frontiers()).toStrictEqual(frontier1); + expect(doc.toJSON()).toStrictEqual({ text: "Hello" }); + + // The fork should also have the correct state at frontier1 + expect(forkResult).toStrictEqual({ text: "Hello" }); + }); });