cojson 0.18.33 → 0.18.34

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 (75) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +9 -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.map +1 -1
  7. package/dist/coValueCore/SessionMap.js +9 -15
  8. package/dist/coValueCore/SessionMap.js.map +1 -1
  9. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  10. package/dist/coValueCore/coValueCore.js +3 -4
  11. package/dist/coValueCore/coValueCore.js.map +1 -1
  12. package/dist/coValueCore/verifiedState.d.ts +3 -1
  13. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  14. package/dist/coValueCore/verifiedState.js +10 -4
  15. package/dist/coValueCore/verifiedState.js.map +1 -1
  16. package/dist/knownState.d.ts +9 -1
  17. package/dist/knownState.d.ts.map +1 -1
  18. package/dist/knownState.js +29 -3
  19. package/dist/knownState.js.map +1 -1
  20. package/dist/localNode.d.ts.map +1 -1
  21. package/dist/localNode.js +2 -1
  22. package/dist/localNode.js.map +1 -1
  23. package/dist/queue/LocalTransactionsSyncQueue.d.ts +10 -9
  24. package/dist/queue/LocalTransactionsSyncQueue.d.ts.map +1 -1
  25. package/dist/queue/LocalTransactionsSyncQueue.js +53 -47
  26. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -1
  27. package/dist/storage/knownState.js +2 -2
  28. package/dist/storage/knownState.js.map +1 -1
  29. package/dist/sync.d.ts +1 -2
  30. package/dist/sync.d.ts.map +1 -1
  31. package/dist/sync.js +0 -1
  32. package/dist/sync.js.map +1 -1
  33. package/dist/tests/coPlainText.test.js +13 -14
  34. package/dist/tests/coPlainText.test.js.map +1 -1
  35. package/dist/tests/coValueCore.isCompletelyDownloaded.test.js +3 -2
  36. package/dist/tests/coValueCore.isCompletelyDownloaded.test.js.map +1 -1
  37. package/dist/tests/coValueCore.isStreaming.test.js +54 -3
  38. package/dist/tests/coValueCore.isStreaming.test.js.map +1 -1
  39. package/dist/tests/group.childKeyRotation.test.js +9 -9
  40. package/dist/tests/group.childKeyRotation.test.js.map +1 -1
  41. package/dist/tests/knownState.test.js +82 -10
  42. package/dist/tests/knownState.test.js.map +1 -1
  43. package/dist/tests/sync.load.test.js +29 -29
  44. package/dist/tests/sync.mesh.test.js +38 -31
  45. package/dist/tests/sync.mesh.test.js.map +1 -1
  46. package/dist/tests/sync.storage.test.js +24 -23
  47. package/dist/tests/sync.storage.test.js.map +1 -1
  48. package/dist/tests/sync.storageAsync.test.js +24 -23
  49. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  50. package/dist/tests/sync.upload.test.js +58 -58
  51. package/dist/tests/testUtils.d.ts +11 -9
  52. package/dist/tests/testUtils.d.ts.map +1 -1
  53. package/dist/tests/testUtils.js +26 -16
  54. package/dist/tests/testUtils.js.map +1 -1
  55. package/package.json +3 -3
  56. package/src/SyncStateManager.ts +8 -2
  57. package/src/coValueCore/SessionMap.ts +18 -15
  58. package/src/coValueCore/coValueCore.ts +4 -11
  59. package/src/coValueCore/verifiedState.ts +20 -5
  60. package/src/knownState.ts +48 -4
  61. package/src/localNode.ts +6 -3
  62. package/src/queue/LocalTransactionsSyncQueue.ts +77 -93
  63. package/src/storage/knownState.ts +2 -2
  64. package/src/sync.ts +0 -1
  65. package/src/tests/coPlainText.test.ts +13 -14
  66. package/src/tests/coValueCore.isCompletelyDownloaded.test.ts +3 -2
  67. package/src/tests/coValueCore.isStreaming.test.ts +84 -2
  68. package/src/tests/group.childKeyRotation.test.ts +9 -9
  69. package/src/tests/knownState.test.ts +106 -9
  70. package/src/tests/sync.load.test.ts +29 -29
  71. package/src/tests/sync.mesh.test.ts +38 -31
  72. package/src/tests/sync.storage.test.ts +24 -23
  73. package/src/tests/sync.storageAsync.test.ts +24 -23
  74. package/src/tests/sync.upload.test.ts +58 -58
  75. package/src/tests/testUtils.ts +30 -18
