cojson 0.20.1 → 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 +9 -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/coValueCore/coValueCore.d.ts +23 -3
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +102 -31
- package/dist/coValueCore/coValueCore.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/sync.d.ts.map +1 -1
- package/dist/sync.js +13 -10
- package/dist/sync.js.map +1 -1
- 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.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/package.json +4 -4
- package/src/GarbageCollector.ts +4 -3
- package/src/coValueCore/coValueCore.ts +114 -34
- package/src/localNode.ts +65 -4
- package/src/sync.ts +11 -9
- package/src/tests/coValueCore.loadFromStorage.test.ts +108 -0
- package/src/tests/knownState.lazyLoading.test.ts +52 -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
|
@@ -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
|
+
});
|