cojson 0.18.33 → 0.18.35

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 (93) 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/coValueContentMessage.d.ts +5 -2
  7. package/dist/coValueContentMessage.d.ts.map +1 -1
  8. package/dist/coValueContentMessage.js +15 -0
  9. package/dist/coValueContentMessage.js.map +1 -1
  10. package/dist/coValueCore/SessionMap.d.ts +4 -3
  11. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  12. package/dist/coValueCore/SessionMap.js +21 -30
  13. package/dist/coValueCore/SessionMap.js.map +1 -1
  14. package/dist/coValueCore/coValueCore.d.ts +14 -6
  15. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  16. package/dist/coValueCore/coValueCore.js +32 -52
  17. package/dist/coValueCore/coValueCore.js.map +1 -1
  18. package/dist/coValueCore/verifiedState.d.ts +6 -4
  19. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  20. package/dist/coValueCore/verifiedState.js +21 -7
  21. package/dist/coValueCore/verifiedState.js.map +1 -1
  22. package/dist/coValues/group.d.ts.map +1 -1
  23. package/dist/coValues/group.js +20 -15
  24. package/dist/coValues/group.js.map +1 -1
  25. package/dist/knownState.d.ts +9 -1
  26. package/dist/knownState.d.ts.map +1 -1
  27. package/dist/knownState.js +29 -3
  28. package/dist/knownState.js.map +1 -1
  29. package/dist/localNode.d.ts +7 -2
  30. package/dist/localNode.d.ts.map +1 -1
  31. package/dist/localNode.js +10 -15
  32. package/dist/localNode.js.map +1 -1
  33. package/dist/queue/LocalTransactionsSyncQueue.d.ts +10 -9
  34. package/dist/queue/LocalTransactionsSyncQueue.d.ts.map +1 -1
  35. package/dist/queue/LocalTransactionsSyncQueue.js +53 -47
  36. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -1
  37. package/dist/storage/knownState.js +2 -2
  38. package/dist/storage/knownState.js.map +1 -1
  39. package/dist/sync.d.ts +1 -2
  40. package/dist/sync.d.ts.map +1 -1
  41. package/dist/sync.js +15 -19
  42. package/dist/sync.js.map +1 -1
  43. package/dist/tests/coPlainText.test.js +13 -14
  44. package/dist/tests/coPlainText.test.js.map +1 -1
  45. package/dist/tests/coValueContentMessage.test.js +130 -1
  46. package/dist/tests/coValueContentMessage.test.js.map +1 -1
  47. package/dist/tests/coValueCore.isCompletelyDownloaded.test.js +3 -2
  48. package/dist/tests/coValueCore.isCompletelyDownloaded.test.js.map +1 -1
  49. package/dist/tests/coValueCore.isStreaming.test.js +54 -3
  50. package/dist/tests/coValueCore.isStreaming.test.js.map +1 -1
  51. package/dist/tests/coValueCore.test.js +3 -6
  52. package/dist/tests/coValueCore.test.js.map +1 -1
  53. package/dist/tests/group.childKeyRotation.test.js +9 -9
  54. package/dist/tests/group.childKeyRotation.test.js.map +1 -1
  55. package/dist/tests/knownState.test.js +82 -10
  56. package/dist/tests/knownState.test.js.map +1 -1
  57. package/dist/tests/sync.load.test.js +29 -29
  58. package/dist/tests/sync.mesh.test.js +38 -31
  59. package/dist/tests/sync.mesh.test.js.map +1 -1
  60. package/dist/tests/sync.storage.test.js +24 -23
  61. package/dist/tests/sync.storage.test.js.map +1 -1
  62. package/dist/tests/sync.storageAsync.test.js +24 -23
  63. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  64. package/dist/tests/sync.upload.test.js +58 -58
  65. package/dist/tests/testUtils.d.ts +11 -9
  66. package/dist/tests/testUtils.d.ts.map +1 -1
  67. package/dist/tests/testUtils.js +26 -16
  68. package/dist/tests/testUtils.js.map +1 -1
  69. package/package.json +3 -4
  70. package/src/SyncStateManager.ts +8 -2
  71. package/src/coValueContentMessage.ts +29 -2
  72. package/src/coValueCore/SessionMap.ts +41 -31
  73. package/src/coValueCore/coValueCore.ts +41 -74
  74. package/src/coValueCore/verifiedState.ts +36 -11
  75. package/src/coValues/group.ts +40 -27
  76. package/src/knownState.ts +39 -4
  77. package/src/localNode.ts +16 -21
  78. package/src/queue/LocalTransactionsSyncQueue.ts +77 -93
  79. package/src/storage/knownState.ts +2 -2
  80. package/src/sync.ts +24 -26
  81. package/src/tests/coPlainText.test.ts +13 -14
  82. package/src/tests/coValueContentMessage.test.ts +197 -2
  83. package/src/tests/coValueCore.isCompletelyDownloaded.test.ts +3 -2
  84. package/src/tests/coValueCore.isStreaming.test.ts +84 -2
  85. package/src/tests/coValueCore.test.ts +7 -10
  86. package/src/tests/group.childKeyRotation.test.ts +9 -9
  87. package/src/tests/knownState.test.ts +106 -9
  88. package/src/tests/sync.load.test.ts +29 -29
  89. package/src/tests/sync.mesh.test.ts +38 -31
  90. package/src/tests/sync.storage.test.ts +24 -23
  91. package/src/tests/sync.storageAsync.test.ts +24 -23
  92. package/src/tests/sync.upload.test.ts +58 -58
  93. package/src/tests/testUtils.ts +30 -18
