cojson 0.19.20 → 0.19.22
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 +13 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts +42 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.js +261 -0
- package/dist/CojsonMessageChannel/CojsonMessageChannel.js.map +1 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts +18 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js +37 -0
- package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js.map +1 -0
- package/dist/CojsonMessageChannel/index.d.ts +3 -0
- package/dist/CojsonMessageChannel/index.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/index.js +2 -0
- package/dist/CojsonMessageChannel/index.js.map +1 -0
- package/dist/CojsonMessageChannel/types.d.ts +149 -0
- package/dist/CojsonMessageChannel/types.d.ts.map +1 -0
- package/dist/CojsonMessageChannel/types.js +36 -0
- package/dist/CojsonMessageChannel/types.js.map +1 -0
- package/dist/GarbageCollector.d.ts +4 -2
- package/dist/GarbageCollector.d.ts.map +1 -1
- package/dist/GarbageCollector.js +5 -3
- package/dist/GarbageCollector.js.map +1 -1
- package/dist/SyncStateManager.d.ts +3 -3
- package/dist/SyncStateManager.d.ts.map +1 -1
- package/dist/SyncStateManager.js +4 -4
- package/dist/SyncStateManager.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +28 -1
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +50 -5
- 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/exports.d.ts +1 -0
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +1 -0
- package/dist/exports.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 +1 -3
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +11 -4
- 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 +10 -3
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +52 -3
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +9 -3
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +27 -3
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +23 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +23 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +136 -45
- package/dist/sync.js.map +1 -1
- package/dist/tests/CojsonMessageChannel.test.d.ts +2 -0
- package/dist/tests/CojsonMessageChannel.test.d.ts.map +1 -0
- package/dist/tests/CojsonMessageChannel.test.js +236 -0
- package/dist/tests/CojsonMessageChannel.test.js.map +1 -0
- package/dist/tests/GarbageCollector.test.js +87 -13
- package/dist/tests/GarbageCollector.test.js.map +1 -1
- package/dist/tests/StorageApiAsync.test.js +124 -1
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/StorageApiSync.test.js +123 -0
- package/dist/tests/StorageApiSync.test.js.map +1 -1
- package/dist/tests/SyncManager.processQueues.test.js +1 -1
- package/dist/tests/SyncManager.processQueues.test.js.map +1 -1
- package/dist/tests/SyncStateManager.test.js +1 -1
- package/dist/tests/SyncStateManager.test.js.map +1 -1
- package/dist/tests/coPlainText.test.js +1 -1
- package/dist/tests/coPlainText.test.js.map +1 -1
- package/dist/tests/coValueCore.loadFromStorage.test.js +2 -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 +167 -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.garbageCollection.test.js +56 -32
- package/dist/tests/sync.garbageCollection.test.js.map +1 -1
- package/dist/tests/sync.load.test.js +387 -1
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +5 -5
- package/dist/tests/sync.mesh.test.js.map +1 -1
- package/dist/tests/sync.peerReconciliation.test.js +3 -3
- package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +9 -9
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.storageAsync.test.js +7 -7
- package/dist/tests/sync.storageAsync.test.js.map +1 -1
- package/dist/tests/sync.tracking.test.js +35 -4
- package/dist/tests/sync.tracking.test.js.map +1 -1
- package/dist/tests/testStorage.js +38 -2
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +38 -4
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +68 -7
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/CojsonMessageChannel/CojsonMessageChannel.ts +332 -0
- package/src/CojsonMessageChannel/MessagePortOutgoingChannel.ts +52 -0
- package/src/CojsonMessageChannel/index.ts +9 -0
- package/src/CojsonMessageChannel/types.ts +200 -0
- package/src/GarbageCollector.ts +5 -5
- package/src/SyncStateManager.ts +6 -6
- package/src/coValueCore/coValueCore.ts +56 -7
- package/src/coValues/account.ts +12 -14
- package/src/exports.ts +1 -0
- package/src/ids.ts +1 -1
- package/src/knownState.ts +24 -0
- package/src/localNode.ts +12 -7
- 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 +66 -4
- package/src/storage/storageSync.ts +37 -4
- package/src/storage/types.ts +32 -0
- package/src/sync.ts +159 -46
- package/src/tests/CojsonMessageChannel.test.ts +306 -0
- package/src/tests/GarbageCollector.test.ts +114 -13
- package/src/tests/StorageApiAsync.test.ts +186 -1
- package/src/tests/StorageApiSync.test.ts +181 -0
- package/src/tests/SyncManager.processQueues.test.ts +1 -1
- package/src/tests/SyncStateManager.test.ts +1 -1
- package/src/tests/coPlainText.test.ts +1 -1
- package/src/tests/coValueCore.loadFromStorage.test.ts +5 -0
- package/src/tests/knownState.lazyLoading.test.ts +219 -0
- package/src/tests/messagesTestUtils.ts +10 -3
- package/src/tests/sync.garbageCollection.test.ts +69 -36
- package/src/tests/sync.load.test.ts +482 -2
- package/src/tests/sync.mesh.test.ts +5 -5
- package/src/tests/sync.peerReconciliation.test.ts +3 -3
- package/src/tests/sync.storage.test.ts +9 -9
- package/src/tests/sync.storageAsync.test.ts +7 -7
- package/src/tests/sync.tracking.test.ts +54 -4
- package/src/tests/testStorage.ts +40 -2
- package/src/tests/testUtils.ts +99 -8
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { assert, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { setGarbageCollectorMaxAge } from "../config";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
blockMessageTypeOnOutgoingPeer,
|
|
6
|
+
TEST_NODE_CONFIG,
|
|
7
|
+
setupTestAccount,
|
|
8
|
+
setupTestNode,
|
|
9
|
+
} from "./testUtils";
|
|
10
|
+
import { createSyncStorage } from "./testStorage.js";
|
|
5
11
|
|
|
6
12
|
// We want to simulate a real world communication that happens asynchronously
|
|
7
13
|
TEST_NODE_CONFIG.withAsyncPeers = true;
|
|
@@ -10,6 +16,8 @@ beforeEach(() => {
|
|
|
10
16
|
// We want to test what happens when the garbage collector kicks in and removes a coValue
|
|
11
17
|
// We set the max age to -1 to make it remove everything
|
|
12
18
|
setGarbageCollectorMaxAge(-1);
|
|
19
|
+
|
|
20
|
+
setupTestNode({ isSyncServer: true });
|
|
13
21
|
});
|
|
14
22
|
|
|
15
23
|
describe("garbage collector", () => {
|
|
@@ -19,13 +27,14 @@ describe("garbage collector", () => {
|
|
|
19
27
|
client.addStorage({
|
|
20
28
|
ourName: "client",
|
|
21
29
|
});
|
|
30
|
+
client.connectToSyncServer();
|
|
22
31
|
client.node.enableGarbageCollector();
|
|
23
32
|
|
|
24
33
|
const group = client.node.createGroup();
|
|
25
34
|
const map = group.createMap();
|
|
26
35
|
map.set("hello", "world", "trusting");
|
|
27
36
|
|
|
28
|
-
await
|
|
37
|
+
await client.node.syncManager.waitForAllCoValuesSync();
|
|
29
38
|
|
|
30
39
|
client.node.garbageCollector?.collect();
|
|
31
40
|
|
|
@@ -40,6 +49,7 @@ describe("garbage collector", () => {
|
|
|
40
49
|
client.addStorage({
|
|
41
50
|
ourName: "client",
|
|
42
51
|
});
|
|
52
|
+
client.connectToSyncServer();
|
|
43
53
|
client.node.enableGarbageCollector();
|
|
44
54
|
|
|
45
55
|
const group = client.node.createGroup();
|
|
@@ -51,7 +61,7 @@ describe("garbage collector", () => {
|
|
|
51
61
|
// This listener keeps the coValue alive
|
|
52
62
|
});
|
|
53
63
|
|
|
54
|
-
await
|
|
64
|
+
await client.node.syncManager.waitForAllCoValuesSync();
|
|
55
65
|
|
|
56
66
|
client.node.garbageCollector?.collect();
|
|
57
67
|
|
|
@@ -66,42 +76,132 @@ describe("garbage collector", () => {
|
|
|
66
76
|
expect(client.node.getCoValue(map.id).isAvailable()).toBe(false);
|
|
67
77
|
});
|
|
68
78
|
|
|
69
|
-
test("coValues are not garbage collected if they are
|
|
70
|
-
const client =
|
|
79
|
+
test("coValues are not garbage collected if they are not synced with server peers", async () => {
|
|
80
|
+
const client = setupTestNode();
|
|
71
81
|
|
|
72
82
|
client.addStorage({
|
|
73
83
|
ourName: "client",
|
|
74
84
|
});
|
|
75
|
-
client.node.enableGarbageCollector(
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
client.node.enableGarbageCollector();
|
|
86
|
+
const { peer: serverPeer } = client.connectToSyncServer();
|
|
87
|
+
// Block sync with server
|
|
88
|
+
const blocker = blockMessageTypeOnOutgoingPeer(serverPeer, "content", {});
|
|
78
89
|
|
|
79
90
|
const group = client.node.createGroup();
|
|
91
|
+
const map = group.createMap();
|
|
92
|
+
map.set("hello", "world", "trusting");
|
|
80
93
|
|
|
81
94
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
82
95
|
|
|
83
96
|
client.node.garbageCollector?.collect();
|
|
84
97
|
|
|
85
|
-
expect(client.node.getCoValue(
|
|
98
|
+
expect(client.node.getCoValue(map.id).isAvailable()).toBe(true);
|
|
99
|
+
|
|
100
|
+
// Resume sync with server
|
|
101
|
+
blocker.sendBlockedMessages();
|
|
102
|
+
blocker.unblock();
|
|
103
|
+
await client.node.syncManager.waitForAllCoValuesSync();
|
|
104
|
+
|
|
105
|
+
// The coValue should now be collected
|
|
106
|
+
client.node.garbageCollector?.collect();
|
|
107
|
+
|
|
108
|
+
expect(client.node.getCoValue(map.id).isAvailable()).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("coValues are garbage collected if there are no server peers", async () => {
|
|
112
|
+
const client = setupTestNode();
|
|
113
|
+
|
|
114
|
+
client.addStorage({
|
|
115
|
+
ourName: "client",
|
|
116
|
+
});
|
|
117
|
+
client.node.enableGarbageCollector();
|
|
118
|
+
// Client is not connected to the sync server
|
|
119
|
+
|
|
120
|
+
const group = client.node.createGroup();
|
|
121
|
+
const map = group.createMap();
|
|
122
|
+
map.set("hello", "world", "trusting");
|
|
123
|
+
|
|
124
|
+
await client.node.syncManager.waitForAllCoValuesSync();
|
|
125
|
+
|
|
126
|
+
client.node.garbageCollector?.collect();
|
|
127
|
+
|
|
128
|
+
expect(client.node.getCoValue(map.id).isAvailable()).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("account coValues are not garbage collected if they have dependencies", async () => {
|
|
132
|
+
const client = await setupTestAccount({
|
|
133
|
+
// Add storage before creating the account so it's persisted
|
|
134
|
+
storage: createSyncStorage({
|
|
135
|
+
nodeName: "client",
|
|
136
|
+
storageName: "storage",
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
// The account is created along with its profile, and the group that owns the profile
|
|
140
|
+
const profile = client.node.expectProfileLoaded(client.accountID);
|
|
141
|
+
const profileId = profile.id;
|
|
142
|
+
const profileOwnerId = profile.group.id;
|
|
143
|
+
|
|
144
|
+
client.connectToSyncServer();
|
|
145
|
+
client.node.enableGarbageCollector();
|
|
146
|
+
|
|
147
|
+
await client.node.syncManager.waitForAllCoValuesSync();
|
|
148
|
+
|
|
149
|
+
// First collect removes the profile
|
|
150
|
+
client.node.garbageCollector?.collect();
|
|
151
|
+
expect(client.node.getCoValue(profileId).isAvailable()).toBe(false);
|
|
152
|
+
expect(client.node.getCoValue(profileOwnerId).isAvailable()).toBe(true);
|
|
153
|
+
expect(client.node.getCoValue(client.accountID).isAvailable()).toBe(true);
|
|
154
|
+
|
|
155
|
+
// Second collect removes the profile owner
|
|
156
|
+
client.node.garbageCollector?.collect();
|
|
157
|
+
expect(client.node.getCoValue(profileOwnerId).isAvailable()).toBe(false);
|
|
158
|
+
expect(client.node.getCoValue(client.accountID).isAvailable()).toBe(true);
|
|
159
|
+
|
|
160
|
+
// Third collect removes the account
|
|
161
|
+
client.node.garbageCollector?.collect();
|
|
86
162
|
expect(client.node.getCoValue(client.accountID).isAvailable()).toBe(false);
|
|
87
163
|
});
|
|
88
164
|
|
|
89
|
-
test("group
|
|
90
|
-
const client =
|
|
165
|
+
test("group coValues are garbage collected if they have no dependencies", async () => {
|
|
166
|
+
const client = setupTestNode();
|
|
91
167
|
|
|
92
168
|
client.addStorage({
|
|
93
169
|
ourName: "client",
|
|
94
170
|
});
|
|
171
|
+
client.connectToSyncServer();
|
|
95
172
|
client.node.enableGarbageCollector();
|
|
96
173
|
|
|
97
174
|
const group = client.node.createGroup();
|
|
98
175
|
|
|
99
|
-
await
|
|
176
|
+
await client.node.syncManager.waitForAllCoValuesSync();
|
|
100
177
|
|
|
101
178
|
client.node.garbageCollector?.collect();
|
|
102
179
|
|
|
180
|
+
expect(client.node.getCoValue(group.id).isAvailable()).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("group coValues are not garbage collected if they have dependencies", async () => {
|
|
184
|
+
const client = setupTestNode();
|
|
185
|
+
|
|
186
|
+
client.addStorage({
|
|
187
|
+
ourName: "client",
|
|
188
|
+
});
|
|
189
|
+
client.node.enableGarbageCollector();
|
|
190
|
+
|
|
191
|
+
const group = client.node.createGroup();
|
|
192
|
+
const map = group.createMap();
|
|
193
|
+
map.set("hello", "world", "trusting");
|
|
194
|
+
|
|
195
|
+
await client.node.syncManager.waitForAllCoValuesSync();
|
|
196
|
+
|
|
197
|
+
// First collect removes the map
|
|
198
|
+
client.node.garbageCollector?.collect();
|
|
103
199
|
expect(client.node.getCoValue(group.id).isAvailable()).toBe(true);
|
|
104
|
-
expect(client.node.getCoValue(
|
|
200
|
+
expect(client.node.getCoValue(map.id).isAvailable()).toBe(false);
|
|
201
|
+
|
|
202
|
+
// Second collect removes the group
|
|
203
|
+
client.node.garbageCollector?.collect();
|
|
204
|
+
expect(client.node.getCoValue(group.id).isAvailable()).toBe(false);
|
|
105
205
|
});
|
|
106
206
|
|
|
107
207
|
test("coValues are not garbage collected if the maxAge is not reached", async () => {
|
|
@@ -112,6 +212,7 @@ describe("garbage collector", () => {
|
|
|
112
212
|
client.addStorage({
|
|
113
213
|
ourName: "client",
|
|
114
214
|
});
|
|
215
|
+
client.connectToSyncServer();
|
|
115
216
|
client.node.enableGarbageCollector();
|
|
116
217
|
|
|
117
218
|
const garbageCollector = client.node.garbageCollector;
|
|
@@ -81,7 +81,7 @@ async function createTestNode(dbPath?: string) {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
onTestFinished(async () => {
|
|
84
|
-
node.gracefulShutdown();
|
|
84
|
+
await node.gracefulShutdown();
|
|
85
85
|
await storage.close();
|
|
86
86
|
});
|
|
87
87
|
|
|
@@ -782,6 +782,55 @@ describe("StorageApiAsync", () => {
|
|
|
782
782
|
const mapOnNode = await loadCoValueOrFail(node, map.id);
|
|
783
783
|
expect(mapOnNode.get("test")).toEqual("value");
|
|
784
784
|
});
|
|
785
|
+
|
|
786
|
+
test("should load dependencies again if they were unmounted", async () => {
|
|
787
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
788
|
+
const { node, storage } = await createTestNode(dbPath);
|
|
789
|
+
|
|
790
|
+
// Create a group and a map owned by that group
|
|
791
|
+
const group = fixturesNode.createGroup();
|
|
792
|
+
group.addMember("everyone", "reader");
|
|
793
|
+
const map = group.createMap({ test: "value" });
|
|
794
|
+
await group.core.waitForSync();
|
|
795
|
+
await map.core.waitForSync();
|
|
796
|
+
|
|
797
|
+
const callback = vi.fn((content) =>
|
|
798
|
+
node.syncManager.handleNewContent(content, "storage"),
|
|
799
|
+
);
|
|
800
|
+
const done = vi.fn();
|
|
801
|
+
|
|
802
|
+
// Load the map (and its group)
|
|
803
|
+
await storage.load(map.id, callback, done);
|
|
804
|
+
callback.mockClear();
|
|
805
|
+
done.mockClear();
|
|
806
|
+
|
|
807
|
+
// Unmount the map and its group
|
|
808
|
+
storage.onCoValueUnmounted(map.id);
|
|
809
|
+
storage.onCoValueUnmounted(group.id);
|
|
810
|
+
|
|
811
|
+
// Load the map. The group dependency should be loaded again
|
|
812
|
+
await storage.load(map.id, callback, done);
|
|
813
|
+
|
|
814
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
815
|
+
expect(callback).toHaveBeenNthCalledWith(
|
|
816
|
+
1,
|
|
817
|
+
expect.objectContaining({
|
|
818
|
+
id: group.id,
|
|
819
|
+
}),
|
|
820
|
+
);
|
|
821
|
+
expect(callback).toHaveBeenNthCalledWith(
|
|
822
|
+
2,
|
|
823
|
+
expect.objectContaining({
|
|
824
|
+
id: map.id,
|
|
825
|
+
}),
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
expect(done).toHaveBeenCalledWith(true);
|
|
829
|
+
|
|
830
|
+
node.setStorage(storage);
|
|
831
|
+
const mapOnNode = await loadCoValueOrFail(node, map.id);
|
|
832
|
+
expect(mapOnNode.get("test")).toEqual("value");
|
|
833
|
+
});
|
|
785
834
|
});
|
|
786
835
|
|
|
787
836
|
describe("waitForSync", () => {
|
|
@@ -823,4 +872,140 @@ describe("StorageApiAsync", () => {
|
|
|
823
872
|
expect(() => storage.close()).not.toThrow();
|
|
824
873
|
});
|
|
825
874
|
});
|
|
875
|
+
|
|
876
|
+
describe("loadKnownState", () => {
|
|
877
|
+
test("should return cached knownState if available", async () => {
|
|
878
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
879
|
+
const { storage } = await createTestNode(dbPath);
|
|
880
|
+
|
|
881
|
+
// Create a group to have data in the database
|
|
882
|
+
const group = fixturesNode.createGroup();
|
|
883
|
+
group.addMember("everyone", "reader");
|
|
884
|
+
await group.core.waitForSync();
|
|
885
|
+
|
|
886
|
+
// First call should hit the database and cache the result
|
|
887
|
+
const result1 = await new Promise<CoValueKnownState | undefined>(
|
|
888
|
+
(resolve) => {
|
|
889
|
+
storage.loadKnownState(group.id, resolve);
|
|
890
|
+
},
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
expect(result1).toBeDefined();
|
|
894
|
+
expect(result1?.id).toBe(group.id);
|
|
895
|
+
expect(result1?.header).toBe(true);
|
|
896
|
+
|
|
897
|
+
// Second call should return from cache
|
|
898
|
+
const result2 = await new Promise<CoValueKnownState | undefined>(
|
|
899
|
+
(resolve) => {
|
|
900
|
+
storage.loadKnownState(group.id, resolve);
|
|
901
|
+
},
|
|
902
|
+
);
|
|
903
|
+
|
|
904
|
+
expect(result2).toEqual(result1);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test("should return undefined for non-existent CoValue", async () => {
|
|
908
|
+
const { storage } = await createTestNode();
|
|
909
|
+
|
|
910
|
+
const result = await new Promise<CoValueKnownState | undefined>(
|
|
911
|
+
(resolve) => {
|
|
912
|
+
storage.loadKnownState("co_nonexistent" as any, resolve);
|
|
913
|
+
},
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
expect(result).toBeUndefined();
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test("should deduplicate concurrent requests for the same ID", async () => {
|
|
920
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
921
|
+
const { storage } = await createTestNode(dbPath);
|
|
922
|
+
|
|
923
|
+
// Create a group to have data in the database
|
|
924
|
+
const group = fixturesNode.createGroup();
|
|
925
|
+
group.addMember("everyone", "reader");
|
|
926
|
+
await group.core.waitForSync();
|
|
927
|
+
|
|
928
|
+
// Clear the cache to force database access
|
|
929
|
+
storage.knownStates.knownStates.clear();
|
|
930
|
+
|
|
931
|
+
// Spy on the database client to track how many times it's called
|
|
932
|
+
const dbClientSpy = vi.spyOn(
|
|
933
|
+
(storage as any).dbClient,
|
|
934
|
+
"getCoValueKnownState",
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
// Make multiple concurrent requests for the same ID
|
|
938
|
+
const promises = [
|
|
939
|
+
new Promise<CoValueKnownState | undefined>((resolve) => {
|
|
940
|
+
storage.loadKnownState(group.id, resolve);
|
|
941
|
+
}),
|
|
942
|
+
new Promise<CoValueKnownState | undefined>((resolve) => {
|
|
943
|
+
storage.loadKnownState(group.id, resolve);
|
|
944
|
+
}),
|
|
945
|
+
new Promise<CoValueKnownState | undefined>((resolve) => {
|
|
946
|
+
storage.loadKnownState(group.id, resolve);
|
|
947
|
+
}),
|
|
948
|
+
];
|
|
949
|
+
|
|
950
|
+
const results = await Promise.all(promises);
|
|
951
|
+
|
|
952
|
+
// All results should be the same
|
|
953
|
+
expect(results[0]).toEqual(results[1]);
|
|
954
|
+
expect(results[1]).toEqual(results[2]);
|
|
955
|
+
expect(results[0]?.id).toBe(group.id);
|
|
956
|
+
|
|
957
|
+
// Database should only be called once due to deduplication
|
|
958
|
+
expect(dbClientSpy).toHaveBeenCalledTimes(1);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
test("should use cache and not query database when cache is populated", async () => {
|
|
962
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
963
|
+
const { storage } = await createTestNode(dbPath);
|
|
964
|
+
|
|
965
|
+
// Create a group to have data in the database
|
|
966
|
+
const group = fixturesNode.createGroup();
|
|
967
|
+
group.addMember("everyone", "reader");
|
|
968
|
+
await group.core.waitForSync();
|
|
969
|
+
|
|
970
|
+
// Spy on the database client to track calls
|
|
971
|
+
const dbClientSpy = vi.spyOn(
|
|
972
|
+
(storage as any).dbClient,
|
|
973
|
+
"getCoValueKnownState",
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
// First call - should hit the database
|
|
977
|
+
const result1 = await new Promise<CoValueKnownState | undefined>(
|
|
978
|
+
(resolve) => {
|
|
979
|
+
storage.loadKnownState(group.id, resolve);
|
|
980
|
+
},
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
expect(result1).toBeDefined();
|
|
984
|
+
expect(dbClientSpy).toHaveBeenCalledTimes(1);
|
|
985
|
+
|
|
986
|
+
// Clear the spy to reset call count
|
|
987
|
+
dbClientSpy.mockClear();
|
|
988
|
+
|
|
989
|
+
// Second call - should use cache, not database
|
|
990
|
+
const result2 = await new Promise<CoValueKnownState | undefined>(
|
|
991
|
+
(resolve) => {
|
|
992
|
+
storage.loadKnownState(group.id, resolve);
|
|
993
|
+
},
|
|
994
|
+
);
|
|
995
|
+
|
|
996
|
+
expect(result2).toEqual(result1);
|
|
997
|
+
// Database should NOT be called since cache was hit
|
|
998
|
+
expect(dbClientSpy).toHaveBeenCalledTimes(0);
|
|
999
|
+
|
|
1000
|
+
// Third call - also from cache
|
|
1001
|
+
const result3 = await new Promise<CoValueKnownState | undefined>(
|
|
1002
|
+
(resolve) => {
|
|
1003
|
+
storage.loadKnownState(group.id, resolve);
|
|
1004
|
+
},
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
expect(result3).toEqual(result1);
|
|
1008
|
+
expect(dbClientSpy).toHaveBeenCalledTimes(0);
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
826
1011
|
});
|
|
@@ -578,6 +578,55 @@ describe("StorageApiSync", () => {
|
|
|
578
578
|
const mapOnNode = await loadCoValueOrFail(node, map.id);
|
|
579
579
|
expect(mapOnNode.get("test")).toEqual("value");
|
|
580
580
|
});
|
|
581
|
+
|
|
582
|
+
test("should load dependencies again if they were unmounted", async () => {
|
|
583
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
584
|
+
const { node, storage } = await createTestNode(dbPath);
|
|
585
|
+
|
|
586
|
+
// Create a group and a map owned by that group
|
|
587
|
+
const group = fixturesNode.createGroup();
|
|
588
|
+
group.addMember("everyone", "reader");
|
|
589
|
+
const map = group.createMap({ test: "value" });
|
|
590
|
+
await group.core.waitForSync();
|
|
591
|
+
await map.core.waitForSync();
|
|
592
|
+
|
|
593
|
+
const callback = vi.fn((content) =>
|
|
594
|
+
node.syncManager.handleNewContent(content, "storage"),
|
|
595
|
+
);
|
|
596
|
+
const done = vi.fn();
|
|
597
|
+
|
|
598
|
+
// Load the map (and its group)
|
|
599
|
+
await storage.load(map.id, callback, done);
|
|
600
|
+
callback.mockClear();
|
|
601
|
+
done.mockClear();
|
|
602
|
+
|
|
603
|
+
// Unmount the map and its group
|
|
604
|
+
storage.onCoValueUnmounted(map.id);
|
|
605
|
+
storage.onCoValueUnmounted(group.id);
|
|
606
|
+
|
|
607
|
+
// Load the map. The group dependency should be loaded again
|
|
608
|
+
await storage.load(map.id, callback, done);
|
|
609
|
+
|
|
610
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
611
|
+
expect(callback).toHaveBeenNthCalledWith(
|
|
612
|
+
1,
|
|
613
|
+
expect.objectContaining({
|
|
614
|
+
id: group.id,
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
617
|
+
expect(callback).toHaveBeenNthCalledWith(
|
|
618
|
+
2,
|
|
619
|
+
expect.objectContaining({
|
|
620
|
+
id: map.id,
|
|
621
|
+
}),
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
expect(done).toHaveBeenCalledWith(true);
|
|
625
|
+
|
|
626
|
+
node.setStorage(storage);
|
|
627
|
+
const mapOnNode = await loadCoValueOrFail(node, map.id);
|
|
628
|
+
expect(mapOnNode.get("test")).toEqual("value");
|
|
629
|
+
});
|
|
581
630
|
});
|
|
582
631
|
|
|
583
632
|
describe("waitForSync", () => {
|
|
@@ -619,4 +668,136 @@ describe("StorageApiSync", () => {
|
|
|
619
668
|
expect(() => storage.close()).not.toThrow();
|
|
620
669
|
});
|
|
621
670
|
});
|
|
671
|
+
|
|
672
|
+
describe("loadKnownState", () => {
|
|
673
|
+
test("should return correct knownState structure for existing CoValue", async () => {
|
|
674
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
675
|
+
const { storage } = await createTestNode(dbPath);
|
|
676
|
+
|
|
677
|
+
// Create a group to have data in the database
|
|
678
|
+
const group = fixturesNode.createGroup();
|
|
679
|
+
group.addMember("everyone", "reader");
|
|
680
|
+
await group.core.waitForSync();
|
|
681
|
+
|
|
682
|
+
const result = await new Promise<CoValueKnownState | undefined>(
|
|
683
|
+
(resolve) => {
|
|
684
|
+
storage.loadKnownState(group.id, resolve);
|
|
685
|
+
},
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
expect(result).toBeDefined();
|
|
689
|
+
expect(result?.id).toBe(group.id);
|
|
690
|
+
expect(result?.header).toBe(true);
|
|
691
|
+
expect(result?.sessions).toEqual(group.core.knownState().sessions);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test("should return undefined for non-existent CoValue", async () => {
|
|
695
|
+
const { storage } = await createTestNode();
|
|
696
|
+
|
|
697
|
+
const result = await new Promise<CoValueKnownState | undefined>(
|
|
698
|
+
(resolve) => {
|
|
699
|
+
storage.loadKnownState("co_nonexistent" as any, resolve);
|
|
700
|
+
},
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
expect(result).toBeUndefined();
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test("should handle CoValue with no sessions (header only)", async () => {
|
|
707
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
708
|
+
const { storage } = await createTestNode(dbPath);
|
|
709
|
+
|
|
710
|
+
// Create a CoValue with just a header (no transactions yet)
|
|
711
|
+
const coValue = fixturesNode.createCoValue({
|
|
712
|
+
type: "comap",
|
|
713
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
714
|
+
meta: null,
|
|
715
|
+
...crypto.createdNowUnique(),
|
|
716
|
+
});
|
|
717
|
+
await coValue.waitForSync();
|
|
718
|
+
|
|
719
|
+
const result = await new Promise<CoValueKnownState | undefined>(
|
|
720
|
+
(resolve) => {
|
|
721
|
+
storage.loadKnownState(coValue.id, resolve);
|
|
722
|
+
},
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
expect(result).toBeDefined();
|
|
726
|
+
expect(result?.id).toBe(coValue.id);
|
|
727
|
+
expect(result?.header).toBe(true);
|
|
728
|
+
// The sessions should have one entry with lastIdx = 0 (just header)
|
|
729
|
+
expect(Object.keys(result?.sessions || {}).length).toBe(0);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test("should handle CoValue with multiple sessions", async () => {
|
|
733
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
734
|
+
const { fixturesNode: fixturesNode2 } = await createFixturesNode(dbPath);
|
|
735
|
+
const { storage } = await createTestNode(dbPath);
|
|
736
|
+
|
|
737
|
+
// Create a CoValue and have two nodes make transactions
|
|
738
|
+
const coValue = fixturesNode.createCoValue({
|
|
739
|
+
type: "comap",
|
|
740
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
741
|
+
meta: null,
|
|
742
|
+
...crypto.createdNowUnique(),
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
coValue.makeTransaction([{ key1: "value1" }], "trusting");
|
|
746
|
+
await coValue.waitForSync();
|
|
747
|
+
|
|
748
|
+
const coValueOnNode2 = await loadCoValueOrFail(
|
|
749
|
+
fixturesNode2,
|
|
750
|
+
coValue.id as CoID<RawCoMap>,
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
coValueOnNode2.set("key2", "value2", "trusting");
|
|
754
|
+
await coValueOnNode2.core.waitForSync();
|
|
755
|
+
|
|
756
|
+
const result = await new Promise<CoValueKnownState | undefined>(
|
|
757
|
+
(resolve) => {
|
|
758
|
+
storage.loadKnownState(coValue.id, resolve);
|
|
759
|
+
},
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
expect(result).toBeDefined();
|
|
763
|
+
expect(result?.id).toBe(coValue.id);
|
|
764
|
+
expect(result?.header).toBe(true);
|
|
765
|
+
// Should have two sessions
|
|
766
|
+
expect(Object.keys(result?.sessions || {}).length).toBe(2);
|
|
767
|
+
// Verify sessions match the expected state
|
|
768
|
+
expect(result?.sessions).toEqual(
|
|
769
|
+
coValueOnNode2.core.knownState().sessions,
|
|
770
|
+
);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test("should use cache when knownState is cached", async () => {
|
|
774
|
+
const { fixturesNode, dbPath } = await createFixturesNode();
|
|
775
|
+
const { storage } = await createTestNode(dbPath);
|
|
776
|
+
|
|
777
|
+
// Create a group to have data in the database
|
|
778
|
+
const group = fixturesNode.createGroup();
|
|
779
|
+
group.addMember("everyone", "reader");
|
|
780
|
+
await group.core.waitForSync();
|
|
781
|
+
|
|
782
|
+
// First call should hit the database and cache the result
|
|
783
|
+
const result1 = await new Promise<CoValueKnownState | undefined>(
|
|
784
|
+
(resolve) => {
|
|
785
|
+
storage.loadKnownState(group.id, resolve);
|
|
786
|
+
},
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
expect(result1).toBeDefined();
|
|
790
|
+
expect(result1?.id).toBe(group.id);
|
|
791
|
+
expect(result1?.header).toBe(true);
|
|
792
|
+
|
|
793
|
+
// Second call should return from cache
|
|
794
|
+
const result2 = await new Promise<CoValueKnownState | undefined>(
|
|
795
|
+
(resolve) => {
|
|
796
|
+
storage.loadKnownState(group.id, resolve);
|
|
797
|
+
},
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
expect(result2).toEqual(result1);
|
|
801
|
+
});
|
|
802
|
+
});
|
|
622
803
|
});
|
|
@@ -46,7 +46,7 @@ describe("SyncStateManager", () => {
|
|
|
46
46
|
const newPeerState = client.node.syncManager.peers[peerState.id]!;
|
|
47
47
|
|
|
48
48
|
expect(updateSpy).toHaveBeenCalledWith(
|
|
49
|
-
peerState.id,
|
|
49
|
+
expect.objectContaining({ id: peerState.id }),
|
|
50
50
|
newPeerState.getKnownState(map.core.id)!,
|
|
51
51
|
{ uploaded: true },
|
|
52
52
|
);
|
|
@@ -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 }>,
|
|
@@ -44,6 +45,7 @@ function createMockStorage(
|
|
|
44
45
|
callback: (unsyncedCoValueIDs: RawCoID[]) => void,
|
|
45
46
|
) => void;
|
|
46
47
|
stopTrackingSyncState?: (id: RawCoID) => void;
|
|
48
|
+
onCoValueUnmounted?: (id: RawCoID) => void;
|
|
47
49
|
close?: () => Promise<unknown> | undefined;
|
|
48
50
|
} = {},
|
|
49
51
|
): StorageAPI {
|
|
@@ -51,10 +53,13 @@ function createMockStorage(
|
|
|
51
53
|
load: opts.load || vi.fn(),
|
|
52
54
|
store: opts.store || vi.fn(),
|
|
53
55
|
getKnownState: opts.getKnownState || vi.fn(),
|
|
56
|
+
loadKnownState:
|
|
57
|
+
opts.loadKnownState || vi.fn((id, callback) => callback(undefined)),
|
|
54
58
|
waitForSync: opts.waitForSync || vi.fn().mockResolvedValue(undefined),
|
|
55
59
|
trackCoValuesSyncState: opts.trackCoValuesSyncState || vi.fn(),
|
|
56
60
|
getUnsyncedCoValueIDs: opts.getUnsyncedCoValueIDs || vi.fn(),
|
|
57
61
|
stopTrackingSyncState: opts.stopTrackingSyncState || vi.fn(),
|
|
62
|
+
onCoValueUnmounted: opts.onCoValueUnmounted || vi.fn(),
|
|
58
63
|
close: opts.close || vi.fn().mockResolvedValue(undefined),
|
|
59
64
|
};
|
|
60
65
|
}
|