Skip to content

loro.js: an insert concurrent with a delete baked into an imported snapshot lands at the wrong position #1163

Closed

Description

@zxch3n

Summary

When loro.js imports a snapshot, and then an update containing a Text insert that is concurrent with a delete already applied in that snapshot, the insert lands at the wrong position. The latest state is wrong, and exports persist the error: Rust re-imports the wrong text. Importing the same history as updates only gives the right result.

This was found in the third review of the loro.js snapshot-checkout PRs (#1126 → #1127 → #1128). It reproduces on main (29553d5b), so those PRs did not introduce it.

Reproduction

The writer is loro-crdt WASM built from 29553d5b:

const a = new LoroDoc(); a.setPeerId(1);
a.getText("t").insert(0, "abcdef"); a.commit();
const b = new LoroDoc(); b.setPeerId(2);
b.import(a.export({ mode: "snapshot" }));
b.getText("t").insert(4, "X"); b.commit();         // between "d" and "e"
const update = b.export({ mode: "update", from: a.oplogVersion() });
a.getText("t").delete(2, 2); a.commit();            // deletes "cd"
const snapshot = a.export({ mode: "snapshot" });
reader text
Rust: import(snapshot), then import(update) abXef
loro.js main: import(snapshot), then import(update) abefX
loro.js main: importBatch([snapshot, update]) abefX
loro.js main: import every change as updates, then import(update) abXef
Rust re-importing loro.js's snapshot export abefX

The review also reports that on styled Rust text that loro.js cannot replay, the insert lands after the text: abYefX, where Rust gives abYXef.

Likely cause

A Text state hydrated from a snapshot has no tombstones for the characters deleted before the snapshot. loro.js therefore maps the concurrent insert onto the snapshot's visible text instead of placing it next to its deleted neighbours. On #1126, after a checkout that crosses one of that container's snapshot operations, the container is replayed from its history, and the same import gives abXef.

Possible fix

Before applying an imported operation that is concurrent with the snapshot's operations on a snapshot-hydrated sequence, replay that container's history first, as #completeSnapshotSequence does on #1126. Rust rich text that loro.js cannot replay would also need the anchor model from #1137.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions