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
4 changes: 4 additions & 0 deletions pages/blog/v1.0.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ console.log(docA.getText("text").toString()); // Hello!Hi!
console.log(docB.getText("text").toString()); // Hello!Hi!
```

If a nested child container may be created lazily by multiple peers under the
same Map key, use [Mergeable Containers](/blog/mergeable-containers) instead of
regular `setContainer`.

</details>
</details>

Expand Down
41 changes: 33 additions & 8 deletions pages/docs/advanced/cid.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ export type ContainerID =
containers
- Contains the Operation ID of its creation within its Container ID

3. **Mergeable Containers**
- Created through `LoroMap.ensureMergeableText`,
`ensureMergeableMap`, `ensureMergeableList`, and the other
`ensureMergeable*` methods
- Use a special form of Root Container ID. The stable name inside that ID is
derived from the logical position `(parent Map, key, container type)`, not
from the operation that first created the child
- Useful when multiple peers may lazily initialize the same child container
under the same Map key

## Container States and IDs

The ContainerID is not a random UUID but is deterministically generated based on the container's context. To understand how ContainerIDs work, it's important to first understand container states.
Expand All @@ -53,6 +63,7 @@ The ContainerID is not a random UUID but is deterministically generated based on
Key points about ContainerID generation:
- **Root containers**: Derive their ID from their name (e.g., "text" in `doc.getText("text")`)
- **Normal containers**: Derive their ID from the operation (OpID) that created them
- **Mergeable containers**: Use a special Root Container ID whose stable name is derived from the parent Map, key, and container type
- **Detached containers**: Have a default placeholder ID until they're inserted into a document

## Container Overwrites
Expand Down Expand Up @@ -92,17 +103,31 @@ document content, overwriting it can lead to the unintended hiding/loss of
critical information. For this reason, it is essential to implement careful and
systematic container initialization practices to prevent such issues.

### Mergeable Containers

The overwrite behavior occurs because parallel creation of regular child
containers results in different container IDs, preventing automatic merging of
their contents. If peers should address the same child by logical Map key, use a
mergeable child container:

```ts no_run
const text = doc.getMap("map").ensureMergeableText("text");
```

This uses a special Root Container ID for the child. Its stable name is derived
from the parent Map, key, and type, rather than from the operation that first
created it.

### Best Practices

1. When containers might be initialized concurrently, prefer initializing them
at the root level rather than as nested containers
1. Use `ensureMergeable*` when a Map child container should be shared by logical
key even if peers initialize it concurrently.

2. When using map containers:
2. When the structure is fixed and known ahead of time:
- If possible, initialize all child containers during the map container's
initialization
- Avoid concurrent creation of child containers with the same key in the map
container to prevent overwrites

The overwrite behavior occurs because parallel creation of child containers
results in different container IDs, preventing automatic merging of their
contents.
3. Use root containers when the child naturally has a stable top-level name.

4. Use regular child containers when each creation should have a distinct
operation-derived identity.
143 changes: 136 additions & 7 deletions pages/docs/api/js.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -360,17 +360,22 @@ doc.configDefaultTextStyle({ expand: "after" });

**📝 Note:** Creating root containers (e.g., `doc.getText("...")`) does not record operations; nested container creation (e.g., `map.setContainer(...)`) does.

**⚠️ Pitfall:** Avoid concurrent creation of child containers with the same key in LoroMaps. Instead of:
**⚠️ Pitfall:** Avoid concurrent creation of regular child containers with the same key in LoroMaps. Instead of:

```ts no_run
// Dangerous - can cause overwrites
// Dangerous - concurrent peers can create different child container IDs
doc.getMap("user").getOrCreateContainer(userId, new LoroMap());
```

Use:
Use a mergeable child container when the child should be identified by its logical position:

```ts no_run
doc.getMap("user").ensureMergeableMap(userId);
```

Alternatively, use a unique root container if that better fits your model:

```ts no_run
// Safe - unique root container per user
doc.getMap("user." + userId);
```

Expand Down Expand Up @@ -3603,7 +3608,7 @@ setContainer<T extends Container>(key: string, container: T): T
<Indent>
Sets a container as the value for a key.

**⚠️ Pitfall:** Concurrent child container creation at the same key on a LoroMap from multiple peers causes overwrites (different container IDs cannot auto-merge). See [Container initialization](/docs/tutorial/tips).
**⚠️ Pitfall:** Concurrent regular child container creation at the same key on a LoroMap from multiple peers causes overwrites (different container IDs cannot auto-merge). If the child should be shared by logical key, use the `ensureMergeable*` methods instead. See [Container initialization](/docs/tutorial/tips).

**Parameters:**

Expand All @@ -3625,6 +3630,125 @@ list.push("item1");

</Indent>

<Method id="LoroMap.ensureMergeableText">
```typescript no_run
ensureMergeableText(key: string): LoroText
```
</Method>
<Indent>
Ensures that `key` points to a mergeable Text child container. Peers that call this method concurrently with the same parent Map and key will address the same logical Text container instead of creating competing child container IDs.

Use this for lazily-created text fields that should behave like one shared document for all peers.

**Parameters:**

- `key` - The Map key for the mergeable child container

**Returns:** The mergeable Text container

**Example:**

```ts no_run
const body = doc.getMap("note").ensureMergeableText("body");
body.insert(0, "Hello");
```

</Indent>

<Method id="LoroMap.ensureMergeableMap">
```typescript no_run
ensureMergeableMap(key: string): LoroMap
```
</Method>
<Indent>
Ensures that `key` points to a mergeable Map child container. Use this when a nested record, profile, settings object, or migration-created child Map may be initialized by multiple peers.

**Parameters:**

- `key` - The Map key for the mergeable child container

**Returns:** The mergeable Map container

**Example:**

```ts no_run
const record = doc.getMap("records").ensureMergeableMap(recordId);
record.set("updated", true);
```

</Indent>

<Method id="LoroMap.ensureMergeableList">
```typescript no_run
ensureMergeableList(key: string): LoroList
```
</Method>
<Indent>
Ensures that `key` points to a mergeable List child container. Use this for dynamic child lists, such as one list per day, where concurrent first creation should produce one shared List.

**Parameters:**

- `key` - The Map key for the mergeable child container

**Returns:** The mergeable List container

**Example:**

```ts no_run
const entries = doc.getMap("days").ensureMergeableList("2026-06-08");
entries.insert(0, "meeting notes");
```

</Indent>

<Method id="LoroMap.ensureMergeableMovableList">
```typescript no_run
ensureMergeableMovableList(key: string): LoroMovableList
```
</Method>
<Indent>
Ensures that `key` points to a mergeable MovableList child container.

**Parameters:**

- `key` - The Map key for the mergeable child container

**Returns:** The mergeable MovableList container

</Indent>

<Method id="LoroMap.ensureMergeableTree">
```typescript no_run
ensureMergeableTree(key: string): LoroTree
```
</Method>
<Indent>
Ensures that `key` points to a mergeable Tree child container.

**Parameters:**

- `key` - The Map key for the mergeable child container

**Returns:** The mergeable Tree container

</Indent>

<Method id="LoroMap.ensureMergeableCounter">
```typescript no_run
ensureMergeableCounter(key: string): LoroCounter
```
</Method>
<Indent>
Ensures that `key` points to a mergeable Counter child container.

**Parameters:**

- `key` - The Map key for the mergeable child container

**Returns:** The mergeable Counter container

</Indent>

<Method id="LoroMap.get">
```typescript no_run
get(key: string): Value | Container | undefined
Expand Down Expand Up @@ -3653,13 +3777,18 @@ const name = map.get("name");

