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
@@ -1,5 +1,6 @@
1
1
  import { beforeEach, describe, expect, test } from "vitest";
2
2
 
3
+ import { expectMap } from "../coValue";
3
4
  import { setGarbageCollectorMaxAge } from "../config";
4
5
  import {
5
6
  SyncMessagesLog,
@@ -177,6 +178,8 @@ describe("sync after the garbage collector has run", () => {
177
178
  const mapOnServer = await loadCoValueOrFail(jazzCloud.node, map.id);
178
179
  expect(mapOnServer.get("hello")).toEqual("updated");
179
180
 
181
+ // With garbageCollected shells, client uses cached knownState (header/1)
182
+ // which is more accurate than asking storage (which returns empty)
180
183
  expect(
181
184
  SyncMessagesLog.getMessages({
182
185
  Group: group.core,
@@ -184,14 +187,14 @@ describe("sync after the garbage collector has run", () => {
184
187
  }),
185
188
  ).toMatchInlineSnapshot(`
186
189
  [
187
- "client -> server | LOAD Map sessions: empty",
190
+ "client -> server | LOAD Map sessions: header/1",
188
191
  "client -> server | LOAD Group sessions: header/3",
189
192
  "client -> storage | CONTENT Group header: true new: After: 0 New: 3",
190
193
  "client -> server | CONTENT Group header: true new: After: 0 New: 3",
191
194
  "client -> storage | CONTENT Map header: true new: After: 0 New: 1",
192
195
  "client -> server | CONTENT Map header: true new: After: 0 New: 1",
193
- "server -> storage | LOAD Map sessions: empty",
194
- "storage -> server | KNOWN Map sessions: empty",
196
+ "server -> storage | GET_KNOWN_STATE Map",
197
+ "storage -> server | GET_KNOWN_STATE_RESULT Map sessions: empty",
195
198
  "server -> client | KNOWN Map sessions: empty",
196
199
  "server -> storage | GET_KNOWN_STATE Group",
197
200
  "storage -> server | GET_KNOWN_STATE_RESULT Group sessions: empty",
@@ -203,4 +206,113 @@ describe("sync after the garbage collector has run", () => {
203
206
  ]
204
207
  `);
205
208
  });
209
+
210
+ test("knownStateWithStreaming returns lastKnownState for garbageCollected CoValues", async () => {
211
+ // This test verifies that knownStateWithStreaming() returns the cached lastKnownState
212
+ // for garbage-collected CoValues, not an empty state. This is important for peer
213
+ // reconciliation where we want to send the last known state to minimize data transfer.
214
+
215
+ const client = setupTestNode();
216
+ client.addStorage({ ourName: "client" });
217
+ client.node.enableGarbageCollector();
218
+
219
+ const group = client.node.createGroup();
220
+ const map = group.createMap();
221
+ map.set("hello", "world", "trusting");
222
+
223
+ // Sync to server
224
+ client.connectToSyncServer();
225
+ await client.node.syncManager.waitForAllCoValuesSync();
226
+
227
+ // Capture known state before GC
228
+ const originalKnownState = map.core.knownState();
229
+ const originalKnownStateWithStreaming = map.core.knownStateWithStreaming();
230
+
231
+ // For available CoValues, both should be equal (no streaming in progress)
232
+ expect(originalKnownState).toEqual(originalKnownStateWithStreaming);
233
+ expect(originalKnownState.header).toBe(true);
234
+ expect(Object.values(originalKnownState.sessions)[0]).toBe(1);
235
+
236
+ // Disconnect before GC
237
+ client.disconnect();
238
+
239
+ // Run GC to create garbageCollected shell
240
+ client.node.garbageCollector?.collect();
241
+ client.node.garbageCollector?.collect();
242
+
243
+ const gcCoValue = client.node.getCoValue(map.id);
244
+ expect(gcCoValue.loadingState).toBe("garbageCollected");
245
+
246
+ // Key assertion: knownStateWithStreaming() should return lastKnownState, not empty state
247
+ const gcKnownState = gcCoValue.knownState();
248
+ const gcKnownStateWithStreaming = gcCoValue.knownStateWithStreaming();
249
+
250
+ // Both should equal the original known state (the cached lastKnownState)
251
+ expect(gcKnownState).toEqual(originalKnownState);
252
+ expect(gcKnownStateWithStreaming).toEqual(originalKnownState);
253
+
254
+ // Specifically verify it's NOT an empty state
255
+ expect(gcKnownStateWithStreaming.header).toBe(true);
256
+ expect(
257
+ Object.keys(gcKnownStateWithStreaming.sessions).length,
258
+ ).toBeGreaterThan(0);
259
+ });
260
+
261
+ test("garbageCollected CoValues read from verified content after reload", async () => {
262
+ // This test verifies that after reloading a GC'd CoValue:
263
+ // 1. lastKnownState is cleared
264
+ // 2. knownState() returns data from verified content (not cached)
265
+ // We prove this by adding a transaction after reload and verifying knownState() updates
266
+
267
+ const client = setupTestNode();
268
+ client.addStorage({ ourName: "client" });
269
+ client.node.enableGarbageCollector();
270
+
271
+ const group = client.node.createGroup();
272
+ const map = group.createMap();
273
+ map.set("hello", "world", "trusting");
274
+
275
+ // Sync to server
276
+ client.connectToSyncServer();
277
+ await client.node.syncManager.waitForAllCoValuesSync();
278
+
279
+ // Capture known state before GC (has 1 transaction)
280
+ const originalKnownState = map.core.knownState();
281
+ const originalSessionCount = Object.values(originalKnownState.sessions)[0];
282
+ expect(originalSessionCount).toBe(1);
283
+
284
+ // Disconnect before GC
285
+ client.disconnect();
286
+
287
+ // Run GC to create garbageCollected shell
288
+ client.node.garbageCollector?.collect();
289
+ client.node.garbageCollector?.collect();
290
+
291
+ const gcMap = client.node.getCoValue(map.id);
292
+ expect(gcMap.loadingState).toBe("garbageCollected");
293
+
294
+ // Verify knownState() returns lastKnownState (still shows 1 transaction)
295
+ expect(gcMap.knownState()).toEqual(originalKnownState);
296
+
297
+ // Reconnect and reload
298
+ client.connectToSyncServer();
299
+ const reloadedCore = await client.node.loadCoValueCore(map.id);
300
+
301
+ // Verify CoValue is now available
302
+ expect(reloadedCore.loadingState).toBe("available");
303
+ expect(reloadedCore.isAvailable()).toBe(true);
304
+
305
+ // At this point, knownState() should be reading from verified content
306
+ // To prove this, we add a new transaction and verify knownState() updates
307
+ const reloadedContent = expectMap(reloadedCore.getCurrentContent());
308
+ reloadedContent.set("hello", "updated locally", "trusting");
309
+
310
+ // Verify knownState() now shows 2 transactions
311
+ // This proves we're reading from verified content, not cached lastKnownState
312
+ const newKnownState = reloadedCore.knownState();
313
+ const newSessionCount = Object.values(newKnownState.sessions)[0];
314
+
315
+ expect(newSessionCount).toBe(2);
316
+ expect(newKnownState).not.toEqual(originalKnownState);
317
+ });
206
318
  });
@@ -99,71 +99,6 @@ describe("multiple servers peers", () => {
99
99
  expect(mapOnSession2.get("count")).toEqual(4);
100
100
  });
101
101
 
102
- expect(
103
- SyncMessagesLog.getMessages({
104
- Group: group.core,
105
- Map: map.core,
106
- }),
107
- ).toMatchInlineSnapshot(`
108
- [
109
- "client -> server1 | LOAD Map sessions: empty",
110
- "client -> server2 | LOAD Map sessions: empty",
111
- "client -> server1 | CONTENT Group header: true new: After: 0 New: 3",
112
- "client -> server2 | CONTENT Group header: true new: After: 0 New: 3",
113
- "client -> server1 | CONTENT Map header: true new: After: 0 New: 1",
114
- "client -> server2 | CONTENT Map header: true new: After: 0 New: 1",
115
- "server1 -> client | KNOWN Map sessions: empty",
116
- "server2 -> client | KNOWN Map sessions: empty",
117
- "server1 -> client | KNOWN Group sessions: header/3",
118
- "server2 -> client | KNOWN Group sessions: header/3",
119
- "server1 -> client | KNOWN Map sessions: header/1",
120
- "server1 -> client | CONTENT Group header: true new: After: 0 New: 3",
121
- "server1 -> client | CONTENT Map header: true new: After: 0 New: 1",
122
- "server2 -> client | KNOWN Map sessions: header/1",
123
- "server2 -> client | CONTENT Group header: true new: After: 0 New: 3",
124
- "server2 -> client | CONTENT Map header: true new: After: 0 New: 1",
125
- "client -> server1 | KNOWN Group sessions: header/3",
126
- "client -> server2 | LOAD Group sessions: header/3",
127
- "client -> server1 | KNOWN Map sessions: header/1",
128
- "client -> server2 | CONTENT Map header: true new: After: 0 New: 1",
129
- "client -> server1 | CONTENT Map header: false new: After: 0 New: 1",
130
- "client -> server2 | CONTENT Map header: false new: After: 0 New: 1",
131
- "client -> server1 | CONTENT Map header: false new: After: 1 New: 1",
132
- "client -> server2 | CONTENT Map header: false new: After: 1 New: 1",
133
- "client -> server2 | KNOWN Group sessions: header/3",
134
- "client -> server2 | CONTENT Group header: false new: After: 0 New: 3",
135
- "client -> server2 | KNOWN Map sessions: header/2",
136
- "server2 -> client | KNOWN Map sessions: header/1",
137
- "server1 -> client | KNOWN Map sessions: header/2",
138
- "server1 -> client | CONTENT Map header: false new: After: 0 New: 1",
139
- "server2 -> client | KNOWN Map sessions: header/2",
140
- "server2 -> client | CONTENT Map header: false new: After: 0 New: 1",
141
- "server1 -> client | KNOWN Map sessions: header/3",
142
- "server1 -> client | CONTENT Map header: false new: After: 1 New: 1",
143
- "server2 -> client | KNOWN Map sessions: header/3",
144
- "server2 -> client | CONTENT Map header: false new: After: 1 New: 1",
145
- "server2 -> client | KNOWN Group sessions: header/3",
146
- "client -> server1 | KNOWN Map sessions: header/3",
147
- "client -> server2 | CONTENT Map header: false new: After: 0 New: 1",
148
- "client -> server2 | KNOWN Map sessions: header/3",
149
- "client -> server1 | KNOWN Map sessions: header/3",
150
- "client -> server2 | CONTENT Map header: false new: After: 1 New: 1",
151
- "client -> server2 | KNOWN Map sessions: header/3",
152
- "server2 -> client | KNOWN Map sessions: header/3",
153
- "server2 -> client | KNOWN Map sessions: header/3",
154
- "client -> server1 | CONTENT Map header: false new: After: 1 New: 1",
155
- "client -> server2 | CONTENT Map header: false new: After: 1 New: 1",
156
- "server1 -> client | KNOWN Map sessions: header/4",
157
- "server1 -> client | CONTENT Map header: false new: After: 1 New: 1",
158
- "server2 -> client | KNOWN Map sessions: header/4",
159
- "server2 -> client | CONTENT Map header: false new: After: 1 New: 1",
160
- "client -> server1 | KNOWN Map sessions: header/4",
161
- "client -> server2 | CONTENT Map header: false new: After: 1 New: 1",
162
- "client -> server2 | KNOWN Map sessions: header/4",
163
- "server2 -> client | KNOWN Map sessions: header/4",
164
- ]
165
- `);
166
-
167
102
  const mapOnServer1 = server1.node.getCoValue(map.id);
168
103
  const mapOnServer2 = server2.node.getCoValue(map.id);
169
104
 
@@ -1,5 +1,6 @@
1
1
  import { assert, beforeEach, describe, expect, test, vi } from "vitest";
2
2
  import { expectMap } from "../coValue";
3
+ import { setGarbageCollectorMaxAge } from "../config";
3
4
  import { RawCoMap } from "../exports";
4
5
  import {
5
6
  SyncMessagesLog,
@@ -17,6 +18,8 @@ let jazzCloud: ReturnType<typeof setupTestNode>;
17
18
  beforeEach(async () => {
18
19
  SyncMessagesLog.clear();
19
20
  jazzCloud = setupTestNode({ isSyncServer: true });
21
+ // Set GC max age to -1 so items are collected immediately when needed
22
+ setGarbageCollectorMaxAge(-1);
20
23
  });
21
24
 
22
25
  describe("peer reconciliation", () => {
@@ -336,3 +339,199 @@ describe("peer reconciliation", () => {
336
339
  );
337
340
  });
338
341
  });
342
+
343
+ describe("peer reconciliation with garbageCollected CoValues", () => {
344
+ test("sends cached known state for garbageCollected CoValues during reconciliation", async () => {
345
+ const client = setupTestNode();
346
+ client.addStorage({ ourName: "client" });
347
+ client.node.enableGarbageCollector();
348
+
349
+ const group = client.node.createGroup();
350
+ const map = group.createMap();
351
+ map.set("hello", "world", "trusting");
352
+
353
+ // Sync to server first
354
+ client.connectToSyncServer();
355
+ await client.node.syncManager.waitForAllCoValuesSync();
356
+
357
+ // Capture the known state before GC
358
+ const mapKnownState = map.core.knownState();
359
+ const groupKnownState = group.core.knownState();
360
+
361
+ // Disconnect first to avoid server reloading CoValues after GC
362
+ client.disconnect();
363
+
364
+ // Run GC to unmount the CoValues (creates garbageCollected shells)
365
+ // GC works because there are no persistent server peers (disconnected)
366
+ client.node.garbageCollector?.collect();
367
+ client.node.garbageCollector?.collect(); // Second pass for dependencies
368
+
369
+ // Verify CoValues are now garbageCollected
370
+ const gcMap = client.node.getCoValue(map.id);
371
+ const gcGroup = client.node.getCoValue(group.id);
372
+ expect(gcMap.loadingState).toBe("garbageCollected");
373
+ expect(gcGroup.loadingState).toBe("garbageCollected");
374
+
375
+ // Verify knownState() returns the cached state (not empty)
376
+ expect(gcMap.knownState()).toEqual(mapKnownState);
377
+ expect(gcGroup.knownState()).toEqual(groupKnownState);
378
+
379
+ // Reconnect to trigger peer reconciliation
380
+ SyncMessagesLog.clear();
381
+ client.connectToSyncServer();
382
+
383
+ // Wait for messages to be exchanged
384
+ await new Promise((resolve) => setTimeout(resolve, 100));
385
+
386
+ // LOAD is sent with cached known state (no storage lookup needed)
387
+ expect(
388
+ SyncMessagesLog.getMessages({
389
+ Group: gcGroup,
390
+ Map: gcMap,
391
+ }),
392
+ ).toMatchInlineSnapshot(`
393
+ [
394
+ "client -> server | LOAD Group sessions: header/3",
395
+ "client -> server | LOAD Map sessions: header/1",
396
+ "server -> client | KNOWN Group sessions: header/3",
397
+ "server -> client | KNOWN Map sessions: header/1",
398
+ ]
399
+ `);
400
+ });
401
+
402
+ test("garbageCollected CoValues restore subscription with minimal data transfer", async () => {
403
+ // Setup: both client and server have the same data
404
+ const client = setupTestNode();
405
+ client.addStorage({ ourName: "client" });
406
+ client.node.enableGarbageCollector();
407
+
408
+ const group = client.node.createGroup();
409
+ const map = group.createMap();
410
+ map.set("hello", "world", "trusting");
411
+
412
+ // Sync to server
413
+ client.connectToSyncServer();
414
+ await client.node.syncManager.waitForAllCoValuesSync();
415
+
416
+ // Verify server has the data
417
+ const serverMap = jazzCloud.node.getCoValue(map.id);
418
+ expect(serverMap.isAvailable()).toBe(true);
419
+
420
+ // Capture known states before GC
421
+ const clientMapKnownState = map.core.knownState();
422
+ const clientGroupKnownState = group.core.knownState();
423
+
424
+ // Disconnect before GC to avoid server reloading CoValues
425
+ client.disconnect();
426
+
427
+ // Run GC
428
+ client.node.garbageCollector?.collect();
429
+ client.node.garbageCollector?.collect();
430
+
431
+ const gcMap = client.node.getCoValue(map.id);
432
+ const gcGroup = client.node.getCoValue(group.id);
433
+
434
+ // Verify garbageCollected state
435
+ expect(gcMap.loadingState).toBe("garbageCollected");
436
+ expect(gcGroup.loadingState).toBe("garbageCollected");
437
+
438
+ // Reconnect to trigger peer reconciliation
439
+ SyncMessagesLog.clear();
440
+ client.connectToSyncServer();
441
+
442
+ // Wait for messages to be exchanged
443
+ await new Promise((resolve) => setTimeout(resolve, 100));
444
+
445
+ // LOAD is sent with cached known state
446
+ // Server responds with KNOWN since client and server have the same data
447
+ expect(
448
+ SyncMessagesLog.getMessages({
449
+ Group: gcGroup,
450
+ Map: gcMap,
451
+ }),
452
+ ).toMatchInlineSnapshot(`
453
+ [
454
+ "client -> server | LOAD Group sessions: header/3",
455
+ "client -> server | LOAD Map sessions: header/1",
456
+ "server -> client | KNOWN Group sessions: header/3",
457
+ "server -> client | KNOWN Map sessions: header/1",
458
+ ]
459
+ `);
460
+ });
461
+
462
+ test("unknown CoValues return empty knownState during reconciliation", async () => {
463
+ const client = setupTestNode();
464
+
465
+ // Create a CoValue on another node that we'll hear about but not load
466
+ const otherClient = setupTestNode();
467
+ const group = otherClient.node.createGroup();
468
+ const map = group.createMap();
469
+ map.set("hello", "world", "trusting");
470
+
471
+ // Sync other client to server
472
+ otherClient.connectToSyncServer();
473
+ await otherClient.node.syncManager.waitForAllCoValuesSync();
474
+
475
+ // Now client connects - it will hear about the CoValue IDs but not load them
476
+ // Create a reference to the CoValue without loading it
477
+ const unknownCoValue = client.node.getCoValue(map.id);
478
+
479
+ // Verify it's in unknown state
480
+ expect(unknownCoValue.loadingState).toBe("unknown");
481
+
482
+ // Verify knownState() returns empty state for unknown CoValues
483
+ const knownState = unknownCoValue.knownState();
484
+ expect(knownState.header).toBe(false);
485
+ expect(knownState.sessions).toEqual({});
486
+ });
487
+
488
+ test("unknown CoValues are skipped during peer reconciliation (no LOAD sent)", async () => {
489
+ const client = setupTestNode();
490
+
491
+ // Create a CoValue on another node
492
+ const otherClient = setupTestNode();
493
+ const group = otherClient.node.createGroup();
494
+ const map = group.createMap();
495
+ map.set("hello", "world", "trusting");
496
+
497
+ // Sync other client to server so the server knows about the CoValue
498
+ otherClient.connectToSyncServer();
499
+ await otherClient.node.syncManager.waitForAllCoValuesSync();
500
+
501
+ // Client creates its own group (so we have something to compare against)
502
+ const clientGroup = client.node.createGroup();
503
+ const clientMap = clientGroup.createMap();
504
+ clientMap.set("foo", "bar", "trusting");
505
+
506
+ // Create a reference to the other client's CoValue WITHOUT loading it
507
+ // This simulates "hearing about" a CoValue ID (e.g., from a reference)
508
+ const unknownCoValue = client.node.getCoValue(map.id);
509
+ expect(unknownCoValue.loadingState).toBe("unknown");
510
+
511
+ // Connect client to server - this triggers peer reconciliation
512
+ SyncMessagesLog.clear();
513
+ client.connectToSyncServer();
514
+
515
+ await client.node.syncManager.waitForAllCoValuesSync();
516
+
517
+ // Verify: LOAD sent for client's own CoValues, but NOT for the unknown CoValue
518
+ expect(
519
+ SyncMessagesLog.getMessages({
520
+ ClientGroup: clientGroup.core,
521
+ ClientMap: clientMap.core,
522
+ UnknownMap: unknownCoValue,
523
+ }),
524
+ ).toMatchInlineSnapshot(`
525
+ [
526
+ "client -> server | LOAD ClientGroup sessions: header/3",
527
+ "client -> server | LOAD ClientMap sessions: header/1",
528
+ "client -> server | CONTENT ClientGroup header: true new: After: 0 New: 3",
529
+ "client -> server | CONTENT ClientMap header: true new: After: 0 New: 1",
530
+ "server -> client | KNOWN ClientGroup sessions: empty",
531
+ "server -> client | KNOWN ClientMap sessions: empty",
532
+ "server -> client | KNOWN ClientGroup sessions: header/3",
533
+ "server -> client | KNOWN ClientMap sessions: header/1",
534
+ ]
535
+ `);
536
+ });
537
+ });
@@ -1024,9 +1024,9 @@ describe("client syncs with a server with storage", () => {
1024
1024
  "server -> bob | CONTENT Map header: true new: After: 0 New: 1",
1025
1025
  "storage -> syncServer | CONTENT ParentGroup header: true new: After: 76 New: 73",
1026
1026
  "server -> bob | CONTENT ParentGroup header: false new: After: 76 New: 73 expectContentUntil: header/205",
1027
- "bob -> server | KNOWN ParentGroup sessions: header/76",
1028
1027
  "storage -> syncServer | CONTENT ParentGroup header: true new: After: 149 New: 56",
1029
1028
  "server -> bob | CONTENT ParentGroup header: false new: After: 149 New: 56",
1029
+ "bob -> server | KNOWN ParentGroup sessions: header/76",
1030
1030
  "bob -> server | KNOWN Group sessions: header/5",
1031
1031
  "bob -> server | KNOWN Map sessions: header/1",
1032
1032
  "bob -> server | KNOWN ParentGroup sessions: header/149",
@@ -163,7 +163,9 @@ export function getDbPath(defaultDbPath?: string) {
163
163
 
164
164
  if (!defaultDbPath) {
165
165
  onTestFinished(() => {
166
- unlinkSync(dbPath);
166
+ setTimeout(() => {
167
+ unlinkSync(dbPath);
168
+ }, 100);
167
169
  });
168
170
  }
169
171
 
@@ -268,6 +268,7 @@ export function blockMessageTypeOnOutgoingPeer(
268
268
  opts: {
269
269
  id?: string;
270
270
  once?: boolean;
271
+ matcher?: (msg: SyncMessage) => boolean;
271
272
  },
272
273
  ) {
273
274
  const push = peer.outgoing.push;
@@ -280,7 +281,8 @@ export function blockMessageTypeOnOutgoingPeer(
280
281
  typeof msg === "object" &&
281
282
  msg.action === messageType &&
282
283
  (!opts.id || msg.id === opts.id) &&
283
- (!opts.once || !blockedIds.has(msg.id))
284
+ (!opts.once || !blockedIds.has(msg.id)) &&
285
+ (!opts.matcher || opts.matcher(msg))
284
286
  ) {
285
287
  blockedMessages.push(msg);
286
288
  blockedIds.add(msg.id);