@verdant-web/common 2.8.0 → 2.9.1

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.
Files changed (47) hide show
  1. package/dist/esm/authz.js.map +1 -1
  2. package/dist/esm/baseline.d.ts +2 -1
  3. package/dist/esm/diffing.d.ts +3 -2
  4. package/dist/esm/diffing.js +79 -19
  5. package/dist/esm/diffing.js.map +1 -1
  6. package/dist/esm/diffing.test.js +967 -183
  7. package/dist/esm/diffing.test.js.map +1 -1
  8. package/dist/esm/oids.d.ts +5 -0
  9. package/dist/esm/oids.js +23 -1
  10. package/dist/esm/oids.js.map +1 -1
  11. package/dist/esm/operation.d.ts +5 -4
  12. package/dist/esm/operation.js +4 -4
  13. package/dist/esm/operation.js.map +1 -1
  14. package/dist/esm/patch.d.ts +17 -13
  15. package/dist/esm/patch.js +42 -6
  16. package/dist/esm/patch.js.map +1 -1
  17. package/dist/esm/schema/fields.d.ts +1 -0
  18. package/dist/esm/schema/fields.js +5 -0
  19. package/dist/esm/schema/fields.js.map +1 -1
  20. package/dist/esm/schema/validation.d.ts +8 -1
  21. package/dist/esm/schema/validation.js +133 -63
  22. package/dist/esm/schema/validation.js.map +1 -1
  23. package/dist/esm/schema/validation.test.d.ts +1 -0
  24. package/dist/esm/schema/validation.test.js +60 -0
  25. package/dist/esm/schema/validation.test.js.map +1 -0
  26. package/dist/esm/timestamp.d.ts +8 -0
  27. package/dist/esm/timestamp.js +15 -0
  28. package/dist/esm/timestamp.js.map +1 -1
  29. package/dist/esm/undo.d.ts +2 -1
  30. package/dist/esm/undo.js +52 -158
  31. package/dist/esm/undo.js.map +1 -1
  32. package/dist/esm/undo.test.js +68 -7
  33. package/dist/esm/undo.test.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/authz.ts +2 -0
  36. package/src/baseline.ts +3 -1
  37. package/src/diffing.test.ts +1089 -225
  38. package/src/diffing.ts +99 -22
  39. package/src/oids.ts +23 -1
  40. package/src/operation.ts +9 -8
  41. package/src/patch.ts +92 -17
  42. package/src/schema/fields.ts +8 -0
  43. package/src/schema/validation.test.ts +66 -0
  44. package/src/schema/validation.ts +156 -73
  45. package/src/timestamp.ts +14 -0
  46. package/src/undo.test.ts +87 -8
  47. package/src/undo.ts +83 -157
@@ -88,6 +88,27 @@ describe('generating diff operations', () => {
88
88
  `);
89
89
  expectDiffProducesResult(from, ops, to);
90
90
  });
91
+
92
+ it('handles null values properly', () => {
93
+ const from = { foo: 'bar', baz: null };
94
+ assignOid(from, 'test/a');
95
+ const to = { foo: null, baz: null };
96
+ assignOid(to, 'test/a');
97
+ const ops = diffToPatches(from, to, createClock());
98
+ expect(ops).toEqual([
99
+ {
100
+ authz: undefined,
101
+ data: {
102
+ name: 'foo',
103
+ op: 'set',
104
+ value: null,
105
+ },
106
+ oid: 'test/a',
107
+ timestamp: '0',
108
+ },
109
+ ]);
110
+ expectDiffProducesResult(from, ops, to);
111
+ });
91
112
  });
92
113
  describe('on nested objects', () => {
93
114
  it('replaces whole nested objects', () => {
@@ -422,38 +443,50 @@ describe('generating diff operations', () => {
422
443
  const from = assignOid([1, 2, 4, 5, 6, 7], 'test/a');
423
444
  const to = assignOid([1, 2, 3, 5, 8], 'test/a');
424
445
  const patches = diffToPatches(from, to, createClock());
425
- expect(patches).toEqual([
426
- {
427
- authz: undefined,
428
- data: {
429
- op: 'list-set',
430
- value: 3,
431
- index: 2,
432
- },
433
- oid: 'test/a',
434
- timestamp: '0',
435
- },
436
- {
437
- authz: undefined,
438
- data: {
439
- op: 'list-insert',
440
- value: 8,
441
- index: 4,
442
- },
443
- oid: 'test/a',
444
- timestamp: '1',
445
- },
446
- {
447
- authz: undefined,
448
- data: {
449
- op: 'list-delete',
450
- index: 5,
451
- count: 2,
452
- },
453
- oid: 'test/a',
454
- timestamp: '2',
455
- },
456
- ] satisfies Operation[]);
446
+ expect(patches).toMatchInlineSnapshot(`
447
+ [
448
+ {
449
+ "authz": undefined,
450
+ "data": {
451
+ "index": 2,
452
+ "op": "list-set",
453
+ "value": 3,
454
+ },
455
+ "oid": "test/a",
456
+ "timestamp": "0",
457
+ },
458
+ {
459
+ "authz": undefined,
460
+ "data": {
461
+ "index": 4,
462
+ "op": "list-insert",
463
+ "value": 8,
464
+ },
465
+ "oid": "test/a",
466
+ "timestamp": "1",
467
+ },
468
+ {
469
+ "authz": undefined,
470
+ "data": {
471
+ "only": "last",
472
+ "op": "list-remove",
473
+ "value": 7,
474
+ },
475
+ "oid": "test/a",
476
+ "timestamp": "2",
477
+ },
478
+ {
479
+ "authz": undefined,
480
+ "data": {
481
+ "only": "last",
482
+ "op": "list-remove",
483
+ "value": 6,
484
+ },
485
+ "oid": "test/a",
486
+ "timestamp": "3",
487
+ },
488
+ ]
489
+ `);
457
490
  expectDiffProducesResult(from, patches, to);
458
491
  });
459
492
  });
@@ -506,196 +539,1024 @@ describe('generating diff operations', () => {
506
539
  `);
507
540
  expectDiffProducesResult(from, patches, to);
508
541
  });