@@ -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
@@ -4,6 +4,8 @@ import { PeerState } from "./PeerState.js";
4
4
  import { SyncStateManager } from "./SyncStateManager.js";
5
5
  import {
6
6
  getContenDebugInfo,
7
+ getNewTransactionsFromContentMessage,
8
+ getSessionEntriesFromContentMessage,
7
9
  getTransactionSize,
8
10
  knownStateFromContent,
9
11
  } from "./coValueContentMessage.js";
@@ -609,56 +611,55 @@ export class SyncManager {
609
611
  /**
610
612
  * The coValue is in memory, load the transactions from the content message
611
613
  */
612
- for (const [sessionID, newContentForSession] of Object.entries(msg.new) as [
613
- SessionID,
614
- SessionNewContent,
615
- ][]) {
616
- const ourKnownTxIdx =
617
- coValue.verified.sessions.get(sessionID)?.transactions.length;
618
- const theirFirstNewTxIdx = newContentForSession.after;
619
-
620
- if ((ourKnownTxIdx || 0) < theirFirstNewTxIdx) {
614
+ for (const [
615
+ sessionID,
616
+ newContentForSession,
617
+ ] of getSessionEntriesFromContentMessage(msg)) {
618
+ const newTransactions = getNewTransactionsFromContentMessage(
619
+ newContentForSession,
620
+ coValue.knownState(),
621
+ sessionID,
622
+ );
623
+
624
+ if (newTransactions === undefined) {
621
625
  invalidStateAssumed = true;
622
626
  continue;
623
627
  }
624
628
 
625
- const alreadyKnownOffset = ourKnownTxIdx
626
- ? ourKnownTxIdx - theirFirstNewTxIdx
627
- : 0;
628
-
629
- const newTransactions =
630
- newContentForSession.newTransactions.slice(alreadyKnownOffset);
631
-
632
629
  if (newTransactions.length === 0) {
633
630
  continue;
634
631
  }
635
632
 
636
633
  // TODO: Handle invalid signatures in the middle of streaming
637
634
  // This could cause a situation where we are unable to load a chunk, and ask for a correction for all the subsequent chunks
638
- const result = coValue.tryAddTransactions(
635
+ const error = coValue.tryAddTransactions(
639
636
  sessionID,
640
637
  newTransactions,
641
638
  newContentForSession.lastSignature,
642
639
  this.skipVerify,
643
640
  );
644
641
 
645
- if (result.isErr()) {
642
+ if (error) {
646
643
  if (peer) {
647
644
  logger.error("Failed to add transactions", {
648
645
  peerId: peer.id,
649
646
  peerRole: peer.role,
650
647
  id: msg.id,
651
- err: result.error,
648
+ errorType: error.type,
649
+ err: error.error,
650
+ sessionID,
652
651
  msgKnownState: knownStateFromContent(msg).sessions,
652
+ msgSummary: getContenDebugInfo(msg),
653
653
  knownState: coValue.knownState().sessions,
654
- newContent: validNewContent.new,
655
654
  });
656
655
  // TODO Mark only the session as errored, not the whole coValue
657
- coValue.markErrored(peer.id, result.error);
656
+ coValue.markErrored(peer.id, error);
658
657
  } else {
659
658
  logger.error("Failed to add transactions from storage", {
660
659
  id: msg.id,
661
- err: result.error,
660
+ err: error.error,
661
+ sessionID,
662
+ errorType: error.type,
662
663
  });
663
664
  }
664
665
  continue;
@@ -669,9 +670,7 @@ export class SyncManager {
669
670
  }
670
671
 
671
672
  // The new content for this session has been verified, so we can store it
672
- if (result.value) {
673
- validNewContent.new[sessionID] = newContentForSession;
674
- }
673
+ validNewContent.new[sessionID] = newContentForSession;
675
674
  }
676
675
 
677
676
  if (peer) {
@@ -771,7 +770,6 @@ export class SyncManager {
771
770
  private syncQueue = new LocalTransactionsSyncQueue((content) =>
772
771
  this.syncContent(content),
773
772
  );
774
- syncHeader = this.syncQueue.syncHeader;
775
773
  syncLocalTransaction = this.syncQueue.syncTransaction;
776
774
  trackDirtyCoValues = this.syncQueue.trackDirtyCoValues;
777
775
 
@@ -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
  `);
@@ -1,10 +1,15 @@
1
1
  import { describe, expect, test } from "vitest";
2
- import { knownStateFromContent } from "../coValueContentMessage.js";
2
+ import {
3
+ getNewTransactionsFromContentMessage,
4
+ knownStateFromContent,
5
+ } from "../coValueContentMessage.js";
3
6
  import { emptyKnownState } from "../knownState.js";
4
- import { NewContentMessage } from "../sync.js";
7
+ import { NewContentMessage, SessionNewContent } from "../sync.js";
5
8
  import type { RawCoID, SessionID } from "../ids.js";
6
9
  import { stableStringify } from "../jsonStringify.js";
7
10
  import { CO_VALUE_PRIORITY } from "../priority.js";
11
+ import { CoValueKnownState } from "../knownState.js";
12
+ import { Transaction } from "../coValueCore/verifiedState.js";
8
13
 
9
14
  describe("knownStateFromContent", () => {
10
15
  const mockCoID: RawCoID = "co_z1234567890abcdef";
@@ -213,3 +218,193 @@ describe("knownStateFromContent", () => {
213
218
  expect(result.sessions[mockSessionID1]).toBe(1); // 0 + 1
214
219
  });
215
220
  });
221
+
222
+ describe("getNewTransactionsFromContentMessage", () => {
223
+ const mockCoID: RawCoID = "co_z1234567890abcdef";
224
+ const mockSessionID: SessionID = "sealer_z123/signer_z456_session_z789";
225
+
226
+ function createTransaction(): Transaction {
227
+ return {
228
+ privacy: "trusting",
229
+ madeAt: Date.now(),
230
+ changes: stableStringify([{ op: "set", key: "test", value: "value" }]),
231
+ };
232
+ }
233
+
234
+ function createKnownState(
235
+ sessionTxIdx: number | undefined,
236
+ ): CoValueKnownState {
237
+ const knownState = emptyKnownState(mockCoID);
238
+ knownState.header = true;
239
+ if (sessionTxIdx !== undefined) {
240
+ knownState.sessions[mockSessionID] = sessionTxIdx;
241
+ }
242
+ return knownState;
243
+ }
244
+
245
+ test("returns all transactions when we know none (ourKnownTxIdx = 0, theirFirstNewTxIdx = 0)", () => {
246
+ const transactions = [createTransaction(), createTransaction()];
247
+ const content: SessionNewContent = {
248
+ after: 0,
249
+ newTransactions: transactions,
250
+ lastSignature: "signature_z1234",
251
+ };
252
+ const knownState = createKnownState(undefined); // defaults to 0
253
+
254
+ const result = getNewTransactionsFromContentMessage(
255
+ content,
256
+ knownState,
257
+ mockSessionID,
258
+ );
259
+
260
+ expect(result).toEqual(transactions);
261
+ expect(result).toHaveLength(2);
262
+ });
263
+
264
+ test("returns subset of transactions when we know some (ourKnownTxIdx = 3, theirFirstNewTxIdx = 0)", () => {
265
+ const transactions = [
266
+ createTransaction(),
267
+ createTransaction(),
268
+ createTransaction(),
269
+ createTransaction(),
270
+ createTransaction(),
271
+ ];
272
+ const content: SessionNewContent = {
273
+ after: 0,
274
+ newTransactions: transactions,
275
+ lastSignature: "signature_z1234",
276
+ };
277
+ const knownState = createKnownState(3); // we already know txs 0, 1, 2
278
+
279
+ const result = getNewTransactionsFromContentMessage(
280
+ content,
281
+ knownState,
282
+ mockSessionID,
283
+ );
284
+
285
+ expect(result).toEqual(transactions.slice(3));
286
+ expect(result).toHaveLength(2);
287
+ });
288
+
289
+ test("returns undefined when we're missing transactions (ourKnownTxIdx = 2, theirFirstNewTxIdx = 5)", () => {
290
+ const transactions = [createTransaction(), createTransaction()];
291
+ const content: SessionNewContent = {
292
+ after: 5, // they're sending txs starting from idx 5
293
+ newTransactions: transactions,
294
+ lastSignature: "signature_z1234",
295
+ };
296
+ const knownState = createKnownState(2); // but we only know up to idx 2
297
+
298
+ const result = getNewTransactionsFromContentMessage(
299
+ content,
300
+ knownState,
301
+ mockSessionID,
302
+ );
303
+
304
+ expect(result).toBeUndefined();
305
+ });
306
+
307
+ test("returns empty array when we know all transactions (ourKnownTxIdx = 5, theirFirstNewTxIdx = 2)", () => {
308
+ const transactions = [
309
+ createTransaction(),
310
+ createTransaction(),
311
+ createTransaction(),
312
+ ];
313
+ const content: SessionNewContent = {
314
+ after: 2, // they're sending txs 2, 3, 4
315
+ newTransactions: transactions,
316
+ lastSignature: "signature_z1234",
317
+ };
318
+ const knownState = createKnownState(5); // we already know txs up to idx 5
319
+
320
+ const result = getNewTransactionsFromContentMessage(
321
+ content,
322
+ knownState,
323
+ mockSessionID,
324
+ );
325
+
326
+ expect(result).toEqual([]);
327
+ expect(result).toHaveLength(0);
328
+ });
329
+
330
+ test("returns all transactions when ourKnownTxIdx equals theirFirstNewTxIdx", () => {
331
+ const transactions = [createTransaction(), createTransaction()];
332
+ const content: SessionNewContent = {
333
+ after: 3,
334
+ newTransactions: transactions,
335
+ lastSignature: "signature_z1234",
336
+ };
337
+ const knownState = createKnownState(3); // we know up to idx 3
338
+
339
+ const result = getNewTransactionsFromContentMessage(
340
+ content,
341
+ knownState,
342
+ mockSessionID,
343
+ );
344
+
345
+ expect(result).toEqual(transactions);
346
+ expect(result).toHaveLength(2);
347
+ });
348
+
349
+ test("handles session not in knownState (defaults to 0)", () => {
350
+ const transactions = [createTransaction()];
351
+ const content: SessionNewContent = {
352
+ after: 1,
353
+ newTransactions: transactions,
354
+ lastSignature: "signature_z1234",
355
+ };
356
+ const knownState = emptyKnownState(mockCoID);
357
+ knownState.header = true;
358
+ // no sessions defined, so mockSessionID defaults to 0
359
+
360
+ const result = getNewTransactionsFromContentMessage(
361
+ content,
362
+ knownState,
363
+ mockSessionID,
364
+ );
365
+
366
+ // ourKnownTxIdx = 0, theirFirstNewTxIdx = 1, so 0 < 1 -> undefined
367
+ expect(result).toBeUndefined();
368
+ });
369
+
370
+ test("returns single transaction when offset is length - 1", () => {
371
+ const transactions = [
372
+ createTransaction(),
373
+ createTransaction(),
374
+ createTransaction(),
375
+ ];
376
+ const content: SessionNewContent = {
377
+ after: 5,
378
+ newTransactions: transactions,
379
+ lastSignature: "signature_z1234",
380
+ };
381
+ const knownState = createKnownState(7); // we know up to idx 7 (5 + 2 transactions)
382
+
383
+ const result = getNewTransactionsFromContentMessage(
384
+ content,
385
+ knownState,
386
+ mockSessionID,
387
+ );
388
+
389
+ expect(result).toEqual([transactions[2]]);
390
+ expect(result).toHaveLength(1);
391
+ });
392
+
393
+ test("handles empty newTransactions array", () => {
394
+ const content: SessionNewContent = {
395
+ after: 5,
396
+ newTransactions: [],
397
+ lastSignature: "signature_z1234",
398
+ };
399
+ const knownState = createKnownState(5);
400
+
401
+ const result = getNewTransactionsFromContentMessage(
402
+ content,
403
+ knownState,
404
+ mockSessionID,
405
+ );
406
+
407
+ expect(result).toEqual([]);
408
+ expect(result).toHaveLength(0);
409
+ });
410
+ });
@@ -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 () => {