<Method id="LoroMap.getOrCreateContainer">
```typescript no_run
/**
* @deprecated Prefer ensureMergeableText/Map/List/... for lazily-created Map child containers.
*/
getOrCreateContainer<T extends Container>(key: string, container: T): T
```
</Method>
<Indent>
Gets an existing container or creates a new one.
**Deprecated:** Prefer the `ensureMergeable*` methods for Map child containers that may be created lazily or concurrently by multiple peers.

Gets an existing container or creates a new regular child container.

**⚠️ Pitfall:** Parallel container creation for the same key across peers causes overwrites. See [Container initialization](/docs/tutorial/tips).
**⚠️ Pitfall:** Parallel container creation for the same key across peers creates different child container IDs and can cause overwrites. This method is kept for compatibility and for cases where you explicitly want a regular child container. For shared dynamic children, use `ensureMergeableText`, `ensureMergeableMap`, `ensureMergeableList`, `ensureMergeableMovableList`, `ensureMergeableTree`, or `ensureMergeableCounter`. See [Container initialization](/docs/tutorial/tips).

**Parameters:**

Expand Down
16 changes: 16 additions & 0 deletions pages/docs/concepts/choose_crdt_type.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,19 @@ Choosing the right CRDT type means understanding their potential behavior in con
For text, you can choose to represent it directly as a Value on a Map (where the Value can be a string type), or you can choose to use a Text CRDT. For the former, each operation completely overwrites the previous one, so if A and B make concurrent modifications, only one of their edits will remain in the end. For the latter, the CRDT will retain all concurrent insertions by both people, and concurrent deletions are combined to complete the deletion. For most text box edits, you might prefer the latter. But for something like editing a link, you might want to use the former.

