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
|
@@ -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:
|
|
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 |
|
|
194
|
-
"storage -> server |
|
|
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",
|
package/src/tests/testStorage.ts
CHANGED
package/src/tests/testUtils.ts
CHANGED
|
@@ -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);
|