cojson 0.19.21 → 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 +19 -1
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +29 -5
- package/dist/coValueCore/coValueCore.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/localNode.d.ts +1 -3
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +3 -2
- package/dist/localNode.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +8 -3
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +12 -3
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +8 -3
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +12 -3
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +5 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +6 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +25 -4
- 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 +33 -1
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/StorageApiSync.test.js +32 -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 +1 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.js +1 -0
- package/dist/tests/knownState.lazyLoading.test.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 +3 -5
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +1 -1
- 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 +2 -2
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +24 -2
- 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 +30 -7
- package/src/exports.ts +1 -0
- package/src/localNode.ts +3 -5
- package/src/storage/storageAsync.ts +15 -4
- package/src/storage/storageSync.ts +15 -4
- package/src/storage/types.ts +6 -0
- package/src/sync.ts +33 -4
- package/src/tests/CojsonMessageChannel.test.ts +306 -0
- package/src/tests/GarbageCollector.test.ts +114 -13
- package/src/tests/StorageApiAsync.test.ts +50 -1
- package/src/tests/StorageApiSync.test.ts +49 -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 +2 -0
- package/src/tests/knownState.lazyLoading.test.ts +2 -0
- package/src/tests/sync.garbageCollection.test.ts +69 -36
- package/src/tests/sync.load.test.ts +3 -5
- package/src/tests/sync.mesh.test.ts +1 -1
- 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 +2 -2
- package/src/tests/testUtils.ts +85 -6
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, assert } from "vitest";
|
|
2
|
+
import { CojsonMessageChannel } from "../CojsonMessageChannel";
|
|
3
|
+
import type { Peer } from "../sync.js";
|
|
4
|
+
import type { RawCoMap } from "../coValues/coMap.js";
|
|
5
|
+
import {
|
|
6
|
+
setupTestNode,
|
|
7
|
+
SyncMessagesLog,
|
|
8
|
+
waitFor,
|
|
9
|
+
createTrackedMessageChannel,
|
|
10
|
+
createMockWorkerWithAccept,
|
|
11
|
+
loadCoValueOrFail,
|
|
12
|
+
} from "./testUtils";
|
|
13
|
+
|
|
14
|
+
describe("CojsonMessageChannel", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
SyncMessagesLog.clear();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("should sync data between two contexts via MessageChannel", async () => {
|
|
20
|
+
// Create two nodes using setupTestNode (handles cleanup automatically)
|
|
21
|
+
const { node: node1 } = setupTestNode();
|
|
22
|
+
const { node: node2 } = setupTestNode();
|
|
23
|
+
|
|
24
|
+
const mockWorker = createMockWorkerWithAccept(async (port) => {
|
|
25
|
+
// This runs in the "worker" context
|
|
26
|
+
const peer = await CojsonMessageChannel.acceptFromPort(port, {
|
|
27
|
+
role: "server",
|
|
28
|
+
});
|
|
29
|
+
node2.syncManager.addPeer(peer);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Host side: expose to the mock worker
|
|
33
|
+
const peer1 = await CojsonMessageChannel.expose(mockWorker, {
|
|
34
|
+
role: "client",
|
|
35
|
+
messageChannel: createTrackedMessageChannel({
|
|
36
|
+
port1Name: "client",
|
|
37
|
+
port2Name: "server",
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
node1.syncManager.addPeer(peer1);
|
|
41
|
+
|
|
42
|
+
// Create data on node1
|
|
43
|
+
const group = node1.createGroup();
|
|
44
|
+
group.addMember("everyone", "writer");
|
|
45
|
+
const map = group.createMap();
|
|
46
|
+
map.set("key", "value", "trusting");
|
|
47
|
+
|
|
48
|
+
// Verify data synced
|
|
49
|
+
const mapOnNode2 = await loadCoValueOrFail<RawCoMap>(node2, map.id);
|
|
50
|
+
expect(mapOnNode2.get("key")).toBe("value");
|
|
51
|
+
|
|
52
|
+
expect(
|
|
53
|
+
SyncMessagesLog.getMessages({
|
|
54
|
+
Map: map.core,
|
|
55
|
+
Group: group.core,
|
|
56
|
+
}),
|
|
57
|
+
).toMatchInlineSnapshot(`
|
|
58
|
+
[
|
|
59
|
+
"server -> client | LOAD Map sessions: empty",
|
|
60
|
+
"client -> server | CONTENT Group header: true new: After: 0 New: 5",
|
|
61
|
+
"client -> server | CONTENT Map header: true new: After: 0 New: 1",
|
|
62
|
+
"server -> client | KNOWN Group sessions: header/5",
|
|
63
|
+
"server -> client | KNOWN Map sessions: header/1",
|
|
64
|
+
]
|
|
65
|
+
`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("should handle disconnection correctly", async () => {
|
|
69
|
+
const { node: node1 } = setupTestNode();
|
|
70
|
+
const { node: node2 } = setupTestNode();
|
|
71
|
+
|
|
72
|
+
const peerId = "disconnect-test-peer";
|
|
73
|
+
let peer2: Peer | null = null;
|
|
74
|
+
|
|
75
|
+
const mockWorker = createMockWorkerWithAccept(async (port) => {
|
|
76
|
+
peer2 = await CojsonMessageChannel.acceptFromPort(port, {
|
|
77
|
+
id: peerId,
|
|
78
|
+
role: "server",
|
|
79
|
+
});
|
|
80
|
+
node2.syncManager.addPeer(peer2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const peer1 = await CojsonMessageChannel.expose(mockWorker, {
|
|
84
|
+
id: peerId,
|
|
85
|
+
role: "client",
|
|
86
|
+
});
|
|
87
|
+
node1.syncManager.addPeer(peer1);
|
|
88
|
+
|
|
89
|
+
// Verify peers are connected (same ID on both sides)
|
|
90
|
+
expect(node1.syncManager.peers["disconnect-test-peer"]).toBeDefined();
|
|
91
|
+
expect(node2.syncManager.peers["disconnect-test-peer"]).toBeDefined();
|
|
92
|
+
|
|
93
|
+
peer1.outgoing.close();
|
|
94
|
+
|
|
95
|
+
expect(node1.syncManager.peers["disconnect-test-peer"]).toBeUndefined();
|
|
96
|
+
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(node2.syncManager.peers["disconnect-test-peer"]).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("should ignore mismatched peer IDs in waitForConnection() when id filter is provided", async () => {
|
|
103
|
+
const { node } = setupTestNode();
|
|
104
|
+
|
|
105
|
+
const hostPeerId = "host-peer-id";
|
|
106
|
+
const wrongPeerId = "wrong-peer-id";
|
|
107
|
+
|
|
108
|
+
let acceptPromiseResolved = false;
|
|
109
|
+
|
|
110
|
+
// Mock worker that expects a different ID
|
|
111
|
+
const mockWorker = createMockWorkerWithAccept(async (port) => {
|
|
112
|
+
// This should not resolve because the ID doesn't match
|
|
113
|
+
const acceptPromise = CojsonMessageChannel.acceptFromPort(port, {
|
|
114
|
+
id: wrongPeerId, // Expecting a different ID
|
|
115
|
+
role: "server",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Set a timeout to detect if it's waiting
|
|
119
|
+
const timeoutPromise = new Promise<null>((resolve) =>
|
|
120
|
+
setTimeout(() => resolve(null), 100),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const result = await Promise.race([acceptPromise, timeoutPromise]);
|
|
124
|
+
if (result !== null) {
|
|
125
|
+
acceptPromiseResolved = true;
|
|
126
|
+
node.syncManager.addPeer(result);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Expose with a different ID than what accept expects
|
|
131
|
+
CojsonMessageChannel.expose(mockWorker, {
|
|
132
|
+
id: hostPeerId,
|
|
133
|
+
role: "client",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Wait a bit to ensure the accept didn't resolve
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
138
|
+
|
|
139
|
+
// The accept should not have resolved because IDs don't match
|
|
140
|
+
expect(acceptPromiseResolved).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("should sync data bidirectionally", async () => {
|
|
144
|
+
const { node: node1 } = setupTestNode();
|
|
145
|
+
const { node: node2 } = setupTestNode();
|
|
146
|
+
|
|
147
|
+
const mockWorker = createMockWorkerWithAccept(async (port) => {
|
|
148
|
+
const peer = await CojsonMessageChannel.acceptFromPort(port, {
|
|
149
|
+
role: "server",
|
|
150
|
+
});
|
|
151
|
+
node2.syncManager.addPeer(peer);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const peer1 = await CojsonMessageChannel.expose(mockWorker, {
|
|
155
|
+
role: "client",
|
|
156
|
+
});
|
|
157
|
+
node1.syncManager.addPeer(peer1);
|
|
158
|
+
|
|
159
|
+
// Create data on node1
|
|
160
|
+
const group1 = node1.createGroup();
|
|
161
|
+
group1.addMember("everyone", "writer");
|
|
162
|
+
const map1 = group1.createMap();
|
|
163
|
+
map1.set("from", "node1", "trusting");
|
|
164
|
+
|
|
165
|
+
// Create data on node2
|
|
166
|
+
const group2 = node2.createGroup();
|
|
167
|
+
group2.addMember("everyone", "writer");
|
|
168
|
+
const map2 = group2.createMap();
|
|
169
|
+
map2.set("from", "node2", "trusting");
|
|
170
|
+
|
|
171
|
+
// Verify data synced in both directions
|
|
172
|
+
const map1OnNode2 = await loadCoValueOrFail<RawCoMap>(node2, map1.id);
|
|
173
|
+
expect(map1OnNode2.get("from")).toBe("node1");
|
|
174
|
+
|
|
175
|
+
const map2OnNode1 = await loadCoValueOrFail<RawCoMap>(node1, map2.id);
|
|
176
|
+
expect(map2OnNode1.get("from")).toBe("node2");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("should invoke onClose callback when connection closes", async () => {
|
|
180
|
+
const { node: node1 } = setupTestNode();
|
|
181
|
+
const { node: node2 } = setupTestNode();
|
|
182
|
+
|
|
183
|
+
let onCloseCalledOnHost = false;
|
|
184
|
+
let onCloseCalledOnWorker = false;
|
|
185
|
+
|
|
186
|
+
const mockWorker = createMockWorkerWithAccept(async (port) => {
|
|
187
|
+
const peer = await CojsonMessageChannel.acceptFromPort(port, {
|
|
188
|
+
role: "server",
|
|
189
|
+
onClose: () => {
|
|
190
|
+
onCloseCalledOnWorker = true;
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
node2.syncManager.addPeer(peer);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const peer1 = await CojsonMessageChannel.expose(mockWorker, {
|
|
197
|
+
role: "client",
|
|
198
|
+
onClose: () => {
|
|
199
|
+
onCloseCalledOnHost = true;
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
node1.syncManager.addPeer(peer1);
|
|
203
|
+
|
|
204
|
+
// Close the connection
|
|
205
|
+
peer1.outgoing.close();
|
|
206
|
+
|
|
207
|
+
// Wait for close to propagate
|
|
208
|
+
await waitFor(() => {
|
|
209
|
+
expect(onCloseCalledOnHost).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(onCloseCalledOnWorker).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("should apply role configuration correctly", async () => {
|
|
218
|
+
const { node: node1 } = setupTestNode();
|
|
219
|
+
const { node: node2 } = setupTestNode();
|
|
220
|
+
|
|
221
|
+
let peer2: Peer | null = null;
|
|
222
|
+
|
|
223
|
+
const mockWorker = createMockWorkerWithAccept(async (port) => {
|
|
224
|
+
peer2 = await CojsonMessageChannel.acceptFromPort(port, {
|
|
225
|
+
role: "server",
|
|
226
|
+
});
|
|
227
|
+
node2.syncManager.addPeer(peer2);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const peer1 = await CojsonMessageChannel.expose(mockWorker, {
|
|
231
|
+
role: "client",
|
|
232
|
+
});
|
|
233
|
+
node1.syncManager.addPeer(peer1);
|
|
234
|
+
|
|
235
|
+
// Verify roles are correctly set
|
|
236
|
+
expect(peer1.role).toBe("client");
|
|
237
|
+
expect(peer2).not.toBeNull();
|
|
238
|
+
expect(peer2!.role).toBe("server");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("should generate and use the same peer ID on both sides when not provided", async () => {
|
|
242
|
+
const { node: node1 } = setupTestNode();
|
|
243
|
+
const { node: node2 } = setupTestNode();
|
|
244
|
+
|
|
245
|
+
let peer2: Peer | null = null;
|
|
246
|
+
|
|
247
|
+
const mockWorker = createMockWorkerWithAccept(async (port) => {
|
|
248
|
+
peer2 = await CojsonMessageChannel.acceptFromPort(port, {
|
|
249
|
+
role: "server",
|
|
250
|
+
});
|
|
251
|
+
node2.syncManager.addPeer(peer2);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Don't provide an id - it should be auto-generated
|
|
255
|
+
const peer1 = await CojsonMessageChannel.expose(mockWorker, {
|
|
256
|
+
role: "client",
|
|
257
|
+
});
|
|
258
|
+
node1.syncManager.addPeer(peer1);
|
|
259
|
+
|
|
260
|
+
// Verify peer1 has an auto-generated ID
|
|
261
|
+
expect(peer1.id).toMatch(/^channel_/);
|
|
262
|
+
|
|
263
|
+
// Verify both peers have the same ID
|
|
264
|
+
expect(peer2).not.toBeNull();
|
|
265
|
+
expect(peer2!.id).toBe(peer1.id);
|
|
266
|
+
|
|
267
|
+
// Verify the peer is accessible in both sync managers with the same ID
|
|
268
|
+
expect(node1.syncManager.peers[peer1.id]).toBeDefined();
|
|
269
|
+
expect(node2.syncManager.peers[peer1.id]).toBeDefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("should handle delayed addPeer on accept side", async () => {
|
|
273
|
+
const { node: node1 } = setupTestNode();
|
|
274
|
+
const { node: node2 } = setupTestNode();
|
|
275
|
+
|
|
276
|
+
let peer2: Peer | null = null;
|
|
277
|
+
|
|
278
|
+
const delay = new Promise((resolve) => setTimeout(resolve, 50));
|
|
279
|
+
|
|
280
|
+
const mockWorker = createMockWorkerWithAccept(async (port) => {
|
|
281
|
+
peer2 = await CojsonMessageChannel.acceptFromPort(port, {
|
|
282
|
+
role: "server",
|
|
283
|
+
});
|
|
284
|
+
// Deliberately delay adding the peer
|
|
285
|
+
await delay;
|
|
286
|
+
node2.syncManager.addPeer(peer2);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const peer1 = await CojsonMessageChannel.expose(mockWorker, {
|
|
290
|
+
role: "client",
|
|
291
|
+
});
|
|
292
|
+
node1.syncManager.addPeer(peer1);
|
|
293
|
+
|
|
294
|
+
// Create data on node1 immediately (before node2 has added the peer)
|
|
295
|
+
const group = node1.createGroup();
|
|
296
|
+
group.addMember("everyone", "writer");
|
|
297
|
+
const map = group.createMap();
|
|
298
|
+
map.set("key", "value", "trusting");
|
|
299
|
+
|
|
300
|
+
await delay;
|
|
301
|
+
|
|
302
|
+
// Verify data synced despite the delay
|
|
303
|
+
const mapOnNode2 = await loadCoValueOrFail<RawCoMap>(node2, map.id);
|
|
304
|
+
expect(mapOnNode2.get("key")).toBe("value");
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -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", () => {
|
|
@@ -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", () => {
|
|
@@ -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
|
);
|
|
@@ -45,6 +45,7 @@ function createMockStorage(
|
|
|
45
45
|
callback: (unsyncedCoValueIDs: RawCoID[]) => void,
|
|
46
46
|
) => void;
|
|
47
47
|
stopTrackingSyncState?: (id: RawCoID) => void;
|
|
48
|
+
onCoValueUnmounted?: (id: RawCoID) => void;
|
|
48
49
|
close?: () => Promise<unknown> | undefined;
|
|
49
50
|
} = {},
|
|
50
51
|
): StorageAPI {
|
|
@@ -58,6 +59,7 @@ function createMockStorage(
|
|
|
58
59
|
trackCoValuesSyncState: opts.trackCoValuesSyncState || vi.fn(),
|
|
59
60
|
getUnsyncedCoValueIDs: opts.getUnsyncedCoValueIDs || vi.fn(),
|
|
60
61
|
stopTrackingSyncState: opts.stopTrackingSyncState || vi.fn(),
|
|
62
|
+
onCoValueUnmounted: opts.onCoValueUnmounted || vi.fn(),
|
|
61
63
|
close: opts.close || vi.fn().mockResolvedValue(undefined),
|
|
62
64
|
};
|
|
63
65
|
}
|
|
@@ -23,6 +23,7 @@ function createMockStorage(
|
|
|
23
23
|
callback: (unsyncedCoValueIDs: RawCoID[]) => void,
|
|
24
24
|
) => void;
|
|
25
25
|
stopTrackingSyncState?: (id: RawCoID) => void;
|
|
26
|
+
onCoValueUnmounted?: (id: RawCoID) => void;
|
|
26
27
|
close?: () => Promise<unknown> | undefined;
|
|
27
28
|
} = {},
|
|
28
29
|
): StorageAPI {
|
|
@@ -36,6 +37,7 @@ function createMockStorage(
|
|
|
36
37
|
trackCoValuesSyncState: opts.trackCoValuesSyncState || vi.fn(),
|
|
37
38
|
getUnsyncedCoValueIDs: opts.getUnsyncedCoValueIDs || vi.fn(),
|
|
38
39
|
stopTrackingSyncState: opts.stopTrackingSyncState || vi.fn(),
|
|
40
|
+
onCoValueUnmounted: opts.onCoValueUnmounted || vi.fn(),
|
|
39
41
|
close: opts.close || vi.fn().mockResolvedValue(undefined),
|
|
40
42
|
};
|
|
41
43
|
}
|