@loro-extended/change 5.0.0 → 5.2.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 +94 -0
- package/dist/index.d.ts +262 -137
- package/dist/index.js +2027 -1930
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/fork-at.test.ts +260 -0
- package/src/functional-helpers.test.ts +402 -0
- package/src/functional-helpers.ts +172 -12
- package/src/index.ts +7 -2
- package/src/loro.ts +26 -2
- package/src/shape.ts +9 -5
- package/src/typed-doc.ts +58 -6
- package/src/typed-refs/base.ts +18 -0
- package/src/typed-refs/doc-ref-internals.ts +2 -2
- package/src/typed-refs/list-ref-base-internals.ts +2 -2
- package/src/typed-refs/list-ref-base.ts +2 -2
- package/src/typed-refs/record-ref-internals.ts +2 -2
- package/src/typed-refs/struct-ref-internals.ts +2 -2
- package/src/typed-refs/struct-ref.ts +7 -1
- package/src/typed-refs/tree-deleted-nodes.test.ts +213 -0
- package/src/typed-refs/tree-loro.test.ts +52 -0
- package/src/typed-refs/tree-node-ref-internals.ts +34 -1
- package/src/typed-refs/tree-node-ref.test.ts +24 -17
- package/src/typed-refs/tree-node-ref.ts +8 -0
- package/src/typed-refs/tree-node.test.ts +54 -22
- package/src/typed-refs/tree-ref.ts +10 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loro-extended/change",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "A schema-driven, type-safe wrapper for Loro CRDT that provides natural JavaScript syntax for collaborative data mutations",
|
|
5
5
|
"author": "Duane Johnson",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"./src/*": "./src/*"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
+
"@typescript/native-preview": "7.0.0-dev.20260103.1",
|
|
24
25
|
"tsup": "^8.5.0",
|
|
25
26
|
"tsx": "^4.20.3",
|
|
26
27
|
"typescript": "^5.9.2",
|
|
@@ -31,8 +32,6 @@
|
|
|
31
32
|
},
|
|
32
33
|
"scripts": {
|
|
33
34
|
"build": "tsup",
|
|
34
|
-
"
|
|
35
|
-
"test": "vitest",
|
|
36
|
-
"typecheck": "tsc --noEmit --skipLibCheck"
|
|
35
|
+
"verify": "verify"
|
|
37
36
|
}
|
|
38
37
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { createTypedDoc, forkAt, loro, Shape } from "./index.js"
|
|
3
|
+
|
|
4
|
+
describe("forkAt", () => {
|
|
5
|
+
describe("TypedDoc.forkAt() method", () => {
|
|
6
|
+
it("should fork at a specific version and return correct state", () => {
|
|
7
|
+
const schema = Shape.doc({
|
|
8
|
+
title: Shape.text(),
|
|
9
|
+
count: Shape.counter(),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const doc = createTypedDoc(schema)
|
|
13
|
+
doc.title.update("Hello")
|
|
14
|
+
doc.count.increment(5)
|
|
15
|
+
|
|
16
|
+
// Get frontiers at this point
|
|
17
|
+
const frontiers = loro(doc).doc.frontiers()
|
|
18
|
+
|
|
19
|
+
// Make more changes
|
|
20
|
+
doc.title.update("World")
|
|
21
|
+
doc.count.increment(10)
|
|
22
|
+
|
|
23
|
+
// Fork at the earlier version
|
|
24
|
+
const forkedDoc = doc.forkAt(frontiers)
|
|
25
|
+
|
|
26
|
+
// Forked doc should have the earlier state
|
|
27
|
+
expect(forkedDoc.title.toString()).toBe("Hello")
|
|
28
|
+
expect(forkedDoc.count.value).toBe(5)
|
|
29
|
+
|
|
30
|
+
// Original doc should still have the latest state
|
|
31
|
+
expect(doc.title.toString()).toBe("World")
|
|
32
|
+
expect(doc.count.value).toBe(15)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("should preserve type safety on forked doc", () => {
|
|
36
|
+
const schema = Shape.doc({
|
|
37
|
+
items: Shape.list(
|
|
38
|
+
Shape.struct({
|
|
39
|
+
name: Shape.text(),
|
|
40
|
+
done: Shape.plain.boolean(),
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const doc = createTypedDoc(schema)
|
|
46
|
+
doc.items.push({ name: "Task 1", done: false })
|
|
47
|
+
|
|
48
|
+
const frontiers = loro(doc).doc.frontiers()
|
|
49
|
+
|
|
50
|
+
doc.items.push({ name: "Task 2", done: true })
|
|
51
|
+
|
|
52
|
+
const forkedDoc = doc.forkAt(frontiers)
|
|
53
|
+
|
|
54
|
+
// Type safety: forkedDoc.items should have the same type
|
|
55
|
+
expect(forkedDoc.items.length).toBe(1)
|
|
56
|
+
const firstItem = forkedDoc.items[0]
|
|
57
|
+
if (firstItem) {
|
|
58
|
+
expect(firstItem.name.toString()).toBe("Task 1")
|
|
59
|
+
expect(firstItem.done).toBe(false)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Can mutate forked doc independently
|
|
63
|
+
forkedDoc.items.push({ name: "Forked Task", done: true })
|
|
64
|
+
expect(forkedDoc.items.length).toBe(2)
|
|
65
|
+
expect(doc.items.length).toBe(2) // Original unchanged
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("should create independent documents (changes don't affect original)", () => {
|
|
69
|
+
const schema = Shape.doc({
|
|
70
|
+
value: Shape.counter(),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const doc = createTypedDoc(schema)
|
|
74
|
+
doc.value.increment(10)
|
|
75
|
+
|
|
76
|
+
const frontiers = loro(doc).doc.frontiers()
|
|
77
|
+
const forkedDoc = doc.forkAt(frontiers)
|
|
78
|
+
|
|
79
|
+
// Mutate forked doc
|
|
80
|
+
forkedDoc.value.increment(100)
|
|
81
|
+
|
|
82
|
+
// Original should be unchanged
|
|
83
|
+
expect(doc.value.value).toBe(10)
|
|
84
|
+
expect(forkedDoc.value.value).toBe(110)
|
|
85
|
+
|
|
86
|
+
// Mutate original
|
|
87
|
+
doc.value.increment(5)
|
|
88
|
+
|
|
89
|
+
// Forked should be unchanged
|
|
90
|
+
expect(doc.value.value).toBe(15)
|
|
91
|
+
expect(forkedDoc.value.value).toBe(110)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("should work with complex schemas (maps, trees)", () => {
|
|
95
|
+
const schema = Shape.doc({
|
|
96
|
+
settings: Shape.record(Shape.plain.string()),
|
|
97
|
+
tree: Shape.tree(
|
|
98
|
+
Shape.struct({
|
|
99
|
+
label: Shape.text(),
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const doc = createTypedDoc(schema)
|
|
105
|
+
doc.settings.set("theme", "dark")
|
|
106
|
+
const node = doc.tree.createNode({ label: "Root" })
|
|
107
|
+
|
|
108
|
+
const frontiers = loro(doc).doc.frontiers()
|
|
109
|
+
|
|
110
|
+
doc.settings.set("theme", "light")
|
|
111
|
+
doc.settings.set("lang", "en")
|
|
112
|
+
node.data.label.update("Updated Root")
|
|
113
|
+
|
|
114
|
+
const forkedDoc = doc.forkAt(frontiers)
|
|
115
|
+
|
|
116
|
+
expect(forkedDoc.settings.get("theme")).toBe("dark")
|
|
117
|
+
// "lang" was not set before the fork, so it should not exist
|
|
118
|
+
// Note: Record returns placeholder value (empty string) for missing keys
|
|
119
|
+
expect(forkedDoc.settings.has("lang")).toBe(false)
|
|
120
|
+
|
|
121
|
+
// Tree should have the earlier state
|
|
122
|
+
const forkedRoots = forkedDoc.tree.roots()
|
|
123
|
+
expect(forkedRoots.length).toBe(1)
|
|
124
|
+
expect(forkedRoots[0].data.label.toString()).toBe("Root")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("should have different PeerID from original", () => {
|
|
128
|
+
const schema = Shape.doc({
|
|
129
|
+
text: Shape.text(),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const doc = createTypedDoc(schema)
|
|
133
|
+
doc.text.update("Hello")
|
|
134
|
+
|
|
135
|
+
const frontiers = loro(doc).doc.frontiers()
|
|
136
|
+
const forkedDoc = doc.forkAt(frontiers)
|
|
137
|
+
|
|
138
|
+
const originalPeerId = loro(doc).doc.peerId
|
|
139
|
+
const forkedPeerId = loro(forkedDoc).doc.peerId
|
|
140
|
+
|
|
141
|
+
expect(forkedPeerId).not.toBe(originalPeerId)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe("forkAt() functional helper", () => {
|
|
146
|
+
it("should fork at a specific version", () => {
|
|
147
|
+
const schema = Shape.doc({
|
|
148
|
+
title: Shape.text(),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const doc = createTypedDoc(schema)
|
|
152
|
+
doc.title.update("Hello")
|
|
153
|
+
|
|
154
|
+
const frontiers = loro(doc).doc.frontiers()
|
|
155
|
+
|
|
156
|
+
doc.title.update("World")
|
|
157
|
+
|
|
158
|
+
// Use functional helper
|
|
159
|
+
const forkedDoc = forkAt(doc, frontiers)
|
|
160
|
+
|
|
161
|
+
expect(forkedDoc.title.toString()).toBe("Hello")
|
|
162
|
+
expect(doc.title.toString()).toBe("World")
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it("should preserve schema from original doc", () => {
|
|
166
|
+
const schema = Shape.doc({
|
|
167
|
+
count: Shape.counter().placeholder(42),
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const doc = createTypedDoc(schema)
|
|
171
|
+
// Don't increment - should use placeholder
|
|
172
|
+
|
|
173
|
+
const frontiers = loro(doc).doc.frontiers()
|
|
174
|
+
const forkedDoc = forkAt(doc, frontiers)
|
|
175
|
+
|
|
176
|
+
// Placeholder should be preserved
|
|
177
|
+
expect(forkedDoc.toJSON().count).toBe(42)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe("raw LoroDoc.forkAt() access", () => {
|
|
182
|
+
it("should still be accessible via loro() escape hatch", () => {
|
|
183
|
+
const schema = Shape.doc({
|
|
184
|
+
text: Shape.text(),
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const doc = createTypedDoc(schema)
|
|
188
|
+
doc.text.update("Hello")
|
|
189
|
+
|
|
190
|
+
const frontiers = loro(doc).doc.frontiers()
|
|
191
|
+
doc.text.update("World")
|
|
192
|
+
|
|
193
|
+
// Raw access returns LoroDoc, not TypedDoc
|
|
194
|
+
const rawForkedDoc = loro(doc).doc.forkAt(frontiers)
|
|
195
|
+
|
|
196
|
+
// It's a plain LoroDoc
|
|
197
|
+
expect(rawForkedDoc.toJSON()).toEqual({ text: "Hello" })
|
|
198
|
+
|
|
199
|
+
// Can wrap it manually if needed
|
|
200
|
+
const typedForkedDoc = createTypedDoc(schema, rawForkedDoc)
|
|
201
|
+
expect(typedForkedDoc.text.toString()).toBe("Hello")
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe("edge cases", () => {
|
|
206
|
+
it("should fork at empty frontiers (initial state)", () => {
|
|
207
|
+
const schema = Shape.doc({
|
|
208
|
+
count: Shape.counter().placeholder(0),
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const doc = createTypedDoc(schema)
|
|
212
|
+
const emptyFrontiers = loro(doc).doc.frontiers()
|
|
213
|
+
|
|
214
|
+
doc.count.increment(10)
|
|
215
|
+
|
|
216
|
+
const forkedDoc = doc.forkAt(emptyFrontiers)
|
|
217
|
+
|
|
218
|
+
// Should be at initial state (placeholder value)
|
|
219
|
+
expect(forkedDoc.count.value).toBe(0)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it("should fork at current frontiers (same state)", () => {
|
|
223
|
+
const schema = Shape.doc({
|
|
224
|
+
text: Shape.text(),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const doc = createTypedDoc(schema)
|
|
228
|
+
doc.text.update("Hello")
|
|
229
|
+
|
|
230
|
+
const currentFrontiers = loro(doc).doc.frontiers()
|
|
231
|
+
const forkedDoc = doc.forkAt(currentFrontiers)
|
|
232
|
+
|
|
233
|
+
expect(forkedDoc.text.toString()).toBe("Hello")
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it("should work with change() on forked doc", () => {
|
|
237
|
+
const schema = Shape.doc({
|
|
238
|
+
items: Shape.list(Shape.plain.number()),
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const doc = createTypedDoc(schema)
|
|
242
|
+
doc.items.push(1)
|
|
243
|
+
doc.items.push(2)
|
|
244
|
+
|
|
245
|
+
const frontiers = loro(doc).doc.frontiers()
|
|
246
|
+
doc.items.push(3)
|
|
247
|
+
|
|
248
|
+
const forkedDoc = doc.forkAt(frontiers)
|
|
249
|
+
|
|
250
|
+
// Use change() on forked doc
|
|
251
|
+
forkedDoc.change(draft => {
|
|
252
|
+
draft.items.push(100)
|
|
253
|
+
draft.items.push(200)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
expect(forkedDoc.items.toJSON()).toEqual([1, 2, 100, 200])
|
|
257
|
+
expect(doc.items.toJSON()).toEqual([1, 2, 3])
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
})
|
|
@@ -458,4 +458,406 @@ describe("functional helpers", () => {
|
|
|
458
458
|
expect(getLoroContainer(doc.tree)).toBe(loro(doc.tree).container)
|
|
459
459
|
})
|
|
460
460
|
})
|
|
461
|
+
|
|
462
|
+
describe("change() on refs", () => {
|
|
463
|
+
describe("ListRef", () => {
|
|
464
|
+
it("should batch push operations", () => {
|
|
465
|
+
const doc = createTypedDoc(fullSchema)
|
|
466
|
+
|
|
467
|
+
change(doc.items, draft => {
|
|
468
|
+
draft.push("item1")
|
|
469
|
+
draft.push("item2")
|
|
470
|
+
draft.push("item3")
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
expect(doc.items.toJSON()).toEqual(["item1", "item2", "item3"])
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it("should batch delete and push operations", () => {
|
|
477
|
+
const doc = createTypedDoc(fullSchema)
|
|
478
|
+
|
|
479
|
+
// Setup initial data
|
|
480
|
+
doc.items.push("a")
|
|
481
|
+
doc.items.push("b")
|
|
482
|
+
doc.items.push("c")
|
|
483
|
+
|
|
484
|
+
change(doc.items, draft => {
|
|
485
|
+
draft.delete(1, 1) // Remove "b"
|
|
486
|
+
draft.push("d")
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
expect(doc.items.toJSON()).toEqual(["a", "c", "d"])
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it("should return the original ref for chaining", () => {
|
|
493
|
+
const doc = createTypedDoc(fullSchema)
|
|
494
|
+
|
|
495
|
+
const result = change(doc.items, draft => {
|
|
496
|
+
draft.push("item1")
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
expect(result).toBe(doc.items)
|
|
500
|
+
result.push("item2")
|
|
501
|
+
expect(doc.items.toJSON()).toEqual(["item1", "item2"])
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it("should support find-and-mutate patterns with value shapes", () => {
|
|
505
|
+
const listSchema = Shape.doc({
|
|
506
|
+
items: Shape.list(
|
|
507
|
+
Shape.plain.struct({
|
|
508
|
+
id: Shape.plain.string(),
|
|
509
|
+
count: Shape.plain.number(),
|
|
510
|
+
}),
|
|
511
|
+
),
|
|
512
|
+
})
|
|
513
|
+
const doc = createTypedDoc(listSchema)
|
|
514
|
+
|
|
515
|
+
// Setup initial data
|
|
516
|
+
doc.items.push({ id: "a", count: 0 })
|
|
517
|
+
doc.items.push({ id: "b", count: 0 })
|
|
518
|
+
|
|
519
|
+
change(doc.items, draft => {
|
|
520
|
+
const item = draft.find(i => i.id === "b")
|
|
521
|
+
if (item) {
|
|
522
|
+
item.count = 10
|
|
523
|
+
}
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
expect(doc.items.toJSON()).toEqual([
|
|
527
|
+
{ id: "a", count: 0 },
|
|
528
|
+
{ id: "b", count: 10 },
|
|
529
|
+
])
|
|
530
|
+
})
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
describe("TextRef", () => {
|
|
534
|
+
it("should batch insert operations", () => {
|
|
535
|
+
const doc = createTypedDoc(fullSchema)
|
|
536
|
+
|
|
537
|
+
change(doc.title, draft => {
|
|
538
|
+
draft.insert(0, "Hello")
|
|
539
|
+
draft.insert(5, " World")
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
expect(doc.title.toString()).toBe("Hello World")
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it("should batch insert and delete operations", () => {
|
|
546
|
+
const doc = createTypedDoc(fullSchema)
|
|
547
|
+
|
|
548
|
+
doc.title.insert(0, "Hello World")
|
|
549
|
+
|
|
550
|
+
change(doc.title, draft => {
|
|
551
|
+
draft.delete(5, 6) // Remove " World"
|
|
552
|
+
draft.insert(5, " Universe")
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
expect(doc.title.toString()).toBe("Hello Universe")
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it("should support update operation", () => {
|
|
559
|
+
const doc = createTypedDoc(fullSchema)
|
|
560
|
+
|
|
561
|
+
doc.title.insert(0, "Old Text")
|
|
562
|
+
|
|
563
|
+
change(doc.title, draft => {
|
|
564
|
+
draft.update("New Text")
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
expect(doc.title.toString()).toBe("New Text")
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it("should return the original ref for chaining", () => {
|
|
571
|
+
const doc = createTypedDoc(fullSchema)
|
|
572
|
+
|
|
573
|
+
const result = change(doc.title, draft => {
|
|
574
|
+
draft.insert(0, "Hello")
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
expect(result).toBe(doc.title)
|
|
578
|
+
result.insert(5, "!")
|
|
579
|
+
expect(doc.title.toString()).toBe("Hello!")
|
|
580
|
+
})
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
describe("CounterRef", () => {
|
|
584
|
+
it("should batch increment operations", () => {
|
|
585
|
+
const doc = createTypedDoc(fullSchema)
|
|
586
|
+
|
|
587
|
+
change(doc.count, draft => {
|
|
588
|
+
draft.increment(5)
|
|
589
|
+
draft.increment(3)
|
|
590
|
+
draft.increment(2)
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
expect(doc.count.value).toBe(10)
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
it("should batch increment and decrement operations", () => {
|
|
597
|
+
const doc = createTypedDoc(fullSchema)
|
|
598
|
+
|
|
599
|
+
doc.count.increment(10)
|
|
600
|
+
|
|
601
|
+
change(doc.count, draft => {
|
|
602
|
+
draft.increment(5)
|
|
603
|
+
draft.decrement(3)
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
expect(doc.count.value).toBe(12)
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
it("should return the original ref for chaining", () => {
|
|
610
|
+
const doc = createTypedDoc(fullSchema)
|
|
611
|
+
|
|
612
|
+
const result = change(doc.count, draft => {
|
|
613
|
+
draft.increment(5)
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
expect(result).toBe(doc.count)
|
|
617
|
+
result.increment(3)
|
|
618
|
+
expect(doc.count.value).toBe(8)
|
|
619
|
+
})
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
describe("StructRef", () => {
|
|
623
|
+
it("should batch property assignments", () => {
|
|
624
|
+
const doc = createTypedDoc(fullSchema)
|
|
625
|
+
|
|
626
|
+
change(doc.profile, draft => {
|
|
627
|
+
draft.bio.insert(0, "Hello")
|
|
628
|
+
draft.age.increment(25)
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
expect(doc.profile.bio.toString()).toBe("Hello")
|
|
632
|
+
expect(doc.profile.age.value).toBe(25)
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
it("should return the original ref for chaining", () => {
|
|
636
|
+
const doc = createTypedDoc(fullSchema)
|
|
637
|
+
|
|
638
|
+
const result = change(doc.profile, draft => {
|
|
639
|
+
draft.bio.insert(0, "Test")
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
expect(result).toBe(doc.profile)
|
|
643
|
+
})
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
describe("RecordRef", () => {
|
|
647
|
+
it("should batch set operations", () => {
|
|
648
|
+
const doc = createTypedDoc(fullSchema)
|
|
649
|
+
|
|
650
|
+
change(doc.users, draft => {
|
|
651
|
+
draft.set("alice", { name: "Alice" })
|
|
652
|
+
draft.set("bob", { name: "Bob" })
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
expect(doc.users.toJSON()).toEqual({
|
|
656
|
+
alice: { name: "Alice" },
|
|
657
|
+
bob: { name: "Bob" },
|
|
658
|
+
})
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
it("should batch set and delete operations", () => {
|
|
662
|
+
const doc = createTypedDoc(fullSchema)
|
|
663
|
+
|
|
664
|
+
doc.users.set("alice", { name: "Alice" })
|
|
665
|
+
doc.users.set("bob", { name: "Bob" })
|
|
666
|
+
|
|
667
|
+
change(doc.users, draft => {
|
|
668
|
+
draft.delete("alice")
|
|
669
|
+
draft.set("charlie", { name: "Charlie" })
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
expect(doc.users.toJSON()).toEqual({
|
|
673
|
+
bob: { name: "Bob" },
|
|
674
|
+
charlie: { name: "Charlie" },
|
|
675
|
+
})
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
it("should return the original ref for chaining", () => {
|
|
679
|
+
const doc = createTypedDoc(fullSchema)
|
|
680
|
+
|
|
681
|
+
const result = change(doc.users, draft => {
|
|
682
|
+
draft.set("alice", { name: "Alice" })
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
expect(result).toBe(doc.users)
|
|
686
|
+
})
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
describe("TreeRef", () => {
|
|
690
|
+
it("should batch createNode operations", () => {
|
|
691
|
+
const doc = createTypedDoc(fullSchema)
|
|
692
|
+
|
|
693
|
+
change(doc.tree, draft => {
|
|
694
|
+
draft.createNode()
|
|
695
|
+
draft.createNode()
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
expect(doc.tree.roots().length).toBe(2)
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it("should batch node creation with initial data", () => {
|
|
702
|
+
const doc = createTypedDoc(fullSchema)
|
|
703
|
+
|
|
704
|
+
change(doc.tree, draft => {
|
|
705
|
+
const node1 = draft.createNode()
|
|
706
|
+
node1.data.name.insert(0, "Node 1")
|
|
707
|
+
|
|
708
|
+
const node2 = draft.createNode()
|
|
709
|
+
node2.data.name.insert(0, "Node 2")
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
const roots = doc.tree.roots()
|
|
713
|
+
expect(roots.length).toBe(2)
|
|
714
|
+
expect(roots[0].data.name.toString()).toBe("Node 1")
|
|
715
|
+
expect(roots[1].data.name.toString()).toBe("Node 2")
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
it("should return the original ref for chaining", () => {
|
|
719
|
+
const doc = createTypedDoc(fullSchema)
|
|
720
|
+
|
|
721
|
+
const result = change(doc.tree, draft => {
|
|
722
|
+
draft.createNode()
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
expect(result).toBe(doc.tree)
|
|
726
|
+
})
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
describe("MovableListRef", () => {
|
|
730
|
+
it("should batch push operations", () => {
|
|
731
|
+
const doc = createTypedDoc(fullSchema)
|
|
732
|
+
|
|
733
|
+
change(doc.movableItems, draft => {
|
|
734
|
+
draft.push("item1")
|
|
735
|
+
draft.push("item2")
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
expect(doc.movableItems.toJSON()).toEqual(["item1", "item2"])
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
it("should return the original ref for chaining", () => {
|
|
742
|
+
const doc = createTypedDoc(fullSchema)
|
|
743
|
+
|
|
744
|
+
const result = change(doc.movableItems, draft => {
|
|
745
|
+
draft.push("item1")
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
expect(result).toBe(doc.movableItems)
|
|
749
|
+
})
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
describe("nested change() calls", () => {
|
|
753
|
+
it("should handle nested change() calls correctly", () => {
|
|
754
|
+
const doc = createTypedDoc(fullSchema)
|
|
755
|
+
|
|
756
|
+
change(doc.items, outerDraft => {
|
|
757
|
+
outerDraft.push("outer1")
|
|
758
|
+
|
|
759
|
+
// Nested change on a different ref
|
|
760
|
+
change(doc.count, innerDraft => {
|
|
761
|
+
innerDraft.increment(10)
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
outerDraft.push("outer2")
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
expect(doc.items.toJSON()).toEqual(["outer1", "outer2"])
|
|
768
|
+
expect(doc.count.value).toBe(10)
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
it("should handle deeply nested change() calls", () => {
|
|
772
|
+
const doc = createTypedDoc(fullSchema)
|
|
773
|
+
|
|
774
|
+
change(doc.items, d1 => {
|
|
775
|
+
d1.push("L1")
|
|
776
|
+
|
|
777
|
+
change(doc.count, d2 => {
|
|
778
|
+
d2.increment(1)
|
|
779
|
+
|
|
780
|
+
change(doc.title, d3 => {
|
|
781
|
+
d3.insert(0, "Deep")
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
d2.increment(2)
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
d1.push("L1-end")
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
expect(doc.items.toJSON()).toEqual(["L1", "L1-end"])
|
|
791
|
+
expect(doc.count.value).toBe(3)
|
|
792
|
+
expect(doc.title.toString()).toBe("Deep")
|
|
793
|
+
})
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
describe("encapsulation use case", () => {
|
|
797
|
+
it("should allow passing refs without exposing the doc", () => {
|
|
798
|
+
const doc = createTypedDoc(fullSchema)
|
|
799
|
+
|
|
800
|
+
// Simulate a library function that only receives the ref
|
|
801
|
+
function addItems(itemsRef: typeof doc.items) {
|
|
802
|
+
change(itemsRef, draft => {
|
|
803
|
+
draft.push("library-item-1")
|
|
804
|
+
draft.push("library-item-2")
|
|
805
|
+
})
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// User code passes the ref, not the doc
|
|
809
|
+
addItems(doc.items)
|
|
810
|
+
|
|
811
|
+
expect(doc.items.toJSON()).toEqual(["library-item-1", "library-item-2"])
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
it("should allow passing TreeRef for state machine use case", () => {
|
|
815
|
+
const doc = createTypedDoc(fullSchema)
|
|
816
|
+
|
|
817
|
+
// Simulate a state machine library
|
|
818
|
+
function addStates(statesRef: typeof doc.tree) {
|
|
819
|
+
change(statesRef, draft => {
|
|
820
|
+
const idle = draft.createNode()
|
|
821
|
+
idle.data.name.insert(0, "idle")
|
|
822
|
+
|
|
823
|
+
const running = draft.createNode()
|
|
824
|
+
running.data.name.insert(0, "running")
|
|
825
|
+
})
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
addStates(doc.tree)
|
|
829
|
+
|
|
830
|
+
const roots = doc.tree.roots()
|
|
831
|
+
expect(roots.length).toBe(2)
|
|
832
|
+
expect(roots[0].data.name.toString()).toBe("idle")
|
|
833
|
+
expect(roots[1].data.name.toString()).toBe("running")
|
|
834
|
+
})
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
describe("regression: doc.change() still works", () => {
|
|
838
|
+
it("should still support doc.change() method", () => {
|
|
839
|
+
const doc = createTypedDoc(fullSchema)
|
|
840
|
+
|
|
841
|
+
doc.change(draft => {
|
|
842
|
+
draft.title.insert(0, "Hello")
|
|
843
|
+
draft.count.increment(5)
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
expect(doc.title.toString()).toBe("Hello")
|
|
847
|
+
expect(doc.count.value).toBe(5)
|
|
848
|
+
})
|
|
849
|
+
|
|
850
|
+
it("should still support change(doc, fn) helper", () => {
|
|
851
|
+
const doc = createTypedDoc(fullSchema)
|
|
852
|
+
|
|
853
|
+
change(doc, draft => {
|
|
854
|
+
draft.title.insert(0, "World")
|
|
855
|
+
draft.count.increment(10)
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
expect(doc.title.toString()).toBe("World")
|
|
859
|
+
expect(doc.count.value).toBe(10)
|
|
860
|
+
})
|
|
861
|
+
})
|
|
862
|
+
})
|
|
461
863
|
})
|