For Lists, concurrently removing the same element and inserting a single element creates a new element, differentiating from the semantics of Set on a Map (we may consider providing a list set method in the future). For representing coordinates, it's better to use a Map rather than a List. If you represent coordinates as [x, y], and the A client updates the y coordinate by deleting the y element and reinserting a new y_a, and the B client also deletes y and inserts y_b, then after merging, the array will become [x, y_a, y_b], which does not conform to the user's schema. Using a Map can prevent this problem.

For nested data, also decide how the child container should get its identity:

- Use a Map value, such as a string or plain object, when the field should have
Last-Write-Wins replacement semantics.
- Use a regular child container with `setContainer` or `insertContainer` when
each creation should produce a distinct child object.
- Use a mergeable child container with `ensureMergeableText`,
`ensureMergeableMap`, `ensureMergeableList`, and the other
`ensureMergeable*` methods when multiple peers may lazily initialize the same
child under the same Map key and should end up editing one shared child.

This distinction matters for dynamic keys, schema migrations, date-keyed lists,
and per-entity subdocuments. In those cases, the child identity usually should
come from the logical position `(parent Map, key, type)` rather than from the
operation that happened to create it first.
36 changes: 28 additions & 8 deletions pages/docs/concepts/container.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,40 @@ document content, overwriting it can lead to the unintended hiding/loss of
critical information. For this reason, it is essential to implement careful and
systematic container initialization practices to prevent such issues.

### Mergeable Containers

The overwrite happens because `setContainer` creates a regular child container.
Its Container ID includes the operation that created it, so two peers that
create `map["text"]` concurrently create two different Text containers.

When a child container should be identified by its logical position in a Map,
use a mergeable child container instead:

```ts no_run
const text = doc.getMap("map").ensureMergeableText("text");
text.insert(0, "A");
```

Peers that call `ensureMergeableText("text")` on the same parent Map address the
same logical Text container. The same pattern is available for Map, List,
MovableList, Tree, and Counter children through the `ensureMergeable*` methods.

### Best Practices

1. When containers might be initialized concurrently, prefer initializing them
at the root level rather than as nested containers
1. For dynamic Map keys where peers may lazily create the same child, use
`ensureMergeableText`, `ensureMergeableMap`, `ensureMergeableList`, and the
other `ensureMergeable*` methods.

2. When using map containers:
2. When the structure is fixed and known ahead of time:
- If possible, initialize all child containers during the map container's
initialization
- Avoid concurrent creation of child containers with the same key in the map
container to prevent overwrites

