@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Container,
|
|
3
3
|
LoroDoc,
|
|
4
|
+
LoroEventBatch,
|
|
4
5
|
LoroMap,
|
|
5
6
|
Subscription,
|
|
6
7
|
Value,
|
|
@@ -166,9 +167,10 @@ export class RecordRefInternals<
|
|
|
166
167
|
} else {
|
|
167
168
|
// For container shapes, try to assign the plain value
|
|
168
169
|
// Use getOrCreateRef to ensure the container is created
|
|
170
|
+
// assignPlainValueToTypedRef handles batching and commits internally
|
|
169
171
|
const ref = this.getOrCreateRef(key)
|
|
170
172
|
if (assignPlainValueToTypedRef(ref as TypedRef<any>, value)) {
|
|
171
|
-
|
|
173
|
+
// Don't call commitIfAuto here - assignPlainValueToTypedRef handles it
|
|
172
174
|
return
|
|
173
175
|
}
|
|
174
176
|
throw new Error(
|
|
@@ -185,6 +187,106 @@ export class RecordRefInternals<
|
|
|
185
187
|
this.commitIfAuto()
|
|
186
188
|
}
|
|
187
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Replace entire contents with new values.
|
|
192
|
+
* Keys not in `values` are removed.
|
|
193
|
+
*/
|
|
194
|
+
replace(values: Record<string, any>): void {
|
|
195
|
+
const container = this.getContainer() as LoroMap
|
|
196
|
+
const currentKeys = new Set(container.keys())
|
|
197
|
+
const newKeys = new Set(Object.keys(values))
|
|
198
|
+
|
|
199
|
+
// Suppress auto-commit during batch operations
|
|
200
|
+
const wasSuppressed = this.isSuppressAutoCommit()
|
|
201
|
+
if (!wasSuppressed) {
|
|
202
|
+
this.setSuppressAutoCommit(true)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
// Delete keys that are not in the new values
|
|
207
|
+
for (const key of currentKeys) {
|
|
208
|
+
if (!newKeys.has(key)) {
|
|
209
|
+
container.delete(key)
|
|
210
|
+
this.refCache.delete(key)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Set new/updated values
|
|
215
|
+
for (const key of newKeys) {
|
|
216
|
+
this.set(key, values[key])
|
|
217
|
+
}
|
|
218
|
+
} finally {
|
|
219
|
+
// Restore auto-commit state
|
|
220
|
+
if (!wasSuppressed) {
|
|
221
|
+
this.setSuppressAutoCommit(false)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Commit once after all operations
|
|
226
|
+
this.commitIfAuto()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Merge values into record.
|
|
231
|
+
* Existing keys not in `values` are kept.
|
|
232
|
+
*/
|
|
233
|
+
merge(values: Record<string, any>): void {
|
|
234
|
+
// Suppress auto-commit during batch operations
|
|
235
|
+
const wasSuppressed = this.isSuppressAutoCommit()
|
|
236
|
+
if (!wasSuppressed) {
|
|
237
|
+
this.setSuppressAutoCommit(true)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
// Set new/updated values (no deletions)
|
|
242
|
+
for (const key of Object.keys(values)) {
|
|
243
|
+
this.set(key, values[key])
|
|
244
|
+
}
|
|
245
|
+
} finally {
|
|
246
|
+
// Restore auto-commit state
|
|
247
|
+
if (!wasSuppressed) {
|
|
248
|
+
this.setSuppressAutoCommit(false)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Commit once after all operations
|
|
253
|
+
this.commitIfAuto()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Remove all entries from the record.
|
|
258
|
+
*/
|
|
259
|
+
clear(): void {
|
|
260
|
+
const container = this.getContainer() as LoroMap
|
|
261
|
+
const keys = container.keys()
|
|
262
|
+
|
|
263
|
+
if (keys.length === 0) {
|
|
264
|
+
return // No-op on empty record
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Suppress auto-commit during batch operations
|
|
268
|
+
const wasSuppressed = this.isSuppressAutoCommit()
|
|
269
|
+
if (!wasSuppressed) {
|
|
270
|
+
this.setSuppressAutoCommit(true)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
// Delete all keys
|
|
275
|
+
for (const key of keys) {
|
|
276
|
+
container.delete(key)
|
|
277
|
+
this.refCache.delete(key)
|
|
278
|
+
}
|
|
279
|
+
} finally {
|
|
280
|
+
// Restore auto-commit state
|
|
281
|
+
if (!wasSuppressed) {
|
|
282
|
+
this.setSuppressAutoCommit(false)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Commit once after all operations
|
|
287
|
+
this.commitIfAuto()
|
|
288
|
+
}
|
|
289
|
+
|
|
188
290
|
/** Absorb mutated plain values back into Loro containers */
|
|
189
291
|
absorbPlainValues(): void {
|
|
190
292
|
absorbCachedPlainValues(this.refCache, () => this.getContainer() as LoroMap)
|
|
@@ -200,7 +302,7 @@ export class RecordRefInternals<
|
|
|
200
302
|
get container(): LoroMap {
|
|
201
303
|
return self.getContainer() as LoroMap
|
|
202
304
|
},
|
|
203
|
-
subscribe(callback: (event:
|
|
305
|
+
subscribe(callback: (event: LoroEventBatch) => void): Subscription {
|
|
204
306
|
return (self.getContainer() as LoroMap).subscribe(callback)
|
|
205
307
|
},
|
|
206
308
|
setContainer(key: string, container: Container): Container {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import { change } from "../functional-helpers.js"
|
|
3
|
-
import { createTypedDoc, Shape } from "../index.js"
|
|
3
|
+
import { createTypedDoc, loro, Shape } from "../index.js"
|
|
4
4
|
|
|
5
5
|
describe("Record Types", () => {
|
|
6
6
|
describe("Shape.record (Container)", () => {
|
|
@@ -405,4 +405,525 @@ describe("Record Types", () => {
|
|
|
405
405
|
}).not.toThrow()
|
|
406
406
|
})
|
|
407
407
|
})
|
|
408
|
+
|
|
409
|
+
describe("RecordRef values() and entries() methods", () => {
|
|
410
|
+
it("should return properly typed values for value-shaped records", () => {
|
|
411
|
+
const schema = Shape.doc({
|
|
412
|
+
scores: Shape.record(Shape.plain.number()),
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const doc = createTypedDoc(schema)
|
|
416
|
+
|
|
417
|
+
change(doc, draft => {
|
|
418
|
+
draft.scores.alice = 100
|
|
419
|
+
draft.scores.bob = 50
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const values = doc.scores.values()
|
|
423
|
+
expect(values).toEqual([100, 50])
|
|
424
|
+
|
|
425
|
+
// Type check: values should be number[]
|
|
426
|
+
const _typeCheck: number[] = values
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it("should return properly typed entries for value-shaped records", () => {
|
|
430
|
+
const schema = Shape.doc({
|
|
431
|
+
scores: Shape.record(Shape.plain.number()),
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
const doc = createTypedDoc(schema)
|
|
435
|
+
|
|
436
|
+
change(doc, draft => {
|
|
437
|
+
draft.scores.alice = 100
|
|
438
|
+
draft.scores.bob = 50
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
const entries = doc.scores.entries()
|
|
442
|
+
expect(entries).toEqual([
|
|
443
|
+
["alice", 100],
|
|
444
|
+
["bob", 50],
|
|
445
|
+
])
|
|
446
|
+
|
|
447
|
+
// Type check: entries should be [string, number][]
|
|
448
|
+
const _typeCheck: [string, number][] = entries
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it("should return properly typed refs for container-shaped records", () => {
|
|
452
|
+
const schema = Shape.doc({
|
|
453
|
+
players: Shape.record(
|
|
454
|
+
Shape.struct({
|
|
455
|
+
name: Shape.plain.string(),
|
|
456
|
+
score: Shape.plain.number(),
|
|
457
|
+
}),
|
|
458
|
+
),
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
const doc = createTypedDoc(schema)
|
|
462
|
+
|
|
463
|
+
change(doc, draft => {
|
|
464
|
+
draft.players.alice = { name: "Alice", score: 100 }
|
|
465
|
+
draft.players.bob = { name: "Bob", score: 50 }
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
const values = doc.players.values()
|
|
469
|
+
expect(values.length).toBe(2)
|
|
470
|
+
// Values should be StructRefs that we can access properties on
|
|
471
|
+
expect(values[0].name).toBe("Alice")
|
|
472
|
+
expect(values[0].score).toBe(100)
|
|
473
|
+
expect(values[1].name).toBe("Bob")
|
|
474
|
+
expect(values[1].score).toBe(50)
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it("should return properly typed entries for container-shaped records", () => {
|
|
478
|
+
const schema = Shape.doc({
|
|
479
|
+
players: Shape.record(
|
|
480
|
+
Shape.struct({
|
|
481
|
+
name: Shape.plain.string(),
|
|
482
|
+
score: Shape.plain.number(),
|
|
483
|
+
}),
|
|
484
|
+
),
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const doc = createTypedDoc(schema)
|
|
488
|
+
|
|
489
|
+
change(doc, draft => {
|
|
490
|
+
draft.players.alice = { name: "Alice", score: 100 }
|
|
491
|
+
draft.players.bob = { name: "Bob", score: 50 }
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
const entries = doc.players.entries()
|
|
495
|
+
expect(entries.length).toBe(2)
|
|
496
|
+
expect(entries[0][0]).toBe("alice")
|
|
497
|
+
expect(entries[0][1].name).toBe("Alice")
|
|
498
|
+
expect(entries[1][0]).toBe("bob")
|
|
499
|
+
expect(entries[1][1].name).toBe("Bob")
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it("should return empty arrays for empty records", () => {
|
|
503
|
+
const schema = Shape.doc({
|
|
504
|
+
scores: Shape.record(Shape.plain.number()),
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
const doc = createTypedDoc(schema)
|
|
508
|
+
|
|
509
|
+
expect(doc.scores.values()).toEqual([])
|
|
510
|
+
expect(doc.scores.entries()).toEqual([])
|
|
511
|
+
})
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
describe("RecordRef bulk update methods", () => {
|
|
515
|
+
describe("replace()", () => {
|
|
516
|
+
it("should clear all entries when replacing with empty object", () => {
|
|
517
|
+
const schema = Shape.doc({
|
|
518
|
+
scores: Shape.record(Shape.plain.number()),
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
const doc = createTypedDoc(schema)
|
|
522
|
+
|
|
523
|
+
change(doc, draft => {
|
|
524
|
+
draft.scores.alice = 100
|
|
525
|
+
draft.scores.bob = 50
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
expect(doc.toJSON().scores).toEqual({ alice: 100, bob: 50 })
|
|
529
|
+
|
|
530
|
+
change(doc, draft => {
|
|
531
|
+
draft.scores.replace({})
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
expect(doc.toJSON().scores).toEqual({})
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it("should add new entries", () => {
|
|
538
|
+
const schema = Shape.doc({
|
|
539
|
+
scores: Shape.record(Shape.plain.number()),
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
const doc = createTypedDoc(schema)
|
|
543
|
+
|
|
544
|
+
change(doc, draft => {
|
|
545
|
+
draft.scores.replace({
|
|
546
|
+
alice: 100,
|
|
547
|
+
bob: 50,
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
expect(doc.toJSON().scores).toEqual({ alice: 100, bob: 50 })
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it("should update existing entries", () => {
|
|
555
|
+
const schema = Shape.doc({
|
|
556
|
+
scores: Shape.record(Shape.plain.number()),
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
const doc = createTypedDoc(schema)
|
|
560
|
+
|
|
561
|
+
change(doc, draft => {
|
|
562
|
+
draft.scores.alice = 100
|
|
563
|
+
draft.scores.bob = 50
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
change(doc, draft => {
|
|
567
|
+
draft.scores.replace({
|
|
568
|
+
alice: 200,
|
|
569
|
+
bob: 75,
|
|
570
|
+
})
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
expect(doc.toJSON().scores).toEqual({ alice: 200, bob: 75 })
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it("should remove entries not in the new object", () => {
|
|
577
|
+
const schema = Shape.doc({
|
|
578
|
+
scores: Shape.record(Shape.plain.number()),
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
const doc = createTypedDoc(schema)
|
|
582
|
+
|
|
583
|
+
change(doc, draft => {
|
|
584
|
+
draft.scores.alice = 100
|
|
585
|
+
draft.scores.bob = 50
|
|
586
|
+
draft.scores.charlie = 25
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
change(doc, draft => {
|
|
590
|
+
draft.scores.replace({
|
|
591
|
+
alice: 150,
|
|
592
|
+
// bob and charlie are removed
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
expect(doc.toJSON().scores).toEqual({ alice: 150 })
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
it("should handle nested struct values", () => {
|
|
600
|
+
const schema = Shape.doc({
|
|
601
|
+
players: Shape.record(
|
|
602
|
+
Shape.struct({
|
|
603
|
+
name: Shape.plain.string(),
|
|
604
|
+
score: Shape.plain.number(),
|
|
605
|
+
}),
|
|
606
|
+
),
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
const doc = createTypedDoc(schema)
|
|
610
|
+
|
|
611
|
+
change(doc, draft => {
|
|
612
|
+
draft.players.replace({
|
|
613
|
+
alice: { name: "Alice", score: 100 },
|
|
614
|
+
bob: { name: "Bob", score: 50 },
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
expect(doc.toJSON().players).toEqual({
|
|
619
|
+
alice: { name: "Alice", score: 100 },
|
|
620
|
+
bob: { name: "Bob", score: 50 },
|
|
621
|
+
})
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
it("should batch all operations into a single commit", () => {
|
|
625
|
+
const schema = Shape.doc({
|
|
626
|
+
scores: Shape.record(Shape.plain.number()),
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
const doc = createTypedDoc(schema)
|
|
630
|
+
|
|
631
|
+
change(doc, draft => {
|
|
632
|
+
draft.scores.alice = 100
|
|
633
|
+
draft.scores.bob = 50
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
// Track subscription calls
|
|
637
|
+
let subscriptionCount = 0
|
|
638
|
+
const unsub = loro(doc.scores).subscribe(() => {
|
|
639
|
+
subscriptionCount++
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
change(doc, draft => {
|
|
643
|
+
draft.scores.replace({
|
|
644
|
+
charlie: 75,
|
|
645
|
+
dave: 25,
|
|
646
|
+
})
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
// Should only have one subscription notification for the batched operation
|
|
650
|
+
expect(subscriptionCount).toBe(1)
|
|
651
|
+
unsub()
|
|
652
|
+
})
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
describe("merge()", () => {
|
|
656
|
+
it("should add new entries", () => {
|
|
657
|
+
const schema = Shape.doc({
|
|
658
|
+
scores: Shape.record(Shape.plain.number()),
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
const doc = createTypedDoc(schema)
|
|
662
|
+
|
|
663
|
+
change(doc, draft => {
|
|
664
|
+
draft.scores.merge({
|
|
665
|
+
alice: 100,
|
|
666
|
+
bob: 50,
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
expect(doc.toJSON().scores).toEqual({ alice: 100, bob: 50 })
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
it("should update existing entries", () => {
|
|
674
|
+
const schema = Shape.doc({
|
|
675
|
+
scores: Shape.record(Shape.plain.number()),
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
const doc = createTypedDoc(schema)
|
|
679
|
+
|
|
680
|
+
change(doc, draft => {
|
|
681
|
+
draft.scores.alice = 100
|
|
682
|
+
draft.scores.bob = 50
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
change(doc, draft => {
|
|
686
|
+
draft.scores.merge({
|
|
687
|
+
alice: 200,
|
|
688
|
+
})
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
expect(doc.toJSON().scores).toEqual({ alice: 200, bob: 50 })
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
it("should NOT remove entries not in the new object", () => {
|
|
695
|
+
const schema = Shape.doc({
|
|
696
|
+
scores: Shape.record(Shape.plain.number()),
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
const doc = createTypedDoc(schema)
|
|
700
|
+
|
|
701
|
+
change(doc, draft => {
|
|
702
|
+
draft.scores.alice = 100
|
|
703
|
+
draft.scores.bob = 50
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
change(doc, draft => {
|
|
707
|
+
draft.scores.merge({
|
|
708
|
+
charlie: 75,
|
|
709
|
+
})
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
expect(doc.toJSON().scores).toEqual({
|
|
713
|
+
alice: 100,
|
|
714
|
+
bob: 50,
|
|
715
|
+
charlie: 75,
|
|
716
|
+
})
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
it("should handle nested struct values", () => {
|
|
720
|
+
const schema = Shape.doc({
|
|
721
|
+
players: Shape.record(
|
|
722
|
+
Shape.struct({
|
|
723
|
+
name: Shape.plain.string(),
|
|
724
|
+
score: Shape.plain.number(),
|
|
725
|
+
}),
|
|
726
|
+
),
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
const doc = createTypedDoc(schema)
|
|
730
|
+
|
|
731
|
+
change(doc, draft => {
|
|
732
|
+
draft.players.alice = { name: "Alice", score: 100 }
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
change(doc, draft => {
|
|
736
|
+
draft.players.merge({
|
|
737
|
+
bob: { name: "Bob", score: 50 },
|
|
738
|
+
})
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
expect(doc.toJSON().players).toEqual({
|
|
742
|
+
alice: { name: "Alice", score: 100 },
|
|
743
|
+
bob: { name: "Bob", score: 50 },
|
|
744
|
+
})
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
it("should batch all operations into a single commit", () => {
|
|
748
|
+
const schema = Shape.doc({
|
|
749
|
+
scores: Shape.record(Shape.plain.number()),
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
const doc = createTypedDoc(schema)
|
|
753
|
+
|
|
754
|
+
// Track subscription calls
|
|
755
|
+
let subscriptionCount = 0
|
|
756
|
+
const unsub = loro(doc.scores).subscribe(() => {
|
|
757
|
+
subscriptionCount++
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
change(doc, draft => {
|
|
761
|
+
draft.scores.merge({
|
|
762
|
+
alice: 100,
|
|
763
|
+
bob: 50,
|
|
764
|
+
charlie: 25,
|
|
765
|
+
})
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
// Should only have one subscription notification for the batched operation
|
|
769
|
+
expect(subscriptionCount).toBe(1)
|
|
770
|
+
unsub()
|
|
771
|
+
})
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
describe("clear()", () => {
|
|
775
|
+
it("should remove all entries", () => {
|
|
776
|
+
const schema = Shape.doc({
|
|
777
|
+
scores: Shape.record(Shape.plain.number()),
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
const doc = createTypedDoc(schema)
|
|
781
|
+
|
|
782
|
+
change(doc, draft => {
|
|
783
|
+
draft.scores.alice = 100
|
|
784
|
+
draft.scores.bob = 50
|
|
785
|
+
draft.scores.charlie = 25
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
expect(doc.toJSON().scores).toEqual({
|
|
789
|
+
alice: 100,
|
|
790
|
+
bob: 50,
|
|
791
|
+
charlie: 25,
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
change(doc, draft => {
|
|
795
|
+
draft.scores.clear()
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
expect(doc.toJSON().scores).toEqual({})
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
it("should be a no-op on empty record", () => {
|
|
802
|
+
const schema = Shape.doc({
|
|
803
|
+
scores: Shape.record(Shape.plain.number()),
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
const doc = createTypedDoc(schema)
|
|
807
|
+
|
|
808
|
+
expect(doc.toJSON().scores).toEqual({})
|
|
809
|
+
|
|
810
|
+
// Should not throw
|
|
811
|
+
change(doc, draft => {
|
|
812
|
+
draft.scores.clear()
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
expect(doc.toJSON().scores).toEqual({})
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
it("should batch all operations into a single commit", () => {
|
|
819
|
+
const schema = Shape.doc({
|
|
820
|
+
scores: Shape.record(Shape.plain.number()),
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
const doc = createTypedDoc(schema)
|
|
824
|
+
|
|
825
|
+
change(doc, draft => {
|
|
826
|
+
draft.scores.alice = 100
|
|
827
|
+
draft.scores.bob = 50
|
|
828
|
+
draft.scores.charlie = 25
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
// Track subscription calls
|
|
832
|
+
let subscriptionCount = 0
|
|
833
|
+
const unsub = loro(doc.scores).subscribe(() => {
|
|
834
|
+
subscriptionCount++
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
change(doc, draft => {
|
|
838
|
+
draft.scores.clear()
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
// Should only have one subscription notification for the batched operation
|
|
842
|
+
expect(subscriptionCount).toBe(1)
|
|
843
|
+
unsub()
|
|
844
|
+
})
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
describe("container-valued records", () => {
|
|
848
|
+
it("should work with replace() on record of structs", () => {
|
|
849
|
+
const schema = Shape.doc({
|
|
850
|
+
players: Shape.record(
|
|
851
|
+
Shape.struct({
|
|
852
|
+
name: Shape.plain.string(),
|
|
853
|
+
score: Shape.counter(),
|
|
854
|
+
}),
|
|
855
|
+
),
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
const doc = createTypedDoc(schema)
|
|
859
|
+
|
|
860
|
+
change(doc, draft => {
|
|
861
|
+
draft.players.alice = { name: "Alice", score: 100 }
|
|
862
|
+
draft.players.bob = { name: "Bob", score: 50 }
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
change(doc, draft => {
|
|
866
|
+
draft.players.replace({
|
|
867
|
+
charlie: { name: "Charlie", score: 75 },
|
|
868
|
+
})
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
expect(doc.toJSON().players).toEqual({
|
|
872
|
+
charlie: { name: "Charlie", score: 75 },
|
|
873
|
+
})
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
it("should work with merge() on record of structs", () => {
|
|
877
|
+
const schema = Shape.doc({
|
|
878
|
+
players: Shape.record(
|
|
879
|
+
Shape.struct({
|
|
880
|
+
name: Shape.plain.string(),
|
|
881
|
+
score: Shape.counter(),
|
|
882
|
+
}),
|
|
883
|
+
),
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
const doc = createTypedDoc(schema)
|
|
887
|
+
|
|
888
|
+
change(doc, draft => {
|
|
889
|
+
draft.players.alice = { name: "Alice", score: 100 }
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
change(doc, draft => {
|
|
893
|
+
draft.players.merge({
|
|
894
|
+
bob: { name: "Bob", score: 50 },
|
|
895
|
+
})
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
expect(doc.toJSON().players).toEqual({
|
|
899
|
+
alice: { name: "Alice", score: 100 },
|
|
900
|
+
bob: { name: "Bob", score: 50 },
|
|
901
|
+
})
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
it("should work with clear() on record of structs", () => {
|
|
905
|
+
const schema = Shape.doc({
|
|
906
|
+
players: Shape.record(
|
|
907
|
+
Shape.struct({
|
|
908
|
+
name: Shape.plain.string(),
|
|
909
|
+
score: Shape.counter(),
|
|
910
|
+
}),
|
|
911
|
+
),
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
const doc = createTypedDoc(schema)
|
|
915
|
+
|
|
916
|
+
change(doc, draft => {
|
|
917
|
+
draft.players.alice = { name: "Alice", score: 100 }
|
|
918
|
+
draft.players.bob = { name: "Bob", score: 50 }
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
change(doc, draft => {
|
|
922
|
+
draft.players.clear()
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
expect(doc.toJSON().players).toEqual({})
|
|
926
|
+
})
|
|
927
|
+
})
|
|
928
|
+
})
|
|
408
929
|
})
|