Skip to content

Commit c00665a

Browse files
authored
docs: clarify container detached/attached states and text methods (#20)
1 parent 3624367 commit c00665a

5 files changed

Lines changed: 223 additions & 10 deletions

File tree

pages/docs/advanced/cid.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ export type ContainerID =
4444
containers
4545
- Contains the Operation ID of its creation within its Container ID
4646

47+
## Container States and IDs
48+
49+
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.
50+
51+
> For a comprehensive guide on containers, including attached vs detached states, see [Container Concepts](/docs/concepts/container).
52+
53+
Key points about ContainerID generation:
54+
- **Root containers**: Derive their ID from their name (e.g., "text" in `doc.getText("text")`)
55+
- **Normal containers**: Derive their ID from the operation (OpID) that created them
56+
- **Detached containers**: Have a default placeholder ID until they're inserted into a document
57+
4758
## Container Overwrites
4859

4960
When initializing child containers in parallel, overwrites can occur instead of

pages/docs/concepts/_meta.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export default {
22
crdt: "What are CRDTs",
3+
container: "Container",
34
choose_crdt_type: "How to Choose the Right CRDT Types",
45
when_not_crdt: "When Not to Rely on CRDTs"
56
}

pages/docs/concepts/container.mdx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Container
2+
3+
Containers are the fundamental building blocks in Loro for organizing and structuring collaborative data. They provide typed data structures that automatically merge when concurrent edits occur.
4+
5+
## Container Types
6+
7+
Loro provides several container types, each optimized for different use cases:
8+
9+
- **LoroMap**: Key-value pairs with Last-Write-Wins semantics
10+
- **LoroList**: Ordered sequences that merge concurrent insertions
11+
- **LoroText**: Text with character-level merging and rich text support
12+
- **LoroTree**: Hierarchical tree structures with move operations
13+
- **LoroMovableList**: Lists with reordering capabilities
14+
- **LoroCounter**: Numerical values with increment/decrement operations
15+
16+
## Container States: Attached vs Detached
17+
18+
Containers in Loro exist in two distinct states that affect their behavior and identity.
19+
20+
### Detached Containers
21+
22+
A container is **detached** when created directly using constructors:
23+
24+
```ts twoslash
25+
import { LoroMap, LoroText, LoroList } from "loro-crdt";
26+
// ---cut---
27+
// These containers are all detached
28+
const map = new LoroMap();
29+
const text = new LoroText();
30+
const list = new LoroList();
31+
```
32+
33+
Characteristics of detached containers:
34+
- Not yet part of any document
35+
- Have a default placeholder ContainerID
36+
- Can be used as templates or temporary data structures
37+
- Will get a proper ContainerID when inserted into a document
38+
39+
### Attached Containers
40+
41+
A container becomes **attached** when it's part of a document hierarchy:
42+
43+
```ts twoslash
44+
import { LoroDoc, LoroMap, LoroText } from "loro-crdt";
45+
// ---cut---
46+
const doc = new LoroDoc();
47+
48+
// Root containers are immediately attached
49+
const rootMap = doc.getMap("myMap");
50+
const rootText = doc.getText("myText");
51+
52+
// Child containers: the returned value is attached
53+
const detachedChild = new LoroText();
54+
const attachedChild = rootMap.setContainer("child", detachedChild);
55+
// Note: detachedChild remains detached
56+
// attachedChild is the attached version with proper ContainerID
57+
```
58+
59+
Characteristics of attached containers:
60+
- Belong to a specific document
61+
- Have a proper ContainerID that uniquely identifies them
62+
- Changes are tracked in the document's history
63+
- Can be synchronized across peers
64+
65+
## Container IDs
66+
67+
Every attached container has a unique ContainerID that identifies it within the distributed system. The ID generation depends on the container type:
68+
69+
- **Root containers**: ID derived from their name (e.g., "myMap" in `doc.getMap("myMap")`)
70+
- **Child containers**: ID based on the operation that created them (OpID)
71+
72+
This deterministic ID generation ensures that:
73+
- The same container can be identified across all peers
74+
- Container IDs are not random but contextually determined
75+
- A detached container cannot have its final ID until insertion
76+
77+
## Working with Containers
78+
79+
### Creating Root Containers
80+
81+
Root containers are created through the document API and are immediately attached:
82+
83+
```ts twoslash
84+
import { LoroDoc } from "loro-crdt";
85+
// ---cut---
86+
const doc = new LoroDoc();
87+
88+
// These methods create or get root containers
89+
const map = doc.getMap("settings");
90+
const text = doc.getText("content");
91+
const list = doc.getList("items");
92+
const tree = doc.getTree("hierarchy");
93+
```
94+
95+
### Nesting Containers
96+
97+
Containers can be nested to create complex data structures:
98+
99+
```ts twoslash
100+
import { LoroDoc, LoroMap, LoroList, LoroText } from "loro-crdt";
101+
// ---cut---
102+
const doc = new LoroDoc();
103+
const rootMap = doc.getMap("root");
104+
105+
// Method 1: Using setContainer (returns attached container)
106+
const childText = rootMap.setContainer("description", new LoroText());
107+
108+
// Method 2: Using insertContainer for lists
109+
const list = doc.getList("items");
110+
const childMap = list.insertContainer(0, new LoroMap());
111+
```
112+
113+
## Container Overwrites
114+
115+
When initializing child containers in parallel, overwrites can occur instead of
116+
automatic merging. For example:
117+
118+
```ts twoslash
119+
import { LoroDoc, LoroText } from "loro-crdt";
120+
// ---cut---
121+
const a: string = "hello";
122+
const doc = new LoroDoc();
123+
const map = doc.getMap("map");
124+
125+
// Parallel initialization of child containers
126+
const docB = doc.fork();
127+
const textA = doc.getMap("map").setContainer("text", new LoroText());
128+
textA.insert(0, "A");
129+
const textB = docB.getMap("map").setContainer("text", new LoroText());
130+
textB.insert(0, "B");
131+
132+
doc.import(docB.export({ mode: "update" }));
133+
// Result: Either { "meta": { "text": "A" } } or { "meta": { "text": "B" } }
134+
```
135+
136+
This behavior poses a significant risk of data loss if the editing history is
137+
not preserved. Even when the complete history is available and allows for data
138+
recovery, the recovery process can be complex.
139+
140+
<aside>
141+
By default, Loro and Automerge preserve the whole editing history in a directed
142+
acyclic graph like Git.
143+
</aside>
144+
145+
When a container holds substantial data or serves as the primary storage for
146+
document content, overwriting it can lead to the unintended hiding/loss of
147+
critical information. For this reason, it is essential to implement careful and
148+
systematic container initialization practices to prevent such issues.
149+
150+
### Best Practices
151+
152+
1. When containers might be initialized concurrently, prefer initializing them
153+
at the root level rather than as nested containers
154+
155+
2. When using map containers:
156+
- If possible, initialize all child containers during the map container's
157+
initialization
158+
- Avoid concurrent creation of child containers with the same key in the map
159+
container to prevent overwrites
160+
161+
The overwrite behavior occurs because parallel creation of child containers
162+
results in different container IDs, preventing automatic merging of their
163+
contents.
164+
165+
166+
## Related Concepts
167+
168+
- [Container ID](/docs/advanced/cid): Deep dive into how Container IDs work
169+
- [Choosing CRDT Types](/docs/concepts/choose_crdt_type): Guide for selecting the right container type
170+
- [Composition](/docs/tutorial/composition): How to compose containers into complex structures

pages/docs/index.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Introduction to Loro
1+
## Introduction to Loro
22

33
It is well-known that syncing data/building realtime collaborative apps is
44
challenging, especially when devices can be offline or part of a peer-to-peer
@@ -92,7 +92,7 @@ import { Cards } from "nextra/components";
9292
</Cards.Card>
9393
</Cards>
9494

95-
# Is Loro Right for You?
95+
## Is Loro Right for You?
9696

9797
### ✅ Use Loro when you need:
9898

@@ -111,7 +111,7 @@ import { Cards } from "nextra/components";
111111

112112
[Learn more about when not to use CRDTs →](/docs/concepts/when_not_crdt)
113113

114-
# Differences from other CRDT libraries
114+
## Differences from other CRDT libraries
115115

116116
The table below summarizes Loro's features, which may not be present in other
117117
CRDT libraries.

pages/docs/tutorial/text.mdx

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ The [loro-prosemirror](https://github.com/loro-dev/loro-prosemirror) package pro
3434

3535
The ProseMirror binding can also be used with [Tiptap](https://tiptap.dev/), a popular rich text editor built on top of ProseMirror. This means you can easily add collaborative editing capabilities to your Tiptap-based applications.
3636

37-
```ts no_run twoslash
37+
```ts no_run
3838
import {
3939
CursorAwareness,
4040
LoroCursorPlugin,
@@ -47,9 +47,7 @@ import { LoroDoc } from "loro-crdt";
4747
import { EditorView } from "prosemirror-view";
4848
import { EditorState } from "prosemirror-state";
4949
import { keymap } from "prosemirror-keymap";
50-
declare const pmPlugins: any[];
51-
declare const editorDom: HTMLElement;
52-
// ---cut---
50+
5351
const doc = new LoroDoc();
5452
const awareness = new CursorAwareness(doc.peerIdStr);
5553
const plugins = [
@@ -76,12 +74,12 @@ The [loro-codemirror](https://github.com/loro-dev/loro-codemirror) package provi
7674
- Cursor awareness
7775
- Undo/Redo functionality
7876

79-
```ts no_run twoslash
77+
```ts no_run
8078
import { EditorState } from "@codemirror/state";
8179
import { EditorView } from "@codemirror/view";
8280
import { LoroExtensions } from "loro-codemirror";
8381
import { Awareness, LoroDoc, UndoManager } from "loro-crdt";
84-
// ---cut---
82+
8583
const doc = new LoroDoc();
8684
const awareness = new Awareness(doc.peerIdStr);
8785
const undoManager = new UndoManager(doc, {});
@@ -316,11 +314,44 @@ const restored = Cursor.decode(bytes);
316314
const pos = doc.getCursorPos(restored);
317315
```
318316

317+
### `toJSON(): string`
318+
319+
Returns the plain text content as a string. This method:
320+
- Returns only the text content without any formatting marks
321+
- Is not affected by any rich text attributes (bold, italic, links, etc.)
322+
- Is equivalent to `toString()` for text containers
323+
324+
If you need rich text information including formatting, use `toDelta()` instead.
325+
319326
### `toDelta(): Delta<string>[]`
320327

321-
Get the rich text value. It's in
328+
Get the rich text value with all formatting information. It's in
322329
[Quill's Delta format](https://quilljs.com/docs/delta/).
323330

331+
Unlike `toJSON()` which returns plain text, `toDelta()` preserves all rich text attributes like bold, italic, links, and custom marks.
332+
333+
Example comparing `toJSON()` vs `toDelta()`:
334+
335+
```ts twoslash
336+
import { LoroDoc } from "loro-crdt";
337+
// ---cut---
338+
const doc = new LoroDoc();
339+
doc.configTextStyle({ bold: { expand: "after" } });
340+
const text = doc.getText("text");
341+
text.insert(0, "Hello World!");
342+
text.mark({ start: 0, end: 5 }, "bold", true);
343+
344+
// toJSON returns plain string without marks
345+
console.log(text.toJSON()); // "Hello World!"
346+
347+
// toDelta returns rich text with formatting
348+
console.log(text.toDelta());
349+
// [
350+
// { insert: "Hello", attributes: { bold: true } },
351+
// { insert: " World!" }
352+
// ]
353+
```
354+
324355
### `mark(range: {start: number, end: number}, key: string, value: any): void`
325356

326357
Mark the given range with a key-value pair.

0 commit comments

Comments
 (0)