@verdant-web/common 2.8.0 → 2.9.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.
@@ -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",