cojson 0.18.31 → 0.18.33

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 (140) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/dist/SyncStateManager.d.ts.map +1 -1
  4. package/dist/SyncStateManager.js +2 -2
  5. package/dist/SyncStateManager.js.map +1 -1
  6. package/dist/coValueCore/SessionMap.d.ts +1 -0
  7. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  8. package/dist/coValueCore/SessionMap.js +17 -2
  9. package/dist/coValueCore/SessionMap.js.map +1 -1
  10. package/dist/coValueCore/coValueCore.d.ts +14 -9
  11. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  12. package/dist/coValueCore/coValueCore.js +62 -47
  13. package/dist/coValueCore/coValueCore.js.map +1 -1
  14. package/dist/coValueCore/verifiedState.d.ts +2 -2
  15. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  16. package/dist/coValueCore/verifiedState.js +86 -75
  17. package/dist/coValueCore/verifiedState.js.map +1 -1
  18. package/dist/coValues/group.d.ts +1 -0
  19. package/dist/coValues/group.d.ts.map +1 -1
  20. package/dist/coValues/group.js +24 -4
  21. package/dist/coValues/group.js.map +1 -1
  22. package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
  23. package/dist/crypto/PureJSCrypto.js +2 -10
  24. package/dist/crypto/PureJSCrypto.js.map +1 -1
  25. package/dist/knownState.d.ts +1 -1
  26. package/dist/knownState.d.ts.map +1 -1
  27. package/dist/knownState.js +1 -1
  28. package/dist/knownState.js.map +1 -1
  29. package/dist/localNode.d.ts.map +1 -1
  30. package/dist/localNode.js +1 -2
  31. package/dist/localNode.js.map +1 -1
  32. package/dist/queue/LocalTransactionsSyncQueue.d.ts.map +1 -1
  33. package/dist/queue/LocalTransactionsSyncQueue.js +16 -1
  34. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -1
  35. package/dist/storage/knownState.js +2 -2
  36. package/dist/storage/knownState.js.map +1 -1
  37. package/dist/storage/sqlite/index.d.ts.map +1 -1
  38. package/dist/storage/sqlite/index.js +17 -3
  39. package/dist/storage/sqlite/index.js.map +1 -1
  40. package/dist/storage/sqlite/sqliteMigrations.d.ts +6 -1
  41. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  42. package/dist/storage/sqlite/sqliteMigrations.js +1 -3
  43. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  44. package/dist/storage/sqlite/types.d.ts +2 -0
  45. package/dist/storage/sqlite/types.d.ts.map +1 -1
  46. package/dist/storage/sqliteAsync/index.d.ts.map +1 -1
  47. package/dist/storage/sqliteAsync/index.js +17 -3
  48. package/dist/storage/sqliteAsync/index.js.map +1 -1
  49. package/dist/storage/sqliteAsync/types.d.ts +2 -0
  50. package/dist/storage/sqliteAsync/types.d.ts.map +1 -1
  51. package/dist/sync.d.ts.map +1 -1
  52. package/dist/sync.js +8 -2
  53. package/dist/sync.js.map +1 -1
  54. package/dist/tests/PureJSCrypto.test.js +1 -1
  55. package/dist/tests/PureJSCrypto.test.js.map +1 -1
  56. package/dist/tests/StorageApiAsync.test.js +11 -11
  57. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  58. package/dist/tests/StorageApiSync.test.js +3 -3
  59. package/dist/tests/StorageApiSync.test.js.map +1 -1
  60. package/dist/tests/WasmCrypto.test.js +1 -1
  61. package/dist/tests/WasmCrypto.test.js.map +1 -1
  62. package/dist/tests/coPlainText.test.js +1 -1
  63. package/dist/tests/coStream.test.js +12 -12
  64. package/dist/tests/coStream.test.js.map +1 -1
  65. package/dist/tests/coValueCore.isCompletelyDownloaded.test.d.ts +2 -0
  66. package/dist/tests/coValueCore.isCompletelyDownloaded.test.d.ts.map +1 -0
  67. package/dist/tests/coValueCore.isCompletelyDownloaded.test.js +421 -0
  68. package/dist/tests/coValueCore.isCompletelyDownloaded.test.js.map +1 -0
  69. package/dist/tests/coValueCore.isStreaming.test.d.ts +2 -0
  70. package/dist/tests/coValueCore.isStreaming.test.d.ts.map +1 -0
  71. package/dist/tests/coValueCore.isStreaming.test.js +181 -0
  72. package/dist/tests/coValueCore.isStreaming.test.js.map +1 -0
  73. package/dist/tests/coValueCore.newContentSince.test.d.ts +2 -0
  74. package/dist/tests/coValueCore.newContentSince.test.d.ts.map +1 -0
  75. package/dist/tests/coValueCore.newContentSince.test.js +808 -0
  76. package/dist/tests/coValueCore.newContentSince.test.js.map +1 -0
  77. package/dist/tests/coreWasm.test.js +2 -2
  78. package/dist/tests/coreWasm.test.js.map +1 -1
  79. package/dist/tests/group.childKeyRotation.test.d.ts +2 -0
  80. package/dist/tests/group.childKeyRotation.test.d.ts.map +1 -0
  81. package/dist/tests/group.childKeyRotation.test.js +261 -0
  82. package/dist/tests/group.childKeyRotation.test.js.map +1 -0
  83. package/dist/tests/group.removeMember.test.js +1 -114
  84. package/dist/tests/group.removeMember.test.js.map +1 -1
  85. package/dist/tests/knownState.test.js +11 -11
  86. package/dist/tests/knownState.test.js.map +1 -1
  87. package/dist/tests/sync.auth.test.js +6 -6
  88. package/dist/tests/sync.load.test.js +68 -5
  89. package/dist/tests/sync.load.test.js.map +1 -1
  90. package/dist/tests/sync.mesh.test.js +11 -17
  91. package/dist/tests/sync.mesh.test.js.map +1 -1
  92. package/dist/tests/sync.peerReconciliation.test.js +1 -1
  93. package/dist/tests/sync.storage.test.js +7 -7
  94. package/dist/tests/sync.storage.test.js.map +1 -1
  95. package/dist/tests/sync.storageAsync.test.js +4 -4
  96. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  97. package/dist/tests/sync.upload.test.js +96 -40
  98. package/dist/tests/sync.upload.test.js.map +1 -1
  99. package/dist/tests/testUtils.d.ts +2 -0
  100. package/dist/tests/testUtils.d.ts.map +1 -1
  101. package/dist/tests/testUtils.js +22 -1
  102. package/dist/tests/testUtils.js.map +1 -1
  103. package/package.json +3 -3
  104. package/src/SyncStateManager.ts +2 -5
  105. package/src/coValueCore/SessionMap.ts +26 -1
  106. package/src/coValueCore/coValueCore.ts +77 -55
  107. package/src/coValueCore/verifiedState.ts +123 -108
  108. package/src/coValues/group.ts +27 -4
  109. package/src/crypto/PureJSCrypto.ts +5 -21
  110. package/src/knownState.ts +1 -1
  111. package/src/localNode.ts +1 -2
  112. package/src/queue/LocalTransactionsSyncQueue.ts +25 -0
  113. package/src/storage/knownState.ts +2 -2
  114. package/src/storage/sqlite/index.ts +16 -3
  115. package/src/storage/sqlite/sqliteMigrations.ts +7 -4
  116. package/src/storage/sqlite/types.ts +2 -0
  117. package/src/storage/sqliteAsync/index.ts +19 -6
  118. package/src/storage/sqliteAsync/types.ts +2 -0
  119. package/src/sync.ts +7 -2
  120. package/src/tests/PureJSCrypto.test.ts +1 -2
  121. package/src/tests/StorageApiAsync.test.ts +11 -11
  122. package/src/tests/StorageApiSync.test.ts +3 -3
  123. package/src/tests/WasmCrypto.test.ts +1 -2
  124. package/src/tests/coPlainText.test.ts +1 -1
  125. package/src/tests/coStream.test.ts +12 -12
  126. package/src/tests/coValueCore.isCompletelyDownloaded.test.ts +589 -0
  127. package/src/tests/coValueCore.isStreaming.test.ts +271 -0
  128. package/src/tests/coValueCore.newContentSince.test.ts +966 -0
  129. package/src/tests/coreWasm.test.ts +2 -2
  130. package/src/tests/group.childKeyRotation.test.ts +431 -0
  131. package/src/tests/group.removeMember.test.ts +1 -184
  132. package/src/tests/knownState.test.ts +11 -11
  133. package/src/tests/sync.auth.test.ts +6 -6
  134. package/src/tests/sync.load.test.ts +80 -5
  135. package/src/tests/sync.mesh.test.ts +11 -17
  136. package/src/tests/sync.peerReconciliation.test.ts +1 -1
  137. package/src/tests/sync.storage.test.ts +7 -7
  138. package/src/tests/sync.storageAsync.test.ts +4 -4
  139. package/src/tests/sync.upload.test.ts +106 -40
  140. package/src/tests/testUtils.ts +24 -2
