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.
Files changed (91) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +21 -0
  3. package/dist/GarbageCollector.d.ts +3 -3
  4. package/dist/GarbageCollector.d.ts.map +1 -1
  5. package/dist/GarbageCollector.js +4 -4
  6. package/dist/GarbageCollector.js.map +1 -1
  7. package/dist/PeerState.d.ts +6 -1
  8. package/dist/PeerState.d.ts.map +1 -1
  9. package/dist/PeerState.js +18 -3
  10. package/dist/PeerState.js.map +1 -1
  11. package/dist/coValueCore/coValueCore.d.ts +26 -5
  12. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  13. package/dist/coValueCore/coValueCore.js +115 -50
  14. package/dist/coValueCore/coValueCore.js.map +1 -1
  15. package/dist/coValues/coList.d.ts +1 -0
  16. package/dist/coValues/coList.d.ts.map +1 -1
  17. package/dist/coValues/coList.js +3 -0
  18. package/dist/coValues/coList.js.map +1 -1
  19. package/dist/config.d.ts +2 -2
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +4 -4
  22. package/dist/config.js.map +1 -1
  23. package/dist/exports.d.ts +3 -3
  24. package/dist/exports.d.ts.map +1 -1
  25. package/dist/exports.js +2 -2
  26. package/dist/exports.js.map +1 -1
  27. package/dist/localNode.d.ts +12 -0
  28. package/dist/localNode.d.ts.map +1 -1
  29. package/dist/localNode.js +51 -3
  30. package/dist/localNode.js.map +1 -1
  31. package/dist/queue/LinkedList.d.ts +9 -3
  32. package/dist/queue/LinkedList.d.ts.map +1 -1
  33. package/dist/queue/LinkedList.js +30 -1
  34. package/dist/queue/LinkedList.js.map +1 -1
  35. package/dist/queue/OutgoingLoadQueue.d.ts +95 -0
  36. package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -0
  37. package/dist/queue/OutgoingLoadQueue.js +240 -0
  38. package/dist/queue/OutgoingLoadQueue.js.map +1 -0
  39. package/dist/sync.d.ts.map +1 -1
  40. package/dist/sync.js +34 -41
  41. package/dist/sync.js.map +1 -1
  42. package/dist/tests/LinkedList.test.js +90 -0
  43. package/dist/tests/LinkedList.test.js.map +1 -1
  44. package/dist/tests/OutgoingLoadQueue.test.d.ts +2 -0
  45. package/dist/tests/OutgoingLoadQueue.test.d.ts.map +1 -0
  46. package/dist/tests/OutgoingLoadQueue.test.js +814 -0
  47. package/dist/tests/OutgoingLoadQueue.test.js.map +1 -0
  48. package/dist/tests/coValueCore.loadFromStorage.test.js +87 -0
  49. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  50. package/dist/tests/knownState.lazyLoading.test.js +44 -0
  51. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  52. package/dist/tests/sync.concurrentLoad.test.d.ts +2 -0
  53. package/dist/tests/sync.concurrentLoad.test.d.ts.map +1 -0
  54. package/dist/tests/sync.concurrentLoad.test.js +481 -0
  55. package/dist/tests/sync.concurrentLoad.test.js.map +1 -0
  56. package/dist/tests/sync.garbageCollection.test.js +87 -3
  57. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  58. package/dist/tests/sync.multipleServers.test.js +0 -62
  59. package/dist/tests/sync.multipleServers.test.js.map +1 -1
  60. package/dist/tests/sync.peerReconciliation.test.js +156 -0
  61. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  62. package/dist/tests/sync.storage.test.js +1 -1
  63. package/dist/tests/testStorage.d.ts.map +1 -1
  64. package/dist/tests/testStorage.js +3 -1
  65. package/dist/tests/testStorage.js.map +1 -1
  66. package/dist/tests/testUtils.d.ts +1 -0
  67. package/dist/tests/testUtils.d.ts.map +1 -1
  68. package/dist/tests/testUtils.js +2 -1
  69. package/dist/tests/testUtils.js.map +1 -1
  70. package/package.json +4 -4
  71. package/src/GarbageCollector.ts +4 -3
  72. package/src/PeerState.ts +26 -3
  73. package/src/coValueCore/coValueCore.ts +129 -53
  74. package/src/coValues/coList.ts +4 -0
  75. package/src/config.ts +4 -4
  76. package/src/exports.ts +2 -2
  77. package/src/localNode.ts +65 -4
  78. package/src/queue/LinkedList.ts +34 -4
  79. package/src/queue/OutgoingLoadQueue.ts +307 -0
  80. package/src/sync.ts +37 -43
  81. package/src/tests/LinkedList.test.ts +111 -0
  82. package/src/tests/OutgoingLoadQueue.test.ts +1129 -0
  83. package/src/tests/coValueCore.loadFromStorage.test.ts +108 -0
  84. package/src/tests/knownState.lazyLoading.test.ts +52 -0
  85. package/src/tests/sync.concurrentLoad.test.ts +650 -0
  86. package/src/tests/sync.garbageCollection.test.ts +115 -3
  87. package/src/tests/sync.multipleServers.test.ts +0 -65
  88. package/src/tests/sync.peerReconciliation.test.ts +199 -0
  89. package/src/tests/sync.storage.test.ts +1 -1
  90. package/src/tests/testStorage.ts +3 -1
  91. package/src/tests/testUtils.ts +3 -1
