Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loro-js-checkout-current-attached.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions loro-js/src/runtime/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3009,9 +3009,25 @@ export class LoroDoc<T extends Record<string, Container> = Record<string, Contai
}
}
const before = this.#frontiersCodec();
// Like Rust's `_checkout_without_emitting`: checking out the current
// version changes nothing, and it re-attaches when that version is the
// latest one.
const currentFrontiers = this.frontiers();
if (sameFrontierSet(currentFrontiers, frontiers)) {
if (sameFrontierSet(frontiers, this.oplogFrontiers())) {
this.#checkoutVersion = undefined;
this.#detached = false;
}
if (this.#detachedEditing) this.#renewPeerId();
return;
}
const currentVersion = this.version();
const targetVersion = this.frontiersToVV(frontiers);
this.#assertVersionNotBeforeShallowRoot(targetVersion);
if (sameFrontierSet(currentFrontiers, this.vvToFrontiers(targetVersion))) {
if (this.#detachedEditing) this.#renewPeerId();
return;
}
const forwardRecords = this.#recordsInVersionRange(currentVersion, targetVersion);
const retreatRecords = this.#recordsInVersionRange(targetVersion, currentVersion);
const changed = changedContainerIds([...forwardRecords, ...retreatRecords]);
Expand Down Expand Up @@ -11172,6 +11188,12 @@ function treeNodeAtPath(
return index === undefined ? undefined : tree._nodeAt(undefined, index);
}

function sameFrontierSet(left: readonly OpId[], right: readonly OpId[]): boolean {
if (left.length !== right.length) return false;
const keys = new Set(left.map((id) => `${id.counter}@${id.peer}`));
return right.every((id) => keys.has(`${id.counter}@${id.peer}`));
}

interface StyleRedaction {
/** Start IDs (`peer:counter`) of the pairs whose values were nulled. */
readonly redacted: Set<string>;
Expand Down
49 changes: 47 additions & 2 deletions loro-js/tests/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() }));
Expand All @@ -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);
Expand Down
Loading