cojson 0.19.20 → 0.19.21
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/dist/coValueCore/coValueCore.d.ts +9 -0
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +21 -0
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValues/account.d.ts.map +1 -1
- package/dist/coValues/account.js +10 -10
- package/dist/coValues/account.js.map +1 -1
- package/dist/ids.d.ts +1 -1
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js.map +1 -1
- package/dist/knownState.d.ts +5 -0
- package/dist/knownState.d.ts.map +1 -1
- package/dist/knownState.js +15 -0
- package/dist/knownState.js.map +1 -1
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +8 -2
- package/dist/localNode.js.map +1 -1
- package/dist/storage/knownState.d.ts +5 -0
- package/dist/storage/knownState.d.ts.map +1 -1
- package/dist/storage/knownState.js +11 -0
- package/dist/storage/knownState.js.map +1 -1
- package/dist/storage/sqlite/client.d.ts +2 -0
- package/dist/storage/sqlite/client.d.ts.map +1 -1
- package/dist/storage/sqlite/client.js +18 -0
- package/dist/storage/sqlite/client.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +2 -0
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +20 -0
- package/dist/storage/sqliteAsync/client.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +2 -0
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +40 -0
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +1 -0
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +15 -0
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +18 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +17 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +111 -41
- package/dist/sync.js.map +1 -1
- package/dist/tests/StorageApiAsync.test.js +91 -0
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/StorageApiSync.test.js +91 -0
- package/dist/tests/StorageApiSync.test.js.map +1 -1
- package/dist/tests/coValueCore.loadFromStorage.test.js +1 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.d.ts +2 -0
- package/dist/tests/knownState.lazyLoading.test.d.ts.map +1 -0
- package/dist/tests/knownState.lazyLoading.test.js +166 -0
- package/dist/tests/knownState.lazyLoading.test.js.map +1 -0
- package/dist/tests/messagesTestUtils.d.ts +5 -2
- package/dist/tests/messagesTestUtils.d.ts.map +1 -1
- package/dist/tests/messagesTestUtils.js +4 -0
- package/dist/tests/messagesTestUtils.js.map +1 -1
- package/dist/tests/sync.load.test.js +388 -0
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +4 -4
- package/dist/tests/testStorage.js +36 -0
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +14 -2
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/coValueCore/coValueCore.ts +26 -0
- package/src/coValues/account.ts +12 -14
- package/src/ids.ts +1 -1
- package/src/knownState.ts +24 -0
- package/src/localNode.ts +9 -2
- package/src/storage/knownState.ts +12 -0
- package/src/storage/sqlite/client.ts +31 -0
- package/src/storage/sqliteAsync/client.ts +35 -0
- package/src/storage/storageAsync.ts +51 -0
- package/src/storage/storageSync.ts +22 -0
- package/src/storage/types.ts +26 -0
- package/src/sync.ts +126 -42
- package/src/tests/StorageApiAsync.test.ts +136 -0
- package/src/tests/StorageApiSync.test.ts +132 -0
- package/src/tests/coValueCore.loadFromStorage.test.ts +3 -0
- package/src/tests/knownState.lazyLoading.test.ts +217 -0
- package/src/tests/messagesTestUtils.ts +10 -3
- package/src/tests/sync.load.test.ts +483 -1
- package/src/tests/sync.mesh.test.ts +4 -4
- package/src/tests/testStorage.ts +38 -0
- package/src/tests/testUtils.ts +14 -2
package/src/sync.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
CoValueKnownState,
|
|
26
26
|
knownStateFrom,
|
|
27
27
|
KnownStateSessions,
|
|
28
|
+
peerHasAllContent,
|
|
28
29
|
} from "./knownState.js";
|
|
29
30
|
import { StorageAPI } from "./storage/index.js";
|
|
30
31
|
|
|
@@ -583,33 +584,125 @@ export class SyncManager {
|
|
|
583
584
|
peer.setKnownState(msg.id, knownStateFrom(msg));
|
|
584
585
|
const coValue = this.local.getCoValue(msg.id);
|
|
585
586
|
|
|
587
|
+
// Fast path: CoValue is already in memory
|
|
586
588
|
if (coValue.isAvailable()) {
|
|
587
589
|
this.sendNewContent(msg.id, peer);
|
|
588
590
|
return;
|
|
589
591
|
}
|
|
590
592
|
|
|
591
|
-
const
|
|
593
|
+
const peerKnownState = peer.getOptimisticKnownState(msg.id);
|
|
592
594
|
|
|
593
|
-
|
|
595
|
+
// Fast path: Peer has no content at all - skip lazy load check, just load directly
|
|
596
|
+
if (!peerKnownState?.header) {
|
|
597
|
+
this.loadFromStorageAndRespond(msg.id, peer, coValue);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Check storage knownState before doing full load (lazy load optimization)
|
|
602
|
+
coValue.getKnownStateFromStorage((storageKnownState) => {
|
|
603
|
+
// Race condition: CoValue might have been loaded while we were waiting for storage
|
|
604
|
+
if (coValue.isAvailable()) {
|
|
605
|
+
this.sendNewContent(msg.id, peer);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (!storageKnownState) {
|
|
610
|
+
// Not in storage, try loading from peers
|
|
611
|
+
this.loadFromPeersAndRespond(msg.id, peer, coValue);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Check if peer already has all content
|
|
616
|
+
if (peerHasAllContent(storageKnownState, peerKnownState)) {
|
|
617
|
+
// Peer already has everything - reply with known message, no full load needed
|
|
618
|
+
peer.trackToldKnownState(msg.id);
|
|
619
|
+
this.trySendToPeer(peer, {
|
|
620
|
+
action: "known",
|
|
621
|
+
...storageKnownState,
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Peer needs content - do full load from storage
|
|
627
|
+
this.loadFromStorageAndRespond(msg.id, peer, coValue);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Helper to load from storage and respond appropriately.
|
|
633
|
+
* Falls back to peers if not found in storage.
|
|
634
|
+
*/
|
|
635
|
+
private loadFromStorageAndRespond(
|
|
636
|
+
id: RawCoID,
|
|
637
|
+
peer: PeerState,
|
|
638
|
+
coValue: CoValueCore,
|
|
639
|
+
) {
|
|
640
|
+
coValue.loadFromStorage((found) => {
|
|
641
|
+
if (found && coValue.isAvailable()) {
|
|
642
|
+
this.sendNewContent(id, peer);
|
|
643
|
+
} else {
|
|
644
|
+
this.loadFromPeersAndRespond(id, peer, coValue);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Helper to load from peers and respond appropriately.
|
|
651
|
+
*/
|
|
652
|
+
private loadFromPeersAndRespond(
|
|
653
|
+
id: RawCoID,
|
|
654
|
+
peer: PeerState,
|
|
655
|
+
coValue: CoValueCore,
|
|
656
|
+
) {
|
|
657
|
+
const peers = this.getServerPeers(id, peer.id);
|
|
658
|
+
coValue.loadFromPeers(peers);
|
|
594
659
|
|
|
595
660
|
const handleLoadResult = () => {
|
|
596
661
|
if (coValue.isAvailable()) {
|
|
662
|
+
this.sendNewContent(id, peer);
|
|
597
663
|
return;
|
|
598
664
|
}
|
|
665
|
+
this.handleLoadNotFound(id, peer);
|
|
666
|
+
};
|
|
599
667
|
|
|
600
|
-
|
|
668
|
+
if (peers.length > 0) {
|
|
669
|
+
coValue.waitForAvailableOrUnavailable().then(handleLoadResult);
|
|
670
|
+
} else {
|
|
671
|
+
handleLoadResult();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Handle case when CoValue is not found.
|
|
677
|
+
*/
|
|
678
|
+
private handleLoadNotFound(id: RawCoID, peer: PeerState) {
|
|
679
|
+
peer.trackToldKnownState(id);
|
|
680
|
+
this.trySendToPeer(peer, {
|
|
681
|
+
action: "known",
|
|
682
|
+
id,
|
|
683
|
+
header: false,
|
|
684
|
+
sessions: {},
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Request full content from a peer when we don't have the CoValue.
|
|
690
|
+
*/
|
|
691
|
+
private requestFullContent(id: RawCoID, peer: PeerState | undefined) {
|
|
692
|
+
if (peer) {
|
|
601
693
|
this.trySendToPeer(peer, {
|
|
602
694
|
action: "known",
|
|
603
|
-
|
|
695
|
+
isCorrection: true,
|
|
696
|
+
id,
|
|
604
697
|
header: false,
|
|
605
698
|
sessions: {},
|
|
606
699
|
});
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
if (peers.length > 0 || this.local.storage) {
|
|
610
|
-
coValue.waitForAvailableOrUnavailable().then(handleLoadResult);
|
|
611
700
|
} else {
|
|
612
|
-
|
|
701
|
+
// The wrong assumption has been made by storage or import, we don't have a recovery mechanism
|
|
702
|
+
// Should never happen
|
|
703
|
+
logger.error("Received new content with no header on a missing CoValue", {
|
|
704
|
+
id,
|
|
705
|
+
});
|
|
613
706
|
}
|
|
614
707
|
}
|
|
615
708
|
|
|
@@ -694,46 +787,37 @@ export class SyncManager {
|
|
|
694
787
|
*/
|
|
695
788
|
if (!coValue.hasVerifiedContent()) {
|
|
696
789
|
/**
|
|
697
|
-
* The peer has assumed we already have the CoValue
|
|
790
|
+
* The peer/import has assumed we already have the CoValue
|
|
698
791
|
*/
|
|
699
792
|
if (!msg.header) {
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
logger.error("Known CoValue not found in storage", {
|
|
711
|
-
id: msg.id,
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
});
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// The peer assumption is not correct, so we ask for the full CoValue
|
|
719
|
-
if (peer) {
|
|
720
|
-
this.trySendToPeer(peer, {
|
|
721
|
-
action: "known",
|
|
722
|
-
isCorrection: true,
|
|
723
|
-
id: msg.id,
|
|
724
|
-
header: false,
|
|
725
|
-
sessions: {},
|
|
726
|
-
});
|
|
727
|
-
} else {
|
|
728
|
-
// The wrong assumption has been made by storage or import, we don't have a recovery mechanism
|
|
729
|
-
// Should never happen
|
|
730
|
-
logger.error(
|
|
731
|
-
"Received new content with no header on a missing CoValue",
|
|
793
|
+
// Content from storage without header - this can happen if:
|
|
794
|
+
// 1. Storage is streaming a large CoValue in chunks
|
|
795
|
+
// 2. Server is under heavy load, so a chunk isn't processed for a long time
|
|
796
|
+
// 3. GC cleanup unmounts the CoValue while streaming is in progress
|
|
797
|
+
// 4. The chunk is finally processed, but the CoValue is no longer available
|
|
798
|
+
// TODO: Fix this by either not unmounting CoValues with active streaming,
|
|
799
|
+
// or by cleaning up the streaming queue on unmount
|
|
800
|
+
if (from === "storage") {
|
|
801
|
+
logger.warn(
|
|
802
|
+
"Received content from storage without header - CoValue may have been garbage collected mid-stream",
|
|
732
803
|
{
|
|
733
804
|
id: msg.id,
|
|
805
|
+
from,
|
|
734
806
|
},
|
|
735
807
|
);
|
|
808
|
+
return;
|
|
736
809
|
}
|
|
810
|
+
|
|
811
|
+
// Try to load from storage - the CoValue might have been garbage collected from memory
|
|
812
|
+
coValue.loadFromStorage((found) => {
|
|
813
|
+
if (found) {
|
|
814
|
+
// CoValue was in storage, process the new content
|
|
815
|
+
this.handleNewContent(msg, from);
|
|
816
|
+
} else {
|
|
817
|
+
// CoValue not in storage, ask peer for full content
|
|
818
|
+
this.requestFullContent(msg.id, peer);
|
|
819
|
+
}
|
|
820
|
+
});
|
|
737
821
|
return;
|
|
738
822
|
}
|
|
739
823
|
|
|
@@ -823,4 +823,140 @@ describe("StorageApiAsync", () => {
|
|
|
823
823
|
expect(() => storage.close()).not.toThrow();
|
|
824
824
|
});
|
|
825
825
|
});
|
|
826
|
+
|
|
827
|
+
describe("loadKnownState", () => {
|
|
828
|
+
test("should return cached knownState if available", async () => {
|
|
829
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
830
|
+
const { storage } = await createTestNode(dbPath);
|
|
831
|
+
|
|
832
|
+
// Create a group to have data in the database
|
|
833
|
+
const group = fixturesNode.createGroup();
|
|
834
|
+
group.addMember("everyone", "reader");
|
|
835
|
+
await group.core.waitForSync();
|
|
836
|
+
|
|
837
|
+
// First call should hit the database and cache the result
|
|
838
|
+
const result1 = await new Promise<CoValueKnownState | undefined>(
|
|
839
|
+
(resolve) => {
|
|
840
|
+
storage.loadKnownState(group.id, resolve);
|
|
841
|
+
},
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
expect(result1).toBeDefined();
|
|
845
|
+
expect(result1?.id).toBe(group.id);
|
|
846
|
+
expect(result1?.header).toBe(true);
|
|
847
|
+
|
|
848
|
+
// Second call should return from cache
|
|
849
|
+
const result2 = await new Promise<CoValueKnownState | undefined>(
|
|
850
|
+
(resolve) => {
|
|
851
|
+
storage.loadKnownState(group.id, resolve);
|
|
852
|
+
},
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
expect(result2).toEqual(result1);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("should return undefined for non-existent CoValue", async () => {
|
|
859
|
+
const { storage } = await createTestNode();
|
|
860
|
+
|
|
861
|
+
const result = await new Promise<CoValueKnownState | undefined>(
|
|
862
|
+
(resolve) => {
|
|
863
|
+
storage.loadKnownState("co_nonexistent" as any, resolve);
|
|
864
|
+
},
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
expect(result).toBeUndefined();
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test("should deduplicate concurrent requests for the same ID", async () => {
|
|
871
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
872
|
+
const { storage } = await createTestNode(dbPath);
|
|
873
|
+
|
|
874
|
+
// Create a group to have data in the database
|
|
875
|
+
const group = fixturesNode.createGroup();
|
|
876
|
+
group.addMember("everyone", "reader");
|
|
877
|
+
await group.core.waitForSync();
|
|
878
|
+
|
|
879
|
+
// Clear the cache to force database access
|
|
880
|
+
storage.knownStates.knownStates.clear();
|
|
881
|
+
|
|
882
|
+
// Spy on the database client to track how many times it's called
|
|
883
|
+
const dbClientSpy = vi.spyOn(
|
|
884
|
+
(storage as any).dbClient,
|
|
885
|
+
"getCoValueKnownState",
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
// Make multiple concurrent requests for the same ID
|
|
889
|
+
const promises = [
|
|
890
|
+
new Promise<CoValueKnownState | undefined>((resolve) => {
|
|
891
|
+
storage.loadKnownState(group.id, resolve);
|
|
892
|
+
}),
|
|
893
|
+
new Promise<CoValueKnownState | undefined>((resolve) => {
|
|
894
|
+
storage.loadKnownState(group.id, resolve);
|
|
895
|
+
}),
|
|
896
|
+
new Promise<CoValueKnownState | undefined>((resolve) => {
|
|
897
|
+
storage.loadKnownState(group.id, resolve);
|
|
898
|
+
}),
|
|
899
|
+
];
|
|
900
|
+
|
|
901
|
+
const results = await Promise.all(promises);
|
|
902
|
+
|
|
903
|
+
// All results should be the same
|
|
904
|
+
expect(results[0]).toEqual(results[1]);
|
|
905
|
+
expect(results[1]).toEqual(results[2]);
|
|
906
|
+
expect(results[0]?.id).toBe(group.id);
|
|
907
|
+
|
|
908
|
+
// Database should only be called once due to deduplication
|
|
909
|
+
expect(dbClientSpy).toHaveBeenCalledTimes(1);
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
test("should use cache and not query database when cache is populated", async () => {
|
|
913
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
914
|
+
const { storage } = await createTestNode(dbPath);
|
|
915
|
+
|
|
916
|
+
// Create a group to have data in the database
|
|
917
|
+
const group = fixturesNode.createGroup();
|
|
918
|
+
group.addMember("everyone", "reader");
|
|
919
|
+
await group.core.waitForSync();
|
|
920
|
+
|
|
921
|
+
// Spy on the database client to track calls
|
|
922
|
+
const dbClientSpy = vi.spyOn(
|
|
923
|
+
(storage as any).dbClient,
|
|
924
|
+
"getCoValueKnownState",
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
// First call - should hit the database
|
|
928
|
+
const result1 = await new Promise<CoValueKnownState | undefined>(
|
|
929
|
+
(resolve) => {
|
|
930
|
+
storage.loadKnownState(group.id, resolve);
|
|
931
|
+
},
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
expect(result1).toBeDefined();
|
|
935
|
+
expect(dbClientSpy).toHaveBeenCalledTimes(1);
|
|
936
|
+
|
|
937
|
+
// Clear the spy to reset call count
|
|
938
|
+
dbClientSpy.mockClear();
|
|
939
|
+
|
|
940
|
+
// Second call - should use cache, not database
|
|
941
|
+
const result2 = await new Promise<CoValueKnownState | undefined>(
|
|
942
|
+
(resolve) => {
|
|
943
|
+
storage.loadKnownState(group.id, resolve);
|
|
944
|
+
},
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
expect(result2).toEqual(result1);
|
|
948
|
+
// Database should NOT be called since cache was hit
|
|
949
|
+
expect(dbClientSpy).toHaveBeenCalledTimes(0);
|
|
950
|
+
|
|
951
|
+
// Third call - also from cache
|
|
952
|
+
const result3 = await new Promise<CoValueKnownState | undefined>(
|
|
953
|
+
(resolve) => {
|
|
954
|
+
storage.loadKnownState(group.id, resolve);
|
|
955
|
+
},
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
expect(result3).toEqual(result1);
|
|
959
|
+
expect(dbClientSpy).toHaveBeenCalledTimes(0);
|
|
960
|
+
});
|
|
961
|
+
});
|
|
826
962
|
});
|
|
@@ -619,4 +619,136 @@ describe("StorageApiSync", () => {
|
|
|
619
619
|
expect(() => storage.close()).not.toThrow();
|
|
620
620
|
});
|
|
621
621
|
});
|
|
622
|
+
|
|
623
|
+
describe("loadKnownState", () => {
|
|
624
|
+
test("should return correct knownState structure for existing CoValue", async () => {
|
|
625
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
626
|
+
const { storage } = await createTestNode(dbPath);
|
|
627
|
+
|
|
628
|
+
// Create a group to have data in the database
|
|
629
|
+
const group = fixturesNode.createGroup();
|
|
630
|
+
group.addMember("everyone", "reader");
|
|
631
|
+
await group.core.waitForSync();
|
|
632
|
+
|
|
633
|
+
const result = await new Promise<CoValueKnownState | undefined>(
|
|
634
|
+
(resolve) => {
|
|
635
|
+
storage.loadKnownState(group.id, resolve);
|
|
636
|
+
},
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
expect(result).toBeDefined();
|
|
640
|
+
expect(result?.id).toBe(group.id);
|
|
641
|
+
expect(result?.header).toBe(true);
|
|
642
|
+
expect(result?.sessions).toEqual(group.core.knownState().sessions);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("should return undefined for non-existent CoValue", async () => {
|
|
646
|
+
const { storage } = await createTestNode();
|
|
647
|
+
|
|
648
|
+
const result = await new Promise<CoValueKnownState | undefined>(
|
|
649
|
+
(resolve) => {
|
|
650
|
+
storage.loadKnownState("co_nonexistent" as any, resolve);
|
|
651
|
+
},
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
expect(result).toBeUndefined();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("should handle CoValue with no sessions (header only)", async () => {
|
|
658
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
659
|
+
const { storage } = await createTestNode(dbPath);
|
|
660
|
+
|
|
661
|
+
// Create a CoValue with just a header (no transactions yet)
|
|
662
|
+
const coValue = fixturesNode.createCoValue({
|
|
663
|
+
type: "comap",
|
|
664
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
665
|
+
meta: null,
|
|
666
|
+
...crypto.createdNowUnique(),
|
|
667
|
+
});
|
|
668
|
+
await coValue.waitForSync();
|
|
669
|
+
|
|
670
|
+
const result = await new Promise<CoValueKnownState | undefined>(
|
|
671
|
+
(resolve) => {
|
|
672
|
+
storage.loadKnownState(coValue.id, resolve);
|
|
673
|
+
},
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
expect(result).toBeDefined();
|
|
677
|
+
expect(result?.id).toBe(coValue.id);
|
|
678
|
+
expect(result?.header).toBe(true);
|
|
679
|
+
// The sessions should have one entry with lastIdx = 0 (just header)
|
|
680
|
+
expect(Object.keys(result?.sessions || {}).length).toBe(0);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("should handle CoValue with multiple sessions", async () => {
|
|
684
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
685
|
+
const { fixturesNode: fixturesNode2 } = await createFixturesNode(dbPath);
|
|
686
|
+
const { storage } = await createTestNode(dbPath);
|
|
687
|
+
|
|
688
|
+
// Create a CoValue and have two nodes make transactions
|
|
689
|
+
const coValue = fixturesNode.createCoValue({
|
|
690
|
+
type: "comap",
|
|
691
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
692
|
+
meta: null,
|
|
693
|
+
...crypto.createdNowUnique(),
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
coValue.makeTransaction([{ key1: "value1" }], "trusting");
|
|
697
|
+
await coValue.waitForSync();
|
|
698
|
+
|
|
699
|
+
const coValueOnNode2 = await loadCoValueOrFail(
|
|
700
|
+
fixturesNode2,
|
|
701
|
+
coValue.id as CoID<RawCoMap>,
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
coValueOnNode2.set("key2", "value2", "trusting");
|
|
705
|
+
await coValueOnNode2.core.waitForSync();
|
|
706
|
+
|
|
707
|
+
const result = await new Promise<CoValueKnownState | undefined>(
|
|
708
|
+
(resolve) => {
|
|
709
|
+
storage.loadKnownState(coValue.id, resolve);
|
|
710
|
+
},
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
expect(result).toBeDefined();
|
|
714
|
+
expect(result?.id).toBe(coValue.id);
|
|
715
|
+
expect(result?.header).toBe(true);
|
|
716
|
+
// Should have two sessions
|
|
717
|
+
expect(Object.keys(result?.sessions || {}).length).toBe(2);
|
|
718
|
+
// Verify sessions match the expected state
|
|
719
|
+
expect(result?.sessions).toEqual(
|
|
720
|
+
coValueOnNode2.core.knownState().sessions,
|
|
721
|
+
);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("should use cache when knownState is cached", async () => {
|
|
725
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
726
|
+
const { storage } = await createTestNode(dbPath);
|
|
727
|
+
|
|
728
|
+
// Create a group to have data in the database
|
|
729
|
+
const group = fixturesNode.createGroup();
|
|
730
|
+
group.addMember("everyone", "reader");
|
|
731
|
+
await group.core.waitForSync();
|
|
732
|
+
|
|
733
|
+
// First call should hit the database and cache the result
|
|
734
|
+
const result1 = await new Promise<CoValueKnownState | undefined>(
|
|
735
|
+
(resolve) => {
|
|
736
|
+
storage.loadKnownState(group.id, resolve);
|
|
737
|
+
},
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
expect(result1).toBeDefined();
|
|
741
|
+
expect(result1?.id).toBe(group.id);
|
|
742
|
+
expect(result1?.header).toBe(true);
|
|
743
|
+
|
|
744
|
+
// Second call should return from cache
|
|
745
|
+
const result2 = await new Promise<CoValueKnownState | undefined>(
|
|
746
|
+
(resolve) => {
|
|
747
|
+
storage.loadKnownState(group.id, resolve);
|
|
748
|
+
},
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
expect(result2).toEqual(result1);
|
|
752
|
+
});
|
|
753
|
+
});
|
|
622
754
|
});
|
|
@@ -36,6 +36,7 @@ function createMockStorage(
|
|
|
36
36
|
) => void;
|
|
37
37
|
store?: (data: any, correctionCallback: any) => void;
|
|
38
38
|
getKnownState?: (id: RawCoID) => any;
|
|
39
|
+
loadKnownState?: (id: string, callback: (knownState: any) => void) => void;
|
|
39
40
|
waitForSync?: (id: string, coValue: any) => Promise<void>;
|
|
40
41
|
trackCoValuesSyncState?: (
|
|
41
42
|
operations: Array<{ id: RawCoID; peerId: PeerID; synced: boolean }>,
|
|
@@ -51,6 +52,8 @@ function createMockStorage(
|
|
|
51
52
|
load: opts.load || vi.fn(),
|
|
52
53
|
store: opts.store || vi.fn(),
|
|
53
54
|
getKnownState: opts.getKnownState || vi.fn(),
|
|
55
|
+
loadKnownState:
|
|
56
|
+
opts.loadKnownState || vi.fn((id, callback) => callback(undefined)),
|
|
54
57
|
waitForSync: opts.waitForSync || vi.fn().mockResolvedValue(undefined),
|
|
55
58
|
trackCoValuesSyncState: opts.trackCoValuesSyncState || vi.fn(),
|
|
56
59
|
getUnsyncedCoValueIDs: opts.getUnsyncedCoValueIDs || vi.fn(),
|