@loro-extended/change 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -9
- package/dist/index.d.ts +174 -13
- package/dist/index.js +402 -22
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/conversion.ts +40 -4
- package/src/functional-helpers.ts +121 -7
- package/src/index.ts +14 -10
- package/src/loro.ts +2 -1
- package/src/nested-container-materialization.test.ts +336 -0
- package/src/replay-diff.test.ts +389 -0
- package/src/replay-diff.ts +229 -0
- package/src/shallow-fork.test.ts +302 -0
- package/src/typed-doc-ownkeys.test.ts +116 -0
- package/src/typed-doc.ts +10 -4
- package/src/typed-refs/base.ts +25 -4
- package/src/typed-refs/counter-ref-internals.ts +7 -2
- package/src/typed-refs/doc-ref-ownkeys.test.ts +78 -0
- package/src/typed-refs/list-ref-base-internals.ts +2 -1
- package/src/typed-refs/list-ref-base.ts +2 -1
- package/src/typed-refs/record-ref-internals.ts +104 -2
- package/src/typed-refs/record-ref.test.ts +522 -1
- package/src/typed-refs/record-ref.ts +72 -3
- package/src/typed-refs/struct-ref-internals.ts +28 -3
- package/src/typed-refs/text-ref-internals.ts +2 -2
- package/src/typed-refs/tree-node-ref-internals.ts +14 -2
- package/src/typed-refs/tree-ref-internals.ts +2 -1
- package/src/typed-refs/utils.ts +65 -8
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { LoroDoc, LoroList, UndoManager } from "loro-crdt"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { replayDiff } from "./replay-diff.js"
|
|
4
|
+
|
|
5
|
+
describe("replayDiff", () => {
|
|
6
|
+
it("should fire subscribeLocalUpdates for text changes", () => {
|
|
7
|
+
// Create source doc with text changes
|
|
8
|
+
const sourceDoc = new LoroDoc()
|
|
9
|
+
sourceDoc.setPeerId("1")
|
|
10
|
+
const sourceText = sourceDoc.getText("text")
|
|
11
|
+
const beforeFrontier = sourceDoc.frontiers()
|
|
12
|
+
|
|
13
|
+
sourceText.insert(0, "Hello World")
|
|
14
|
+
const afterFrontier = sourceDoc.frontiers()
|
|
15
|
+
|
|
16
|
+
// Get the diff
|
|
17
|
+
const diff = sourceDoc.diff(beforeFrontier, afterFrontier, false)
|
|
18
|
+
|
|
19
|
+
// Create target doc and track local updates
|
|
20
|
+
const targetDoc = new LoroDoc()
|
|
21
|
+
targetDoc.setPeerId("2")
|
|
22
|
+
targetDoc.getText("text") // Ensure container exists
|
|
23
|
+
|
|
24
|
+
const localUpdates: Uint8Array[] = []
|
|
25
|
+
targetDoc.subscribeLocalUpdates(update => {
|
|
26
|
+
localUpdates.push(update)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Replay the diff
|
|
30
|
+
replayDiff(targetDoc, diff)
|
|
31
|
+
targetDoc.commit() // Commit to trigger subscribeLocalUpdates
|
|
32
|
+
|
|
33
|
+
// Verify local updates were fired
|
|
34
|
+
expect(localUpdates.length).toBeGreaterThan(0)
|
|
35
|
+
|
|
36
|
+
// Verify the text was applied
|
|
37
|
+
expect(targetDoc.getText("text").toString()).toBe("Hello World")
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("should fire subscribeLocalUpdates for map changes", () => {
|
|
41
|
+
// Create source doc with map changes
|
|
42
|
+
const sourceDoc = new LoroDoc()
|
|
43
|
+
sourceDoc.setPeerId("1")
|
|
44
|
+
const sourceMap = sourceDoc.getMap("map")
|
|
45
|
+
const beforeFrontier = sourceDoc.frontiers()
|
|
46
|
+
|
|
47
|
+
sourceMap.set("name", "Alice")
|
|
48
|
+
sourceMap.set("age", 30)
|
|
49
|
+
const afterFrontier = sourceDoc.frontiers()
|
|
50
|
+
|
|
51
|
+
// Get the diff
|
|
52
|
+
const diff = sourceDoc.diff(beforeFrontier, afterFrontier, false)
|
|
53
|
+
|
|
54
|
+
// Create target doc and track local updates
|
|
55
|
+
const targetDoc = new LoroDoc()
|
|
56
|
+
targetDoc.setPeerId("2")
|
|
57
|
+
targetDoc.getMap("map") // Ensure container exists
|
|
58
|
+
|
|
59
|
+
const localUpdates: Uint8Array[] = []
|
|
60
|
+
targetDoc.subscribeLocalUpdates(update => {
|
|
61
|
+
localUpdates.push(update)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Replay the diff
|
|
65
|
+
replayDiff(targetDoc, diff)
|
|
66
|
+
targetDoc.commit() // Commit to trigger subscribeLocalUpdates
|
|
67
|
+
|
|
68
|
+
// Verify local updates were fired
|
|
69
|
+
expect(localUpdates.length).toBeGreaterThan(0)
|
|
70
|
+
|
|
71
|
+
// Verify the map was applied
|
|
72
|
+
const targetMap = targetDoc.getMap("map")
|
|
73
|
+
expect(targetMap.get("name")).toBe("Alice")
|
|
74
|
+
expect(targetMap.get("age")).toBe(30)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("should fire subscribeLocalUpdates for list changes", () => {
|
|
78
|
+
// Create source doc with list changes
|
|
79
|
+
const sourceDoc = new LoroDoc()
|
|
80
|
+
sourceDoc.setPeerId("1")
|
|
81
|
+
const sourceList = sourceDoc.getList("list")
|
|
82
|
+
const beforeFrontier = sourceDoc.frontiers()
|
|
83
|
+
|
|
84
|
+
sourceList.insert(0, "first")
|
|
85
|
+
sourceList.insert(1, "second")
|
|
86
|
+
sourceList.insert(2, "third")
|
|
87
|
+
const afterFrontier = sourceDoc.frontiers()
|
|
88
|
+
|
|
89
|
+
// Get the diff
|
|
90
|
+
const diff = sourceDoc.diff(beforeFrontier, afterFrontier, false)
|
|
91
|
+
|
|
92
|
+
// Create target doc and track local updates
|
|
93
|
+
const targetDoc = new LoroDoc()
|
|
94
|
+
targetDoc.setPeerId("2")
|
|
95
|
+
targetDoc.getList("list") // Ensure container exists
|
|
96
|
+
|
|
97
|
+
const localUpdates: Uint8Array[] = []
|
|
98
|
+
targetDoc.subscribeLocalUpdates(update => {
|
|
99
|
+
localUpdates.push(update)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Replay the diff
|
|
103
|
+
replayDiff(targetDoc, diff)
|
|
104
|
+
targetDoc.commit() // Commit to trigger subscribeLocalUpdates
|
|
105
|
+
|
|
106
|
+
// Verify local updates were fired
|
|
107
|
+
expect(localUpdates.length).toBeGreaterThan(0)
|
|
108
|
+
|
|
109
|
+
// Verify the list was applied
|
|
110
|
+
const targetList = targetDoc.getList("list")
|
|
111
|
+
expect(targetList.toJSON()).toEqual(["first", "second", "third"])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("should fire subscribeLocalUpdates for counter changes", () => {
|
|
115
|
+
// Create source doc with counter changes
|
|
116
|
+
const sourceDoc = new LoroDoc()
|
|
117
|
+
sourceDoc.setPeerId("1")
|
|
118
|
+
const sourceCounter = sourceDoc.getCounter("counter")
|
|
119
|
+
const beforeFrontier = sourceDoc.frontiers()
|
|
120
|
+
|
|
121
|
+
sourceCounter.increment(5)
|
|
122
|
+
const afterFrontier = sourceDoc.frontiers()
|
|
123
|
+
|
|
124
|
+
// Get the diff
|
|
125
|
+
const diff = sourceDoc.diff(beforeFrontier, afterFrontier, false)
|
|
126
|
+
|
|
127
|
+
// Create target doc and track local updates
|
|
128
|
+
const targetDoc = new LoroDoc()
|
|
129
|
+
targetDoc.setPeerId("2")
|
|
130
|
+
targetDoc.getCounter("counter") // Ensure container exists
|
|
131
|
+
|
|
132
|
+
const localUpdates: Uint8Array[] = []
|
|
133
|
+
targetDoc.subscribeLocalUpdates(update => {
|
|
134
|
+
localUpdates.push(update)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Replay the diff
|
|
138
|
+
replayDiff(targetDoc, diff)
|
|
139
|
+
targetDoc.commit() // Commit to trigger subscribeLocalUpdates
|
|
140
|
+
|
|
141
|
+
// Verify local updates were fired
|
|
142
|
+
expect(localUpdates.length).toBeGreaterThan(0)
|
|
143
|
+
|
|
144
|
+
// Verify the counter was applied
|
|
145
|
+
expect(targetDoc.getCounter("counter").toJSON()).toBe(5)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("should work with UndoManager", () => {
|
|
149
|
+
// Create source doc with changes
|
|
150
|
+
const sourceDoc = new LoroDoc()
|
|
151
|
+
sourceDoc.setPeerId("1")
|
|
152
|
+
const sourceText = sourceDoc.getText("text")
|
|
153
|
+
const beforeFrontier = sourceDoc.frontiers()
|
|
154
|
+
|
|
155
|
+
sourceText.insert(0, "Hello")
|
|
156
|
+
const afterFrontier = sourceDoc.frontiers()
|
|
157
|
+
|
|
158
|
+
// Get the diff
|
|
159
|
+
const diff = sourceDoc.diff(beforeFrontier, afterFrontier, false)
|
|
160
|
+
|
|
161
|
+
// Create target doc with UndoManager
|
|
162
|
+
const targetDoc = new LoroDoc()
|
|
163
|
+
targetDoc.setPeerId("2")
|
|
164
|
+
const targetText = targetDoc.getText("text")
|
|
165
|
+
|
|
166
|
+
const undoManager = new UndoManager(targetDoc, {})
|
|
167
|
+
|
|
168
|
+
// Replay the diff
|
|
169
|
+
replayDiff(targetDoc, diff)
|
|
170
|
+
targetDoc.commit()
|
|
171
|
+
|
|
172
|
+
// Verify the text was applied
|
|
173
|
+
expect(targetText.toString()).toBe("Hello")
|
|
174
|
+
|
|
175
|
+
// Undo should work
|
|
176
|
+
const canUndo = undoManager.canUndo()
|
|
177
|
+
expect(canUndo).toBe(true)
|
|
178
|
+
|
|
179
|
+
undoManager.undo()
|
|
180
|
+
expect(targetText.toString()).toBe("")
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it("should handle nested containers", () => {
|
|
184
|
+
// Create source doc with nested containers
|
|
185
|
+
const sourceDoc = new LoroDoc()
|
|
186
|
+
sourceDoc.setPeerId("1")
|
|
187
|
+
const sourceMap = sourceDoc.getMap("root")
|
|
188
|
+
const beforeFrontier = sourceDoc.frontiers()
|
|
189
|
+
|
|
190
|
+
// Create a nested structure: map -> list -> text
|
|
191
|
+
const nestedList = sourceMap.setContainer("items", new LoroList())
|
|
192
|
+
nestedList.insert(0, "item1")
|
|
193
|
+
nestedList.insert(1, "item2")
|
|
194
|
+
|
|
195
|
+
const afterFrontier = sourceDoc.frontiers()
|
|
196
|
+
|
|
197
|
+
// Get the diff
|
|
198
|
+
const diff = sourceDoc.diff(beforeFrontier, afterFrontier, false)
|
|
199
|
+
|
|
200
|
+
// Create target doc and track local updates
|
|
201
|
+
const targetDoc = new LoroDoc()
|
|
202
|
+
targetDoc.setPeerId("2")
|
|
203
|
+
targetDoc.getMap("root") // Ensure container exists
|
|
204
|
+
|
|
205
|
+
const localUpdates: Uint8Array[] = []
|
|
206
|
+
targetDoc.subscribeLocalUpdates(update => {
|
|
207
|
+
localUpdates.push(update)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// Replay the diff
|
|
211
|
+
replayDiff(targetDoc, diff)
|
|
212
|
+
targetDoc.commit() // Commit to trigger subscribeLocalUpdates
|
|
213
|
+
|
|
214
|
+
// Verify local updates were fired
|
|
215
|
+
expect(localUpdates.length).toBeGreaterThan(0)
|
|
216
|
+
|
|
217
|
+
// Verify the nested structure was applied
|
|
218
|
+
const targetMap = targetDoc.getMap("root")
|
|
219
|
+
const items = targetMap.get("items") as LoroList
|
|
220
|
+
expect(items).toBeDefined()
|
|
221
|
+
expect(items.toJSON()).toEqual(["item1", "item2"])
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it("should handle map deletions", () => {
|
|
225
|
+
// Create source doc with initial state
|
|
226
|
+
const sourceDoc = new LoroDoc()
|
|
227
|
+
sourceDoc.setPeerId("1")
|
|
228
|
+
const sourceMap = sourceDoc.getMap("map")
|
|
229
|
+
sourceMap.set("keep", "value1")
|
|
230
|
+
sourceMap.set("remove", "value2")
|
|
231
|
+
|
|
232
|
+
const beforeFrontier = sourceDoc.frontiers()
|
|
233
|
+
|
|
234
|
+
// Delete a key
|
|
235
|
+
sourceMap.delete("remove")
|
|
236
|
+
const afterFrontier = sourceDoc.frontiers()
|
|
237
|
+
|
|
238
|
+
// Get the diff
|
|
239
|
+
const diff = sourceDoc.diff(beforeFrontier, afterFrontier, false)
|
|
240
|
+
|
|
241
|
+
// Create target doc with same initial state
|
|
242
|
+
const targetDoc = new LoroDoc()
|
|
243
|
+
targetDoc.setPeerId("2")
|
|
244
|
+
const targetMap = targetDoc.getMap("map")
|
|
245
|
+
targetMap.set("keep", "value1")
|
|
246
|
+
targetMap.set("remove", "value2")
|
|
247
|
+
|
|
248
|
+
const localUpdates: Uint8Array[] = []
|
|
249
|
+
targetDoc.subscribeLocalUpdates(update => {
|
|
250
|
+
localUpdates.push(update)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Replay the diff
|
|
254
|
+
replayDiff(targetDoc, diff)
|
|
255
|
+
targetDoc.commit() // Commit to trigger subscribeLocalUpdates
|
|
256
|
+
|
|
257
|
+
// Verify local updates were fired
|
|
258
|
+
expect(localUpdates.length).toBeGreaterThan(0)
|
|
259
|
+
|
|
260
|
+
// Verify the deletion was applied
|
|
261
|
+
expect(targetMap.get("keep")).toBe("value1")
|
|
262
|
+
expect(targetMap.get("remove")).toBeUndefined()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it("should handle list deletions", () => {
|
|
266
|
+
// Create source doc with initial state
|
|
267
|
+
const sourceDoc = new LoroDoc()
|
|
268
|
+
sourceDoc.setPeerId("1")
|
|
269
|
+
const sourceList = sourceDoc.getList("list")
|
|
270
|
+
sourceList.insert(0, "a")
|
|
271
|
+
sourceList.insert(1, "b")
|
|
272
|
+
sourceList.insert(2, "c")
|
|
273
|
+
|
|
274
|
+
const beforeFrontier = sourceDoc.frontiers()
|
|
275
|
+
|
|
276
|
+
// Delete middle element
|
|
277
|
+
sourceList.delete(1, 1)
|
|
278
|
+
const afterFrontier = sourceDoc.frontiers()
|
|
279
|
+
|
|
280
|
+
// Get the diff
|
|
281
|
+
const diff = sourceDoc.diff(beforeFrontier, afterFrontier, false)
|
|
282
|
+
|
|
283
|
+
// Create target doc with same initial state
|
|
284
|
+
const targetDoc = new LoroDoc()
|
|
285
|
+
targetDoc.setPeerId("2")
|
|
286
|
+
const targetList = targetDoc.getList("list")
|
|
287
|
+
targetList.insert(0, "a")
|
|
288
|
+
targetList.insert(1, "b")
|
|
289
|
+
targetList.insert(2, "c")
|
|
290
|
+
|
|
291
|
+
const localUpdates: Uint8Array[] = []
|
|
292
|
+
targetDoc.subscribeLocalUpdates(update => {
|
|
293
|
+
localUpdates.push(update)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// Replay the diff
|
|
297
|
+
replayDiff(targetDoc, diff)
|
|
298
|
+
targetDoc.commit() // Commit to trigger subscribeLocalUpdates
|
|
299
|
+
|
|
300
|
+
// Verify local updates were fired
|
|
301
|
+
expect(localUpdates.length).toBeGreaterThan(0)
|
|
302
|
+
|
|
303
|
+
// Verify the deletion was applied
|
|
304
|
+
expect(targetList.toJSON()).toEqual(["a", "c"])
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it("should handle counter decrement", () => {
|
|
308
|
+
// Create source doc with initial counter value
|
|
309
|
+
const sourceDoc = new LoroDoc()
|
|
310
|
+
sourceDoc.setPeerId("1")
|
|
311
|
+
const sourceCounter = sourceDoc.getCounter("counter")
|
|
312
|
+
sourceCounter.increment(10)
|
|
313
|
+
|
|
314
|
+
const beforeFrontier = sourceDoc.frontiers()
|
|
315
|
+
|
|
316
|
+
// Decrement
|
|
317
|
+
sourceCounter.decrement(3)
|
|
318
|
+
const afterFrontier = sourceDoc.frontiers()
|
|
319
|
+
|
|
320
|
+
// Get the diff
|
|
321
|
+
const diff = sourceDoc.diff(beforeFrontier, afterFrontier, false)
|
|
322
|
+
|
|
323
|
+
// Create target doc with same initial state
|
|
324
|
+
const targetDoc = new LoroDoc()
|
|
325
|
+
targetDoc.setPeerId("2")
|
|
326
|
+
const targetCounter = targetDoc.getCounter("counter")
|
|
327
|
+
targetCounter.increment(10)
|
|
328
|
+
|
|
329
|
+
const localUpdates: Uint8Array[] = []
|
|
330
|
+
targetDoc.subscribeLocalUpdates(update => {
|
|
331
|
+
localUpdates.push(update)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// Replay the diff
|
|
335
|
+
replayDiff(targetDoc, diff)
|
|
336
|
+
targetDoc.commit() // Commit to trigger subscribeLocalUpdates
|
|
337
|
+
|
|
338
|
+
// Verify local updates were fired
|
|
339
|
+
expect(localUpdates.length).toBeGreaterThan(0)
|
|
340
|
+
|
|
341
|
+
// Verify the decrement was applied
|
|
342
|
+
expect(targetCounter.toJSON()).toBe(7)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it("should handle text with attributes", () => {
|
|
346
|
+
// Create source doc with rich text
|
|
347
|
+
const sourceDoc = new LoroDoc()
|
|
348
|
+
sourceDoc.setPeerId("1")
|
|
349
|
+
sourceDoc.configTextStyle({ bold: { expand: "after" } })
|
|
350
|
+
const sourceText = sourceDoc.getText("text")
|
|
351
|
+
const beforeFrontier = sourceDoc.frontiers()
|
|
352
|
+
|
|
353
|
+
sourceText.insert(0, "Hello World")
|
|
354
|
+
sourceText.mark({ start: 0, end: 5 }, "bold", true)
|
|
355
|
+
const afterFrontier = sourceDoc.frontiers()
|
|
356
|
+
|
|
357
|
+
// Get the diff
|
|
358
|
+
const diff = sourceDoc.diff(beforeFrontier, afterFrontier, false)
|
|
359
|
+
|
|
360
|
+
// Create target doc
|
|
361
|
+
const targetDoc = new LoroDoc()
|
|
362
|
+
targetDoc.setPeerId("2")
|
|
363
|
+
targetDoc.configTextStyle({ bold: { expand: "after" } })
|
|
364
|
+
targetDoc.getText("text") // Ensure container exists
|
|
365
|
+
|
|
366
|
+
const localUpdates: Uint8Array[] = []
|
|
367
|
+
targetDoc.subscribeLocalUpdates(update => {
|
|
368
|
+
localUpdates.push(update)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
// Replay the diff
|
|
372
|
+
replayDiff(targetDoc, diff)
|
|
373
|
+
targetDoc.commit() // Commit to trigger subscribeLocalUpdates
|
|
374
|
+
|
|
375
|
+
// Verify local updates were fired
|
|
376
|
+
expect(localUpdates.length).toBeGreaterThan(0)
|
|
377
|
+
|
|
378
|
+
// Verify the text was applied
|
|
379
|
+
const targetText = targetDoc.getText("text")
|
|
380
|
+
expect(targetText.toString()).toBe("Hello World")
|
|
381
|
+
|
|
382
|
+
// Verify the delta includes the bold attribute
|
|
383
|
+
const delta = targetText.toDelta()
|
|
384
|
+
expect(delta[0]).toMatchObject({
|
|
385
|
+
insert: "Hello",
|
|
386
|
+
attributes: { bold: true },
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
})
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Container,
|
|
3
|
+
ContainerID,
|
|
4
|
+
CounterDiff,
|
|
5
|
+
Diff,
|
|
6
|
+
ListDiff,
|
|
7
|
+
LoroCounter,
|
|
8
|
+
LoroDoc,
|
|
9
|
+
LoroList,
|
|
10
|
+
LoroMap,
|
|
11
|
+
LoroMovableList,
|
|
12
|
+
LoroText,
|
|
13
|
+
LoroTree,
|
|
14
|
+
MapDiff,
|
|
15
|
+
TextDiff,
|
|
16
|
+
TreeDiff,
|
|
17
|
+
TreeDiffItem,
|
|
18
|
+
Value,
|
|
19
|
+
} from "loro-crdt"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Replay a diff as local operations on a document.
|
|
23
|
+
*
|
|
24
|
+
* Unlike doc.import() which creates import events, this creates LOCAL events
|
|
25
|
+
* that are captured by subscribeLocalUpdates() and UndoManager.
|
|
26
|
+
*
|
|
27
|
+
* @param doc - The target document to apply changes to
|
|
28
|
+
* @param diff - The diff from doc.diff(from, to, false)
|
|
29
|
+
*/
|
|
30
|
+
export function replayDiff(doc: LoroDoc, diff: [ContainerID, Diff][]): void {
|
|
31
|
+
// Map from source container IDs to target containers
|
|
32
|
+
// This is needed because when we create new containers, they get different IDs
|
|
33
|
+
const containerMap = new Map<ContainerID, Container>()
|
|
34
|
+
|
|
35
|
+
for (const [containerId, containerDiff] of diff) {
|
|
36
|
+
// First, try to get the container from our map (for newly created containers)
|
|
37
|
+
let container = containerMap.get(containerId)
|
|
38
|
+
|
|
39
|
+
// If not in map, try to get it from the doc (for existing containers)
|
|
40
|
+
if (!container) {
|
|
41
|
+
container = doc.getContainerById(containerId)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!container) {
|
|
45
|
+
// Container doesn't exist yet - this can happen for newly created containers
|
|
46
|
+
// that haven't been mapped yet. Skip for now, it will be created when
|
|
47
|
+
// processing the parent container's diff.
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
switch (containerDiff.type) {
|
|
52
|
+
case "text":
|
|
53
|
+
replayTextDiff(container as LoroText, containerDiff)
|
|
54
|
+
break
|
|
55
|
+
case "list":
|
|
56
|
+
replayListDiff(
|
|
57
|
+
container as LoroList | LoroMovableList,
|
|
58
|
+
containerDiff,
|
|
59
|
+
containerMap,
|
|
60
|
+
)
|
|
61
|
+
break
|
|
62
|
+
case "map":
|
|
63
|
+
replayMapDiff(container as LoroMap, containerDiff, containerMap)
|
|
64
|
+
break
|
|
65
|
+
case "tree":
|
|
66
|
+
replayTreeDiff(container as LoroTree, containerDiff)
|
|
67
|
+
break
|
|
68
|
+
case "counter":
|
|
69
|
+
replayCounterDiff(container as LoroCounter, containerDiff)
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Replay text diff operations
|
|
77
|
+
*/
|
|
78
|
+
function replayTextDiff(text: LoroText, diff: TextDiff): void {
|
|
79
|
+
// LoroText has applyDelta which handles the delta format directly
|
|
80
|
+
text.applyDelta(diff.diff)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Replay list diff operations
|
|
85
|
+
*/
|
|
86
|
+
function replayListDiff(
|
|
87
|
+
list: LoroList | LoroMovableList,
|
|
88
|
+
diff: ListDiff,
|
|
89
|
+
containerMap: Map<ContainerID, Container>,
|
|
90
|
+
): void {
|
|
91
|
+
let index = 0
|
|
92
|
+
|
|
93
|
+
for (const delta of diff.diff) {
|
|
94
|
+
if (delta.retain !== undefined) {
|
|
95
|
+
// Retain: skip over existing elements
|
|
96
|
+
index += delta.retain
|
|
97
|
+
} else if (delta.delete !== undefined) {
|
|
98
|
+
// Delete: remove elements at current position
|
|
99
|
+
list.delete(index, delta.delete)
|
|
100
|
+
// Don't advance index - next operation is at same position
|
|
101
|
+
} else if (delta.insert !== undefined) {
|
|
102
|
+
// Insert: add elements at current position
|
|
103
|
+
const values = delta.insert
|
|
104
|
+
for (let i = 0; i < values.length; i++) {
|
|
105
|
+
const value = values[i]
|
|
106
|
+
if (isContainer(value)) {
|
|
107
|
+
// For containers, we need to insert a new container of the same type
|
|
108
|
+
// The container's contents will be handled by its own diff entry
|
|
109
|
+
const newContainer = createContainerOfSameType(value)
|
|
110
|
+
const insertedContainer = (list as LoroList).insertContainer(
|
|
111
|
+
index + i,
|
|
112
|
+
newContainer,
|
|
113
|
+
)
|
|
114
|
+
// Map the source container ID to the newly created container
|
|
115
|
+
containerMap.set(value.id, insertedContainer)
|
|
116
|
+
} else {
|
|
117
|
+
;(list as LoroList).insert(
|
|
118
|
+
index + i,
|
|
119
|
+
value as Exclude<Value, Container>,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
index += values.length
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Replay map diff operations
|
|
130
|
+
*/
|
|
131
|
+
function replayMapDiff(
|
|
132
|
+
map: LoroMap,
|
|
133
|
+
diff: MapDiff,
|
|
134
|
+
containerMap: Map<ContainerID, Container>,
|
|
135
|
+
): void {
|
|
136
|
+
for (const [key, value] of Object.entries(diff.updated)) {
|
|
137
|
+
if (value === undefined) {
|
|
138
|
+
// Delete the key
|
|
139
|
+
map.delete(key)
|
|
140
|
+
} else if (isContainer(value)) {
|
|
141
|
+
// Set a container - create a new one of the same type
|
|
142
|
+
const newContainer = createContainerOfSameType(value)
|
|
143
|
+
const insertedContainer = map.setContainer(key, newContainer)
|
|
144
|
+
// Map the source container ID to the newly created container
|
|
145
|
+
containerMap.set(value.id, insertedContainer)
|
|
146
|
+
} else {
|
|
147
|
+
// Set a primitive value
|
|
148
|
+
map.set(key, value as Exclude<Value, Container>)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Replay tree diff operations
|
|
155
|
+
*/
|
|
156
|
+
function replayTreeDiff(tree: LoroTree, diff: TreeDiff): void {
|
|
157
|
+
for (const item of diff.diff) {
|
|
158
|
+
replayTreeDiffItem(tree, item)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Replay a single tree diff item
|
|
164
|
+
*/
|
|
165
|
+
function replayTreeDiffItem(tree: LoroTree, item: TreeDiffItem): void {
|
|
166
|
+
switch (item.action) {
|
|
167
|
+
case "create":
|
|
168
|
+
// Create a new node under the specified parent
|
|
169
|
+
// Note: The node ID is determined by the CRDT, we can't specify it
|
|
170
|
+
// This means we're creating a NEW node, not recreating the exact same one
|
|
171
|
+
tree.createNode(item.parent, item.index)
|
|
172
|
+
break
|
|
173
|
+
case "delete":
|
|
174
|
+
// Delete the node
|
|
175
|
+
tree.delete(item.target)
|
|
176
|
+
break
|
|
177
|
+
case "move":
|
|
178
|
+
// Move the node to a new parent/position
|
|
179
|
+
tree.move(item.target, item.parent, item.index)
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Replay counter diff operations
|
|
186
|
+
*/
|
|
187
|
+
function replayCounterDiff(counter: LoroCounter, diff: CounterDiff): void {
|
|
188
|
+
if (diff.increment > 0) {
|
|
189
|
+
counter.increment(diff.increment)
|
|
190
|
+
} else if (diff.increment < 0) {
|
|
191
|
+
counter.decrement(-diff.increment)
|
|
192
|
+
}
|
|
193
|
+
// If increment is 0, no operation needed
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if a value is a Container
|
|
198
|
+
*/
|
|
199
|
+
function isContainer(value: Value | Container): value is Container {
|
|
200
|
+
return (
|
|
201
|
+
value !== null &&
|
|
202
|
+
typeof value === "object" &&
|
|
203
|
+
"kind" in value &&
|
|
204
|
+
typeof (value as Container).kind === "function"
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a new detached container of the same type as the given container
|
|
210
|
+
*/
|
|
211
|
+
function createContainerOfSameType(container: Container): Container {
|
|
212
|
+
const kind = container.kind()
|
|
213
|
+
switch (kind) {
|
|
214
|
+
case "List":
|
|
215
|
+
return new (container.constructor as new () => LoroList)()
|
|
216
|
+
case "Map":
|
|
217
|
+
return new (container.constructor as new () => LoroMap)()
|
|
218
|
+
case "Text":
|
|
219
|
+
return new (container.constructor as new () => LoroText)()
|
|
220
|
+
case "Tree":
|
|
221
|
+
return new (container.constructor as new () => LoroTree)()
|
|
222
|
+
case "Counter":
|
|
223
|
+
return new (container.constructor as new () => LoroCounter)()
|
|
224
|
+
case "MovableList":
|
|
225
|
+
return new (container.constructor as new () => LoroMovableList)()
|
|
226
|
+
default:
|
|
227
|
+
throw new Error(`Unknown container kind: ${kind}`)
|
|
228
|
+
}
|
|
229
|
+
}
|