@@ -0,0 +1,650 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
+ import {
3
+ CO_VALUE_LOADING_CONFIG,
4
+ setMaxInFlightLoadsPerPeer,
5
+ } from "../config.js";
6
+ import {
7
+ blockMessageTypeOnOutgoingPeer,
8
+ fillCoMapWithLargeData,
9
+ loadCoValueOrFail,
10
+ importContentIntoNode,
11
+ setupTestNode,
12
+ SyncMessagesLog,
13
+ TEST_NODE_CONFIG,
14
+ waitFor,
15
+ } from "./testUtils.js";
16
+
17
+ let jazzCloud: ReturnType<typeof setupTestNode>;
18
+
19
+ // Store original config values
20
+ let originalMaxInFlightLoads: number;
21
+ let originalTimeout: number;
22
+
23
+ beforeEach(async () => {
24
+ // We want to simulate a real world communication that happens asynchronously
25
+ TEST_NODE_CONFIG.withAsyncPeers = true;
26
+
27
+ originalMaxInFlightLoads =
28
+ CO_VALUE_LOADING_CONFIG.MAX_IN_FLIGHT_LOADS_PER_PEER;
29
+ originalTimeout = CO_VALUE_LOADING_CONFIG.TIMEOUT;
30
+
31
+ SyncMessagesLog.clear();
32
+ jazzCloud = setupTestNode({ isSyncServer: true });
33
+ });
34
+
35
+ afterEach(() => {
36
+ // Restore original config
37
+ setMaxInFlightLoadsPerPeer(originalMaxInFlightLoads);
38
+ CO_VALUE_LOADING_CONFIG.TIMEOUT = originalTimeout;
39
+ vi.useRealTimers();
40
+ });
41
+
42
+ describe("concurrent load", () => {
43
+ test("should throttle load requests when at capacity", async () => {
44
+ setMaxInFlightLoadsPerPeer(2);
45
+
46
+ const client = setupTestNode({
47
+ connected: false,
48
+ });
49
+
50
+ const { peerOnServer } = client.connectToSyncServer();
51
+
52
+ // Create multiple CoValues on the server
53
+ const group = jazzCloud.node.createGroup();
54
+ const map1 = group.createMap();
55
+ const map2 = group.createMap();
56
+ const map3 = group.createMap();
57
+
58
+ map1.set("key", "value1");
59
+ map2.set("key", "value2");
60
+ map3.set("key", "value3");
61
+
62
+ // Block content responses to see the throttling effect
63
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {});
64
+
65
+ // Start loading all three
66
+ const promise1 = client.node.loadCoValueCore(map1.id);
67
+ const promise2 = client.node.loadCoValueCore(map2.id);
68
+ const promise3 = client.node.loadCoValueCore(map3.id);
69
+
70
+ // Wait for messages to be sent
71
+ await new Promise((resolve) => setTimeout(resolve, 10));
72
+
73
+ // Get the LOAD messages sent
74
+ const loadMessages = SyncMessagesLog.messages.filter(
75
+ (m) => m.msg.action === "load",
76
+ );
77
+
78
+ // Only 2 LOAD messages should have been sent (throttled)
79
+ expect(loadMessages.length).toBe(2);
80
+
81
+ // Unblock and let it complete
82
+ blocker.unblock();
83
+ blocker.sendBlockedMessages();
84
+
85
+ await Promise.all([promise1, promise2, promise3]);
86
+
87
+ // After completion, all 3 should have been loaded
88
+ const allLoadMessages = SyncMessagesLog.messages.filter(
89
+ (m) => m.msg.action === "load",
90
+ );
91
+ expect(allLoadMessages.length).toBe(3);
92
+
93
+ // Verify all were loaded successfully despite throttling
94
+ expect(
95
+ SyncMessagesLog.getMessages({
96
+ Group: group.core,
97
+ Map1: map1.core,
98
+ Map2: map2.core,
99
+ Map3: map3.core,
100
+ }),
101
+ ).toMatchInlineSnapshot(`
102
+ [
103
+ "client -> server | LOAD Map1 sessions: empty",
104
+ "client -> server | LOAD Map2 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
+ "server -> client | CONTENT Map2 header: true new: After: 0 New: 1",
108
+ "client -> server | KNOWN Group sessions: header/3",
109
+ "client -> server | KNOWN Map1 sessions: header/1",
110
+ "client -> server | LOAD Map3 sessions: empty",
111
+ "client -> server | KNOWN Map2 sessions: header/1",
112
+ "server -> client | CONTENT Map3 header: true new: After: 0 New: 1",
113
+ "client -> server | KNOWN Map3 sessions: header/1",
114
+ ]
115
+ `);
116
+ });
117
+
118
+ test("should process pending loads when capacity becomes available", async () => {
119
+ setMaxInFlightLoadsPerPeer(1);
120
+
121
+ const client = setupTestNode({
122
+ connected: true,
123
+ });
124
+
125
+ // Create multiple CoValues on the server
126
+ const group = jazzCloud.node.createGroup();
127
+ const map1 = group.createMap();
128
+ const map2 = group.createMap();
129
+
130
+ map1.set("key", "value1", "trusting");
131
+ map2.set("key", "value2", "trusting");
132
+
133
+ // Load both sequentially due to throttling
134
+ const [result1, result2] = await Promise.all([
135
+ loadCoValueOrFail(client.node, map1.id),
136
+ loadCoValueOrFail(client.node, map2.id),
137
+ ]);
138
+
139
+ expect(result1.get("key")).toBe("value1");
140
+ expect(result2.get("key")).toBe("value2");
141
+
142
+ // Verify both were loaded successfully despite throttling
143
+ expect(
144
+ SyncMessagesLog.getMessages({
145
+ Group: group.core,
146
+ Map1: map1.core,
147
+ Map2: map2.core,
148
+ }),
149
+ ).toMatchInlineSnapshot(`
150
+ [
151
+ "client -> server | LOAD Map1 sessions: empty",
152
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
153
+ "server -> client | CONTENT Map1 header: true new: After: 0 New: 1",
154
+ "client -> server | KNOWN Group sessions: header/3",
155
+ "client -> server | KNOWN Map1 sessions: header/1",
156
+ "client -> server | LOAD Map2 sessions: empty",
157
+ "server -> client | CONTENT Map2 header: true new: After: 0 New: 1",
158
+ "client -> server | KNOWN Map2 sessions: header/1",
159
+ ]
160
+ `);
161
+ });
162
+
163
+ test("should prioritize unavailable CoValues over available ones", async () => {
164
+ setMaxInFlightLoadsPerPeer(1);
165
+
166
+ const client = setupTestNode({
167
+ connected: true,
168
+ });
169
+
170
+ // Create CoValues on the server
171
+ const group = jazzCloud.node.createGroup();
172
+ const map1 = group.createMap();
173
+ const map2 = group.createMap();
174
+ const map3 = group.createMap();
175
+
176
+ map1.set("key", "value1", "trusting");
177
+ map2.set("key", "value2", "trusting");
178
+ map3.set("key", "value3", "trusting");
179
+
180
+ // First, load map1 to make it "available" locally
181
+ await loadCoValueOrFail(client.node, map1.id);
182
+
183
+ SyncMessagesLog.clear();
184
+
185
+ // Update map1 on server (so client has stale version)
186
+ map1.set("key", "updated1", "trusting");
187
+
188
+ // Now load map2 (unavailable) and reload map1 (available but outdated)
189
+ // map2 should be prioritized
190
+ const [result1, result2] = await Promise.all([
191
+ loadCoValueOrFail(client.node, map1.id), // Available, lower priority
192
+ loadCoValueOrFail(client.node, map2.id), // Unavailable, higher priority
193
+ ]);
194
+
195
+ // Both should succeed
196
+ expect(result2.get("key")).toBe("value2");
197
+
198
+ // map2 (unavailable) should have been loaded first
199
+ const loadMessages = SyncMessagesLog.messages.filter(
200
+ (m) => m.msg.action === "load",
201
+ );
202
+ expect(loadMessages.length).toBeGreaterThanOrEqual(1);
203
+ // The first load should be for map2 (unavailable, high priority)
204
+ expect(loadMessages[0]?.msg).toMatchObject({
205
+ action: "load",
206
+ id: map2.id,
207
+ });
208
+ });
209
+
210
+ test("should handle high load with many concurrent requests", async () => {
211
+ setMaxInFlightLoadsPerPeer(5);
212
+
213
+ const client = setupTestNode({
214
+ connected: true,
215
+ });
216
+
217
+ // Create many CoValues on the server
218
+ const group = jazzCloud.node.createGroup();
219
+ const maps = Array.from({ length: 20 }, (_, i) => {
220
+ const map = group.createMap();
221
+ map.set("index", i, "trusting");
222
+ return map;
223
+ });
224
+
225
+ // Load all of them concurrently
226
+ const results = await Promise.all(
227
+ maps.map((map) => loadCoValueOrFail(client.node, map.id)),
228
+ );
229
+
230
+ // All should have been loaded successfully
231
+ results.forEach((result, i) => {
232
+ expect(result.get("index")).toBe(i);
233
+ });
234
+ });
235
+
236
+ test("should timeout load requests that take too long", async () => {
237
+ vi.useFakeTimers();
238
+ setMaxInFlightLoadsPerPeer(1);
239
+ CO_VALUE_LOADING_CONFIG.TIMEOUT = 1000;
240
+
241
+ const client = setupTestNode({
242
+ connected: false,
243
+ });
244
+
245
+ const { peerOnServer } = client.connectToSyncServer();
246
+
247
+ // Create a CoValue on the server
248
+ const group = jazzCloud.node.createGroup();
249
+ const map = group.createMap();
250
+ map.set("key", "value");
251
+
252
+ // Block content to simulate a slow/unresponsive server
253
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
254
+ id: map.id,
255
+ });
256
+
257
+ const loadPromise = client.node.loadCoValueCore(map.id);
258
+
259
+ // Advance past the timeout
260
+ await vi.advanceTimersByTimeAsync(CO_VALUE_LOADING_CONFIG.TIMEOUT + 100);
261
+
262
+ // The queue slot should be freed
263
+ // The second retry attempt should happen after RETRY_DELAY
264
+ await vi.advanceTimersByTimeAsync(
265
+ CO_VALUE_LOADING_CONFIG.RETRY_DELAY + 100,
266
+ );
267
+
268
+ // Unblock to let retries succeed
269
+ blocker.sendBlockedMessages();
270
+ blocker.unblock();
271
+
272
+ // Wait for the retry to complete
273
+ await vi.advanceTimersByTimeAsync(100);
274
+
275
+ const result = await loadPromise;
276
+
277
+ // The retry should have succeeded (since we unblocked)
278
+ expect(result.isAvailable()).toBe(true);
279
+ });
280
+
281
+ test("should free queue slots on disconnect", async () => {
282
+ setMaxInFlightLoadsPerPeer(2);
283
+
284
+ const client = setupTestNode({
285
+ connected: false,
286
+ });
287
+
288
+ const { peerState, peerOnServer } = client.connectToSyncServer();
289
+
290
+ // Create CoValues on the server
291
+ const group = jazzCloud.node.createGroup();
292
+ const map1 = group.createMap();
293
+ const map2 = group.createMap();
294
+ const map3 = group.createMap();
295
+
296
+ // Block content to keep requests in-flight
297
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {});
298
+
299
+ // Start loading (will be in-flight)
300
+ client.node.loadCoValueCore(map1.id);
301
+ client.node.loadCoValueCore(map2.id);
302
+ client.node.loadCoValueCore(map3.id);
303
+
304
+ await new Promise((resolve) => setTimeout(resolve, 10));
305
+
306
+ // Disconnect
307
+ peerState.gracefulShutdown();
308
+
309
+ // Queue should be cleared
310
+ // Reconnect and verify new requests can be sent
311
+ client.connectToSyncServer();
312
+
313
+ const result = await loadCoValueOrFail(client.node, map1.id);
314
+ expect(result.get("key")).toBeUndefined(); // map1 was created without a key
315
+
316
+ blocker.unblock();
317
+ });
318
+
319
+ test("should handle reconnection with pending loads", async () => {
320
+ setMaxInFlightLoadsPerPeer(1);
321
+
322
+ const client = setupTestNode({
323
+ connected: false,
324
+ });
325
+
326
+ const { peerState, peerOnServer } = client.connectToSyncServer({
327
+ persistent: true,
328
+ });
329
+
330
+ // Create a CoValue on the server
331
+ const group = jazzCloud.node.createGroup();
332
+ const map = group.createMap();
333
+ map.set("key", "value");
334
+
335
+ // Block content to keep request in-flight
336
+ blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
337
+ id: map.id,
338
+ });
339
+
340
+ // Start loading
341
+ const loadPromise = client.node.loadCoValueCore(map.id);
342
+
343
+ await new Promise((resolve) => setTimeout(resolve, 10));
344
+
345
+ // Disconnect
346
+ peerState.gracefulShutdown();
347
+
348
+ // Reconnect
349
+ client.connectToSyncServer({
350
+ persistent: true,
351
+ });
352
+
353
+ // The load should complete after reconnection
354
+ const result = await loadPromise;
355
+ expect(result.isAvailable()).toBe(true);
356
+ });
357
+
358
+ test("should maintain FIFO order for queued requests", async () => {
359
+ setMaxInFlightLoadsPerPeer(1);
360
+
361
+ const client = setupTestNode({
362
+ connected: false,
363
+ });
364
+
365
+ const { peerOnServer } = client.connectToSyncServer();
366
+
367
+ // Create CoValues on the server
368
+ const group = jazzCloud.node.createGroup();
369
+ const maps = Array.from({ length: 5 }, () => group.createMap());
370
+
371
+ // Block content to build up the queue
372
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {});
373
+
374
+ // Start loading all maps (first one goes in-flight, rest queued)
375
+ const loadPromises = maps.map((map) => client.node.loadCoValueCore(map.id));
376
+
377
+ await new Promise((resolve) => setTimeout(resolve, 10));
378
+
379
+ // Get the LOAD messages before unblocking
380
+ const loadMessagesBefore = SyncMessagesLog.messages.filter(
381
+ (m) => m.msg.action === "load",
382
+ );
383
+
384
+ // Only 1 should be sent (at capacity)
385
+ expect(loadMessagesBefore.length).toBe(1);
386
+ expect(loadMessagesBefore[0]?.msg).toMatchObject({
387
+ action: "load",
388
+ id: maps[0]?.id,
389
+ });
390
+
391
+ // Unblock to process the queue
392
+ blocker.sendBlockedMessages();
393
+ blocker.unblock();
394
+
395
+ await Promise.all(loadPromises);
396
+
397
+ // Verify all LOAD messages were sent
398
+ const allLoadMessages = SyncMessagesLog.messages.filter(
399
+ (m) => m.msg.action === "load",
400
+ );
401
+
402
+ // All 5 should eventually be sent
403
+ expect(allLoadMessages.length).toBe(5);
404
+
405
+ // They should be in order (maps[0], maps[1], maps[2], maps[3], maps[4])
406
+ for (let i = 0; i < allLoadMessages.length; i++) {
407
+ expect(allLoadMessages[i]?.msg).toMatchObject({
408
+ action: "load",
409
+ id: maps[i]?.id,
410
+ });
411
+ }
412
+ });
413
+
414
+ test("should allow dependency loads to overflow the concurrency limit", async () => {
415
+ setMaxInFlightLoadsPerPeer(1);
416
+
417
+ const server = setupTestNode();
418
+
419
+ const client = setupTestNode({ connected: false });
420
+ client.connectToSyncServer({
421
+ ourName: "client",
422
+ syncServerName: "server",
423
+ syncServer: server.node,
424
+ });
425
+
426
+ // Create a CoValue on the server - the Map depends on the Group
427
+ const group = server.node.createGroup();
428
+ group.addMember("everyone", "writer");
429
+ const map = group.createMap();
430
+ map.set("key", "value");
431
+
432
+ // Delete the Group from server so it won't be pushed with the Map content
433
+ // skipVerify prevents the server from checking dependencies before sending
434
+ server.node.syncManager.disableTransactionVerification();
435
+ server.node.internalDeleteCoValue(group.id);
436
+
437
+ // Load the map from the client
438
+ // The flow is:
439
+ // 1. Client sends LOAD Map to server (takes the slot, limit=1)
440
+ // 2. Server responds with Map content (no deps pushed because skipVerify + Group deleted)
441
+ // 3. Client sees missing dependency (Group), sends LOAD Group to server
442
+ // This would be blocked by limit=1 without allowOverflow since Map load slot is taken
443
+ // 4. With allowOverflow, Group load bypasses the queue
444
+ // 5. Server responds with KNOWN Group (doesn't have it - was deleted)
445
+ // 6. Group content is moved back to server (simulating it becoming available)
446
+ // 7. Server responds with Group content
447
+ // 8. Client can now process Map content
448
+ const promise = loadCoValueOrFail(client.node, map.id);
449
+
450
+ // Wait for the Map content to be sent
451
+ await waitFor(() => SyncMessagesLog.messages.length >= 2);
452
+
453
+ importContentIntoNode(group.core, server.node, 1);
454
+
455
+ const result = await promise;
456
+ expect(result.get("key")).toBe("value");
457
+
458
+ // Verify both were loaded successfully despite throttling
459
+ expect(
460
+ SyncMessagesLog.getMessages({
461
+ Group: group.core,
462
+ Map: map.core,
463
+ }),
464
+ ).toMatchInlineSnapshot(`
465
+ [
466
+ "client -> server | LOAD Map sessions: empty",
467
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
468
+ "client -> server | LOAD Group sessions: empty",
469
+ "server -> client | KNOWN Group sessions: empty",
470
+ "server -> client | CONTENT Group header: true new: After: 0 New: 5",
471
+ "client -> server | KNOWN Group sessions: header/5",
472
+ "client -> server | KNOWN Map sessions: header/1",
473
+ ]
474
+ `);
475
+ });
476
+
477
+ test("should keep load slot occupied while streaming large CoValues", async () => {
478
+ setMaxInFlightLoadsPerPeer(1);
479
+
480
+ const client = setupTestNode({
481
+ connected: false,
482
+ });
483
+
484
+ const { peerState, peerOnServer } = client.connectToSyncServer();
485
+
486
+ // Create a large CoValue that requires multiple chunks to stream
487
+ const group = jazzCloud.node.createGroup();
488
+ const largeMap = group.createMap();
489
+ fillCoMapWithLargeData(largeMap);
490
+
491
+ // Create a small CoValue that will be queued
492
+ const smallMap = group.createMap();
493
+ smallMap.set("key", "value", "trusting");
494
+
495
+ // Block all the streaming chunks, except the first content message
496
+ const blocker = blockMessageTypeOnOutgoingPeer(peerOnServer, "content", {
497
+ id: largeMap.id,
498
+ matcher: (msg) => msg.action === "content" && !msg.expectContentUntil,
499
+ });
500
+
501
+ // Start loading both maps concurrently
502
+ const largeMapOnClient = await client.node.loadCoValueCore(largeMap.id);
503
+ const smallMapPromise = client.node.loadCoValueCore(smallMap.id);
504
+
505
+ expect(client.node.getCoValue(largeMap.id).isStreaming()).toBe(true);
506
+
507
+ await new Promise((resolve) => setTimeout(resolve, 10));
508
+
509
+ // The SmallMap load should still be waiting in the queue
510
+ expect(
511
+ SyncMessagesLog.getMessages({
512
+ Group: group.core,
513
+ LargeMap: largeMapOnClient,
514
+ SmallMap: smallMap.core,
515
+ }),
516
+ ).toMatchInlineSnapshot(`
517
+ [
518
+ "client -> server | LOAD LargeMap sessions: empty",
519
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
520
+ "server -> client | CONTENT LargeMap header: true new: After: 0 New: 73 expectContentUntil: header/200",
521
+ "client -> server | KNOWN Group sessions: header/3",
522
+ "client -> server | KNOWN LargeMap sessions: header/73",
523
+ ]
524
+ `);
525
+
526
+ // Now unblock and send all remaining chunks to complete streaming
527
+ blocker.unblock();
528
+ blocker.sendBlockedMessages();
529
+
530
+ await client.node.getCoValue(largeMap.id).waitForFullStreaming();
531
+
532
+ const loadedSmallMap = await smallMapPromise;
533
+ expect(loadedSmallMap.isAvailable()).toBe(true);
534
+ });
535
+
536
+ test("should prioritize user-initiated loads over peer reconciliation loads", async () => {
537
+ setMaxInFlightLoadsPerPeer(1);
538
+
539
+ // Create CoValues on the server before the client connects
540
+ const group = jazzCloud.node.createGroup();
541
+
542
+ const [a, b, c] = [
543
+ group.createMap({ test: "a" }),
544
+ group.createMap({ test: "b" }),
545
+ group.createMap({ test: "c" }),
546
+ ];
547
+
548
+ const client = setupTestNode({
549
+ connected: false,
550
+ });
551
+ const { peerState } = client.connectToSyncServer();
552
+
553
+ // Load a CoValue to make it available locally
554
+ await loadCoValueOrFail(client.node, a.id);
555
+ await loadCoValueOrFail(client.node, b.id);
556
+
557
+ // Close the peer connection
558
+ peerState.gracefulShutdown();
559
+
560
+ SyncMessagesLog.clear();
561
+
562
+ // Reconnect to the server to trigger the reconciliation load
563
+ client.connectToSyncServer();
564
+
565
+ // The reconciliation load should be in the low-priority queue
566
+ // Now make a user-initiated load for a different CoValue
567
+ await loadCoValueOrFail(client.node, c.id);
568
+
569
+ // Wait for the reconciliation loads to be sent
570
+ await waitFor(() => SyncMessagesLog.messages.length >= 8);
571
+
572
+ // Expect Group, C, A, B
573
+ expect(
574
+ SyncMessagesLog.getMessages({
575
+ Group: group.core,
576
+ A: a.core,
577
+ B: b.core,
578
+ C: c.core,
579
+ }),
580
+ ).toMatchInlineSnapshot(`
581
+ [
582
+ "client -> server | LOAD Group sessions: header/3",
583
+ "server -> client | KNOWN Group sessions: header/3",
584
+ "client -> server | LOAD C sessions: empty",
585
+ "server -> client | CONTENT C header: true new: After: 0 New: 1",
586
+ "client -> server | KNOWN C sessions: header/1",
587
+ "client -> server | LOAD A sessions: header/1",
588
+ "server -> client | KNOWN A sessions: header/1",
589
+ "client -> server | LOAD B sessions: header/1",
590
+ "server -> client | KNOWN B sessions: header/1",
591
+ ]
592
+ `);
593
+ });
594
+
595
+ test("should upgrade low-priority reconciliation load to high-priority when user requests it", async () => {
596
+ setMaxInFlightLoadsPerPeer(1);
597
+
598
+ // Create CoValues on the server before the client connects
599
+ const group = jazzCloud.node.createGroup();
600
+
601
+ const [a, b, c] = [
602
+ group.createMap({ test: "a" }),
603
+ group.createMap({ test: "b" }),
604
+ group.createMap({ test: "c" }),
605
+ ];
606
+
607
+ const client = setupTestNode({
608
+ connected: false,
609
+ });
610
+
611
+ // Load both CoValues to make them marked as unavailable
612
+ await client.node.loadCoValueCore(a.id);
613
+ await client.node.loadCoValueCore(b.id);
614
+ await client.node.loadCoValueCore(c.id);
615
+
616
+ // Reconnect to the server to trigger the reconciliation load
617
+ client.connectToSyncServer();
618
+
619
+ // The reconciliation load should be in the low-priority queue
620
+ // Now try to bump-up the priority of the load for c
621
+ client.node.loadCoValueCore(c.id);
622
+
623
+ // Wait for the reconciliation loads to be sent
624
+ await waitFor(() => SyncMessagesLog.messages.length >= 6);
625
+
626
+ // Expect A, C, B
627
+ expect(
628
+ SyncMessagesLog.getMessages({
629
+ Group: group.core,
630
+ A: a.core,
631
+ B: b.core,
632
+ C: c.core,
633
+ }),
634
+ ).toMatchInlineSnapshot(`
635
+ [
636
+ "client -> server | LOAD A sessions: empty",
637
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
638
+ "server -> client | CONTENT A header: true new: After: 0 New: 1",
639
+ "client -> server | KNOWN Group sessions: header/3",
640
+ "client -> server | KNOWN A sessions: header/1",
641
+ "client -> server | LOAD C sessions: empty",
642
+ "server -> client | CONTENT C header: true new: After: 0 New: 1",
643
+ "client -> server | KNOWN C sessions: header/1",
644
+ "client -> server | LOAD B sessions: empty",
645
+ "server -> client | CONTENT B header: true new: After: 0 New: 1",
646
+ "client -> server | KNOWN B sessions: header/1",
647
+ ]
648
+ `);
649
+ });
650
+ });