@@ -0,0 +1,589 @@
1
+ import { assert, beforeEach, describe, expect, test } from "vitest";
2
+ import {
3
+ SyncMessagesLog,
4
+ TEST_NODE_CONFIG,
5
+ loadCoValueOrFail,
6
+ setupTestAccount,
7
+ setupTestNode,
8
+ waitFor,
9
+ } from "./testUtils.js";
10
+ import { expectMap } from "../coValue.js";
11
+
12
+ // We want to simulate a real world communication that happens asynchronously
13
+ TEST_NODE_CONFIG.withAsyncPeers = true;
14
+
15
+ let jazzCloud: ReturnType<typeof setupTestNode>;
16
+
17
+ beforeEach(async () => {
18
+ SyncMessagesLog.clear();
19
+ jazzCloud = setupTestNode({ isSyncServer: true });
20
+ });
21
+
22
+ describe("CoValueCore.isCompletelyDownloaded", () => {
23
+ let admin: Awaited<ReturnType<typeof setupTestAccount>>;
24
+ let bob: Awaited<ReturnType<typeof setupTestAccount>>;
25
+
26
+ beforeEach(async () => {
27
+ admin = await setupTestAccount({
28
+ connected: true,
29
+ });
30
+ bob = await setupTestAccount({
31
+ connected: true,
32
+ });
33
+ });
34
+
35
+ describe("streaming from start", () => {
36
+ test("should return false when the value itself is streaming", async () => {
37
+ const group = admin.node.createGroup();
38
+
39
+ const map = group.createMap();
40
+ map.set("initial", "value");
41
+
42
+ await map.core.waitForSync();
43
+
44
+ // Disconnect the admin node to sync the content manually and simulate the delay of the last chunk
45
+ admin.disconnect();
46
+
47
+ // Make the map large enough to require multiple messages to be synced
48
+ for (let i = 0; i < 100; i++) {
49
+ map.set(`key${i}`, "1".repeat(1024));
50
+ }
51
+
52
+ const content = map.core.verified.newContentSince(undefined);
53
+ assert(content);
54
+ expect(content.length).toBeGreaterThan(1);
55
+
56
+ const lastChunk = content.pop();
57
+ assert(lastChunk);
58
+
59
+ // Create a new session for bob
60
+ const bobSession = bob.node;
61
+
62
+ // Feed all chunks except the last one
63
+ for (const chunk of content) {
64
+ bobSession.syncManager.handleNewContent(chunk, "import");
65
+ }
66
+
67
+ const mapOnBob = await loadCoValueOrFail(bobSession, map.id);
68
+
69
+ // The map should not be completely downloaded yet
70
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
71
+
72
+ // Feed the last chunk
73
+ bobSession.syncManager.handleNewContent(lastChunk, "import");
74
+
75
+ // Wait for the notification to be scheduled and executed
76
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
77
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
78
+ });
79
+
80
+ test("should return false when the owner of the value is streaming", async () => {
81
+ const group = admin.node.createGroup();
82
+
83
+ const map = group.createMap();
84
+ map.set("test", "value");
85
+
86
+ await map.core.waitForSync();
87
+
88
+ // Disconnect the admin node to sync the content manually
89
+ admin.disconnect();
90
+
91
+ // Make the group large enough to require multiple messages to be synced
92
+ for (let i = 0; i < 100; i++) {
93
+ // @ts-expect-error - test property is not part of the group shape
94
+ group.set(`key${i}`, "1".repeat(1024));
95
+ }
96
+
97
+ const content = group.core.verified.newContentSince(undefined);
98
+ assert(content);
99
+ expect(content.length).toBeGreaterThan(1);
100
+
101
+ const lastChunk = content.pop();
102
+ assert(lastChunk);
103
+
104
+ // Create a new session for bob
105
+ const bobSession = bob.node;
106
+
107
+ // Feed all chunks except the last one
108
+ for (const chunk of content) {
109
+ bobSession.syncManager.handleNewContent(chunk, "import");
110
+ }
111
+
112
+ const mapOnBob = await loadCoValueOrFail(bobSession, map.id);
113
+
114
+ // The map should not be completely downloaded because its owner (group) is still streaming
115
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
116
+
117
+ // Feed the last chunk
118
+ bobSession.syncManager.handleNewContent(lastChunk, "import");
119
+
120
+ // Wait for the notification to be scheduled and executed
121
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
122
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
123
+ });
124
+
125
+ test("should return false when the parent of the owner is streaming", async () => {
126
+ const parentGroup = admin.node.createGroup();
127
+ const childGroup = admin.node.createGroup();
128
+
129
+ childGroup.extend(parentGroup);
130
+
131
+ const map = childGroup.createMap();
132
+ map.set("test", "value");
133
+
134
+ await map.core.waitForSync();
135
+
136
+ // Disconnect the admin node to sync the content manually
137
+ admin.disconnect();
138
+
139
+ // Make the parent group large enough to require multiple messages to be synced
140
+ for (let i = 0; i < 100; i++) {
141
+ // @ts-expect-error - test property is not part of the group shape
142
+ parentGroup.set(`key${i}`, "1".repeat(1024));
143
+ }
144
+
145
+ const content = parentGroup.core.verified.newContentSince(undefined);
146
+ assert(content);
147
+ expect(content.length).toBeGreaterThan(1);
148
+
149
+ const lastChunk = content.pop();
150
+ assert(lastChunk);
151
+
152
+ // Create a new session for bob
153
+ const bobSession = bob.node;
154
+
155
+ // Feed all chunks except the last one
156
+ for (const chunk of content) {
157
+ bobSession.syncManager.handleNewContent(chunk, "import");
158
+ }
159
+
160
+ const mapOnBob = await loadCoValueOrFail(bobSession, map.id);
161
+
162
+ // The map should not be completely downloaded because the parent of its owner is still streaming
163
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
164
+
165
+ // Feed the last chunk
166
+ bobSession.syncManager.handleNewContent(lastChunk, "import");
167
+
168
+ // Wait for the notification to be scheduled and executed
169
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
170
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
171
+ });
172
+
173
+ test("should return false when the grandparent of the owner is streaming", async () => {
174
+ const grandParentGroup = admin.node.createGroup();
175
+ const parentGroup = admin.node.createGroup();
176
+ const childGroup = admin.node.createGroup();
177
+
178
+ parentGroup.extend(grandParentGroup);
179
+ childGroup.extend(parentGroup);
180
+
181
+ const map = childGroup.createMap();
182
+ map.set("test", "value");
183
+
184
+ await map.core.waitForSync();
185
+
186
+ // Disconnect the admin node to sync the content manually
187
+ admin.disconnect();
188
+
189
+ // Make the grandparent group large enough to require multiple messages to be synced
190
+ for (let i = 0; i < 100; i++) {
191
+ // @ts-expect-error - test property is not part of the group shape
192
+ grandParentGroup.set(`key${i}`, "1".repeat(1024));
193
+ }
194
+
195
+ const content = grandParentGroup.core.verified.newContentSince(undefined);
196
+ assert(content);
197
+ expect(content.length).toBeGreaterThan(1);
198
+
199
+ const lastChunk = content.pop();
200
+ assert(lastChunk);
201
+
202
+ // Create a new session for bob
203
+ const bobSession = bob.node;
204
+
205
+ // Feed all chunks except the last one
206
+ for (const chunk of content) {
207
+ bobSession.syncManager.handleNewContent(chunk, "import");
208
+ }
209
+
210
+ const mapOnBob = await loadCoValueOrFail(bobSession, map.id);
211
+
212
+ // The map should not be completely downloaded because the grandparent of its owner is still streaming
213
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
214
+
215
+ // Feed the last chunk
216
+ bobSession.syncManager.handleNewContent(lastChunk, "import");
217
+
218
+ // Wait for the notification to be scheduled and executed
219
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
220
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
221
+ });
222
+
223
+ test("should return false when multiple parents of the owner are streaming", async () => {
224
+ const parentGroup1 = admin.node.createGroup();
225
+ const parentGroup2 = admin.node.createGroup();
226
+ const childGroup = admin.node.createGroup();
227
+
228
+ childGroup.extend(parentGroup1);
229
+ childGroup.extend(parentGroup2);
230
+
231
+ const map = childGroup.createMap();
232
+ map.set("test", "value");
233
+
234
+ await map.core.waitForSync();
235
+
236
+ // Disconnect the admin node to sync the content manually
237
+ admin.disconnect();
238
+
239
+ // Make both parent groups large enough to require multiple messages to be synced
240
+ for (let i = 0; i < 100; i++) {
241
+ // @ts-expect-error - test property is not part of the group shape
242
+ parentGroup1.set(`key1_${i}`, "1".repeat(1024));
243
+ // @ts-expect-error - test property is not part of the group shape
244
+ parentGroup2.set(`key2_${i}`, "1".repeat(1024));
245
+ }
246
+
247
+ const content1 = parentGroup1.core.verified.newContentSince(undefined);
248
+ const content2 = parentGroup2.core.verified.newContentSince(undefined);
249
+ assert(content1);
250
+ assert(content2);
251
+ expect(content1.length).toBeGreaterThan(1);
252
+ expect(content2.length).toBeGreaterThan(1);
253
+
254
+ const lastChunk1 = content1.pop();
255
+ const lastChunk2 = content2.pop();
256
+ assert(lastChunk1);
257
+ assert(lastChunk2);
258
+
259
+ // Create a new session for bob
260
+ const bobSession = bob.node;
261
+
262
+ // Feed all chunks except the last ones from both parents
263
+ for (const chunk of content1) {
264
+ bobSession.syncManager.handleNewContent(chunk, "import");
265
+ }
266
+ for (const chunk of content2) {
267
+ bobSession.syncManager.handleNewContent(chunk, "import");
268
+ }
269
+
270
+ const mapOnBob = await loadCoValueOrFail(bobSession, map.id);
271
+
272
+ // The map should not be completely downloaded because both parents are still streaming
273
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
274
+
275
+ // Feed the last chunk from first parent
276
+ bobSession.syncManager.handleNewContent(lastChunk1, "import");
277
+
278
+ // Still should not be completely downloaded because second parent is still streaming
279
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
280
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
281
+
282
+ // Feed the last chunk from second parent
283
+ bobSession.syncManager.handleNewContent(lastChunk2, "import");
284
+
285
+ // Now it should be completely downloaded
286
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
287
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
288
+ });
289
+
290
+ test("should return false when the source branch of the value is streaming", async () => {
291
+ const group = admin.node.createGroup();
292
+ const originalMap = group.createMap();
293
+
294
+ // Add initial data to original map
295
+ originalMap.set("key1", "value1");
296
+
297
+ await originalMap.core.waitForSync();
298
+
299
+ // Disconnect the admin node to sync the content manually
300
+ admin.disconnect();
301
+
302
+ // Make the original map large enough to require multiple messages to be synced
303
+ for (let i = 0; i < 100; i++) {
304
+ originalMap.set(`key${i}`, "1".repeat(1024));
305
+ }
306
+
307
+ // Create a branch from the original map
308
+ const branch = expectMap(
309
+ originalMap.core
310
+ .createBranch("feature-branch", group.id)
311
+ .getCurrentContent(),
312
+ );
313
+
314
+ branch.set("branchKey", "branchValue");
315
+
316
+ const originalMapContent =
317
+ originalMap.core.verified.newContentSince(undefined);
318
+ const branchContent = branch.core.verified.newContentSince(undefined);
319
+
320
+ assert(originalMapContent);
321
+ assert(branchContent);
322
+ expect(originalMapContent.length).toBeGreaterThan(1);
323
+
324
+ const lastOriginalMapChunk = originalMapContent.pop();
325
+ assert(lastOriginalMapChunk);
326
+
327
+ // Create a new session for bob
328
+ const bobSession = bob.node;
329
+
330
+ // Feed all chunks except the last one from the original map
331
+ for (const chunk of originalMapContent) {
332
+ bobSession.syncManager.handleNewContent(chunk, "import");
333
+ }
334
+
335
+ // Feed all branch content
336
+ for (const chunk of branchContent) {
337
+ bobSession.syncManager.handleNewContent(chunk, "import");
338
+ }
339
+
340
+ const branchOnBob = await loadCoValueOrFail(bobSession, branch.id);
341
+
342
+ // The branch should not be completely downloaded because its source (original map) is still streaming
343
+ expect(branchOnBob.core.isCompletelyDownloaded()).toBe(false);
344
+
345
+ // Feed the last chunk from the original map
346
+ bobSession.syncManager.handleNewContent(lastOriginalMapChunk, "import");
347
+
348
+ // Now the branch should be completely downloaded
349
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
350
+ expect(branchOnBob.core.isCompletelyDownloaded()).toBe(true);
351
+ });
352
+
353
+ test("should return true when the value and all its dependencies are fully downloaded", async () => {
354
+ const parentGroup = admin.node.createGroup();
355
+ const childGroup = admin.node.createGroup();
356
+
357
+ childGroup.extend(parentGroup);
358
+
359
+ const map = childGroup.createMap();
360
+ map.set("test", "value");
361
+
362
+ await map.core.waitForSync();
363
+
364
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
365
+
366
+ // Everything should be fully synced
367
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
368
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
369
+ });
370
+
371
+ test("should handle complex nested hierarchy with streaming at different levels", async () => {
372
+ const grandParentGroup = admin.node.createGroup();
373
+ const parentGroup = admin.node.createGroup();
374
+ const childGroup = admin.node.createGroup();
375
+
376
+ parentGroup.extend(grandParentGroup);
377
+ childGroup.extend(parentGroup);
378
+
379
+ const map = childGroup.createMap();
380
+ map.set("test", "value");
381
+
382
+ await map.core.waitForSync();
383
+
384
+ // Disconnect the admin node
385
+ admin.disconnect();
386
+
387
+ // Make both the map and grandparent group large enough to require multiple chunks
388
+ for (let i = 0; i < 100; i++) {
389
+ map.set(`mapKey${i}`, "1".repeat(1024));
390
+ // @ts-expect-error - test property is not part of the group shape
391
+ grandParentGroup.set(`groupKey${i}`, "1".repeat(1024));
392
+ }
393
+
394
+ const mapContent = map.core.verified.newContentSince(undefined);
395
+ const groupContent =
396
+ grandParentGroup.core.verified.newContentSince(undefined);
397
+
398
+ assert(mapContent);
399
+ assert(groupContent);
400
+ expect(mapContent.length).toBeGreaterThan(1);
401
+ expect(groupContent.length).toBeGreaterThan(1);
402
+
403
+ const lastMapChunk = mapContent.pop();
404
+ const lastGroupChunk = groupContent.pop();
405
+ assert(lastMapChunk);
406
+ assert(lastGroupChunk);
407
+
408
+ // Create a new session for bob
409
+ const bobSession = bob.node;
410
+
411
+ // Feed all chunks except the last ones
412
+ for (const chunk of mapContent) {
413
+ bobSession.syncManager.handleNewContent(chunk, "import");
414
+ }
415
+ for (const chunk of groupContent) {
416
+ bobSession.syncManager.handleNewContent(chunk, "import");
417
+ }
418
+
419
+ const mapOnBob = await loadCoValueOrFail(bobSession, map.id);
420
+
421
+ // Should not be completely downloaded
422
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
423
+
424
+ // Feed the last map chunk
425
+ bobSession.syncManager.handleNewContent(lastMapChunk, "import");
426
+
427
+ // Still should not be completely downloaded because grandparent is still streaming
428
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
429
+
430
+ // Feed the last group chunk
431
+ bobSession.syncManager.handleNewContent(lastGroupChunk, "import");
432
+
433
+ // Now it should be completely downloaded
434
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
435
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
436
+ });
437
+
438
+ test("should emit an update when isCompletelyDownloaded status changes due to parent group streaming", async () => {
439
+ const group = admin.node.createGroup();
440
+ const map = group.createMap();
441
+ map.set("test", "value");
442
+
443
+ await map.core.waitForSync();
444
+
445
+ // Disconnect the admin node to sync the content manually
446
+ admin.disconnect();
447
+
448
+ // Make the group large enough to require multiple messages to be synced
449
+ for (let i = 0; i < 100; i++) {
450
+ // @ts-expect-error - test property is not part of the group shape
451
+ group.set(`key${i}`, "1".repeat(1024));
452
+ }
453
+
454
+ const content = group.core.verified.newContentSince(undefined);
455
+ assert(content);
456
+ expect(content.length).toBeGreaterThan(1);
457
+
458
+ const lastChunk = content.pop();
459
+ assert(lastChunk);
460
+
461
+ // Create a new session for bob
462
+ const bobSession = bob.node;
463
+
464
+ // Feed all chunks except the last one
465
+ for (const chunk of content) {
466
+ bobSession.syncManager.handleNewContent(chunk, "import");
467
+ }
468
+
469
+ const mapOnBob = await loadCoValueOrFail(bobSession, map.id);
470
+
471
+ // Verify the map is not completely downloaded yet because its owner (group) is still streaming
472
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
473
+
474
+ // Set up a listener to track updates
475
+ const updates: boolean[] = [];
476
+ mapOnBob.core.subscribe((content) => {
477
+ updates.push(content.isCompletelyDownloaded());
478
+ });
479
+
480
+ // Feed the last chunk
481
+ bobSession.syncManager.handleNewContent(lastChunk, "import");
482
+
483
+ // Wait for the notification to be scheduled and executed
484
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
485
+
486
+ // Verify the CoValue is now completely downloaded
487
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
488
+
489
+ // Verify the listener was called with the new status
490
+ expect(updates).toContain(true);
491
+ expect(updates[updates.length - 1]).toBe(true);
492
+ });
493
+ });
494
+
495
+ describe("streaming from the middle", () => {
496
+ test("should return false when the value itself is streaming", async () => {
497
+ const group = admin.node.createGroup();
498
+
499
+ const map = group.createMap();
500
+ map.set("initial", "value");
501
+
502
+ const initialKnownState = map.core.knownState();
503
+
504
+ await map.core.waitForSync();
505
+
506
+ // Disconnect the admin node to sync the content manually and simulate the delay of the last chunk
507
+ admin.disconnect();
508
+
509
+ // Create a new session for bob
510
+ const bobSession = bob.node;
511
+
512
+ const mapOnBob = await loadCoValueOrFail(bobSession, map.id);
513
+
514
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
515
+
516
+ // Make the map large enough to require multiple messages to be synced
517
+ for (let i = 0; i < 100; i++) {
518
+ map.set(`key${i}`, "1".repeat(1024));
519
+ }
520
+
521
+ const content = map.core.verified.newContentSince(initialKnownState);
522
+ assert(content);
523
+ expect(content.length).toBeGreaterThan(1);
524
+
525
+ const lastChunk = content.pop();
526
+ assert(lastChunk);
527
+
528
+ // Feed all chunks except the last one
529
+ for (const chunk of content) {
530
+ bobSession.syncManager.handleNewContent(chunk, "import");
531
+ }
532
+
533
+ // The map should not be completely downloaded yet
534
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
535
+
536
+ // Feed the last chunk
537
+ bobSession.syncManager.handleNewContent(lastChunk, "import");
538
+
539
+ // Wait for the notification to be scheduled and executed
540
+ await waitFor(() => mapOnBob.core.isCompletelyDownloaded());
541
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
542
+ });
543
+
544
+ test.skip("should return false when the owner of the value is streaming", async () => {
545
+ const group = admin.node.createGroup();
546
+
547
+ const map = group.createMap();
548
+ map.set("test", "value");
549
+
550
+ await map.core.waitForSync();
551
+
552
+ // Disconnect the admin node to sync the content manually
553
+ admin.disconnect();
554
+
555
+ const bobSession = bob.node;
556
+ const mapOnBob = await loadCoValueOrFail(bobSession, map.id);
557
+
558
+ // The map should not be completely downloaded because its owner (group) is still streaming
559
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
560
+
561
+ // Make the group large enough to require multiple messages to be synced
562
+ for (let i = 0; i < 100; i++) {
563
+ // @ts-expect-error - test property is not part of the group shape
564
+ group.set(`key${i}`, "1".repeat(1024));
565
+ }
566
+
567
+ const content = group.core.verified.newContentSince(undefined);
568
+ assert(content);
569
+ expect(content.length).toBeGreaterThan(1);
570
+
571
+ const lastChunk = content.pop();
572
+ assert(lastChunk);
573
+
574
+ // Feed all chunks except the last one
575
+ for (const chunk of content) {
576
+ bobSession.syncManager.handleNewContent(chunk, "import");
577
+ }
578
+
579
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(false);
580
+
581
+ // Feed the last chunk
582
+ bobSession.syncManager.handleNewContent(lastChunk, "import");
583
+
584
+ // Wait for the notification to be scheduled and executed
585
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
586
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
587
+ });
588
+ });
589
+ });