cojson 0.20.0 → 0.20.2
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 +21 -0
- package/dist/GarbageCollector.d.ts +3 -3
- package/dist/GarbageCollector.d.ts.map +1 -1
- package/dist/GarbageCollector.js +4 -4
- package/dist/GarbageCollector.js.map +1 -1
- package/dist/PeerState.d.ts +6 -1
- package/dist/PeerState.d.ts.map +1 -1
- package/dist/PeerState.js +18 -3
- package/dist/PeerState.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +26 -5
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +115 -50
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValues/coList.d.ts +1 -0
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coList.js +3 -0
- package/dist/coValues/coList.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -4
- package/dist/config.js.map +1 -1
- package/dist/exports.d.ts +3 -3
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +2 -2
- package/dist/exports.js.map +1 -1
- package/dist/localNode.d.ts +12 -0
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +51 -3
- package/dist/localNode.js.map +1 -1
- package/dist/queue/LinkedList.d.ts +9 -3
- package/dist/queue/LinkedList.d.ts.map +1 -1
- package/dist/queue/LinkedList.js +30 -1
- package/dist/queue/LinkedList.js.map +1 -1
- package/dist/queue/OutgoingLoadQueue.d.ts +95 -0
- package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -0
- package/dist/queue/OutgoingLoadQueue.js +240 -0
- package/dist/queue/OutgoingLoadQueue.js.map +1 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +34 -41
- package/dist/sync.js.map +1 -1
- package/dist/tests/LinkedList.test.js +90 -0
- package/dist/tests/LinkedList.test.js.map +1 -1
- package/dist/tests/OutgoingLoadQueue.test.d.ts +2 -0
- package/dist/tests/OutgoingLoadQueue.test.d.ts.map +1 -0
- package/dist/tests/OutgoingLoadQueue.test.js +814 -0
- package/dist/tests/OutgoingLoadQueue.test.js.map +1 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js +87 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- package/dist/tests/knownState.lazyLoading.test.js +44 -0
- package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
- package/dist/tests/sync.concurrentLoad.test.d.ts +2 -0
- package/dist/tests/sync.concurrentLoad.test.d.ts.map +1 -0
- package/dist/tests/sync.concurrentLoad.test.js +481 -0
- package/dist/tests/sync.concurrentLoad.test.js.map +1 -0
- package/dist/tests/sync.garbageCollection.test.js +87 -3
- package/dist/tests/sync.garbageCollection.test.js.map +1 -1
- package/dist/tests/sync.multipleServers.test.js +0 -62
- package/dist/tests/sync.multipleServers.test.js.map +1 -1
- package/dist/tests/sync.peerReconciliation.test.js +156 -0
- package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +1 -1
- package/dist/tests/testStorage.d.ts.map +1 -1
- package/dist/tests/testStorage.js +3 -1
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +1 -0
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +2 -1
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/GarbageCollector.ts +4 -3
- package/src/PeerState.ts +26 -3
- package/src/coValueCore/coValueCore.ts +129 -53
- package/src/coValues/coList.ts +4 -0
- package/src/config.ts +4 -4
- package/src/exports.ts +2 -2
- package/src/localNode.ts +65 -4
- package/src/queue/LinkedList.ts +34 -4
- package/src/queue/OutgoingLoadQueue.ts +307 -0
- package/src/sync.ts +37 -43
- package/src/tests/LinkedList.test.ts +111 -0
- package/src/tests/OutgoingLoadQueue.test.ts +1129 -0
- package/src/tests/coValueCore.loadFromStorage.test.ts +108 -0
- package/src/tests/knownState.lazyLoading.test.ts +52 -0
- package/src/tests/sync.concurrentLoad.test.ts +650 -0
- package/src/tests/sync.garbageCollection.test.ts +115 -3
- package/src/tests/sync.multipleServers.test.ts +0 -65
- package/src/tests/sync.peerReconciliation.test.ts +199 -0
- package/src/tests/sync.storage.test.ts +1 -1
- package/src/tests/testStorage.ts +3 -1
- package/src/tests/testUtils.ts +3 -1
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { CO_VALUE_LOADING_CONFIG, setMaxInFlightLoadsPerPeer, } from "../config.js";
|
|
3
|
+
import { blockMessageTypeOnOutgoingPeer, fillCoMapWithLargeData, loadCoValueOrFail, importContentIntoNode, setupTestNode, SyncMessagesLog, TEST_NODE_CONFIG, waitFor, } from "./testUtils.js";
|
|
4
|
+
let jazzCloud;
|
|
5
|
+
// Store original config values
|
|
6
|
+
let originalMaxInFlightLoads;
|
|
7
|
+
let originalTimeout;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
// We want to simulate a real world communication that happens asynchronously
|
|
10
|
+
TEST_NODE_CONFIG.withAsyncPeers = true;
|
|
11
|
+
originalMaxInFlightLoads =
|
|
12
|
+
CO_VALUE_LOADING_CONFIG.MAX_IN_FLIGHT_LOADS_PER_PEER;
|
|
13
|
+
originalTimeout = CO_VALUE_LOADING_CONFIG.TIMEOUT;
|
|
14
|
+
SyncMessagesLog.clear();
|
|
15
|
+
jazzCloud = setupTestNode({ isSyncServer: true });
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
// Restore original config
|
|
19
|
+
setMaxInFlightLoadsPerPeer(originalMaxInFlightLoads);
|
|
20
|
+
CO_VALUE_LOADING_CONFIG.TIMEOUT = originalTimeout;
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
describe("concurrent load", () => {
|
|
24
|
+
test("should throttle load requests when at capacity", async () => {
|
|
25
|
+
setMaxInFlightLoadsPerPeer(2);
|
|
26
|
+
const client = setupTestNode({
|
|
27
|
+
connected: false,
|
|
28
|
+
});
|
|
29
|
+
const { peerOnServer } = client.connectToSyncServer();
|
|
30
|
+
// Create multiple CoValues on the server
|
|
31
|
+
const group = jazzCloud.node.createGroup();
|
|
32
|
+
const map1 = group.createMap();
|
|
33
|
+
const map2 = group.createMap();
|
|
34
|
+
const map3 = group.createMap();
|
|
35
|
+
map1.set("key", "value1");
|
|
36
|
+
map2.set("key", "value2");
|
|
37
|
+
map3.set("key", "value3");
|
|
38
|
+
// Block content responses to see the throttling effect
|
|
39
|
+
const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {});
|
|
40
|
+
// Start loading all three
|
|
41
|
+
const promise1 = client.node.loadCoValueCore(map1.id);
|
|
42
|
+
const promise2 = client.node.loadCoValueCore(map2.id);
|
|
43
|
+
const promise3 = client.node.loadCoValueCore(map3.id);
|
|
44
|
+
// Wait for messages to be sent
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
46
|
+
// Get the LOAD messages sent
|
|
47
|
+
const loadMessages = SyncMessagesLog.messages.filter((m) => m.msg.action === "load");
|
|
48
|
+
// Only 2 LOAD messages should have been sent (throttled)
|
|
49
|
+
expect(loadMessages.length).toBe(2);
|
|
50
|
+
// Unblock and let it complete
|
|
51
|
+
blocker.unblock();
|
|
52
|
+
blocker.sendBlockedMessages();
|
|
53
|
+
await Promise.all([promise1, promise2, promise3]);
|
|
54
|
+
// After completion, all 3 should have been loaded
|
|
55
|
+
const allLoadMessages = SyncMessagesLog.messages.filter((m) => m.msg.action === "load");
|
|
56
|
+
expect(allLoadMessages.length).toBe(3);
|
|
57
|
+
// Verify all were loaded successfully despite throttling
|
|
58
|
+
expect(SyncMessagesLog.getMessages({
|
|
59
|
+
Group: group.core,
|
|
60
|
+
Map1: map1.core,
|
|
61
|
+
Map2: map2.core,
|
|
62
|
+
Map3: map3.core,
|
|
63
|
+
})).toMatchInlineSnapshot(`
|
|
64
|
+
[
|
|
65
|
+
"client -> server | LOAD Map1 sessions: empty",
|
|
66
|
+
"client -> server | LOAD Map2 sessions: empty",
|
|
67
|
+
"server -> client | CONTENT Group header: true new: After: 0 New: 3",
|
|
68
|
+
"server -> client | CONTENT Map1 header: true new: After: 0 New: 1",
|
|
69
|
+
"server -> client | CONTENT Map2 header: true new: After: 0 New: 1",
|
|
70
|
+
"client -> server | KNOWN Group sessions: header/3",
|
|
71
|
+
"client -> server | KNOWN Map1 sessions: header/1",
|
|
72
|
+
"client -> server | LOAD Map3 sessions: empty",
|
|
73
|
+
"client -> server | KNOWN Map2 sessions: header/1",
|
|
74
|
+
"server -> client | CONTENT Map3 header: true new: After: 0 New: 1",
|
|
75
|
+
"client -> server | KNOWN Map3 sessions: header/1",
|
|
76
|
+
]
|
|
77
|
+
`);
|
|
78
|
+
});
|
|
79
|
+
test("should process pending loads when capacity becomes available", async () => {
|
|
80
|
+
setMaxInFlightLoadsPerPeer(1);
|
|
81
|
+
const client = setupTestNode({
|
|
82
|
+
connected: true,
|
|
83
|
+
});
|
|
84
|
+
// Create multiple CoValues on the server
|
|
85
|
+
const group = jazzCloud.node.createGroup();
|
|
86
|
+
const map1 = group.createMap();
|
|
87
|
+
const map2 = group.createMap();
|
|
88
|
+
map1.set("key", "value1", "trusting");
|
|
89
|
+
map2.set("key", "value2", "trusting");
|
|
90
|
+
// Load both sequentially due to throttling
|
|
91
|
+
const [result1, result2] = await Promise.all([
|
|
92
|
+
loadCoValueOrFail(client.node, map1.id),
|
|
93
|
+
loadCoValueOrFail(client.node, map2.id),
|
|
94
|
+
]);
|
|
95
|
+
expect(result1.get("key")).toBe("value1");
|
|
96
|
+
expect(result2.get("key")).toBe("value2");
|
|
97
|
+
// Verify both were loaded successfully despite throttling
|
|
98
|
+
expect(SyncMessagesLog.getMessages({
|
|
99
|
+
Group: group.core,
|
|
100
|
+
Map1: map1.core,
|
|
101
|
+
Map2: map2.core,
|
|
102
|
+
})).toMatchInlineSnapshot(`
|
|
103
|
+
[
|
|
104
|
+
"client -> server | LOAD Map1 sessions: empty",
|
|
105
|
+
"server -> client | CONTENT Group header: true new: After: 0 New: 3",
|
|
106
|
+
"server -> client | CONTENT Map1 header: true new: After: 0 New: 1",
|
|
107
|
+
"client -> server | KNOWN Group sessions: header/3",
|
|
108
|
+
"client -> server | KNOWN Map1 sessions: header/1",
|
|
109
|
+
"client -> server | LOAD Map2 sessions: empty",
|
|
110
|
+
"server -> client | CONTENT Map2 header: true new: After: 0 New: 1",
|
|
111
|
+
"client -> server | KNOWN Map2 sessions: header/1",
|
|
112
|
+
]
|
|
113
|
+
`);
|
|
114
|
+
});
|
|
115
|
+
test("should prioritize unavailable CoValues over available ones", async () => {
|
|
116
|
+
setMaxInFlightLoadsPerPeer(1);
|
|
117
|
+
const client = setupTestNode({
|
|
118
|
+
connected: true,
|
|
119
|
+
});
|
|
120
|
+
// Create CoValues on the server
|
|
121
|
+
const group = jazzCloud.node.createGroup();
|
|
122
|
+
const map1 = group.createMap();
|
|
123
|
+
const map2 = group.createMap();
|
|
124
|
+
const map3 = group.createMap();
|
|
125
|
+
map1.set("key", "value1", "trusting");
|
|
126
|
+
map2.set("key", "value2", "trusting");
|
|
127
|
+
map3.set("key", "value3", "trusting");
|
|
128
|
+
// First, load map1 to make it "available" locally
|
|
129
|
+
await loadCoValueOrFail(client.node, map1.id);
|
|
130
|
+
SyncMessagesLog.clear();
|
|
131
|
+
// Update map1 on server (so client has stale version)
|
|
132
|
+
map1.set("key", "updated1", "trusting");
|
|
133
|
+
// Now load map2 (unavailable) and reload map1 (available but outdated)
|
|
134
|
+
// map2 should be prioritized
|
|
135
|
+
const [result1, result2] = await Promise.all([
|
|
136
|
+
loadCoValueOrFail(client.node, map1.id), // Available, lower priority
|
|
137
|
+
loadCoValueOrFail(client.node, map2.id), // Unavailable, higher priority
|
|
138
|
+
]);
|
|
139
|
+
// Both should succeed
|
|
140
|
+
expect(result2.get("key")).toBe("value2");
|
|
141
|
+
// map2 (unavailable) should have been loaded first
|
|
142
|
+
const loadMessages = SyncMessagesLog.messages.filter((m) => m.msg.action === "load");
|
|
143
|
+
expect(loadMessages.length).toBeGreaterThanOrEqual(1);
|
|
144
|
+
// The first load should be for map2 (unavailable, high priority)
|
|
145
|
+
expect(loadMessages[0]?.msg).toMatchObject({
|
|
146
|
+
action: "load",
|
|
147
|
+
id: map2.id,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
test("should handle high load with many concurrent requests", async () => {
|
|
151
|
+
setMaxInFlightLoadsPerPeer(5);
|
|
152
|
+
const client = setupTestNode({
|
|
153
|
+
connected: true,
|
|
154
|
+
});
|
|
155
|
+
// Create many CoValues on the server
|
|
156
|
+
const group = jazzCloud.node.createGroup();
|
|
157
|
+
const maps = Array.from({ length: 20 }, (_, i) => {
|
|
158
|
+
const map = group.createMap();
|
|
159
|
+
map.set("index", i, "trusting");
|
|
160
|
+
return map;
|
|
161
|
+
});
|
|
162
|
+
// Load all of them concurrently
|
|
163
|
+
const results = await Promise.all(maps.map((map) => loadCoValueOrFail(client.node, map.id)));
|
|
164
|
+
// All should have been loaded successfully
|
|
165
|
+
results.forEach((result, i) => {
|
|
166
|
+
expect(result.get("index")).toBe(i);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
test("should timeout load requests that take too long", async () => {
|
|
170
|
+
vi.useFakeTimers();
|
|
171
|
+
setMaxInFlightLoadsPerPeer(1);
|
|
172
|
+
CO_VALUE_LOADING_CONFIG.TIMEOUT = 1000;
|
|
173
|
+
const client = setupTestNode({
|
|
174
|
+
connected: false,
|
|
175
|
+
});
|
|
176
|
+
const { peerOnServer } = client.connectToSyncServer();
|
|
177
|
+
// Create a CoValue on the server
|
|
178
|
+
const group = jazzCloud.node.createGroup();
|
|
179
|
+
const map = group.createMap();
|
|
180
|
+
map.set("key", "value");
|
|
181
|
+
// Block content to simulate a slow/unresponsive server
|
|
182
|
+
const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
|
|
183
|
+
id: map.id,
|
|
184
|
+
});
|
|
185
|
+
const loadPromise = client.node.loadCoValueCore(map.id);
|
|
186
|
+
// Advance past the timeout
|
|
187
|
+
await vi.advanceTimersByTimeAsync(CO_VALUE_LOADING_CONFIG.TIMEOUT + 100);
|
|
188
|
+
// The queue slot should be freed
|
|
189
|
+
// The second retry attempt should happen after RETRY_DELAY
|
|
190
|
+
await vi.advanceTimersByTimeAsync(CO_VALUE_LOADING_CONFIG.RETRY_DELAY + 100);
|
|
191
|
+
// Unblock to let retries succeed
|
|
192
|
+
blocker.sendBlockedMessages();
|
|
193
|
+
blocker.unblock();
|
|
194
|
+
// Wait for the retry to complete
|
|
195
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
196
|
+
const result = await loadPromise;
|
|
197
|
+
// The retry should have succeeded (since we unblocked)
|
|
198
|
+
expect(result.isAvailable()).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
test("should free queue slots on disconnect", async () => {
|
|
201
|
+
setMaxInFlightLoadsPerPeer(2);
|
|
202
|
+
const client = setupTestNode({
|
|
203
|
+
connected: false,
|
|
204
|
+
});
|
|
205
|
+
const { peerState, peerOnServer } = client.connectToSyncServer();
|
|
206
|
+
// Create CoValues on the server
|
|
207
|
+
const group = jazzCloud.node.createGroup();
|
|
208
|
+
const map1 = group.createMap();
|
|
209
|
+
const map2 = group.createMap();
|
|
210
|
+
const map3 = group.createMap();
|
|
211
|
+
// Block content to keep requests in-flight
|
|
212
|
+
const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {});
|
|
213
|
+
// Start loading (will be in-flight)
|
|
214
|
+
client.node.loadCoValueCore(map1.id);
|
|
215
|
+
client.node.loadCoValueCore(map2.id);
|
|
216
|
+
client.node.loadCoValueCore(map3.id);
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
218
|
+
// Disconnect
|
|
219
|
+
peerState.gracefulShutdown();
|
|
220
|
+
// Queue should be cleared
|
|
221
|
+
// Reconnect and verify new requests can be sent
|
|
222
|
+
client.connectToSyncServer();
|
|
223
|
+
const result = await loadCoValueOrFail(client.node, map1.id);
|
|
224
|
+
expect(result.get("key")).toBeUndefined(); // map1 was created without a key
|
|
225
|
+
blocker.unblock();
|
|
226
|
+
});
|
|
227
|
+
test("should handle reconnection with pending loads", async () => {
|
|
228
|
+
setMaxInFlightLoadsPerPeer(1);
|
|
229
|
+
const client = setupTestNode({
|
|
230
|
+
connected: false,
|
|
231
|
+
});
|
|
232
|
+
const { peerState, peerOnServer } = client.connectToSyncServer({
|
|
233
|
+
persistent: true,
|
|
234
|
+
});
|
|
235
|
+
// Create a CoValue on the server
|
|
236
|
+
const group = jazzCloud.node.createGroup();
|
|
237
|
+
const map = group.createMap();
|
|
238
|
+
map.set("key", "value");
|
|
239
|
+
// Block content to keep request in-flight
|
|
240
|
+
blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
|
|
241
|
+
id: map.id,
|
|
242
|
+
});
|
|
243
|
+
// Start loading
|
|
244
|
+
const loadPromise = client.node.loadCoValueCore(map.id);
|
|
245
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
246
|
+
// Disconnect
|
|
247
|
+
peerState.gracefulShutdown();
|
|
248
|
+
// Reconnect
|
|
249
|
+
client.connectToSyncServer({
|
|
250
|
+
persistent: true,
|
|
251
|
+
});
|
|
252
|
+
// The load should complete after reconnection
|
|
253
|
+
const result = await loadPromise;
|
|
254
|
+
expect(result.isAvailable()).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
test("should maintain FIFO order for queued requests", async () => {
|
|
257
|
+
setMaxInFlightLoadsPerPeer(1);
|
|
258
|
+
const client = setupTestNode({
|
|
259
|
+
connected: false,
|
|
260
|
+
});
|
|
261
|
+
const { peerOnServer } = client.connectToSyncServer();
|
|
262
|
+
// Create CoValues on the server
|
|
263
|
+
const group = jazzCloud.node.createGroup();
|
|
264
|
+
const maps = Array.from({ length: 5 }, () => group.createMap());
|
|
265
|
+
// Block content to build up the queue
|
|
266
|
+
const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {});
|
|
267
|
+
// Start loading all maps (first one goes in-flight, rest queued)
|
|
268
|
+
const loadPromises = maps.map((map) => client.node.loadCoValueCore(map.id));
|
|
269
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
270
|
+
// Get the LOAD messages before unblocking
|
|
271
|
+
const loadMessagesBefore = SyncMessagesLog.messages.filter((m) => m.msg.action === "load");
|
|
272
|
+
// Only 1 should be sent (at capacity)
|
|
273
|
+
expect(loadMessagesBefore.length).toBe(1);
|
|
274
|
+
expect(loadMessagesBefore[0]?.msg).toMatchObject({
|
|
275
|
+
action: "load",
|
|
276
|
+
id: maps[0]?.id,
|
|
277
|
+
});
|
|
278
|
+
// Unblock to process the queue
|
|
279
|
+
blocker.sendBlockedMessages();
|
|
280
|
+
blocker.unblock();
|
|
281
|
+
await Promise.all(loadPromises);
|
|
282
|
+
// Verify all LOAD messages were sent
|
|
283
|
+
const allLoadMessages = SyncMessagesLog.messages.filter((m) => m.msg.action === "load");
|
|
284
|
+
// All 5 should eventually be sent
|
|
285
|
+
expect(allLoadMessages.length).toBe(5);
|
|
286
|
+
// They should be in order (maps[0], maps[1], maps[2], maps[3], maps[4])
|
|
287
|
+
for (let i = 0; i < allLoadMessages.length; i++) {
|
|
288
|
+
expect(allLoadMessages[i]?.msg).toMatchObject({
|
|
289
|
+
action: "load",
|
|
290
|
+
id: maps[i]?.id,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
test("should allow dependency loads to overflow the concurrency limit", async () => {
|
|
295
|
+
setMaxInFlightLoadsPerPeer(1);
|
|
296
|
+
const server = setupTestNode();
|
|
297
|
+
const client = setupTestNode({ connected: false });
|
|
298
|
+
client.connectToSyncServer({
|
|
299
|
+
ourName: "client",
|
|
300
|
+
syncServerName: "server",
|
|
301
|
+
syncServer: server.node,
|
|
302
|
+
});
|
|
303
|
+
// Create a CoValue on the server - the Map depends on the Group
|
|
304
|
+
const group = server.node.createGroup();
|
|
305
|
+
group.addMember("everyone", "writer");
|
|
306
|
+
const map = group.createMap();
|
|
307
|
+
map.set("key", "value");
|
|
308
|
+
// Delete the Group from server so it won't be pushed with the Map content
|
|
309
|
+
// skipVerify prevents the server from checking dependencies before sending
|
|
310
|
+
server.node.syncManager.disableTransactionVerification();
|
|
311
|
+
server.node.internalDeleteCoValue(group.id);
|
|
312
|
+
// Load the map from the client
|
|
313
|
+
// The flow is:
|
|
314
|
+
// 1. Client sends LOAD Map to server (takes the slot, limit=1)
|
|
315
|
+
// 2. Server responds with Map content (no deps pushed because skipVerify + Group deleted)
|
|
316
|
+
// 3. Client sees missing dependency (Group), sends LOAD Group to server
|
|
317
|
+
// This would be blocked by limit=1 without allowOverflow since Map load slot is taken
|
|
318
|
+
// 4. With allowOverflow, Group load bypasses the queue
|
|
319
|
+
// 5. Server responds with KNOWN Group (doesn't have it - was deleted)
|
|
320
|
+
// 6. Group content is moved back to server (simulating it becoming available)
|
|
321
|
+
// 7. Server responds with Group content
|
|
322
|
+
// 8. Client can now process Map content
|
|
323
|
+
const promise = loadCoValueOrFail(client.node, map.id);
|
|
324
|
+
// Wait for the Map content to be sent
|
|
325
|
+
await waitFor(() => SyncMessagesLog.messages.length >= 2);
|
|
326
|
+
importContentIntoNode(group.core, server.node, 1);
|
|
327
|
+
const result = await promise;
|
|
328
|
+
expect(result.get("key")).toBe("value");
|
|
329
|
+
// Verify both were loaded successfully despite throttling
|
|
330
|
+
expect(SyncMessagesLog.getMessages({
|
|
331
|
+
Group: group.core,
|
|
332
|
+
Map: map.core,
|
|
333
|
+
})).toMatchInlineSnapshot(`
|
|
334
|
+
[
|
|
335
|
+
"client -> server | LOAD Map sessions: empty",
|
|
336
|
+
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
|
|
337
|
+
"client -> server | LOAD Group sessions: empty",
|
|
338
|
+
"server -> client | KNOWN Group sessions: empty",
|
|
339
|
+
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
|
|
340
|
+
"client -> server | KNOWN Group sessions: header/5",
|
|
341
|
+
"client -> server | KNOWN Map sessions: header/1",
|
|
342
|
+
]
|
|
343
|
+
`);
|
|
344
|
+
});
|
|
345
|
+
test("should keep load slot occupied while streaming large CoValues", async () => {
|
|
346
|
+
setMaxInFlightLoadsPerPeer(1);
|
|
347
|
+
const client = setupTestNode({
|
|
348
|
+
connected: false,
|
|
349
|
+
});
|
|
350
|
+
const { peerState, peerOnServer } = client.connectToSyncServer();
|
|
351
|
+
// Create a large CoValue that requires multiple chunks to stream
|
|
352
|
+
const group = jazzCloud.node.createGroup();
|
|
353
|
+
const largeMap = group.createMap();
|
|
354
|
+
fillCoMapWithLargeData(largeMap);
|
|
355
|
+
// Create a small CoValue that will be queued
|
|
356
|
+
const smallMap = group.createMap();
|
|
357
|
+
smallMap.set("key", "value", "trusting");
|
|
358
|
+
// Block all the streaming chunks, except the first content message
|
|
359
|
+
const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
|
|
360
|
+
id: largeMap.id,
|
|
361
|
+
matcher: (msg) => msg.action === "content" && !msg.expectContentUntil,
|
|
362
|
+
});
|
|
363
|
+
// Start loading both maps concurrently
|
|
364
|
+
const largeMapOnClient = await client.node.loadCoValueCore(largeMap.id);
|
|
365
|
+
const smallMapPromise = client.node.loadCoValueCore(smallMap.id);
|
|
366
|
+
expect(client.node.getCoValue(largeMap.id).isStreaming()).toBe(true);
|
|
367
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
368
|
+
// The SmallMap load should still be waiting in the queue
|
|
369
|
+
expect(SyncMessagesLog.getMessages({
|
|
370
|
+
Group: group.core,
|
|
371
|
+
LargeMap: largeMapOnClient,
|
|
372
|
+
SmallMap: smallMap.core,
|
|
373
|
+
})).toMatchInlineSnapshot(`
|
|
374
|
+
[
|
|
375
|
+
"client -> server | LOAD LargeMap sessions: empty",
|
|
376
|
+
"server -> client | CONTENT Group header: true new: After: 0 New: 3",
|
|
377
|
+
"server -> client | CONTENT LargeMap header: true new: After: 0 New: 73 expectContentUntil: header/200",
|
|
378
|
+
"client -> server | KNOWN Group sessions: header/3",
|
|
379
|
+
"client -> server | KNOWN LargeMap sessions: header/73",
|
|
380
|
+
]
|
|
381
|
+
`);
|
|
382
|
+
// Now unblock and send all remaining chunks to complete streaming
|
|
383
|
+
blocker.unblock();
|
|
384
|
+
blocker.sendBlockedMessages();
|
|
385
|
+
await client.node.getCoValue(largeMap.id).waitForFullStreaming();
|
|
386
|
+
const loadedSmallMap = await smallMapPromise;
|
|
387
|
+
expect(loadedSmallMap.isAvailable()).toBe(true);
|
|
388
|
+
});
|
|
389
|
+
test("should prioritize user-initiated loads over peer reconciliation loads", async () => {
|
|
390
|
+
setMaxInFlightLoadsPerPeer(1);
|
|
391
|
+
// Create CoValues on the server before the client connects
|
|
392
|
+
const group = jazzCloud.node.createGroup();
|
|
393
|
+
const [a, b, c] = [
|
|
394
|
+
group.createMap({ test: "a" }),
|
|
395
|
+
group.createMap({ test: "b" }),
|
|
396
|
+
group.createMap({ test: "c" }),
|
|
397
|
+
];
|
|
398
|
+
const client = setupTestNode({
|
|
399
|
+
connected: false,
|
|
400
|
+
});
|
|
401
|
+
const { peerState } = client.connectToSyncServer();
|
|
402
|
+
// Load a CoValue to make it available locally
|
|
403
|
+
await loadCoValueOrFail(client.node, a.id);
|
|
404
|
+
await loadCoValueOrFail(client.node, b.id);
|
|
405
|
+
// Close the peer connection
|
|
406
|
+
peerState.gracefulShutdown();
|
|
407
|
+
SyncMessagesLog.clear();
|
|
408
|
+
// Reconnect to the server to trigger the reconciliation load
|
|
409
|
+
client.connectToSyncServer();
|
|
410
|
+
// The reconciliation load should be in the low-priority queue
|
|
411
|
+
// Now make a user-initiated load for a different CoValue
|
|
412
|
+
await loadCoValueOrFail(client.node, c.id);
|
|
413
|
+
// Wait for the reconciliation loads to be sent
|
|
414
|
+
await waitFor(() => SyncMessagesLog.messages.length >= 8);
|
|
415
|
+
// Expect Group, C, A, B
|
|
416
|
+
expect(SyncMessagesLog.getMessages({
|
|
417
|
+
Group: group.core,
|
|
418
|
+
A: a.core,
|
|
419
|
+
B: b.core,
|
|
420
|
+
C: c.core,
|
|
421
|
+
})).toMatchInlineSnapshot(`
|
|
422
|
+
[
|
|
423
|
+
"client -> server | LOAD Group sessions: header/3",
|
|
424
|
+
"server -> client | KNOWN Group sessions: header/3",
|
|
425
|
+
"client -> server | LOAD C sessions: empty",
|
|
426
|
+
"server -> client | CONTENT C header: true new: After: 0 New: 1",
|
|
427
|
+
"client -> server | KNOWN C sessions: header/1",
|
|
428
|
+
"client -> server | LOAD A sessions: header/1",
|
|
429
|
+
"server -> client | KNOWN A sessions: header/1",
|
|
430
|
+
"client -> server | LOAD B sessions: header/1",
|
|
431
|
+
"server -> client | KNOWN B sessions: header/1",
|
|
432
|
+
]
|
|
433
|
+
`);
|
|
434
|
+
});
|
|
435
|
+
test("should upgrade low-priority reconciliation load to high-priority when user requests it", async () => {
|
|
436
|
+
setMaxInFlightLoadsPerPeer(1);
|
|
437
|
+
// Create CoValues on the server before the client connects
|
|
438
|
+
const group = jazzCloud.node.createGroup();
|
|
439
|
+
const [a, b, c] = [
|
|
440
|
+
group.createMap({ test: "a" }),
|
|
441
|
+
group.createMap({ test: "b" }),
|
|
442
|
+
group.createMap({ test: "c" }),
|
|
443
|
+
];
|
|
444
|
+
const client = setupTestNode({
|
|
445
|
+
connected: false,
|
|
446
|
+
});
|
|
447
|
+
// Load both CoValues to make them marked as unavailable
|
|
448
|
+
await client.node.loadCoValueCore(a.id);
|
|
449
|
+
await client.node.loadCoValueCore(b.id);
|
|
450
|
+
await client.node.loadCoValueCore(c.id);
|
|
451
|
+
// Reconnect to the server to trigger the reconciliation load
|
|
452
|
+
client.connectToSyncServer();
|
|
453
|
+
// The reconciliation load should be in the low-priority queue
|
|
454
|
+
// Now try to bump-up the priority of the load for c
|
|
455
|
+
client.node.loadCoValueCore(c.id);
|
|
456
|
+
// Wait for the reconciliation loads to be sent
|
|
457
|
+
await waitFor(() => SyncMessagesLog.messages.length >= 6);
|
|
458
|
+
// Expect A, C, B
|
|
459
|
+
expect(SyncMessagesLog.getMessages({
|
|
460
|
+
Group: group.core,
|
|
461
|
+
A: a.core,
|
|
462
|
+
B: b.core,
|
|
463
|
+
C: c.core,
|
|
464
|
+
})).toMatchInlineSnapshot(`
|
|
465
|
+
[
|
|
466
|
+
"client -> server | LOAD A sessions: empty",
|
|
467
|
+
"server -> client | CONTENT Group header: true new: After: 0 New: 3",
|
|
468
|
+
"server -> client | CONTENT A header: true new: After: 0 New: 1",
|
|
469
|
+
"client -> server | KNOWN Group sessions: header/3",
|
|
470
|
+
"client -> server | KNOWN A sessions: header/1",
|
|
471
|
+
"client -> server | LOAD C sessions: empty",
|
|
472
|
+
"server -> client | CONTENT C header: true new: After: 0 New: 1",
|
|
473
|
+
"client -> server | KNOWN C sessions: header/1",
|
|
474
|
+
"client -> server | LOAD B sessions: empty",
|
|
475
|
+
"server -> client | CONTENT B header: true new: After: 0 New: 1",
|
|
476
|
+
"client -> server | KNOWN B sessions: header/1",
|
|
477
|
+
]
|
|
478
|
+
`);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
//# sourceMappingURL=sync.concurrentLoad.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.concurrentLoad.test.js","sourceRoot":"","sources":["../../src/tests/sync.concurrentLoad.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { expectMap } from "../coValue";
|
|
2
3
|
import { setGarbageCollectorMaxAge } from "../config";
|
|
3
4
|
import { SyncMessagesLog, TEST_NODE_CONFIG, loadCoValueOrFail, setupTestNode, } from "./testUtils";
|
|
4
5
|
// We want to simulate a real world communication that happens asynchronously
|
|
@@ -126,19 +127,21 @@ describe("sync after the garbage collector has run", () => {
|
|
|
126
127
|
// The storage should work even after the coValue is unmounted, so the load should be successful
|
|
127
128
|
const mapOnServer = await loadCoValueOrFail(jazzCloud.node, map.id);
|
|
128
129
|
expect(mapOnServer.get("hello")).toEqual("updated");
|
|
130
|
+
// With garbageCollected shells, client uses cached knownState (header/1)
|
|
131
|
+
// which is more accurate than asking storage (which returns empty)
|
|
129
132
|
expect(SyncMessagesLog.getMessages({
|
|
130
133
|
Group: group.core,
|
|
131
134
|
Map: map.core,
|
|
132
135
|
})).toMatchInlineSnapshot(`
|
|
133
136
|
[
|
|
134
|
-
"client -> server | LOAD Map sessions:
|
|
137
|
+
"client -> server | LOAD Map sessions: header/1",
|
|
135
138
|
"client -> server | LOAD Group sessions: header/3",
|
|
136
139
|
"client -> storage | CONTENT Group header: true new: After: 0 New: 3",
|
|
137
140
|
"client -> server | CONTENT Group header: true new: After: 0 New: 3",
|
|
138
141
|
"client -> storage | CONTENT Map header: true new: After: 0 New: 1",
|
|
139
142
|
"client -> server | CONTENT Map header: true new: After: 0 New: 1",
|
|
140
|
-
"server -> storage |
|
|
141
|
-
"storage -> server |
|
|
143
|
+
"server -> storage | GET_KNOWN_STATE Map",
|
|
144
|
+
"storage -> server | GET_KNOWN_STATE_RESULT Map sessions: empty",
|
|
142
145
|
"server -> client | KNOWN Map sessions: empty",
|
|
143
146
|
"server -> storage | GET_KNOWN_STATE Group",
|
|
144
147
|
"storage -> server | GET_KNOWN_STATE_RESULT Group sessions: empty",
|
|
@@ -150,5 +153,86 @@ describe("sync after the garbage collector has run", () => {
|
|
|
150
153
|
]
|
|
151
154
|
`);
|
|
152
155
|
});
|
|
156
|
+
test("knownStateWithStreaming returns lastKnownState for garbageCollected CoValues", async () => {
|
|
157
|
+
// This test verifies that knownStateWithStreaming() returns the cached lastKnownState
|
|
158
|
+
// for garbage-collected CoValues, not an empty state. This is important for peer
|
|
159
|
+
// reconciliation where we want to send the last known state to minimize data transfer.
|
|
160
|
+
const client = setupTestNode();
|
|
161
|
+
client.addStorage({ ourName: "client" });
|
|
162
|
+
client.node.enableGarbageCollector();
|
|
163
|
+
const group = client.node.createGroup();
|
|
164
|
+
const map = group.createMap();
|
|
165
|
+
map.set("hello", "world", "trusting");
|
|
166
|
+
// Sync to server
|
|
167
|
+
client.connectToSyncServer();
|
|
168
|
+
await client.node.syncManager.waitForAllCoValuesSync();
|
|
169
|
+
// Capture known state before GC
|
|
170
|
+
const originalKnownState = map.core.knownState();
|
|
171
|
+
const originalKnownStateWithStreaming = map.core.knownStateWithStreaming();
|
|
172
|
+
// For available CoValues, both should be equal (no streaming in progress)
|
|
173
|
+
expect(originalKnownState).toEqual(originalKnownStateWithStreaming);
|
|
174
|
+
expect(originalKnownState.header).toBe(true);
|
|
175
|
+
expect(Object.values(originalKnownState.sessions)[0]).toBe(1);
|
|
176
|
+
// Disconnect before GC
|
|
177
|
+
client.disconnect();
|
|
178
|
+
// Run GC to create garbageCollected shell
|
|
179
|
+
client.node.garbageCollector?.collect();
|
|
180
|
+
client.node.garbageCollector?.collect();
|
|
181
|
+
const gcCoValue = client.node.getCoValue(map.id);
|
|
182
|
+
expect(gcCoValue.loadingState).toBe("garbageCollected");
|
|
183
|
+
// Key assertion: knownStateWithStreaming() should return lastKnownState, not empty state
|
|
184
|
+
const gcKnownState = gcCoValue.knownState();
|
|
185
|
+
const gcKnownStateWithStreaming = gcCoValue.knownStateWithStreaming();
|
|
186
|
+
// Both should equal the original known state (the cached lastKnownState)
|
|
187
|
+
expect(gcKnownState).toEqual(originalKnownState);
|
|
188
|
+
expect(gcKnownStateWithStreaming).toEqual(originalKnownState);
|
|
189
|
+
// Specifically verify it's NOT an empty state
|
|
190
|
+
expect(gcKnownStateWithStreaming.header).toBe(true);
|
|
191
|
+
expect(Object.keys(gcKnownStateWithStreaming.sessions).length).toBeGreaterThan(0);
|
|
192
|
+
});
|
|
193
|
+
test("garbageCollected CoValues read from verified content after reload", async () => {
|
|
194
|
+
// This test verifies that after reloading a GC'd CoValue:
|
|
195
|
+
// 1. lastKnownState is cleared
|
|
196
|
+
// 2. knownState() returns data from verified content (not cached)
|
|
197
|
+
// We prove this by adding a transaction after reload and verifying knownState() updates
|
|
198
|
+
const client = setupTestNode();
|
|
199
|
+
client.addStorage({ ourName: "client" });
|
|
200
|
+
client.node.enableGarbageCollector();
|
|
201
|
+
const group = client.node.createGroup();
|
|
202
|
+
const map = group.createMap();
|
|
203
|
+
map.set("hello", "world", "trusting");
|
|
204
|
+
// Sync to server
|
|
205
|
+
client.connectToSyncServer();
|
|
206
|
+
await client.node.syncManager.waitForAllCoValuesSync();
|
|
207
|
+
// Capture known state before GC (has 1 transaction)
|
|
208
|
+
const originalKnownState = map.core.knownState();
|
|
209
|
+
const originalSessionCount = Object.values(originalKnownState.sessions)[0];
|
|
210
|
+
expect(originalSessionCount).toBe(1);
|
|
211
|
+
// Disconnect before GC
|
|
212
|
+
client.disconnect();
|
|
213
|
+
// Run GC to create garbageCollected shell
|
|
214
|
+
client.node.garbageCollector?.collect();
|
|
215
|
+
client.node.garbageCollector?.collect();
|
|
216
|
+
const gcMap = client.node.getCoValue(map.id);
|
|
217
|
+
expect(gcMap.loadingState).toBe("garbageCollected");
|
|
218
|
+
// Verify knownState() returns lastKnownState (still shows 1 transaction)
|
|
219
|
+
expect(gcMap.knownState()).toEqual(originalKnownState);
|
|
220
|
+
// Reconnect and reload
|
|
221
|
+
client.connectToSyncServer();
|
|
222
|
+
const reloadedCore = await client.node.loadCoValueCore(map.id);
|
|
223
|
+
// Verify CoValue is now available
|
|
224
|
+
expect(reloadedCore.loadingState).toBe("available");
|
|
225
|
+
expect(reloadedCore.isAvailable()).toBe(true);
|
|
226
|
+
// At this point, knownState() should be reading from verified content
|
|
227
|
+
// To prove this, we add a new transaction and verify knownState() updates
|
|
228
|
+
const reloadedContent = expectMap(reloadedCore.getCurrentContent());
|
|
229
|
+
reloadedContent.set("hello", "updated locally", "trusting");
|
|
230
|
+
// Verify knownState() now shows 2 transactions
|
|
231
|
+
// This proves we're reading from verified content, not cached lastKnownState
|
|
232
|
+
const newKnownState = reloadedCore.knownState();
|
|
233
|
+
const newSessionCount = Object.values(newKnownState.sessions)[0];
|
|
234
|
+
expect(newSessionCount).toBe(2);
|
|
235
|
+
expect(newKnownState).not.toEqual(originalKnownState);
|
|
236
|
+
});
|
|
153
237
|
});
|
|
154
238
|
//# sourceMappingURL=sync.garbageCollection.test.js.map
|