|
1 | 1 | # Operations and Changes |
2 | 2 |
|
3 | | -Operations and Changes are fundamental concepts in Loro that define how edits are tracked, grouped, and synchronized across collaborative documents. Understanding these concepts is essential for effectively using Loro's collaboration features and optimizing performance. |
| 3 | +## Quick Reference |
4 | 4 |
|
5 | | -## What are Operations? |
| 5 | +**Operations** are atomic edits. **Changes** are logical groups of operations with metadata. Understanding these helps optimize sync and performance. |
6 | 6 |
|
7 | | -An **Operation** (Op) is the atomic unit of change in Loro. Every basic edit to a document creates one or more operations: |
| 7 | +## Key Concepts |
8 | 8 |
|
9 | | -- Setting a key-value pair in a Map |
10 | | -- Adding or removing an item in a List |
11 | | -- Inserting or deleting characters in Text |
12 | | -- Any other modification to a CRDT container |
| 9 | +### Operations |
| 10 | +- Atomic units of change (insert, delete, set, etc.) |
| 11 | +- Automatically merged internally for efficiency |
| 12 | +- Each has unique ID: `(peerId, counter)` |
13 | 13 |
|
14 | | -Operations are the smallest indivisible units that Loro tracks. While this might sound expensive, Loro optimizes storage by automatically merging consecutive operations internally (like consecutive text insertions). |
| 14 | +### Changes |
| 15 | +- Groups of consecutive operations |
| 16 | +- Include metadata (timestamp, dependencies, peer ID) |
| 17 | +- Created by `commit()` or auto-commit |
15 | 18 |
|
16 | 19 | ```ts twoslash |
17 | 20 | import { LoroDoc } from "loro-crdt"; |
18 | 21 | // ---cut--- |
19 | 22 | const doc = new LoroDoc(); |
20 | 23 | const text = doc.getText("text"); |
21 | 24 |
|
22 | | -// This creates 3 operations (one for each character) |
23 | | -text.insert(0, "abc"); |
24 | | - |
25 | | -// This creates 2 operations (one for insertion, one for deletion) |
26 | | -text.insert(3, "d"); |
27 | | -text.delete(1, 1); |
| 25 | +text.insert(0, "Hello"); // Operation |
| 26 | +text.insert(5, " World"); // Operation |
| 27 | +doc.commit(); // Groups into one Change |
28 | 28 | ``` |
29 | 29 |
|
30 | | -## What are Changes? |
31 | | - |
32 | | -A **Change** is a logical grouping of one or more consecutive local operations. Changes provide a higher-level view of document modifications and include metadata about the edit session: |
33 | | - |
34 | | -- **ID**: The ID of the first operation in the Change |
35 | | -- **Timestamp**: Optional timestamp (when enabled via `setRecordTimestamp(true)`) |
36 | | -- **Dependency IDs**: Operations this Change directly depends on (for causal ordering) |
37 | | -- **Commit Message**: Optional description of the Change (coming soon) |
38 | | -- **Peer ID**: The peer that created this Change |
39 | | -- **Lamport timestamp**: For establishing total order across peers |
| 30 | +## Automatic Merging |
40 | 31 |
|
41 | | -Changes are created when you call `doc.commit()` or when Loro automatically commits operations (like during export). |
| 32 | +Consecutive operations from same peer merge into one Change: |
42 | 33 |
|
43 | 34 | ```ts twoslash |
44 | 35 | import { LoroDoc } from "loro-crdt"; |
45 | 36 | // ---cut--- |
46 | 37 | const doc = new LoroDoc(); |
47 | | -doc.setPeerId("alice"); |
48 | 38 | const text = doc.getText("text"); |
49 | 39 |
|
50 | | -// These operations are not yet part of a Change |
51 | | -text.insert(0, "Hello"); |
52 | | -text.insert(5, " World"); |
53 | | - |
54 | | -// Calling commit() groups the operations into a Change |
55 | | -doc.commit(); |
56 | | - |
57 | | -// View the created Change |
58 | | -const changes = doc.getAllChanges(); |
59 | | -console.log(changes); |
60 | | -// Map(1) { "alice" => [{ ... }] } |
61 | | -``` |
62 | | - |
63 | | -## How Operations Merge into Changes |
64 | | - |
65 | | -Loro intelligently merges operations to minimize metadata overhead while preserving collaboration semantics. By default, consecutive commits from the same peer are merged into a single Change when possible. |
66 | | - |
67 | | -### Basic Merging Example |
68 | | - |
69 | | -```ts twoslash |
70 | | -import { LoroDoc } from "loro-crdt"; |
71 | | -// ---cut--- |
72 | | -const doc = new LoroDoc(); |
73 | | -doc.setPeerId("0"); |
74 | | -const text = doc.getText("text"); |
75 | | - |
76 | | -// First set of operations |
77 | | -text.insert(0, "123"); |
78 | | -doc.commit(); // Creates Change #1 |
79 | | - |
80 | | -// Second set of operations |
81 | | -text.insert(0, "ab"); |
82 | | -doc.commit(); // Merges with Change #1 (no new Change created) |
83 | | - |
84 | | -const changes = doc.getAllChanges(); |
85 | | -console.log(changes.get("0")?.length); // 1 - Only one Change exists |
86 | | -``` |
87 | | - |
88 | | -### Merging in Transactions |
89 | | - |
90 | | -When using transactions, all operations within the transaction are grouped into a single Change: |
| 40 | +text.insert(0, "abc"); |
| 41 | +doc.commit(); // Change #1 |
91 | 42 |
|
92 | | -```ts twoslash |
93 | | -import { LoroDoc } from "loro-crdt"; |
94 | | -// ---cut--- |
95 | | -const doc = new LoroDoc(); |
| 43 | +text.insert(3, "def"); |
| 44 | +doc.commit(); // Merges with #1 (same peer, consecutive) |
96 | 45 |
|
| 46 | +// Transaction = guaranteed single Change |
97 | 47 | doc.transact(() => { |
98 | | - const text = doc.getText("text"); |
99 | | - const list = doc.getList("list"); |
100 | | - |
101 | | - // All these operations will be in one Change |
102 | 48 | text.insert(0, "Hello"); |
103 | | - list.push("item1"); |
104 | | - list.push("item2"); |
| 49 | + text.insert(5, " World"); |
105 | 50 | }); |
106 | | -// Implicit commit at end of transaction creates one Change |
107 | | -``` |
108 | | - |
109 | | -## When New Changes are Created |
110 | | - |
111 | | -While Loro attempts to merge Changes for efficiency, new Changes are created in specific situations: |
112 | | - |
113 | | -### 1. Cross-Peer Dependencies |
114 | | - |
115 | | -When local operations depend on recently imported remote operations, a new Change must be created to record this causal relationship: |
116 | | - |
117 | | -```ts twoslash |
118 | | -import { LoroDoc } from "loro-crdt"; |
119 | | -// ---cut--- |
120 | | -const docA = new LoroDoc(); |
121 | | -docA.setPeerId("0"); |
122 | | -const textA = docA.getText("text"); |
123 | | - |
124 | | -// Create initial content |
125 | | -textA.insert(0, "Hello"); |
126 | | -docA.commit(); // Change #1 for peer 0 |
127 | | - |
128 | | -// Create docB and make changes |
129 | | -const docB = LoroDoc.fromSnapshot(docA.export({ mode: "snapshot" })); |
130 | | -docB.setPeerId("1"); |
131 | | -const textB = docB.getText("text"); |
132 | | -textB.insert(5, " World"); // Depends on peer 0's content |
133 | | - |
134 | | -// Import changes from docB |
135 | | -const updates = docB.export({ mode: "update" }); // Implicit commit |
136 | | -docA.import(updates); |
137 | | - |
138 | | -// New local changes after import |
139 | | -textA.insert(0, "Say: "); |
140 | | -docA.commit(); // Creates Change #2 for peer 0 (new dependency on peer 1) |
141 | | - |
142 | | -const changes = docA.getAllChanges(); |
143 | | -console.log(changes.get("0")?.length); // 2 - Two separate Changes |
144 | | -``` |
145 | | - |
146 | | -### 2. Time-based Separation |
147 | | - |
148 | | -When timestamp recording is enabled, Changes are separated if too much time passes between commits: |
149 | | - |
150 | | -```ts twoslash |
151 | | -import { LoroDoc } from "loro-crdt"; |
152 | | -// ---cut--- |
153 | | -const doc = new LoroDoc(); |
154 | | -doc.setRecordTimestamp(true); |
155 | | -// Default merge interval is 1000 seconds |
156 | | -// Changes separated if commits are >1000s apart |
157 | | - |
158 | | -const text = doc.getText("text"); |
159 | | -text.insert(0, "First"); |
160 | | -doc.commit(); // Change #1 |
161 | | - |
162 | | -// Simulate time passing... |
163 | | -// If >1000 seconds pass in real usage: |
164 | | -text.insert(5, " Second"); |
165 | | -doc.commit(); // Would create Change #2 |
166 | 51 | ``` |
167 | 52 |
|
168 | | -### 3. Different Commit Messages |
| 53 | +## When New Changes Are Created |
169 | 54 |
|
170 | | -When commit messages are enabled (upcoming feature), Changes with different messages won't merge: |
171 | | - |
172 | | -```ts |
173 | | -// Future API (not yet available) |
174 | | -doc.commit("Feature: Add user authentication"); |
175 | | -// ... more operations ... |
176 | | -doc.commit("Fix: Handle edge case"); // New Change due to different message |
177 | | -``` |
178 | | - |
179 | | -## Relationship to Transactions |
180 | | - |
181 | | -Transactions provide a way to group multiple operations atomically. The relationship between transactions and Changes is straightforward: |
182 | | - |
183 | | -- Operations within a transaction are always grouped into the same Change |
184 | | -- The Change is created when the transaction completes (implicit commit) |
185 | | -- Nested transactions contribute to the parent transaction's Change |
| 55 | +1. **Cross-peer dependencies**: After importing remote operations |
| 56 | +2. **Time separation**: When timestamps enabled and >1000s between commits |
| 57 | +3. **Different commit messages**: (Future feature) |
186 | 58 |
|
187 | 59 | ```ts twoslash |
188 | 60 | import { LoroDoc } from "loro-crdt"; |
189 | 61 | // ---cut--- |
190 | 62 | const doc = new LoroDoc(); |
| 63 | +doc.setText("text").insert(0, "v1"); |
| 64 | +doc.commit(); // Change #1 |
191 | 65 |
|
192 | | -// Without transaction - multiple Changes possible |
193 | | -const text = doc.getText("text"); |
194 | | -text.insert(0, "A"); |
195 | | -doc.commit(); // Change #1 |
196 | | -text.insert(1, "B"); |
197 | | -doc.commit(); // Might merge with Change #1 |
| 66 | +// Import from another peer |
| 67 | +const remote = new Uint8Array(); |
| 68 | +doc.import(remote); |
198 | 69 |
|
199 | | -// With transaction - guaranteed single Change |
200 | | -doc.transact(() => { |
201 | | - const text = doc.getText("text"); |
202 | | - text.insert(0, "C"); |
203 | | - text.insert(1, "D"); |
204 | | - // Multiple operations, but only one Change created |
205 | | -}); |
| 70 | +// Next commit creates new Change (dependency on remote) |
| 71 | +doc.setText("text").insert(0, "v2"); |
| 72 | +doc.commit(); // Change #2 |
206 | 73 | ``` |
207 | 74 |
|
208 | | -## Impact on History and Synchronization |
209 | | - |
210 | | -Understanding Operations and Changes is crucial for: |
211 | | - |
212 | | -### History Tracking |
213 | | - |
214 | | -Changes form the basis of Loro's history system. Each Change represents a logical unit of work that can be: |
215 | | -- Tracked over time (with timestamps) |
216 | | -- Attributed to specific peers |
217 | | -- Used for undo/redo operations (when implemented at application level) |
218 | 75 |
|
219 | | -### Synchronization Efficiency |
| 76 | +## Impact on Sync & Storage |
220 | 77 |
|
221 | | -The Change structure optimizes synchronization: |
222 | | -- Dependency tracking ensures correct causal ordering |
223 | | -- Change merging reduces metadata overhead |
224 | | -- Peer-based grouping simplifies conflict resolution |
| 78 | +- **History**: Changes track logical units of work |
| 79 | +- **Sync**: Dependencies ensure causal ordering |
| 80 | +- **Storage**: Auto-merging reduces metadata overhead |
225 | 81 |
|
226 | | -### Storage Optimization |
227 | | - |
228 | | -Loro automatically optimizes storage by: |
229 | | -- Merging consecutive operations within Changes |
230 | | -- Compressing Change metadata where possible |
231 | | -- Maintaining minimal dependency information |
232 | | - |
233 | | -## Practical Examples |
234 | | - |
235 | | -### Real-time Collaboration |
236 | | - |
237 | | -In a real-time editor where each keystroke triggers a commit: |
| 82 | +## Code Example |
238 | 83 |
|
239 | 84 | ```ts twoslash |
240 | 85 | import { LoroDoc } from "loro-crdt"; |
241 | 86 | // ---cut--- |
242 | 87 | const doc = new LoroDoc(); |
243 | | -doc.setPeerId("user1"); |
244 | | -const text = doc.getText("text"); |
| 88 | +doc.setPeerId("alice"); |
245 | 89 |
|
246 | | -// Simulate rapid typing |
| 90 | +// Real-time typing - operations merge |
247 | 91 | "Hello".split("").forEach(char => { |
248 | | - text.insert(text.toString().length, char); |
249 | | - doc.commit(); // Each keystroke commits |
| 92 | + doc.getText("text").insert(0, char); |
| 93 | + doc.commit(); |
250 | 94 | }); |
| 95 | +// Likely creates just 1 Change due to merging |
251 | 96 |
|
252 | | -// Despite 5 commits, likely only 1 Change due to merging |
| 97 | +// View Change history |
253 | 98 | const changes = doc.getAllChanges(); |
254 | | -console.log(changes.get("user1")?.length); // Likely 1 |
255 | | -``` |
256 | | - |
257 | | -### Asynchronous Collaboration |
258 | | - |
259 | | -In a Git-like workflow with larger, meaningful commits: |
260 | | - |
261 | | -```ts twoslash |
262 | | -import { LoroDoc } from "loro-crdt"; |
263 | | -// ---cut--- |
264 | | -const doc = new LoroDoc(); |
265 | | - |
266 | | -// Feature implementation |
267 | | -doc.transact(() => { |
268 | | - const config = doc.getMap("config"); |
269 | | - config.set("feature", true); |
270 | | - config.set("version", "1.0"); |
271 | | - // All changes grouped in one Change |
272 | | -}); |
273 | | - |
274 | | -// Later: bug fix |
275 | | -doc.transact(() => { |
276 | | - const config = doc.getMap("config"); |
277 | | - config.set("bugfix", true); |
278 | | - // Separate Change for different logical work |
279 | | -}); |
280 | | -``` |
281 | | - |
282 | | -### Viewing Change History |
283 | | - |
284 | | -```ts twoslash |
285 | | -import { Change, LoroDoc } from "loro-crdt"; |
286 | | -// ---cut--- |
287 | | -const doc = new LoroDoc(); |
288 | | -doc.setPeerId("alice"); |
289 | | - |
290 | | -// Make some changes |
291 | | -const text = doc.getText("text"); |
292 | | -text.insert(0, "Hello World"); |
293 | | -doc.commit(); |
294 | | - |
295 | | -// Examine the Change structure |
296 | | -const changeMap: Map<`${number}`, Change[]> = doc.getAllChanges(); |
297 | | -for (const [peerId, changes] of changeMap) { |
298 | | - console.log(`Peer ${peerId}:`); |
299 | | - for (const change of changes) { |
300 | | - console.log(` - ${change.length} ops at lamport ${change.lamport}`); |
301 | | - console.log(` Dependencies: ${change.deps.length > 0 ? |
302 | | - change.deps.map(d => `${d.peer}:${d.counter}`).join(", ") : |
303 | | - "none"}`); |
| 99 | +for (const [peerId, peerChanges] of changes) { |
| 100 | + for (const change of peerChanges) { |
| 101 | + console.log(`${change.length} ops, deps: ${change.deps.length}`); |
304 | 102 | } |
305 | 103 | } |
306 | 104 | ``` |
307 | 105 |
|
308 | 106 | ## Best Practices |
309 | 107 |
|
310 | | -1. **Use transactions** for logically related operations to ensure they're grouped together |
311 | | -2. **Call commit() appropriately** - not too frequently (overhead) but not too rarely (large Changes) |
312 | | -3. **Enable timestamps** only when needed for time-travel or audit features |
313 | | -4. **Understand merge behavior** when building undo/redo or history features |
314 | | -5. **Monitor Change size** in real-time applications to balance responsiveness and efficiency |
315 | | - |
316 | | -## Key Takeaways |
| 108 | +- Use transactions for related operations |
| 109 | +- Balance commit frequency (not too often, not too rare) |
| 110 | +- Enable timestamps only when needed |
| 111 | +- Monitor Change size for performance |
317 | 112 |
|
318 | | -- **Operations** are atomic edits; **Changes** are logical groups of operations |
319 | | -- Changes automatically merge to reduce overhead while preserving causal relationships |
320 | | -- New Changes are created when crossing peer boundaries or time thresholds |
321 | | -- Transactions guarantee operations stay in the same Change |
322 | | -- Understanding this model helps optimize Loro usage for your specific collaboration needs |
| 113 | +## Related Documentation |
323 | 114 |
|
324 | | -Unlike Git commits, Loro Changes are designed to be mergeable and flexible, adapting to both real-time (frequent small edits) and asynchronous (batched meaningful changes) collaboration patterns. |
| 115 | +- [Transaction Model](./transaction_model) - Grouping operations |
| 116 | +- [Version Representations](./version_representations) - How Changes form versions |
| 117 | +- [Synchronization](../user-guide/sync) - Using Changes for sync |
0 commit comments