cojson 0.20.1 → 0.20.3

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 (47) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +19 -7
  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/coValueCore/coValueCore.d.ts +23 -3
  8. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore/coValueCore.js +116 -36
  10. package/dist/coValueCore/coValueCore.js.map +1 -1
  11. package/dist/localNode.d.ts +12 -0
  12. package/dist/localNode.d.ts.map +1 -1
  13. package/dist/localNode.js +51 -3
  14. package/dist/localNode.js.map +1 -1
  15. package/dist/permissions.d.ts.map +1 -1
  16. package/dist/permissions.js +5 -0
  17. package/dist/permissions.js.map +1 -1
  18. package/dist/sync.d.ts.map +1 -1
  19. package/dist/sync.js +13 -10
  20. package/dist/sync.js.map +1 -1
  21. package/dist/tests/coValueCore.loadFromStorage.test.js +87 -0
  22. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  23. package/dist/tests/group.parentGroupCache.test.js +2 -2
  24. package/dist/tests/group.parentGroupCache.test.js.map +1 -1
  25. package/dist/tests/knownState.lazyLoading.test.js +44 -0
  26. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  27. package/dist/tests/permissions.test.js +83 -2
  28. package/dist/tests/permissions.test.js.map +1 -1
  29. package/dist/tests/sync.garbageCollection.test.js +87 -3
  30. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  31. package/dist/tests/sync.multipleServers.test.js +0 -62
  32. package/dist/tests/sync.multipleServers.test.js.map +1 -1
  33. package/dist/tests/sync.peerReconciliation.test.js +156 -0
  34. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  35. package/package.json +4 -4
  36. package/src/GarbageCollector.ts +4 -3
  37. package/src/coValueCore/coValueCore.ts +129 -39
  38. package/src/localNode.ts +65 -4
  39. package/src/permissions.ts +6 -0
  40. package/src/sync.ts +11 -9
  41. package/src/tests/coValueCore.loadFromStorage.test.ts +108 -0
  42. package/src/tests/group.parentGroupCache.test.ts +2 -2
  43. package/src/tests/knownState.lazyLoading.test.ts +52 -0
  44. package/src/tests/permissions.test.ts +118 -1
  45. package/src/tests/sync.garbageCollection.test.ts +115 -3
  46. package/src/tests/sync.multipleServers.test.ts +0 -65
  47. package/src/tests/sync.peerReconciliation.test.ts +199 -0
@@ -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
+ });