Source linked

Loro фиксирует CRDT Child Creation Race, которая тихо проглощает редакции

Когда два пользователя добавляют контент в одну и ту же пустую записку в автономном режиме, редактирования одного пользователя исчезают после синхронизации.

lorocrdtmergeable containerszixuan chenalexis williamsconflict resolution

Two users edit the same empty note offline, sync, and one user's work vanishes from the document view—but the data is still in history. That's not a bug in merge logic; it's a design flaw in how CRDTs assign child container identities.

Why Concurrent First Creation Looks Like Data Loss

In JSON-like CRDTs (Loro, Yjs, Automerge), the problem isn't merging list inserts or text edits—those work fine. The issue is one layer earlier: when both peers call map.setContainer("key", new LoroList()) offline, two different List containers get created, each tied to its own operation ID. After sync, the map's conflict rule picks one winner; the other container still exists in history but is invisible to doc.getMap().get("key"). From the application's perspective, that looks like data loss.

The Root Cause: Container IDs Tied to Creation Operations

A normal child container's ID includes the OpID of the creation operation. Two concurrent first creations inevitably produce different IDs. The map can only show one value per key, so it picks one. The workaround—pre-initialize all children when the parent map is created—works for fixed schemas but fails for dynamic ones. Calendar apps creating containers by date, schema migrations adding fields, or user-defined keys all hit this race.

Mergeable Containers: Identity from Logical Position

Loro's fix borrows the trick used for root containers (doc.getMap("state")): stable identity from a deterministic name, not from the operation that created it. Mergeable Containers derive their identity from the map key itself. Now two peers calling map.ensureMergeableList("2026-06-08") offline both target the same logical List, and their edits merge normally after sync.

API Change: ensureMergeableList vs setContainer

The new ensureMergeable* methods (ensureMergeableText, ensureMergeableMap, ensureMergeableList, ensureMergeableMovableList, ensureMergeableTree, ensureMergeableCounter) are idempotent and return the child, creating it only on first access. If the key already holds a regular value or container, the API returns an error—no silent overwrites. Zixuan Chen and Alexis Williams from Synapdeck handled the design and implementation.

Loro's approach doesn't change existing setContainer behavior; it adds a deliberate opt-in. For any field that should behave as one shared piece of data across all peers—a shared Text, a shared List, a shared Map—use ensureMergeable* and let the logical position be the source of truth. Dynamic schemas, calendar entries, and user-defined indices no longer need fragile upfront initialization.


Source: CRDTs merge concurrent edits. Why not concurrent creation?
Domain: loro.dev

Read original source ->

External source stays available while the OJO article and comment thread stay local.

Comments load interactively on the live page.