cojson 0.18.5 → 0.18.7
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +18 -0
- package/dist/coValueContentMessage.d.ts +2 -0
- package/dist/coValueContentMessage.d.ts.map +1 -1
- package/dist/coValueContentMessage.js +7 -0
- package/dist/coValueContentMessage.js.map +1 -1
- package/dist/coValueCore/SessionMap.d.ts +2 -2
- package/dist/coValueCore/SessionMap.d.ts.map +1 -1
- package/dist/coValueCore/SessionMap.js +2 -4
- package/dist/coValueCore/SessionMap.js.map +1 -1
- package/dist/coValueCore/branching.d.ts +35 -13
- package/dist/coValueCore/branching.d.ts.map +1 -1
- package/dist/coValueCore/branching.js +55 -37
- package/dist/coValueCore/branching.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +15 -7
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +126 -35
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValueCore/verifiedState.d.ts +4 -2
- package/dist/coValueCore/verifiedState.d.ts.map +1 -1
- package/dist/coValueCore/verifiedState.js +6 -4
- package/dist/coValueCore/verifiedState.js.map +1 -1
- package/dist/coValues/coList.d.ts +8 -2
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coList.js +95 -58
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/coMap.d.ts +2 -2
- package/dist/coValues/coMap.d.ts.map +1 -1
- package/dist/coValues/coMap.js +8 -8
- package/dist/coValues/coMap.js.map +1 -1
- package/dist/coValues/group.d.ts.map +1 -1
- package/dist/coValues/group.js +14 -1
- package/dist/coValues/group.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
- package/dist/crypto/PureJSCrypto.js +14 -6
- package/dist/crypto/PureJSCrypto.js.map +1 -1
- package/dist/exports.d.ts +5 -4
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +3 -3
- package/dist/exports.js.map +1 -1
- package/dist/ids.d.ts +1 -0
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js.map +1 -1
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +7 -2
- package/dist/localNode.js.map +1 -1
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +7 -4
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +7 -4
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/sync.d.ts +1 -3
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +8 -18
- package/dist/sync.js.map +1 -1
- package/dist/tests/branching.test.js +166 -4
- package/dist/tests/branching.test.js.map +1 -1
- package/dist/tests/coList.test.js +367 -1
- package/dist/tests/coList.test.js.map +1 -1
- package/dist/tests/coValueCore.test.js +45 -1
- package/dist/tests/coValueCore.test.js.map +1 -1
- package/dist/tests/messagesTestUtils.d.ts +1 -1
- package/dist/tests/sync.content.test.d.ts +2 -0
- package/dist/tests/sync.content.test.d.ts.map +1 -0
- package/dist/tests/sync.content.test.js +120 -0
- package/dist/tests/sync.content.test.js.map +1 -0
- package/dist/tests/sync.load.test.js +4 -4
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +1 -1
- package/dist/tests/sync.storageAsync.test.js +6 -10
- package/dist/tests/sync.storageAsync.test.js.map +1 -1
- package/dist/tests/sync.upload.test.js +2 -2
- package/dist/tests/testUtils.d.ts +2 -2
- package/package.json +2 -2
- package/src/coValueContentMessage.ts +13 -0
- package/src/coValueCore/SessionMap.ts +2 -2
- package/src/coValueCore/branching.ts +105 -60
- package/src/coValueCore/coValueCore.ts +163 -41
- package/src/coValueCore/verifiedState.ts +8 -0
- package/src/coValues/coList.ts +129 -78
- package/src/coValues/coMap.ts +10 -12
- package/src/coValues/group.ts +14 -1
- package/src/config.ts +9 -0
- package/src/crypto/PureJSCrypto.ts +25 -13
- package/src/exports.ts +13 -2
- package/src/ids.ts +1 -0
- package/src/localNode.ts +8 -2
- package/src/storage/storageAsync.ts +8 -5
- package/src/storage/storageSync.ts +8 -4
- package/src/sync.ts +8 -32
- package/src/tests/branching.test.ts +255 -4
- package/src/tests/coList.test.ts +529 -1
- package/src/tests/coValueCore.test.ts +62 -2
- package/src/tests/sync.content.test.ts +153 -0
- package/src/tests/sync.load.test.ts +4 -4
- package/src/tests/sync.storage.test.ts +1 -1
- package/src/tests/sync.storageAsync.test.ts +9 -12
- package/src/tests/sync.upload.test.ts +2 -2
package/src/tests/coList.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, expect, test } from "vitest";
|
|
1
|
+
import { beforeEach, describe, expect, test } from "vitest";
|
|
2
2
|
import { expectList } from "../coValue.js";
|
|
3
3
|
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
4
4
|
import { LocalNode } from "../localNode.js";
|
|
@@ -428,3 +428,531 @@ test("Should ignore unknown meta transactions", () => {
|
|
|
428
428
|
|
|
429
429
|
expect(content.toJSON()).toEqual(["first"]);
|
|
430
430
|
});
|
|
431
|
+
|
|
432
|
+
describe("CoList Branching", () => {
|
|
433
|
+
test("should handle concurrent appends from multiple branches", async () => {
|
|
434
|
+
const client1 = setupTestNode({
|
|
435
|
+
connected: true,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const group = client1.node.createGroup();
|
|
439
|
+
group.addMember("everyone", "writer");
|
|
440
|
+
|
|
441
|
+
const groceryList = group.createList(["milk"]);
|
|
442
|
+
|
|
443
|
+
// Create branches for each client
|
|
444
|
+
const aliceBranch = expectList(
|
|
445
|
+
groceryList.core
|
|
446
|
+
.createBranch("alice-branch", group.id)
|
|
447
|
+
.getCurrentContent(),
|
|
448
|
+
);
|
|
449
|
+
const bobBranch = expectList(
|
|
450
|
+
groceryList.core.createBranch("bob-branch", group.id).getCurrentContent(),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// Both branches append grocery items
|
|
454
|
+
aliceBranch.append("bread", undefined, "trusting");
|
|
455
|
+
aliceBranch.append("butter", undefined, "trusting");
|
|
456
|
+
bobBranch.append("eggs", undefined, "trusting");
|
|
457
|
+
|
|
458
|
+
// Merge both branches back to source
|
|
459
|
+
aliceBranch.core.mergeBranch();
|
|
460
|
+
bobBranch.core.mergeBranch();
|
|
461
|
+
|
|
462
|
+
// Wait for sync
|
|
463
|
+
await groceryList.core.waitForSync();
|
|
464
|
+
|
|
465
|
+
// Source list should contain all items from both branches
|
|
466
|
+
expect(groceryList.toJSON()).toMatchInlineSnapshot(`
|
|
467
|
+
[
|
|
468
|
+
"milk",
|
|
469
|
+
"eggs",
|
|
470
|
+
"bread",
|
|
471
|
+
"butter",
|
|
472
|
+
]
|
|
473
|
+
`);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("should handle concurrent prepends from multiple branches", async () => {
|
|
477
|
+
const client1 = setupTestNode({
|
|
478
|
+
connected: true,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const group = client1.node.createGroup();
|
|
482
|
+
group.addMember("everyone", "writer");
|
|
483
|
+
|
|
484
|
+
const groceryList = group.createList(["milk"]);
|
|
485
|
+
|
|
486
|
+
// Create branches for each client
|
|
487
|
+
const aliceBranch = expectList(
|
|
488
|
+
groceryList.core
|
|
489
|
+
.createBranch("alice-branch", group.id)
|
|
490
|
+
.getCurrentContent(),
|
|
491
|
+
);
|
|
492
|
+
const bobBranch = expectList(
|
|
493
|
+
groceryList.core.createBranch("bob-branch", group.id).getCurrentContent(),
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
// Both branches prepend grocery items
|
|
497
|
+
aliceBranch.prepend("bread", undefined, "trusting");
|
|
498
|
+
aliceBranch.prepend("butter", undefined, "trusting");
|
|
499
|
+
bobBranch.prepend("eggs", undefined, "trusting");
|
|
500
|
+
|
|
501
|
+
// Merge both branches back to source
|
|
502
|
+
aliceBranch.core.mergeBranch();
|
|
503
|
+
bobBranch.core.mergeBranch();
|
|
504
|
+
|
|
505
|
+
// Wait for sync
|
|
506
|
+
await groceryList.core.waitForSync();
|
|
507
|
+
|
|
508
|
+
// Source list should contain all items from both branches
|
|
509
|
+
expect(groceryList.toJSON()).toMatchInlineSnapshot(`
|
|
510
|
+
[
|
|
511
|
+
"eggs",
|
|
512
|
+
"butter",
|
|
513
|
+
"bread",
|
|
514
|
+
"milk",
|
|
515
|
+
]
|
|
516
|
+
`);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("should handle concurrent deletes from multiple branches", async () => {
|
|
520
|
+
const client1 = setupTestNode({
|
|
521
|
+
connected: true,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const group = client1.node.createGroup();
|
|
525
|
+
group.addMember("everyone", "writer");
|
|
526
|
+
|
|
527
|
+
const groceryList = group.createList(["milk", "bread", "eggs", "cheese"]);
|
|
528
|
+
|
|
529
|
+
// Create branches for each client
|
|
530
|
+
const aliceBranch = expectList(
|
|
531
|
+
groceryList.core
|
|
532
|
+
.createBranch("alice-branch", group.id)
|
|
533
|
+
.getCurrentContent(),
|
|
534
|
+
);
|
|
535
|
+
const bobBranch = expectList(
|
|
536
|
+
groceryList.core.createBranch("bob-branch", group.id).getCurrentContent(),
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
// Both branches delete different grocery items
|
|
540
|
+
aliceBranch.delete(aliceBranch.asArray().indexOf("bread"), "trusting");
|
|
541
|
+
bobBranch.delete(bobBranch.asArray().indexOf("eggs"), "trusting");
|
|
542
|
+
|
|
543
|
+
// Merge both branches back to source
|
|
544
|
+
aliceBranch.core.mergeBranch();
|
|
545
|
+
bobBranch.core.mergeBranch();
|
|
546
|
+
|
|
547
|
+
// Wait for sync
|
|
548
|
+
await groceryList.core.waitForSync();
|
|
549
|
+
|
|
550
|
+
// Source list should reflect both deletions
|
|
551
|
+
expect(groceryList.toJSON()).toEqual(["milk", "cheese"]);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("should handle concurrent insertions at different positions from branches", async () => {
|
|
555
|
+
const client1 = setupTestNode({
|
|
556
|
+
connected: true,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const group = client1.node.createGroup();
|
|
560
|
+
group.addMember("everyone", "writer");
|
|
561
|
+
|
|
562
|
+
const groceryList = group.createList(["milk", "cheese", "butter"]);
|
|
563
|
+
|
|
564
|
+
// Create branches for each client
|
|
565
|
+
const aliceBranch = expectList(
|
|
566
|
+
groceryList.core
|
|
567
|
+
.createBranch("alice-branch", group.id)
|
|
568
|
+
.getCurrentContent(),
|
|
569
|
+
);
|
|
570
|
+
const bobBranch = expectList(
|
|
571
|
+
groceryList.core.createBranch("bob-branch", group.id).getCurrentContent(),
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
// Both branches insert grocery items at position 1
|
|
575
|
+
aliceBranch.append("bread", 0, "trusting");
|
|
576
|
+
bobBranch.append("eggs", 0, "trusting");
|
|
577
|
+
|
|
578
|
+
// Merge both branches back to source
|
|
579
|
+
aliceBranch.core.mergeBranch();
|
|
580
|
+
bobBranch.core.mergeBranch();
|
|
581
|
+
|
|
582
|
+
// Wait for sync
|
|
583
|
+
await groceryList.core.waitForSync();
|
|
584
|
+
|
|
585
|
+
// Source list should contain both insertions
|
|
586
|
+
expect(groceryList.toJSON()).toMatchInlineSnapshot(`
|
|
587
|
+
[
|
|
588
|
+
"milk",
|
|
589
|
+
"eggs",
|
|
590
|
+
"bread",
|
|
591
|
+
"cheese",
|
|
592
|
+
"butter",
|
|
593
|
+
]
|
|
594
|
+
`);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test("should handle multiple branches modifying different list items", async () => {
|
|
598
|
+
const client1 = setupTestNode({
|
|
599
|
+
connected: true,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const group = client1.node.createGroup();
|
|
603
|
+
group.addMember("everyone", "writer");
|
|
604
|
+
|
|
605
|
+
const groceryList = group.createList(["milk", "bread", "eggs"]);
|
|
606
|
+
|
|
607
|
+
// Create branches for each client
|
|
608
|
+
const aliceBranch = expectList(
|
|
609
|
+
groceryList.core
|
|
610
|
+
.createBranch("alice-branch", group.id)
|
|
611
|
+
.getCurrentContent(),
|
|
612
|
+
);
|
|
613
|
+
const bobBranch = expectList(
|
|
614
|
+
groceryList.core.createBranch("bob-branch", group.id).getCurrentContent(),
|
|
615
|
+
);
|
|
616
|
+
const charlieBranch = expectList(
|
|
617
|
+
groceryList.core
|
|
618
|
+
.createBranch("charlie-branch", group.id)
|
|
619
|
+
.getCurrentContent(),
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// Each branch modifies different grocery items
|
|
623
|
+
aliceBranch.replace(0, "organic milk", "trusting");
|
|
624
|
+
bobBranch.replace(1, "whole wheat bread", "trusting");
|
|
625
|
+
charlieBranch.replace(2, "free-range eggs", "trusting");
|
|
626
|
+
|
|
627
|
+
// Merge all branches back to source
|
|
628
|
+
aliceBranch.core.mergeBranch();
|
|
629
|
+
bobBranch.core.mergeBranch();
|
|
630
|
+
charlieBranch.core.mergeBranch();
|
|
631
|
+
|
|
632
|
+
// Wait for sync
|
|
633
|
+
await groceryList.core.waitForSync();
|
|
634
|
+
|
|
635
|
+
// Source list should contain all modifications
|
|
636
|
+
const expected = ["organic milk", "whole wheat bread", "free-range eggs"];
|
|
637
|
+
expect(groceryList.toJSON()).toEqual(expected);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("should handle appendItems from multiple branches", async () => {
|
|
641
|
+
const client1 = setupTestNode({
|
|
642
|
+
connected: true,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const group = client1.node.createGroup();
|
|
646
|
+
group.addMember("everyone", "writer");
|
|
647
|
+
|
|
648
|
+
const groceryList = group.createList(["milk"]);
|
|
649
|
+
|
|
650
|
+
// Create branches for each client
|
|
651
|
+
const aliceBranch = expectList(
|
|
652
|
+
groceryList.core
|
|
653
|
+
.createBranch("alice-branch", group.id)
|
|
654
|
+
.getCurrentContent(),
|
|
655
|
+
);
|
|
656
|
+
const bobBranch = expectList(
|
|
657
|
+
groceryList.core.createBranch("bob-branch", group.id).getCurrentContent(),
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// Both branches append multiple grocery items
|
|
661
|
+
aliceBranch.appendItems(["bread", "butter"], undefined, "trusting");
|
|
662
|
+
bobBranch.appendItems(["eggs", "cheese"], undefined, "trusting");
|
|
663
|
+
|
|
664
|
+
// Merge both branches back to source
|
|
665
|
+
aliceBranch.core.mergeBranch();
|
|
666
|
+
bobBranch.core.mergeBranch();
|
|
667
|
+
|
|
668
|
+
// Wait for sync
|
|
669
|
+
await groceryList.core.waitForSync();
|
|
670
|
+
|
|
671
|
+
// Source list should contain all items
|
|
672
|
+
expect(groceryList.toJSON()).toMatchInlineSnapshot(`
|
|
673
|
+
[
|
|
674
|
+
"milk",
|
|
675
|
+
"eggs",
|
|
676
|
+
"cheese",
|
|
677
|
+
"bread",
|
|
678
|
+
"butter",
|
|
679
|
+
]
|
|
680
|
+
`);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("should handle complex operations from multiple branches", async () => {
|
|
684
|
+
const client1 = setupTestNode({
|
|
685
|
+
connected: true,
|
|
686
|
+
});
|
|
687
|
+
const client2 = setupTestNode({
|
|
688
|
+
connected: true,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const group = client1.node.createGroup();
|
|
692
|
+
group.addMember("everyone", "writer");
|
|
693
|
+
|
|
694
|
+
const groceryList = group.createList(["milk", "bread", "eggs"]);
|
|
695
|
+
|
|
696
|
+
// Create branches for each client
|
|
697
|
+
const aliceBranch = expectList(
|
|
698
|
+
groceryList.core
|
|
699
|
+
.createBranch("alice-branch", group.id)
|
|
700
|
+
.getCurrentContent(),
|
|
701
|
+
);
|
|
702
|
+
const bobBranch = expectList(
|
|
703
|
+
groceryList.core.createBranch("bob-branch", group.id).getCurrentContent(),
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
// Alice performs multiple operations
|
|
707
|
+
aliceBranch.delete(1, "trusting"); // Remove "bread"
|
|
708
|
+
aliceBranch.append("cheese", undefined, "trusting");
|
|
709
|
+
aliceBranch.prepend("yogurt", undefined, "trusting");
|
|
710
|
+
|
|
711
|
+
// Bob performs different operations
|
|
712
|
+
bobBranch.replace(0, "organic milk", "trusting");
|
|
713
|
+
bobBranch.append("butter", undefined, "trusting");
|
|
714
|
+
bobBranch.delete(2, "trusting"); // Remove "eggs"
|
|
715
|
+
|
|
716
|
+
// Merge both branches back to source
|
|
717
|
+
aliceBranch.core.mergeBranch();
|
|
718
|
+
bobBranch.core.mergeBranch();
|
|
719
|
+
|
|
720
|
+
// Wait for sync
|
|
721
|
+
await groceryList.core.waitForSync();
|
|
722
|
+
|
|
723
|
+
// Source list should contain the combined result
|
|
724
|
+
expect(groceryList.toJSON()).toMatchInlineSnapshot(`
|
|
725
|
+
[
|
|
726
|
+
"yogurt",
|
|
727
|
+
"organic milk",
|
|
728
|
+
"butter",
|
|
729
|
+
"cheese",
|
|
730
|
+
]
|
|
731
|
+
`);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test("should handle multiple branch merges with incremental changes", async () => {
|
|
735
|
+
const client1 = setupTestNode({
|
|
736
|
+
connected: true,
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const group = client1.node.createGroup();
|
|
740
|
+
group.addMember("everyone", "writer");
|
|
741
|
+
|
|
742
|
+
const groceryList = group.createList(["milk"]);
|
|
743
|
+
|
|
744
|
+
// Create branches for each client
|
|
745
|
+
const aliceBranch = expectList(
|
|
746
|
+
groceryList.core
|
|
747
|
+
.createBranch("alice-branch", group.id)
|
|
748
|
+
.getCurrentContent(),
|
|
749
|
+
);
|
|
750
|
+
const bobBranch = expectList(
|
|
751
|
+
groceryList.core.createBranch("bob-branch", group.id).getCurrentContent(),
|
|
752
|
+
);
|
|
753
|
+
const charlieBranch = expectList(
|
|
754
|
+
groceryList.core
|
|
755
|
+
.createBranch("charlie-branch", group.id)
|
|
756
|
+
.getCurrentContent(),
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
// Each branch adds grocery items
|
|
760
|
+
aliceBranch.append("bread", undefined, "trusting");
|
|
761
|
+
bobBranch.append("eggs", undefined, "trusting");
|
|
762
|
+
charlieBranch.append("cheese", undefined, "trusting");
|
|
763
|
+
|
|
764
|
+
// Merge branches one by one
|
|
765
|
+
aliceBranch.core.mergeBranch();
|
|
766
|
+
await groceryList.core.waitForSync();
|
|
767
|
+
expect(groceryList.toJSON()).toEqual(["milk", "bread"]);
|
|
768
|
+
|
|
769
|
+
bobBranch.core.mergeBranch();
|
|
770
|
+
await groceryList.core.waitForSync();
|
|
771
|
+
expect(groceryList.toJSON()).toEqual(["milk", "eggs", "bread"]);
|
|
772
|
+
|
|
773
|
+
charlieBranch.core.mergeBranch();
|
|
774
|
+
await groceryList.core.waitForSync();
|
|
775
|
+
expect(groceryList.toJSON()).toEqual(["milk", "cheese", "eggs", "bread"]);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test("should resolve conflicts when multiple branches modify the same position", async () => {
|
|
779
|
+
const client1 = setupTestNode({
|
|
780
|
+
connected: true,
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const group = client1.node.createGroup();
|
|
784
|
+
group.addMember("everyone", "writer");
|
|
785
|
+
|
|
786
|
+
const groceryList = group.createList(["milk", "bread", "eggs"]);
|
|
787
|
+
|
|
788
|
+
// Create branches for each client
|
|
789
|
+
const aliceBranch = expectList(
|
|
790
|
+
groceryList.core
|
|
791
|
+
.createBranch("alice-branch", group.id)
|
|
792
|
+
.getCurrentContent(),
|
|
793
|
+
);
|
|
794
|
+
const bobBranch = expectList(
|
|
795
|
+
groceryList.core.createBranch("bob-branch", group.id).getCurrentContent(),
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
// Both branches try to replace the same item at position 1
|
|
799
|
+
aliceBranch.replace(1, "whole wheat bread", "trusting");
|
|
800
|
+
bobBranch.replace(1, "sourdough bread", "trusting");
|
|
801
|
+
|
|
802
|
+
// Merge both branches back to source
|
|
803
|
+
aliceBranch.core.mergeBranch();
|
|
804
|
+
bobBranch.core.mergeBranch();
|
|
805
|
+
|
|
806
|
+
// Wait for sync
|
|
807
|
+
await groceryList.core.waitForSync();
|
|
808
|
+
|
|
809
|
+
// Source list should have a consistent state after conflict resolution
|
|
810
|
+
expect(groceryList.toJSON()).toContain("whole wheat bread");
|
|
811
|
+
expect(groceryList.toJSON()).toContain("sourdough bread");
|
|
812
|
+
expect(groceryList.toJSON()).toMatchInlineSnapshot(`
|
|
813
|
+
[
|
|
814
|
+
"milk",
|
|
815
|
+
"sourdough bread",
|
|
816
|
+
"whole wheat bread",
|
|
817
|
+
"eggs",
|
|
818
|
+
]
|
|
819
|
+
`);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test("should handle concurrent deletions at overlapping positions across branches", async () => {
|
|
823
|
+
const client1 = setupTestNode({
|
|
824
|
+
connected: true,
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
const group = client1.node.createGroup();
|
|
828
|
+
group.addMember("everyone", "writer");
|
|
829
|
+
|
|
830
|
+
const list = group.createList(["milk", "bread", "eggs"]);
|
|
831
|
+
|
|
832
|
+
// Create branches for each client
|
|
833
|
+
const mondayShoppingBranch = expectList(
|
|
834
|
+
list.core.createBranch("monday-shopping", group.id).getCurrentContent(),
|
|
835
|
+
);
|
|
836
|
+
const tuesdayShoppingBranch = expectList(
|
|
837
|
+
list.core.createBranch("tuesday-shopping", group.id).getCurrentContent(),
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
mondayShoppingBranch.delete(list.asArray().indexOf("milk"), "trusting");
|
|
841
|
+
tuesdayShoppingBranch.delete(list.asArray().indexOf("eggs"), "trusting");
|
|
842
|
+
|
|
843
|
+
// Merge both branches back to source
|
|
844
|
+
mondayShoppingBranch.core.mergeBranch();
|
|
845
|
+
tuesdayShoppingBranch.core.mergeBranch();
|
|
846
|
+
|
|
847
|
+
// Source list should reflect all deletions
|
|
848
|
+
expect(list.toJSON()).toEqual(["bread"]);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
test("should handle branching from different sessions and merging back", async () => {
|
|
852
|
+
const client1 = setupTestNode({
|
|
853
|
+
connected: true,
|
|
854
|
+
});
|
|
855
|
+
const client2 = setupTestNode({
|
|
856
|
+
connected: true,
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
const group = client1.node.createGroup();
|
|
860
|
+
group.addMember("everyone", "writer");
|
|
861
|
+
|
|
862
|
+
const groceryList = group.createList(["milk", "bread"]);
|
|
863
|
+
|
|
864
|
+
// Client1 creates a branch
|
|
865
|
+
const aliceBranch = expectList(
|
|
866
|
+
groceryList.core
|
|
867
|
+
.createBranch("alice-shopping-branch", group.id)
|
|
868
|
+
.getCurrentContent(),
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
// Client1 adds items to the branch
|
|
872
|
+
aliceBranch.append("eggs", undefined, "trusting");
|
|
873
|
+
|
|
874
|
+
// Client2 loads the branch from a different session
|
|
875
|
+
const branchOnClient2 = await loadCoValueOrFail(
|
|
876
|
+
client2.node,
|
|
877
|
+
aliceBranch.id,
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
// Client2 adds more items and removes some existing ones
|
|
881
|
+
branchOnClient2.append("cheese", undefined, "trusting");
|
|
882
|
+
branchOnClient2.delete(
|
|
883
|
+
branchOnClient2.asArray().indexOf("bread"),
|
|
884
|
+
"trusting",
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
// Merge the branch back to source
|
|
888
|
+
branchOnClient2.core.mergeBranch();
|
|
889
|
+
|
|
890
|
+
// Wait for sync
|
|
891
|
+
await groceryList.core.waitForSync();
|
|
892
|
+
|
|
893
|
+
// Source list should contain the final state
|
|
894
|
+
expect(groceryList.toJSON()).toEqual(["milk", "eggs", "cheese"]);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
test("should handle multiple branches from different sessions with complex operations", async () => {
|
|
898
|
+
const client1 = setupTestNode({
|
|
899
|
+
connected: true,
|
|
900
|
+
});
|
|
901
|
+
const client2 = setupTestNode({
|
|
902
|
+
connected: true,
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
const group = client1.node.createGroup();
|
|
906
|
+
group.addMember("everyone", "writer");
|
|
907
|
+
|
|
908
|
+
const groceryList = group.createList(["milk"]);
|
|
909
|
+
|
|
910
|
+
// Client1 creates first branch
|
|
911
|
+
const aliceBranch = expectList(
|
|
912
|
+
groceryList.core
|
|
913
|
+
.createBranch("alice-shopping-branch", group.id)
|
|
914
|
+
.getCurrentContent(),
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
// Client1 adds items to first branch
|
|
918
|
+
aliceBranch.append("bread", undefined, "trusting");
|
|
919
|
+
|
|
920
|
+
// Client2 creates second branch
|
|
921
|
+
const bobBranch = expectList(
|
|
922
|
+
groceryList.core
|
|
923
|
+
.createBranch("bob-shopping-branch", group.id)
|
|
924
|
+
.getCurrentContent(),
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
// Client2 adds different items to second branch
|
|
928
|
+
bobBranch.append("eggs", undefined, "trusting");
|
|
929
|
+
|
|
930
|
+
// Client2 loads first branch and modifies it
|
|
931
|
+
const aliceBranchOnClient2 = await loadCoValueOrFail(
|
|
932
|
+
client2.node,
|
|
933
|
+
aliceBranch.id,
|
|
934
|
+
);
|
|
935
|
+
aliceBranchOnClient2.append("butter", undefined, "trusting");
|
|
936
|
+
|
|
937
|
+
// Merge all branches back to source
|
|
938
|
+
aliceBranchOnClient2.core.mergeBranch();
|
|
939
|
+
|
|
940
|
+
// Make the second merge happen later than the previous one, so the ordering is not based on the random sessionIDs
|
|
941
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
942
|
+
|
|
943
|
+
bobBranch.core.mergeBranch();
|
|
944
|
+
|
|
945
|
+
// Wait for sync
|
|
946
|
+
await groceryList.core.waitForSync();
|
|
947
|
+
|
|
948
|
+
// Source list should contain all changes
|
|
949
|
+
expect(groceryList.toJSON()).toMatchInlineSnapshot(`
|
|
950
|
+
[
|
|
951
|
+
"milk",
|
|
952
|
+
"eggs",
|
|
953
|
+
"bread",
|
|
954
|
+
"butter",
|
|
955
|
+
]
|
|
956
|
+
`);
|
|
957
|
+
});
|
|
958
|
+
});
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
vi,
|
|
9
9
|
} from "vitest";
|
|
10
10
|
import { CoValueCore } from "../coValueCore/coValueCore.js";
|
|
11
|
-
import { Transaction } from "../coValueCore/verifiedState.js";
|
|
12
11
|
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
13
12
|
import { stableStringify } from "../jsonStringify.js";
|
|
14
13
|
import { LocalNode } from "../localNode.js";
|
|
@@ -24,7 +23,7 @@ import {
|
|
|
24
23
|
tearDownTestMetricReader,
|
|
25
24
|
} from "./testUtils.js";
|
|
26
25
|
import { CO_VALUE_PRIORITY } from "../priority.js";
|
|
27
|
-
import {
|
|
26
|
+
import { setMaxTxSizeBytes } from "../config.js";
|
|
28
27
|
|
|
29
28
|
const Crypto = await WasmCrypto.create();
|
|
30
29
|
|
|
@@ -57,6 +56,7 @@ test("transactions with wrong signature are rejected", () => {
|
|
|
57
56
|
node.getCurrentAgent(),
|
|
58
57
|
[{ hello: "world" }],
|
|
59
58
|
undefined,
|
|
59
|
+
Date.now(),
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
transaction.madeAt = Date.now() + 1000;
|
|
@@ -87,6 +87,66 @@ test("transactions with wrong signature are rejected", () => {
|
|
|
87
87
|
expect(newEntry.getValidSortedTransactions().length).toBe(0);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
describe("transactions that exceed the byte size limit are rejected", () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
setMaxTxSizeBytes(1 * 1024);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
setMaxTxSizeBytes(1 * 1024 * 1024);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("makeTransaction should throw error when transaction exceeds byte size limit", () => {
|
|
100
|
+
const [agent, sessionID] = randomAgentAndSessionID();
|
|
101
|
+
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|
|
102
|
+
|
|
103
|
+
const coValue = node.createCoValue({
|
|
104
|
+
type: "costream",
|
|
105
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
106
|
+
meta: null,
|
|
107
|
+
...Crypto.createdNowUnique(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const largeBinaryData = "x".repeat(1024 + 100);
|
|
111
|
+
|
|
112
|
+
expect(() => {
|
|
113
|
+
coValue.makeTransaction(
|
|
114
|
+
[
|
|
115
|
+
{
|
|
116
|
+
data: largeBinaryData,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
"trusting",
|
|
120
|
+
);
|
|
121
|
+
}).toThrow(/Transaction is too large to be synced/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("makeTransaction should work for transactions under byte size limit", () => {
|
|
125
|
+
const [agent, sessionID] = randomAgentAndSessionID();
|
|
126
|
+
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|
|
127
|
+
|
|
128
|
+
const coValue = node.createCoValue({
|
|
129
|
+
type: "costream",
|
|
130
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
131
|
+
meta: null,
|
|
132
|
+
...Crypto.createdNowUnique(),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const smallData = "Hello, world!";
|
|
136
|
+
|
|
137
|
+
const success = coValue.makeTransaction(
|
|
138
|
+
[
|
|
139
|
+
{
|
|
140
|
+
data: smallData,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
"trusting",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(success).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
90
150
|
test("New transactions in a group correctly update owned values, including subscriptions", async () => {
|
|
91
151
|
const [agent, sessionID] = randomAgentAndSessionID();
|
|
92
152
|
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|