package/src/knownState.ts CHANGED
@@ -106,18 +106,62 @@ export function cloneKnownState(knownState: CoValueKnownState) {
106
106
  };
107
107
  }
108
108
 
109
+ /**
110
+ * Checks if all the local sessions have the same counters as in remote.
111
+ */
112
+ export function areCurrentSessionsInSyncWith(
113
+ current: Record<string, number>,
114
+ target: Record<string, number>,
115
+ ) {
116
+ for (const [sessionId, currentCount] of Object.entries(current) as [
117
+ SessionID,
118
+ number,
119
+ ][]) {
120
+ const targetCount = target[sessionId] ?? 0;
121
+ if (currentCount !== targetCount) {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ return true;
127
+ }
128
+
109
129
  /**
110
130
  * Checks if all the local sessions have the same counters as in remote.
111
131
  */
112
132
  export function isKnownStateSubsetOf(
113
- local: Record<string, number>,
114
- remote: Record<string, number>,
133
+ current: Record<string, number>,
134
+ target: Record<string, number>,
115
135
  ) {
116
- for (const sessionId of Object.keys(local)) {
117
- if (local[sessionId] !== remote[sessionId]) {
136
+ for (const [sessionId, currentCount] of Object.entries(current) as [
137
+ SessionID,
138
+ number,
139
+ ][]) {
140
+ const targetCount = target[sessionId] ?? 0;
141
+ if (currentCount > targetCount) {
118
142
  return false;
119
143
  }
120
144
  }
121
145
 
122
146
  return true;
123
147
  }
148
+
149
+ /**
150
+ * Returns the record with the sessions that need to be sent to the target
151
+ */
152
+ export function getKnownStateToSend(
153
+ current: Record<string, number>,
154
+ target: Record<string, number>,
155
+ ) {
156
+ const toSend: Record<string, number> = {};
157
+ for (const [sessionId, currentCount] of Object.entries(current) as [
158
+ SessionID,
159
+ number,
160
+ ][]) {
161
+ const targetCount = target[sessionId] ?? 0;
162
+ if (currentCount > targetCount) {
163
+ toSend[sessionId] = currentCount;
164
+ }
165
+ }
166
+ return toSend;
167
+ }
package/src/localNode.ts CHANGED
@@ -10,7 +10,6 @@ import {
10
10
  import {
11
11
  type CoValueHeader,
12
12
  type CoValueUniqueness,
13
- VerifiedState,
14
13
  } from "./coValueCore/verifiedState.js";
15
14
  import {
16
15
  AccountMeta,
@@ -31,7 +30,7 @@ import {
31
30
  type RawGroup,
32
31
  secretSeedFromInviteSecret,
33
32
  } from "./coValues/group.js";
34
- import { CO_VALUE_LOADING_CONFIG, GARBAGE_COLLECTOR_CONFIG } from "./config.js";
33
+ import { CO_VALUE_LOADING_CONFIG } from "./config.js";
35
34
  import { AgentSecret, CryptoProvider } from "./crypto/crypto.js";
36
35
  import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
37
36
  import { logger } from "./logger.js";
@@ -41,6 +40,7 @@ import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromS
41
40
  import { expectGroup } from "./typeUtils/expectGroup.js";
42
41
  import { canBeBranched } from "./coValueCore/branching.js";
43
42
  import { connectedPeers } from "./streamUtils.js";
43
+ import { emptyKnownState } from "./knownState.js";
44
44
 
45
45
  /** A `LocalNode` represents a local view of a set of loaded `CoValue`s, from the perspective of a particular account (or primitive cryptographic agent).
46
46
 
@@ -380,7 +380,10 @@ export class LocalNode {
380
380
  }
381
381
 
382
382
  this.garbageCollector?.trackCoValueAccess(coValue);
383
- this.syncManager.syncHeader(coValue.verified);
383
+ this.syncManager.syncLocalTransaction(
384
+ coValue.verified,
385
+ emptyKnownState(id),
386
+ );
384
387
 
385
388
  return coValue;
386
389
  }
@@ -1,17 +1,8 @@
1
- import {
2
- addTransactionToContentMessage,
3
- createContentMessage,
4
- knownStateFromContent,
5
- } from "../coValueContentMessage.js";
6
- import { Transaction, VerifiedState } from "../coValueCore/verifiedState.js";
7
- import { Signature } from "../crypto/crypto.js";
8
- import { RawCoID, SessionID } from "../ids.js";
9
- import {
10
- combineKnownStateSessions,
11
- KnownStateSessions,
12
- } from "../knownState.js";
1
+ import { knownStateFromContent } from "../coValueContentMessage.js";
2
+ import { VerifiedState } from "../coValueCore/verifiedState.js";
3
+ import { RawCoID } from "../ids.js";
4
+ import { combineKnownStateSessions, CoValueKnownState } from "../knownState.js";
13
5
  import { NewContentMessage } from "../sync.js";
14
- import { LinkedList } from "./LinkedList.js";
15
6
 
16
7
  /**
17
8
  * This queue is used to batch the sync of local transactions while preserving the order of updates between CoValues.
@@ -23,65 +14,97 @@ import { LinkedList } from "./LinkedList.js";
23
14
  * 2. If we do multiple updates on the same CoMap, the updates will be batched because it's safe to do so.
24
15
  */
25
16
  export class LocalTransactionsSyncQueue {
26
- private readonly queue = new LinkedList<NewContentMessage>();
17
+ private batch: NewContentMessage[] = [];
18
+ private firstChunks = new Map<RawCoID, NewContentMessage>();
19
+ private lastUpdatedValue: VerifiedState | undefined;
20
+ private lastUpdatedValueKnownState: CoValueKnownState | undefined;
27
21
 
28
22
  constructor(private readonly sync: (content: NewContentMessage) => void) {}
29
23
 
30
- syncHeader = (coValue: VerifiedState) => {
31
- const lastPendingSync = this.queue.tail?.value;
24
+ syncTransaction = (
25
+ coValue: VerifiedState,
26
+ knownStateBefore: CoValueKnownState,
27
+ ) => {
28
+ const lastUpdatedValue = this.lastUpdatedValue;
29
+ const lastUpdatedValueKnownState = this.lastUpdatedValueKnownState;
32
30
 
33
- if (lastPendingSync?.id === coValue.id) {
34
- return;
31
+ if (lastUpdatedValue && lastUpdatedValueKnownState) {
32
+ if (lastUpdatedValue.id === coValue.id) {
33
+ return;
34
+ }
35
+
36
+ this.addContentToBatch(lastUpdatedValue, lastUpdatedValueKnownState);
37
+ }
38
+
39
+ this.lastUpdatedValue = coValue;
40
+ this.lastUpdatedValueKnownState = knownStateBefore;
41
+
42
+ for (const trackingSet of this.dirtyCoValuesTrackingSets) {
43
+ trackingSet.add(coValue.id);
35
44
  }
36
45
 
37
- this.enqueue(createContentMessage(coValue.id, coValue.header));
46
+ this.scheduleNextBatch();
38
47
  };
39
48
 
40
- syncTransaction = (
49
+ private addContentToBatch(
41
50
  coValue: VerifiedState,
42
- transaction: Transaction,
43
- sessionID: SessionID,
44
- signature: Signature,
45
- txIdx: number,
46
- ) => {
47
- const lastPendingSync = this.queue.tail?.value;
48
- const lastSignatureIdx = coValue.getLastSignatureCheckpoint(sessionID);
49
- const isSignatureCheckpoint =
50
- lastSignatureIdx > -1 && lastSignatureIdx === txIdx;
51
-
52
- if (lastPendingSync?.id === coValue.id && !isSignatureCheckpoint) {
53
- addTransactionToContentMessage(
54
- lastPendingSync,
55
- transaction,
56
- sessionID,
57
- signature,
58
- txIdx,
59
- );
51
+ knownStateBefore: CoValueKnownState,
52
+ ) {
53
+ const content = coValue.newContentSince(knownStateBefore, {
54
+ skipExpectContentUntil: true, // we need to calculate the streaming header considering the current batch
55
+ });
60
56
 
57
+ if (!content) {
61
58
  return;
62
59
  }
63
60
 
64
- const content = createContentMessage(coValue.id, coValue.header, false);
61
+ let firstChunk = this.firstChunks.get(coValue.id);
65
62
 
66
- addTransactionToContentMessage(
67
- content,
68
- transaction,
69
- sessionID,
70
- signature,
71
- txIdx,
72
- );
63
+ for (const piece of content) {
64
+ this.batch.push(piece);
73
65
 
74
- this.enqueue(content);
75
- };
66
+ // Check if the local content updates are in streaming, if so we need to add the info to the first chunk
67
+ if (firstChunk) {
68
+ if (!firstChunk.expectContentUntil) {
69
+ firstChunk.expectContentUntil =
70
+ knownStateFromContent(firstChunk).sessions;
71
+ }
72
+ combineKnownStateSessions(
73
+ firstChunk.expectContentUntil,
74
+ knownStateFromContent(piece).sessions,
75
+ );
76
+ } else {
77
+ firstChunk = piece;
78
+ this.firstChunks.set(coValue.id, firstChunk);
79
+ }
80
+ }
81
+ }
76
82
 
77
- enqueue(content: NewContentMessage) {
78
- this.queue.push(content);
83
+ private nextBatchScheduled = false;
84
+ scheduleNextBatch() {
85
+ if (this.nextBatchScheduled) return;
79
86
 
80
- this.processPendingSyncs();
87
+ this.nextBatchScheduled = true;
81
88
 
82
- for (const trackingSet of this.dirtyCoValuesTrackingSets) {
83
- trackingSet.add(content.id);
84
- }
89
+ queueMicrotask(() => {
90
+ if (this.lastUpdatedValue && this.lastUpdatedValueKnownState) {
91
+ this.addContentToBatch(
92
+ this.lastUpdatedValue,
93
+ this.lastUpdatedValueKnownState,
94
+ );
95
+ }
96
+ const batch = this.batch;
97
+
98
+ this.lastUpdatedValue = undefined;
99
+ this.lastUpdatedValueKnownState = undefined;
100
+ this.firstChunks = new Map();
101
+ this.batch = [];
102
+ this.nextBatchScheduled = false;
103
+
104
+ for (const content of batch) {
105
+ this.sync(content);
106
+ }
107
+ });
85
108
  }
86
109
 
87
110
  private dirtyCoValuesTrackingSets: Set<Set<RawCoID>> = new Set();
@@ -110,43 +133,4 @@ export class LocalTransactionsSyncQueue {
110
133
  },
111
134
  };
112
135
  };
113
-
114
- private processingSyncs = false;
115
- processPendingSyncs() {
116
- if (this.processingSyncs) return;
117
-
118
- this.processingSyncs = true;
119
-
120
- queueMicrotask(() => {
121
- const firstContentPieceMap = new Map<RawCoID, NewContentMessage>();
122
-
123
- while (this.queue.head) {
124
- const content = this.queue.head.value;
125
-
126
- const firstContentPiece = firstContentPieceMap.get(content.id);
127
-
128
- if (!firstContentPiece) {
129
- firstContentPieceMap.set(content.id, content);
130
- } else {
131
- // There is already a content piece for this coValue, so this means that we need to flag
132
- // that this content is going to be streamed
133
- if (!firstContentPiece.expectContentUntil) {
134
- firstContentPiece.expectContentUntil =
135
- knownStateFromContent(firstContentPiece).sessions;
136
- }
137
-
138
- combineKnownStateSessions(
139
- firstContentPiece.expectContentUntil,
140
- knownStateFromContent(content).sessions,
141
- );
142
- }
143
-
144
- this.sync(content);
145
-
146
- this.queue.shift();
147
- }
148
-
149
- this.processingSyncs = false;
150
- });
151
- }
152
136
  }
@@ -3,7 +3,7 @@ import { RawCoID } from "../ids.js";
3
3
  import {
4
4
  CoValueKnownState,
5
5
  emptyKnownState,
6
- isKnownStateSubsetOf,
6
+ areCurrentSessionsInSyncWith,
7
7
  } from "../knownState.js";
8
8
 
9
9
  /**
@@ -87,7 +87,7 @@ function isInSync(
87
87
  return false;
88
88
  }
89
89
 
90
- return isKnownStateSubsetOf(
90
+ return areCurrentSessionsInSyncWith(
91
91
  knownState.sessions,
92
92
  knownStateFromStorage.sessions,
93
93
  );
package/src/sync.ts CHANGED
@@ -771,7 +771,6 @@ export class SyncManager {
771
771
  private syncQueue = new LocalTransactionsSyncQueue((content) =>
772
772
  this.syncContent(content),
773
773
  );
774
- syncHeader = this.syncQueue.syncHeader;
775
774
  syncLocalTransaction = this.syncQueue.syncTransaction;
776
775
  trackDirtyCoValues = this.syncQueue.trackDirtyCoValues;
777
776
 
@@ -371,13 +371,12 @@ test("chunks transactions when when the chars are longer than MAX_RECOMMENDED_TX
371
371
  }),
372
372
  ).toMatchInlineSnapshot(`
373
373
  [
374
- "client -> storage | CONTENT CoPlainText header: true new: After: 0 New: 1 expectContentUntil: header/42",
375
- "client -> storage | CONTENT CoPlainText header: false new: After: 1 New: 1",
374
+ "client -> storage | CONTENT CoPlainText header: true new: After: 0 New: 2 expectContentUntil: header/42",
376
375
  "client -> storage | CONTENT CoPlainText header: false new: After: 2 New: 1",
377
376
  "client -> storage | CONTENT CoPlainText header: false new: After: 3 New: 1",
378
377
  "client -> storage | CONTENT CoPlainText header: false new: After: 4 New: 1",
379
- "client -> storage | CONTENT CoPlainText header: false new: After: 5 New: 2",
380
- "client -> storage | CONTENT CoPlainText header: false new: After: 7 New: 1",
378
+ "client -> storage | CONTENT CoPlainText header: false new: After: 5 New: 1",
379
+ "client -> storage | CONTENT CoPlainText header: false new: After: 6 New: 2",
381
380
  "client -> storage | CONTENT CoPlainText header: false new: After: 8 New: 1",
382
381
  "client -> storage | CONTENT CoPlainText header: false new: After: 9 New: 1",
383
382
  "client -> storage | CONTENT CoPlainText header: false new: After: 10 New: 1",
@@ -389,8 +388,8 @@ test("chunks transactions when when the chars are longer than MAX_RECOMMENDED_TX
389
388
  "client -> storage | CONTENT CoPlainText header: false new: After: 16 New: 1",
390
389
  "client -> storage | CONTENT CoPlainText header: false new: After: 17 New: 1",
391
390
  "client -> storage | CONTENT CoPlainText header: false new: After: 18 New: 1",
392
- "client -> storage | CONTENT CoPlainText header: false new: After: 19 New: 2",
393
- "client -> storage | CONTENT CoPlainText header: false new: After: 21 New: 1",
391
+ "client -> storage | CONTENT CoPlainText header: false new: After: 19 New: 1",
392
+ "client -> storage | CONTENT CoPlainText header: false new: After: 20 New: 2",
394
393
  "client -> storage | CONTENT CoPlainText header: false new: After: 22 New: 1",
395
394
  "client -> storage | CONTENT CoPlainText header: false new: After: 23 New: 1",
396
395
  "client -> storage | CONTENT CoPlainText header: false new: After: 24 New: 1",
@@ -406,16 +405,16 @@ test("chunks transactions when when the chars are longer than MAX_RECOMMENDED_TX
406
405
  "client -> storage | CONTENT CoPlainText header: false new: After: 34 New: 1",
407
406
  "client -> storage | CONTENT CoPlainText header: false new: After: 35 New: 1",
408
407
  "client -> storage | CONTENT CoPlainText header: false new: After: 36 New: 1",
409
- "client -> storage | CONTENT CoPlainText header: false new: After: 37 New: 3",
410
- "client -> storage | CONTENT CoPlainText header: false new: After: 40 New: 1",
408
+ "client -> storage | CONTENT CoPlainText header: false new: After: 37 New: 1",
409
+ "client -> storage | CONTENT CoPlainText header: false new: After: 38 New: 3",
411
410
  "client -> storage | CONTENT CoPlainText header: false new: After: 41 New: 1",
412
411
  "client -> storage | LOAD CoPlainText sessions: empty",
413
412
  "storage -> client | CONTENT CoPlainText header: true new: After: 0 New: 2 expectContentUntil: header/42",
414
413
  "storage -> client | CONTENT CoPlainText header: true new: After: 2 New: 1",
415
414
  "storage -> client | CONTENT CoPlainText header: true new: After: 3 New: 1",
416
415
  "storage -> client | CONTENT CoPlainText header: true new: After: 4 New: 1",
417
- "storage -> client | CONTENT CoPlainText header: true new: After: 5 New: 2",
418
- "storage -> client | CONTENT CoPlainText header: true new: After: 7 New: 1",
416
+ "storage -> client | CONTENT CoPlainText header: true new: After: 5 New: 1",
417
+ "storage -> client | CONTENT CoPlainText header: true new: After: 6 New: 2",
419
418
  "storage -> client | CONTENT CoPlainText header: true new: After: 8 New: 1",
420
419
  "storage -> client | CONTENT CoPlainText header: true new: After: 9 New: 1",
421
420
  "storage -> client | CONTENT CoPlainText header: true new: After: 10 New: 1",
@@ -427,8 +426,8 @@ test("chunks transactions when when the chars are longer than MAX_RECOMMENDED_TX
427
426
  "storage -> client | CONTENT CoPlainText header: true new: After: 16 New: 1",
428
427
  "storage -> client | CONTENT CoPlainText header: true new: After: 17 New: 1",
429
428
  "storage -> client | CONTENT CoPlainText header: true new: After: 18 New: 1",
430
- "storage -> client | CONTENT CoPlainText header: true new: After: 19 New: 2",
431
- "storage -> client | CONTENT CoPlainText header: true new: After: 21 New: 1",
429
+ "storage -> client | CONTENT CoPlainText header: true new: After: 19 New: 1",
430
+ "storage -> client | CONTENT CoPlainText header: true new: After: 20 New: 2",
432
431
  "storage -> client | CONTENT CoPlainText header: true new: After: 22 New: 1",
433
432
  "storage -> client | CONTENT CoPlainText header: true new: After: 23 New: 1",
434
433
  "storage -> client | CONTENT CoPlainText header: true new: After: 24 New: 1",
@@ -444,8 +443,8 @@ test("chunks transactions when when the chars are longer than MAX_RECOMMENDED_TX
444
443
  "storage -> client | CONTENT CoPlainText header: true new: After: 34 New: 1",
445
444
  "storage -> client | CONTENT CoPlainText header: true new: After: 35 New: 1",
446
445
  "storage -> client | CONTENT CoPlainText header: true new: After: 36 New: 1",
447
- "storage -> client | CONTENT CoPlainText header: true new: After: 37 New: 3",
448
- "storage -> client | CONTENT CoPlainText header: true new: After: 40 New: 1",
446
+ "storage -> client | CONTENT CoPlainText header: true new: After: 37 New: 1",
447
+ "storage -> client | CONTENT CoPlainText header: true new: After: 38 New: 3",
449
448
  "storage -> client | CONTENT CoPlainText header: true new: After: 41 New: 1",
450
449
  ]
451
450
  `);
@@ -537,8 +537,9 @@ describe("CoValueCore.isCompletelyDownloaded", () => {
537
537
  bobSession.syncManager.handleNewContent(lastChunk, "import");
538
538
 
539
539
  // Wait for the notification to be scheduled and executed
540
- await waitFor(() => mapOnBob.core.isCompletelyDownloaded());
541
- expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
540
+ await waitFor(() => {
541
+ expect(mapOnBob.core.isCompletelyDownloaded()).toBe(true);
542
+ });
542
543
  });
543
544
 
544
545
  test.skip("should return false when the owner of the value is streaming", async () => {
@@ -3,9 +3,12 @@ import {
3
3
  SyncMessagesLog,
4
4
  TEST_NODE_CONFIG,
5
5
  loadCoValueOrFail,
6
+ setupTestAccount,
6
7
  setupTestNode,
7
8
  waitFor,
8
9
  } from "./testUtils";
10
+ import type { RawCoMap } from "../exports";
11
+ import type { CoID } from "../coValue";
9
12
 
10
13
  let jazzCloud: ReturnType<typeof setupTestNode>;
11
14
 
@@ -82,7 +85,7 @@ describe("isStreaming", () => {
82
85
  expect(mapInNewSession.core.isStreaming()).toBe(false);
83
86
  });
84
87
 
85
- test("loading a large content update should be streaming until all chunks are sent", async () => {
88
+ test("loading a large content update should be streaming until all chunks are sent", async () => {
86
89
  const client = setupTestNode({
87
90
  connected: true,
88
91
  });
@@ -222,7 +225,8 @@ describe("isStreaming", () => {
222
225
  await map.core.waitForSync();
223
226
  const newSession = client.spawnNewSession();
224
227
 
225
- await loadCoValueOrFail(newSession.node, map.id);
228
+ const mapInNewSession1 = await loadCoValueOrFail(newSession.node, map.id);
229
+ await mapInNewSession1.core.waitForFullStreaming();
226
230
 
227
231
  const content = map.core.verified.newContentSince(undefined);
228
232
  assert(content);
@@ -268,4 +272,82 @@ describe("isStreaming", () => {
268
272
 
269
273
  expect(mapInNewSession.core.isStreaming()).toBe(false);
270
274
  });
275
+
276
+ test("mixed updates should not leave isStreaming to true (3 sessions)", async () => {
277
+ const aliceLaptop = await setupTestAccount({
278
+ connected: true,
279
+ });
280
+
281
+ const group = aliceLaptop.node.createGroup();
282
+ const map = group.createMap();
283
+
284
+ map.set("count", 0, "trusting");
285
+ map.set("count", 1, "trusting");
286
+
287
+ await map.core.waitForSync();
288
+
289
+ const alicePhone = await aliceLaptop.spawnNewSession();
290
+
291
+ const mapOnPhone = await loadCoValueOrFail(alicePhone.node, map.id);
292
+
293
+ mapOnPhone.set("count", 2, "trusting");
294
+ mapOnPhone.set("count", 3, "trusting");
295
+ mapOnPhone.set("count", 4, "trusting");
296
+
297
+ await mapOnPhone.core.waitForSync();
298
+
299
+ const aliceTablet = await alicePhone.spawnNewSession();
300
+ const mapOnTablet = await loadCoValueOrFail(aliceTablet.node, map.id);
301
+
302
+ mapOnTablet.set("count", 5, "trusting");
303
+ mapOnTablet.set("count", 6, "trusting");
304
+ mapOnTablet.set("count", 7, "trusting");
305
+
306
+ await mapOnTablet.core.waitForSync();
307
+
308
+ map.set("count", 8, "trusting");
309
+ map.set("count", 9, "trusting");
310
+ map.set("count", 10, "trusting");
311
+
312
+ mapOnPhone.set("count", 11, "trusting");
313
+ mapOnTablet.set("count", 12, "trusting");
314
+
315
+ await map.core.waitForSync();
316
+ await mapOnPhone.core.waitForSync();
317
+ await mapOnTablet.core.waitForSync();
318
+
319
+ expect(map.core.isStreaming()).toBe(false);
320
+ expect(mapOnPhone.core.isStreaming()).toBe(false);
321
+ expect(mapOnTablet.core.isStreaming()).toBe(false);
322
+
323
+ const mapBranch = map.core.createBranch("test-branch");
324
+
325
+ const aliceTv = await aliceTablet.spawnNewSession();
326
+
327
+ const mapBranchOnTv = await loadCoValueOrFail(
328
+ aliceTv.node,
329
+ mapBranch.id as unknown as CoID<RawCoMap>,
330
+ );
331
+
332
+ mapBranchOnTv.set("count", 13, "trusting");
333
+ mapBranchOnTv.set("count", 14, "trusting");
334
+ mapBranchOnTv.set("count", 15, "trusting");
335
+
336
+ await mapBranchOnTv.core.waitForSync();
337
+
338
+ group.addMember("everyone", "reader");
339
+
340
+ const bob = await setupTestAccount({
341
+ connected: true,
342
+ });
343
+
344
+ const mapBranchOnBob = await loadCoValueOrFail(bob.node, mapBranchOnTv.id);
345
+
346
+ expect(mapBranchOnBob.core.isStreaming()).toBe(false);
347
+ expect(map.core.isStreaming()).toBe(false);
348
+ expect(mapOnPhone.core.isStreaming()).toBe(false);
349
+ expect(mapOnTablet.core.isStreaming()).toBe(false);
350
+
351
+ expect(mapBranchOnBob.get("count")).toBe(15);
352
+ });
271
353
  });
@@ -89,7 +89,7 @@ describe("Group.childKeyRotation", () => {
89
89
  const newBobSession = await bob.spawnNewSession();
90
90
 
91
91
  const childGroupOnNewBobNode = await loadCoValueOrFail(
92
- newBobSession,
92
+ newBobSession.node,
93
93
  childGroup.id,
94
94
  );
95
95
 
@@ -138,15 +138,15 @@ describe("Group.childKeyRotation", () => {
138
138
  const newBobSession = await bob.spawnNewSession();
139
139
 
140
140
  for (const chunk of content) {
141
- newBobSession.syncManager.handleNewContent(chunk, "import");
141
+ newBobSession.node.syncManager.handleNewContent(chunk, "import");
142
142
  }
143
143
 
144
144
  const childGroupOnNewBobNode = await loadCoValueOrFail(
145
- newBobSession,
145
+ newBobSession.node,
146
146
  childGroup.id,
147
147
  );
148
148
 
149
- newBobSession.syncManager.handleNewContent(lastChunk, "import");
149
+ newBobSession.node.syncManager.handleNewContent(lastChunk, "import");
150
150
 
151
151
  // The migration waits for the group to be completely downloaded
152
152
  await childGroupOnNewBobNode.core.waitForAsync((core) =>
@@ -205,15 +205,15 @@ describe("Group.childKeyRotation", () => {
205
205
  const newBobSession = await bob.spawnNewSession();
206
206
 
207
207
  for (const chunk of content) {
208
- newBobSession.syncManager.handleNewContent(chunk, "import");
208
+ newBobSession.node.syncManager.handleNewContent(chunk, "import");
209
209
  }
210
210
 
211
211
  const childGroupOnNewBobNode = await loadCoValueOrFail(
212
- newBobSession,
212
+ newBobSession.node,
213
213
  childGroup.id,
214
214
  );
215
215
 
216
- newBobSession.syncManager.handleNewContent(lastChunk, "import");
216
+ newBobSession.node.syncManager.handleNewContent(lastChunk, "import");
217
217
 
218
218
  // The migration waits for the group to be completely downloaded, this includes full streaming of the parent group
219
219
  await childGroupOnNewBobNode.core.waitForAsync((core) =>
@@ -268,7 +268,7 @@ describe("Group.childKeyRotation", () => {
268
268
 
269
269
  // Instead Bob is an admin, so when loading the child group he can rotate the readKey
270
270
  const newBobSession = await bob.spawnNewSession();
271
- const mapOnNewBobNode = await loadCoValueOrFail(newBobSession, map.id);
271
+ const mapOnNewBobNode = await loadCoValueOrFail(newBobSession.node, map.id);
272
272
 
273
273
  mapOnNewBobNode.set("test", "Not readable by charlie");
274
274
 
@@ -416,7 +416,7 @@ describe("Group.childKeyRotation", () => {
416
416
 
417
417
  const newBobSession = await bob.spawnNewSession();
418
418
  const childGroupOnNewBobNode = await loadCoValueOrFail(
419
- newBobSession,
419
+ newBobSession.node,
420
420
  childGroup.id,
421
421
  );
422
422