The overwrite behavior occurs because parallel creation of child containers
results in different container IDs, preventing automatic merging of their
contents.
3. Use regular `setContainer` / `insertContainer` when each creation should
produce a distinct child object, or when you are modeling replacement rather
than shared initialization.

4. A unique root container name can still be a good fit when the child naturally
belongs at the root, for example `doc.getMap("user." + userId)`.


## Related Concepts
Expand Down
1 change: 1 addition & 0 deletions pages/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ CRDT libraries.
| Time Travel | ✅ | ✅ | ✅[1] | ✅ |
| [Fugue](https://arxiv.org/abs/2305.00583) / Maximal non-interleaving | ✅ | ✅ | ❌ | ❌ |
| JSON Types | ✅ | ❓ | ✅ | ✅ |
| [Mergeable Containers](/blog/mergeable-containers) | ✅ | ❌ | ❌ | ❌ |
| Merging Elements in Memory by Run Length Encoding | ✅ | ✅ | ✅ Inventor | ❌ |
| Byzantine-fault-tolerance | ❌ | ❌ | ❌ | ✅ |
| Version Control | ✅ | ❌ | ❌ | ✅ |
Expand Down
9 changes: 9 additions & 0 deletions pages/docs/tutorial/composition.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,12 @@ expect(doc.toJSON()).toStrictEqual({ map: { list: [0, 1, "Hello, World!"] } });
expect(callTimes).toBe(2);
}
```

`setContainer` creates a regular child container. This is fine for fixed
schemas, or when each creation should produce a distinct child object. If
multiple peers may lazily create the same child under the same Map key, use a
mergeable child container instead:

```ts no_run
const sharedList = map.ensureMergeableList("list");
```
2 changes: 1 addition & 1 deletion pages/docs/tutorial/loro_doc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ map.set("complex", {
});
```

Note: When you need to store a CRDT container (like LoroText, LoroList, etc.) within another container, use `setContainer()` or `insertContainer()` methods instead of regular `set()` or `insert()`. This creates a proper sub-container relationship that maintains CRDT properties.
Note: When you need to store a CRDT container (like LoroText, LoroList, etc.) within another container, use `setContainer()` or `insertContainer()` methods instead of regular `set()` or `insert()`. This creates a proper sub-container relationship that maintains CRDT properties. If a child container under a Map may be created lazily by multiple peers at the same key, use the `ensureMergeable*` methods instead; see [Containers](/docs/concepts/container) and [Composing CRDTs](/docs/tutorial/composition).

## Container Types

Expand Down
10 changes: 10 additions & 0 deletions pages/docs/tutorial/map.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ description: "how to use loro map crdt and show all APIs of loro map crdt."

Loro's Map uses LWW (Last-Write-Wins) semantics. When concurrent edits conflict, it compares Lamport logic timestamps to determine the winner.

Map slot conflicts are still resolved by LWW. If the value at a key is a child
container, regular child containers have operation-derived identities, so two
peers that create a child at the same key can create two different children. If
the child should be shared by logical key, use a mergeable child container:

```ts no_run
const body = doc.getMap("notes").ensureMergeableText(noteId);
body.insert(0, "Hello");
```

Here is how to use it:

```ts twoslash
Expand Down
6 changes: 6 additions & 0 deletions pages/docs/tutorial/sync.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ expect(docA.toJSON()).toStrictEqual(docB.toJSON());

Due to CRDT properties, document consistency is guaranteed when peers receive the same updates, regardless of order or duplicates.

Consistency does not remove the need to choose the right child container
identity. If two offline peers create regular child containers under the same
Map key, sync will converge to one visible Map value and the other child can be
hidden by the Map conflict rule. For dynamic child containers that should be
shared by logical key, use `ensureMergeable*` when modeling the document.

### Sync Strategies

1. **First Sync** (Initial synchronization between peers):
Expand Down
Loading
Loading