509
- it.todo('moves objects by identity');
510
- it.todo('removes objects by identity');
511
- });
512
- describe.todo('on lists of lists', () => {
513
- it.todo('rejects operations by value or identity');
514
- });
515
- it('should work for complex nested arrays and objects', () => {
516
- const from = {
517
- foo: {
518
- bar: [1, 2, 3],
519
- },
520
- baz: [
521
- {
522
- corge: true,
523
- },
524
- ],
525
- };
526
- assignOid(from, 'test/a');
527
- assignOid(from.foo, 'test/a:1');
528
- assignOid(from.foo.bar, 'test/a:2');
529
- assignOid(from.baz, 'test/a:3');
530
- assignOid(from.baz[0], 'test/a:4');
531
-
532
- const to = {
533
- foo: {
534
- bar: [1, 2],
535
- bop: [0],
536
- },
537
- baz: [
538
- {
539
- corge: false,
540
- },
541
- {
542
- corge: false,
543
- },
544
- ],
545
- };
546
- assignOid(to, 'test/a');
547
- assignOid(to.foo, 'test/a:1');
548
- assignOid(to.foo.bar, 'test/a:2');
549
- assignOid(to.foo.bop, 'test/a:6');
550
- assignOid(to.baz, 'test/a:3');
551
- assignOid(to.baz[0], 'test/a:4');
552
- assignOid(to.baz[1], 'test/a:5');
553
- const patches = diffToPatches(from, to, createClock(), createClock(7));
554
- expect(patches).toMatchInlineSnapshot(`
555
- [
556
- {
557
- "authz": undefined,
558
- "data": {
559
- "count": 1,
560
- "index": 2,
561
- "op": "list-delete",
562
- },
563
- "oid": "test/a:2",
564
- "timestamp": "0",
565
- },
566
- {
567
- "data": {
568
- "op": "initialize",
569
- "value": [
570
- 0,
571
- ],
572
- },
573
- "oid": "test/a:7",
574
- "timestamp": "1",
575
- },
576
- {
577
- "authz": undefined,
578
- "data": {
579
- "name": "bop",
580
- "op": "set",
581
- "value": {
582
- "@@type": "ref",
583
- "id": "test/a:7",
584
- },
585
- },
586
- "oid": "test/a:1",
587
- "timestamp": "2",
588
- },
589
- {
590
- "authz": undefined,
591
- "data": {
592
- "name": "corge",
593
- "op": "set",
594
- "value": false,
595
- },
596
- "oid": "test/a:4",
597
- "timestamp": "3",
598
- },
599
- {
600
- "data": {
601
- "op": "initialize",
602
- "value": {
603
- "corge": false,
604
- },
605
- },
606
- "oid": "test/a:8",
607
- "timestamp": "4",
608
- },
609
- {
610
- "authz": undefined,
611
- "data": {
612
- "op": "list-push",
613
- "value": {
614
- "@@type": "ref",
615
- "id": "test/a:8",
616
- },
617
- },
618
- "oid": "test/a:3",
619
- "timestamp": "5",
620
- },
621
- ]
622
- `);
623
- expectDiffProducesResult(from, patches, to);
624
- });
625
-
626
- it('should try to merge unknown objects if specified to do so', () => {
627
- const from = {
628
- foo: {
629
- bar: [1, 2, 3],
630
- },
631
- baz: [
632
- {
633
- corge: true,
634
- },
635
- ],
636
- };
637
- assignOid(from, 'test/a');
638
- assignOidsToAllSubObjects(from, createClock());
639
-
640
- const to = {
641
- foo: {
642
- bar: [1, 2],
643
- bop: [0],
644
- },
645
- baz: [
646
- {
647
- corge: false,
648
- },
649
- {
650
- corge: false,
651
- },
652
- ],
653
- };
654
- const patches = diffToPatches(from, to, createClock(), createClock(5), [], {
655
- mergeUnknownObjects: true,
656
- });
657
- expect(patches).toMatchInlineSnapshot(`
658
- [
659
- {
660
- "authz": undefined,
661
- "data": {
662
- "count": 1,
663
- "index": 2,
664
- "op": "list-delete",
665
- },
666
- "oid": "test/a:1",
667
- "timestamp": "0",
668
- },
669
- {
670
- "data": {
671
- "op": "initialize",
672
- "value": [
673
- 0,
674
- ],
675
- },
676
- "oid": "test/a:5",
677
- "timestamp": "1",
678
- },
679
- {
680
- "authz": undefined,
681
- "data": {
682
- "name": "bop",
683
- "op": "set",
684
- "value": {
685
- "@@type": "ref",
686
- "id": "test/a:5",
687
- },
688
- },
689
- "oid": "test/a:0",
690
- "timestamp": "2",
691
- },
692
- {
693
- "authz": undefined,
694
- "data": {
695
- "name": "corge",
696
- "op": "set",
697
- "value": false,
698
- },
542
+ it('deletes one item from a list', () => {
543
+ // this replicates a bug I found while testing pruning canonization
544
+ const from = assignOid(
545
+ [
546
+ { id: '1', value: 'Item one' },
547
+ { id: '2', value: 'Item two' },
548
+ {},
549
+ { id: '4', value: 'Item four' },
550
+ ],
551
+ 'test/a',
552
+ );
553
+ assignOidsToAllSubObjects(from, createClock());
554
+ const to = assignOid(
555
+ [
556
+ assignOid(
557
+ {
558
+ id: '1',
559
+ value: 'Item one',
560
+ },
561
+ 'test/a:0',
562
+ ),
563
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
564
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
565
+ ],
566
+ 'test/a',
567
+ );
568
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
569
+ expect(patches).toMatchInlineSnapshot(`
570
+ [
571
+ {
572
+ "authz": undefined,
573
+ "data": {
574
+ "index": 2,
575
+ "op": "list-move-by-ref",
576
+ "value": {
577
+ "@@type": "ref",
578
+ "id": "test/a:3",
579
+ },
580
+ },
581
+ "oid": "test/a",
582
+ "timestamp": "0",
583
+ },
584
+ {
585
+ "authz": undefined,
586
+ "data": {
587
+ "op": "delete",
588
+ },
589
+ "oid": "test/a:2",
590
+ "timestamp": "1",
591
+ },
592
+ {
593
+ "authz": undefined,
594
+ "data": {
595
+ "only": "last",
596
+ "op": "list-remove",
597
+ "value": {
598
+ "@@type": "ref",
599
+ "id": "test/a:2",
600
+ },
601
+ },
602
+ "oid": "test/a",
603
+ "timestamp": "2",
604
+ },
605
+ ]
606
+ `);
607
+ expectDiffProducesResult(from, patches, to);
608
+ });
609
+ it('moves an item from the start to the end of the list', () => {
610
+ // FIXME: the operations end up very inefficient for this. each
611
+ // other item in the list gets moved before the target item is
612
+ // moved to the end. can this be improved?
613
+ const from = assignOid(
614
+ [
615
+ { id: '1', value: 'Item one' },
616
+ { id: '2', value: 'Item two' },
617
+ { id: '3', value: 'Item three' },
618
+ { id: '4', value: 'Item four' },
619
+ ],
620
+ 'test/a',
621
+ );
622
+ assignOidsToAllSubObjects(from, createClock());
623
+ const to = assignOid(
624
+ [
625
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
626
+ assignOid({ id: '3', value: 'Item three' }, 'test/a:2'),
627
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
628
+ assignOid({ id: '1', value: 'Item one' }, 'test/a:0'),
629
+ ],
630
+ 'test/a',
631
+ );
632
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
633
+ expect(patches).toMatchInlineSnapshot(`
634
+ [
635
+ {
636
+ "authz": undefined,
637
+ "data": {
638
+ "index": 0,
639
+ "op": "list-move-by-ref",
640
+ "value": {
641
+ "@@type": "ref",
642
+ "id": "test/a:1",
643
+ },
644
+ },
645
+ "oid": "test/a",
646
+ "timestamp": "0",
647
+ },
648
+ {
649
+ "authz": undefined,
650
+ "data": {
651
+ "index": 1,
652
+ "op": "list-move-by-ref",
653
+ "value": {
654
+ "@@type": "ref",
655
+ "id": "test/a:2",
656
+ },
657
+ },
658
+ "oid": "test/a",
659
+ "timestamp": "1",
660
+ },
661
+ {
662
+ "authz": undefined,
663
+ "data": {
664
+ "index": 2,
665
+ "op": "list-move-by-ref",
666
+ "value": {
667
+ "@@type": "ref",
668
+ "id": "test/a:3",
669
+ },
670
+ },
671
+ "oid": "test/a",
672
+ "timestamp": "2",
673
+ },
674
+ {
675
+ "authz": undefined,
676
+ "data": {
677
+ "index": 3,
678
+ "op": "list-move-by-ref",
679
+ "value": {
680
+ "@@type": "ref",
681
+ "id": "test/a:0",
682
+ },
683
+ },
684
+ "oid": "test/a",
685
+ "timestamp": "3",
686
+ },
687
+ ]
688
+ `);
689
+ expectDiffProducesResult(from, patches, to);
690
+ });
691
+ it('moves an item from the end to the start of a list', () => {
692
+ const from = assignOid(
693
+ [
694
+ { id: '1', value: 'Item one' },
695
+ { id: '2', value: 'Item two' },
696
+ { id: '3', value: 'Item three' },
697
+ { id: '4', value: 'Item four' },
698
+ ],
699
+ 'test/a',
700
+ );
701
+ assignOidsToAllSubObjects(from, createClock());
702
+ const to = assignOid(
703
+ [
704
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
705
+ assignOid({ id: '1', value: 'Item one' }, 'test/a:0'),
706
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
707
+ assignOid({ id: '3', value: 'Item three' }, 'test/a:2'),
708
+ ],
709
+ 'test/a',
710
+ );
711
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
712
+ expect(patches).toMatchInlineSnapshot(`
713
+ [
714
+ {
715
+ "authz": undefined,
716
+ "data": {
717
+ "index": 0,
718
+ "op": "list-move-by-ref",
719
+ "value": {
720
+ "@@type": "ref",
721
+ "id": "test/a:3",
722
+ },
723
+ },
724
+ "oid": "test/a",
725
+ "timestamp": "0",
726
+ },
727
+ {
728
+ "authz": undefined,
729
+ "data": {
730
+ "index": 1,
731
+ "op": "list-move-by-ref",
732
+ "value": {
733
+ "@@type": "ref",
734
+ "id": "test/a:0",
735
+ },
736
+ },
737
+ "oid": "test/a",
738
+ "timestamp": "1",
739
+ },
740
+ {
741
+ "authz": undefined,
742
+ "data": {
743
+ "index": 2,
744
+ "op": "list-move-by-ref",
745
+ "value": {
746
+ "@@type": "ref",
747
+ "id": "test/a:1",
748
+ },
749
+ },
750
+ "oid": "test/a",
751
+ "timestamp": "2",
752
+ },
753
+ {
754
+ "authz": undefined,
755
+ "data": {
756
+ "index": 3,
757
+ "op": "list-move-by-ref",
758
+ "value": {
759
+ "@@type": "ref",
760
+ "id": "test/a:2",
761
+ },
762
+ },
763
+ "oid": "test/a",
764
+ "timestamp": "3",
765
+ },
766
+ ]
767
+ `);
768
+ expectDiffProducesResult(from, patches, to);
769
+ });
770
+ it('swaps two items by reference', () => {
771
+ const from = assignOid(
772
+ [
773
+ { id: '1', value: 'Item one' },
774
+ { id: '2', value: 'Item two' },
775
+ { id: '3', value: 'Item three' },
776
+ { id: '4', value: 'Item four' },
777
+ ],
778
+ 'test/a',
779
+ );
780
+ assignOidsToAllSubObjects(from, createClock());
781
+ const to = assignOid(
782
+ [
783
+ assignOid({ id: '1', value: 'Item one' }, 'test/a:0'),
784
+ assignOid({ id: '3', value: 'Item three' }, 'test/a:2'),
785
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
786
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
787
+ ],
788
+ 'test/a',
789
+ );
790
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
791
+ expect(patches).toMatchInlineSnapshot(`
792
+ [
793
+ {
794
+ "authz": undefined,
795
+ "data": {
796
+ "index": 1,
797
+ "op": "list-move-by-ref",
798
+ "value": {
799
+ "@@type": "ref",
800
+ "id": "test/a:2",
801
+ },
802
+ },
803
+ "oid": "test/a",
804
+ "timestamp": "0",
805
+ },
806
+ {
807
+ "authz": undefined,
808
+ "data": {
809
+ "index": 2,
810
+ "op": "list-move-by-ref",
811
+ "value": {
812
+ "@@type": "ref",
813
+ "id": "test/a:1",
814
+ },
815
+ },
816
+ "oid": "test/a",
817
+ "timestamp": "1",
818
+ },
819
+ ]
820
+ `);
821
+ expectDiffProducesResult(from, patches, to);
822
+ });
823
+ it('swaps a lot of items by reference', () => {
824
+ const from = assignOid(
825
+ [
826
+ { id: '1', value: 'Item one' },
827
+ { id: '2', value: 'Item two' },
828
+ { id: '3', value: 'Item three' },
829
+ { id: '4', value: 'Item four' },
830
+ ],
831
+ 'test/a',
832
+ );
833
+ assignOidsToAllSubObjects(from, createClock());
834
+ const to = assignOid(
835
+ [
836
+ assignOid({ id: '1', value: 'Item one' }, 'test/a:0'),
837
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
838
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
839
+ assignOid({ id: '3', value: 'Item three' }, 'test/a:2'),
840
+ ],
841
+ 'test/a',
842
+ );
843
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
844
+ expect(patches).toMatchInlineSnapshot(`
845
+ [
846
+ {
847
+ "authz": undefined,
848
+ "data": {
849
+ "index": 1,
850
+ "op": "list-move-by-ref",
851
+ "value": {
852
+ "@@type": "ref",
853
+ "id": "test/a:3",
854
+ },
855
+ },
856
+ "oid": "test/a",
857
+ "timestamp": "0",
858
+ },
859
+ {
860
+ "authz": undefined,
861
+ "data": {
862
+ "index": 2,
863
+ "op": "list-move-by-ref",
864
+ "value": {
865
+ "@@type": "ref",
866
+ "id": "test/a:1",
867
+ },
868
+ },
869
+ "oid": "test/a",
870
+ "timestamp": "1",
871
+ },
872
+ {
873
+ "authz": undefined,
874
+ "data": {
875
+ "index": 3,
876
+ "op": "list-move-by-ref",
877
+ "value": {
878
+ "@@type": "ref",
879
+ "id": "test/a:2",
880
+ },
881
+ },
882
+ "oid": "test/a",
883
+ "timestamp": "2",
884
+ },
885
+ ]
886
+ `);
887
+ expectDiffProducesResult(from, patches, to);
888
+ });
889
+ it('produces changes which are resilient to intervening item moves', () => {
890
+ const from = assignOid(
891
+ [
892
+ { id: '1', value: 'Item one' },
893
+ { id: '2', value: 'Item two' },
894
+ { id: '3', value: 'Item three' },
895
+ { id: '4', value: 'Item four' },
896
+ ],
897
+ 'test/a',
898
+ );
899
+ assignOidsToAllSubObjects(from, createClock());
900
+ const to = assignOid(
901
+ [
902
+ assignOid({ id: '1', value: 'Item one' }, 'test/a:0'),
903
+ assignOid({ id: '4', value: 'Item four' }, 'test/a:3'),
904
+ assignOid({ id: '2', value: 'Item two' }, 'test/a:1'),
905
+ assignOid({ id: '3', value: 'Item three' }, 'test/a:2'),
906
+ ],
907
+ 'test/a',
908
+ );
909
+ const patchClock = createClock(10); // 'the future'
910
+ const patches = diffToPatches(from, to, patchClock, createClock(4));
911
+ expect(patches).toMatchInlineSnapshot(`
912
+ [
913
+ {
914
+ "authz": undefined,
915
+ "data": {
916
+ "index": 1,
917
+ "op": "list-move-by-ref",
918
+ "value": {
919
+ "@@type": "ref",
920
+ "id": "test/a:3",
921
+ },
922
+ },
923
+ "oid": "test/a",
924
+ "timestamp": "10",
925
+ },
926
+ {
927
+ "authz": undefined,
928
+ "data": {
929
+ "index": 2,
930
+ "op": "list-move-by-ref",
931
+ "value": {
932
+ "@@type": "ref",
933
+ "id": "test/a:1",
934
+ },
935
+ },
936
+ "oid": "test/a",
937
+ "timestamp": "11",
938
+ },
939
+ {
940
+ "authz": undefined,
941
+ "data": {
942
+ "index": 3,
943
+ "op": "list-move-by-ref",
944
+ "value": {
945
+ "@@type": "ref",
946
+ "id": "test/a:2",
947
+ },
948
+ },
949
+ "oid": "test/a",
950
+ "timestamp": "12",
951
+ },
952
+ ]
953
+ `);
954
+ // in the meantime, suppose a user manipulates the list...
955
+ const interveningClock = createClock(0);
956
+ const interveningChanges: Operation[] = [
957
+ {
958
+ oid: 'test/a',
959
+ data: {
960
+ op: 'list-move-by-index',
961
+ from: 1,
962
+ to: 2,
963
+ },
964
+ timestamp: interveningClock(),
965
+ },
966
+ {
967
+ oid: 'test/a',
968
+ data: {
969
+ op: 'list-delete',
970
+ index: 0,
971
+ count: 1,
972
+ },
973
+ timestamp: interveningClock(),
974
+ },
975
+ {
976
+ oid: 'test/a:x',
977
+ data: {
978
+ op: 'initialize',
979
+ value: {
980
+ id: 'x',
981
+ value: 'Item x',
982
+ },
983
+ },
984
+ timestamp: interveningClock(),
985
+ },
986
+ {
987
+ oid: 'test/a',
988
+ data: {
989
+ op: 'list-push',
990
+ value: {
991
+ '@@type': 'ref',
992
+ id: 'test/a:x',
993
+ },
994
+ },
995
+ timestamp: interveningClock(),
996
+ },
997
+ ];
998
+ expectDiffProducesResult(
999
+ from,
1000
+ [...interveningChanges, ...patches],
1001
+ [
1002
+ { id: '4', value: 'Item four' },
1003
+ { id: '2', value: 'Item two' },
1004
+ { id: 'x', value: 'Item x' },
1005
+ { id: '3', value: 'Item three' },
1006
+ ],
1007
+ );
1008
+ });
1009
+ });
1010
+ describe('on lists of files', () => {
1011
+ // FIXME: file refs should work the same as object refs
1012
+ it('pushes items', () => {
1013
+ const from = assignOid(
1014
+ [createFileRef('file-1'), createFileRef('file-2')],
1015
+ 'test/a',
1016
+ );
1017
+ const to = assignOid(
1018
+ [
1019
+ createFileRef('file-1'),
1020
+ createFileRef('file-2'),
1021
+ createFileRef('file-3'),
1022
+ ],
1023
+ 'test/a',
1024
+ );
1025
+ const patches = diffToPatches(from, to, createClock(), createClock(2));
1026
+ expect(patches).toMatchInlineSnapshot(`
1027
+ [
1028
+ {
1029
+ "authz": undefined,
1030
+ "data": {
1031
+ "op": "list-push",
1032
+ "value": {
1033
+ "@@type": "file",
1034
+ "id": "file-3",
1035
+ },
1036
+ },
1037
+ "oid": "test/a",
1038
+ "timestamp": "0",
1039
+ },
1040
+ ]
1041
+ `);
1042
+ expectDiffProducesResult(from, patches, to);
1043
+ });
1044
+ it('deletes one item from a list', () => {
1045
+ const from = assignOid(
1046
+ [
1047
+ createFileRef('file-1'),
1048
+ createFileRef('file-2'),
1049
+ createFileRef('file-3'),
1050
+ createFileRef('file-4'),
1051
+ ],
1052
+ 'test/a',
1053
+ );
1054
+ const to = assignOid(
1055
+ [
1056
+ createFileRef('file-1'),
1057
+ createFileRef('file-2'),
1058
+ createFileRef('file-4'),
1059
+ ],
1060
+ 'test/a',
1061
+ );
1062
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
1063
+ expect(patches).toMatchInlineSnapshot(`
1064
+ [
1065
+ {
1066
+ "authz": undefined,
1067
+ "data": {
1068
+ "index": 2,
1069
+ "op": "list-insert",
1070
+ "value": {
1071
+ "@@type": "file",
1072
+ "id": "file-4",
1073
+ },
1074
+ },
1075
+ "oid": "test/a",
1076
+ "timestamp": "0",
1077
+ },
1078
+ {
1079
+ "authz": undefined,
1080
+ "data": {
1081
+ "only": "last",
1082
+ "op": "list-remove",
1083
+ "value": {
1084
+ "@@type": "file",
1085
+ "id": "file-4",
1086
+ },
1087
+ },
1088
+ "oid": "test/a",
1089
+ "timestamp": "1",
1090
+ },
1091
+ {
1092
+ "authz": undefined,
1093
+ "data": {
1094
+ "only": "last",
1095
+ "op": "list-remove",
1096
+ "value": {
1097
+ "@@type": "file",
1098
+ "id": "file-3",
1099
+ },
1100
+ },
1101
+ "oid": "test/a",
1102
+ "timestamp": "2",
1103
+ },
1104
+ ]
1105
+ `);
1106
+ expectDiffProducesResult(from, patches, to);
1107
+ });
1108
+ it('moves an item from the start to the end of the list', () => {
1109
+ // FIXME: the operations end up very inefficient for this. each
1110
+ // other item in the list gets moved before the target item is
1111
+ // moved to the end. can this be improved?
1112
+ const from = assignOid(
1113
+ [
1114
+ createFileRef('file-1'),
1115
+ createFileRef('file-2'),
1116
+ createFileRef('file-3'),
1117
+ createFileRef('file-4'),
1118
+ ],
1119
+ 'test/a',
1120
+ );
1121
+ assignOidsToAllSubObjects(from, createClock());
1122
+ const to = assignOid(
1123
+ [
1124
+ createFileRef('file-2'),
1125
+ createFileRef('file-3'),
1126
+ createFileRef('file-4'),
1127
+ createFileRef('file-1'),
1128
+ ],
1129
+ 'test/a',
1130
+ );
1131
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
1132
+ expect(patches).toMatchInlineSnapshot(`
1133
+ [
1134
+ {
1135
+ "authz": undefined,
1136
+ "data": {
1137
+ "index": 0,
1138
+ "op": "list-set",
1139
+ "value": {
1140
+ "@@type": "file",
1141
+ "id": "file-2",
1142
+ },
1143
+ },
1144
+ "oid": "test/a",
1145
+ "timestamp": "0",
1146
+ },
1147
+ {
1148
+ "authz": undefined,
1149
+ "data": {
1150
+ "index": 1,
1151
+ "op": "list-set",
1152
+ "value": {
1153
+ "@@type": "file",
1154
+ "id": "file-3",
1155
+ },
1156
+ },
1157
+ "oid": "test/a",
1158
+ "timestamp": "1",
1159
+ },
1160
+ {
1161
+ "authz": undefined,
1162
+ "data": {
1163
+ "index": 2,
1164
+ "op": "list-set",
1165
+ "value": {
1166
+ "@@type": "file",
1167
+ "id": "file-4",
1168
+ },
1169
+ },
1170
+ "oid": "test/a",
1171
+ "timestamp": "2",
1172
+ },
1173
+ {
1174
+ "authz": undefined,
1175
+ "data": {
1176
+ "index": 3,
1177
+ "op": "list-set",
1178
+ "value": {
1179
+ "@@type": "file",
1180
+ "id": "file-1",
1181
+ },
1182
+ },
1183
+ "oid": "test/a",
1184
+ "timestamp": "3",
1185
+ },
1186
+ ]
1187
+ `);
1188
+ expectDiffProducesResult(from, patches, to);
1189
+ });
1190
+ it('moves an item from the end to the start of a list', () => {
1191
+ const from = assignOid(
1192
+ [
1193
+ createFileRef('file-1'),
1194
+ createFileRef('file-2'),
1195
+ createFileRef('file-3'),
1196
+ createFileRef('file-4'),
1197
+ ],
1198
+ 'test/a',
1199
+ );
1200
+ assignOidsToAllSubObjects(from, createClock());
1201
+ const to = assignOid(
1202
+ [
1203
+ createFileRef('file-4'),
1204
+ createFileRef('file-1'),
1205
+ createFileRef('file-2'),
1206
+ createFileRef('file-3'),
1207
+ ],
1208
+ 'test/a',
1209
+ );
1210
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
1211
+ expect(patches).toMatchInlineSnapshot(`
1212
+ [
1213
+ {
1214
+ "authz": undefined,
1215
+ "data": {
1216
+ "index": 0,
1217
+ "op": "list-insert",
1218
+ "value": {
1219
+ "@@type": "file",
1220
+ "id": "file-4",
1221
+ },
1222
+ },
1223
+ "oid": "test/a",
1224
+ "timestamp": "0",
1225
+ },
1226
+ {
1227
+ "authz": undefined,
1228
+ "data": {
1229
+ "only": "last",
1230
+ "op": "list-remove",
1231
+ "value": {
1232
+ "@@type": "file",
1233
+ "id": "file-4",
1234
+ },
1235
+ },
1236
+ "oid": "test/a",
1237
+ "timestamp": "1",
1238
+ },
1239
+ ]
1240
+ `);
1241
+ expectDiffProducesResult(from, patches, to);
1242
+ });
1243
+ it('swaps two items by reference', () => {
1244
+ const from = assignOid(
1245
+ [
1246
+ createFileRef('file-1'),
1247
+ createFileRef('file-2'),
1248
+ createFileRef('file-3'),
1249
+ createFileRef('file-4'),
1250
+ ],
1251
+ 'test/a',
1252
+ );
1253
+ assignOidsToAllSubObjects(from, createClock());
1254
+ const to = assignOid(
1255
+ [
1256
+ createFileRef('file-1'),
1257
+ createFileRef('file-3'),
1258
+ createFileRef('file-2'),
1259
+ createFileRef('file-4'),
1260
+ ],
1261
+ 'test/a',
1262
+ );
1263
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
1264
+ expect(patches).toMatchInlineSnapshot(`
1265
+ [
1266
+ {
1267
+ "authz": undefined,
1268
+ "data": {
1269
+ "index": 1,
1270
+ "op": "list-insert",
1271
+ "value": {
1272
+ "@@type": "file",
1273
+ "id": "file-3",
1274
+ },
1275
+ },
1276
+ "oid": "test/a",
1277
+ "timestamp": "0",
1278
+ },
1279
+ {
1280
+ "authz": undefined,
1281
+ "data": {
1282
+ "index": 3,
1283
+ "op": "list-insert",
1284
+ "value": {
1285
+ "@@type": "file",
1286
+ "id": "file-4",
1287
+ },
1288
+ },
1289
+ "oid": "test/a",
1290
+ "timestamp": "1",
1291
+ },
1292
+ {
1293
+ "authz": undefined,
1294
+ "data": {
1295
+ "only": "last",
1296
+ "op": "list-remove",
1297
+ "value": {
1298
+ "@@type": "file",
1299
+ "id": "file-4",
1300
+ },
1301
+ },
1302
+ "oid": "test/a",
1303
+ "timestamp": "2",
1304
+ },
1305
+ {
1306
+ "authz": undefined,
1307
+ "data": {
1308
+ "only": "last",
1309
+ "op": "list-remove",
1310
+ "value": {
1311
+ "@@type": "file",
1312
+ "id": "file-3",
1313
+ },
1314
+ },
1315
+ "oid": "test/a",
1316
+ "timestamp": "3",
1317
+ },
1318
+ ]
1319
+ `);
1320
+ expectDiffProducesResult(from, patches, to);
1321
+ });
1322
+ it('swaps a lot of items by reference', () => {
1323
+ const from = assignOid(
1324
+ [
1325
+ createFileRef('file-1'),
1326
+ createFileRef('file-2'),
1327
+ createFileRef('file-3'),
1328
+ createFileRef('file-4'),
1329
+ ],
1330
+ 'test/a',
1331
+ );
1332
+ assignOidsToAllSubObjects(from, createClock());
1333
+ const to = assignOid(
1334
+ [
1335
+ createFileRef('file-1'),
1336
+ createFileRef('file-4'),
1337
+ createFileRef('file-2'),
1338
+ createFileRef('file-3'),
1339
+ ],
1340
+ 'test/a',
1341
+ );
1342
+ const patches = diffToPatches(from, to, createClock(), createClock(4));
1343
+ expect(patches).toMatchInlineSnapshot(`
1344
+ [
1345
+ {
1346
+ "authz": undefined,
1347
+ "data": {
1348
+ "index": 1,
1349
+ "op": "list-insert",
1350
+ "value": {
1351
+ "@@type": "file",
1352
+ "id": "file-4",
1353
+ },
1354
+ },
1355
+ "oid": "test/a",
1356
+ "timestamp": "0",
1357
+ },
1358
+ {
1359
+ "authz": undefined,
1360
+ "data": {
1361
+ "only": "last",
1362
+ "op": "list-remove",
1363
+ "value": {
1364
+ "@@type": "file",
1365
+ "id": "file-4",
1366
+ },
1367
+ },
1368
+ "oid": "test/a",
1369
+ "timestamp": "1",
1370
+ },
1371
+ ]
1372
+ `);
1373
+ expectDiffProducesResult(from, patches, to);
1374
+ });
1375
+ });
1376
+ it('should work for complex nested arrays and objects', () => {
1377
+ const from = {
1378
+ foo: {
1379
+ bar: [1, 2, 3],
1380
+ },
1381
+ baz: [
1382
+ {
1383
+ corge: true,
1384
+ },
1385
+ ],
1386
+ };
1387
+ assignOid(from, 'test/a');
1388
+ assignOid(from.foo, 'test/a:1');
1389
+ assignOid(from.foo.bar, 'test/a:2');
1390
+ assignOid(from.baz, 'test/a:3');
1391
+ assignOid(from.baz[0], 'test/a:4');
1392
+
1393
+ const to = {
1394
+ foo: {
1395
+ bar: [1, 2],
1396
+ bop: [0],
1397
+ },
1398
+ baz: [
1399
+ {
1400
+ corge: false,
1401
+ },
1402
+ {
1403
+ corge: false,
1404
+ },
1405
+ ],
1406
+ };
1407
+ assignOid(to, 'test/a');
1408
+ assignOid(to.foo, 'test/a:1');
1409
+ assignOid(to.foo.bar, 'test/a:2');
1410
+ assignOid(to.foo.bop, 'test/a:6');
1411
+ assignOid(to.baz, 'test/a:3');
1412
+ assignOid(to.baz[0], 'test/a:4');
1413
+ assignOid(to.baz[1], 'test/a:5');
1414
+ const patches = diffToPatches(from, to, createClock(), createClock(7));
1415
+ expect(patches).toMatchInlineSnapshot(`
1416
+ [
1417
+ {
1418
+ "authz": undefined,
1419
+ "data": {
1420
+ "only": "last",
1421
+ "op": "list-remove",
1422
+ "value": 3,
1423
+ },
1424
+ "oid": "test/a:2",
1425
+ "timestamp": "0",
1426
+ },
1427
+ {
1428
+ "data": {
1429
+ "op": "initialize",
1430
+ "value": [
1431
+ 0,
1432
+ ],
1433
+ },
1434
+ "oid": "test/a:7",
1435
+ "timestamp": "1",
1436
+ },
1437
+ {
1438
+ "authz": undefined,
1439
+ "data": {
1440
+ "name": "bop",
1441
+ "op": "set",
1442
+ "value": {
1443
+ "@@type": "ref",
1444
+ "id": "test/a:7",
1445
+ },
1446
+ },
1447
+ "oid": "test/a:1",
1448
+ "timestamp": "2",
1449
+ },
1450
+ {
1451
+ "authz": undefined,
1452
+ "data": {
1453
+ "name": "corge",
1454
+ "op": "set",
1455
+ "value": false,
1456
+ },
1457
+ "oid": "test/a:4",
1458
+ "timestamp": "3",
1459
+ },
1460
+ {
1461
+ "data": {
1462
+ "op": "initialize",
1463
+ "value": {
1464
+ "corge": false,
1465
+ },
1466
+ },
1467
+ "oid": "test/a:8",
1468
+ "timestamp": "4",
1469
+ },
1470
+ {
1471
+ "authz": undefined,
1472
+ "data": {
1473
+ "op": "list-push",
1474
+ "value": {
1475
+ "@@type": "ref",
1476
+ "id": "test/a:8",
1477
+ },
1478
+ },
1479
+ "oid": "test/a:3",
1480
+ "timestamp": "5",
1481
+ },
1482
+ ]
1483
+ `);
1484
+ expectDiffProducesResult(from, patches, to);
1485
+ });
1486
+
1487
+ it('should try to merge unknown objects if specified to do so', () => {
1488
+ const from = {
1489
+ foo: {
1490
+ bar: [1, 2, 3],
1491
+ },
1492
+ baz: [
1493
+ {
1494
+ corge: true,
1495
+ },
1496
+ ],
1497
+ };
1498
+ assignOid(from, 'test/a');
1499
+ assignOidsToAllSubObjects(from, createClock());
1500
+
1501
+ const to = {
1502
+ foo: {
1503
+ bar: [1, 2],
1504
+ bop: [0],
1505
+ },
1506
+ baz: [
1507
+ {
1508
+ corge: false,
1509
+ },
1510
+ {
1511
+ corge: false,
1512
+ },
1513
+ ],
1514
+ };
1515
+ const patches = diffToPatches(from, to, createClock(), createClock(5), [], {
1516
+ mergeUnknownObjects: true,
1517
+ });
1518
+ expect(patches).toMatchInlineSnapshot(`
1519
+ [
1520
+ {
1521
+ "authz": undefined,
1522
+ "data": {
1523
+ "only": "last",
1524
+ "op": "list-remove",
1525
+ "value": 3,
1526
+ },
1527
+ "oid": "test/a:1",
1528
+ "timestamp": "0",
1529
+ },
1530
+ {
1531
+ "data": {
1532
+ "op": "initialize",
1533
+ "value": [
1534
+ 0,
1535
+ ],
1536
+ },
1537
+ "oid": "test/a:5",
1538
+ "timestamp": "1",
1539
+ },
1540
+ {
1541
+ "authz": undefined,
1542
+ "data": {
1543
+ "name": "bop",
1544
+ "op": "set",
1545
+ "value": {
1546
+ "@@type": "ref",
1547
+ "id": "test/a:5",
1548
+ },
1549
+ },
1550
+ "oid": "test/a:0",
1551
+ "timestamp": "2",
1552
+ },
1553
+ {
1554
+ "authz": undefined,
1555
+ "data": {
1556
+ "name": "corge",
1557
+ "op": "set",
1558
+ "value": false,
1559
+ },
699
1560
  "oid": "test/a:3",
700
1561
  "timestamp": "3",
701
1562
  },
@@ -763,9 +1624,12 @@ describe('generating diff operations', () => {
763
1624
  {
764
1625
  "authz": undefined,
765
1626
  "data": {
766
- "count": 1,
767
- "index": 1,
768
- "op": "list-delete",
1627
+ "only": "last",
1628
+ "op": "list-remove",
1629
+ "value": {
1630
+ "@@type": "file",
1631
+ "id": "ghi789",
1632
+ },
769
1633
  },
770
1634
  "oid": "test/a:1",
771
1635
  "timestamp": "1",