cojson 0.16.2 → 0.16.4

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 (113) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +9 -0
  3. package/dist/coValue.d.ts +1 -1
  4. package/dist/coValueContentMessage.d.ts +10 -0
  5. package/dist/coValueContentMessage.d.ts.map +1 -0
  6. package/dist/coValueContentMessage.js +46 -0
  7. package/dist/coValueContentMessage.js.map +1 -0
  8. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore/coValueCore.js +5 -3
  10. package/dist/coValueCore/coValueCore.js.map +1 -1
  11. package/dist/coValueCore/verifiedState.d.ts +1 -0
  12. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  13. package/dist/coValueCore/verifiedState.js +14 -27
  14. package/dist/coValueCore/verifiedState.js.map +1 -1
  15. package/dist/coValues/group.d.ts.map +1 -1
  16. package/dist/coValues/group.js +16 -8
  17. package/dist/coValues/group.js.map +1 -1
  18. package/dist/localNode.d.ts +6 -1
  19. package/dist/localNode.d.ts.map +1 -1
  20. package/dist/localNode.js +7 -2
  21. package/dist/localNode.js.map +1 -1
  22. package/dist/queue/LocalTransactionsSyncQueue.d.ts +24 -0
  23. package/dist/queue/LocalTransactionsSyncQueue.d.ts.map +1 -0
  24. package/dist/queue/LocalTransactionsSyncQueue.js +55 -0
  25. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -0
  26. package/dist/queue/StoreQueue.d.ts +9 -6
  27. package/dist/queue/StoreQueue.d.ts.map +1 -1
  28. package/dist/queue/StoreQueue.js +10 -2
  29. package/dist/queue/StoreQueue.js.map +1 -1
  30. package/dist/storage/storageAsync.d.ts +11 -3
  31. package/dist/storage/storageAsync.d.ts.map +1 -1
  32. package/dist/storage/storageAsync.js +59 -46
  33. package/dist/storage/storageAsync.js.map +1 -1
  34. package/dist/storage/storageSync.d.ts +9 -3
  35. package/dist/storage/storageSync.d.ts.map +1 -1
  36. package/dist/storage/storageSync.js +48 -35
  37. package/dist/storage/storageSync.js.map +1 -1
  38. package/dist/storage/syncUtils.d.ts +2 -1
  39. package/dist/storage/syncUtils.d.ts.map +1 -1
  40. package/dist/storage/syncUtils.js +4 -0
  41. package/dist/storage/syncUtils.js.map +1 -1
  42. package/dist/storage/types.d.ts +3 -2
  43. package/dist/storage/types.d.ts.map +1 -1
  44. package/dist/sync.d.ts +6 -6
  45. package/dist/sync.d.ts.map +1 -1
  46. package/dist/sync.js +33 -56
  47. package/dist/sync.js.map +1 -1
  48. package/dist/tests/StorageApiAsync.test.d.ts +2 -0
  49. package/dist/tests/StorageApiAsync.test.d.ts.map +1 -0
  50. package/dist/tests/StorageApiAsync.test.js +574 -0
  51. package/dist/tests/StorageApiAsync.test.js.map +1 -0
  52. package/dist/tests/StorageApiSync.test.d.ts +2 -0
  53. package/dist/tests/StorageApiSync.test.d.ts.map +1 -0
  54. package/dist/tests/StorageApiSync.test.js +426 -0
  55. package/dist/tests/StorageApiSync.test.js.map +1 -0
  56. package/dist/tests/StoreQueue.test.js +9 -21
  57. package/dist/tests/StoreQueue.test.js.map +1 -1
  58. package/dist/tests/SyncStateManager.test.js +18 -8
  59. package/dist/tests/SyncStateManager.test.js.map +1 -1
  60. package/dist/tests/group.inheritance.test.js +79 -2
  61. package/dist/tests/group.inheritance.test.js.map +1 -1
  62. package/dist/tests/sync.auth.test.js +22 -10
  63. package/dist/tests/sync.auth.test.js.map +1 -1
  64. package/dist/tests/sync.load.test.js +25 -23
  65. package/dist/tests/sync.load.test.js.map +1 -1
  66. package/dist/tests/sync.mesh.test.js +12 -6
  67. package/dist/tests/sync.mesh.test.js.map +1 -1
  68. package/dist/tests/sync.peerReconciliation.test.js +6 -4
  69. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  70. package/dist/tests/sync.storage.test.js +8 -14
  71. package/dist/tests/sync.storage.test.js.map +1 -1
  72. package/dist/tests/sync.storageAsync.test.js +31 -14
  73. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  74. package/dist/tests/sync.test.js +5 -9
  75. package/dist/tests/sync.test.js.map +1 -1
  76. package/dist/tests/sync.upload.test.js +31 -1
  77. package/dist/tests/sync.upload.test.js.map +1 -1
  78. package/dist/tests/testStorage.d.ts +2 -3
  79. package/dist/tests/testStorage.d.ts.map +1 -1
  80. package/dist/tests/testStorage.js +16 -8
  81. package/dist/tests/testStorage.js.map +1 -1
  82. package/dist/tests/testUtils.d.ts +3 -0
  83. package/dist/tests/testUtils.d.ts.map +1 -1
  84. package/dist/tests/testUtils.js +17 -4
  85. package/dist/tests/testUtils.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/coValueContentMessage.ts +73 -0
  88. package/src/coValueCore/coValueCore.ts +14 -5
  89. package/src/coValueCore/verifiedState.ts +28 -35
  90. package/src/coValues/group.ts +20 -9
  91. package/src/localNode.ts +8 -3
  92. package/src/queue/LocalTransactionsSyncQueue.ts +96 -0
  93. package/src/queue/StoreQueue.ts +22 -12
  94. package/src/storage/storageAsync.ts +78 -56
  95. package/src/storage/storageSync.ts +66 -45
  96. package/src/storage/syncUtils.ts +9 -1
  97. package/src/storage/types.ts +6 -5
  98. package/src/sync.ts +47 -67
  99. package/src/tests/StorageApiAsync.test.ts +829 -0
  100. package/src/tests/StorageApiSync.test.ts +628 -0
  101. package/src/tests/StoreQueue.test.ts +10 -24
  102. package/src/tests/SyncStateManager.test.ts +22 -21
  103. package/src/tests/group.inheritance.test.ts +136 -1
  104. package/src/tests/sync.auth.test.ts +22 -10
  105. package/src/tests/sync.load.test.ts +27 -24
  106. package/src/tests/sync.mesh.test.ts +12 -6
  107. package/src/tests/sync.peerReconciliation.test.ts +6 -4
  108. package/src/tests/sync.storage.test.ts +8 -14
  109. package/src/tests/sync.storageAsync.test.ts +39 -14
  110. package/src/tests/sync.test.ts +6 -14
  111. package/src/tests/sync.upload.test.ts +38 -1
  112. package/src/tests/testStorage.ts +19 -13
  113. package/src/tests/testUtils.ts +24 -5
@@ -2,9 +2,9 @@ import { UpDownCounter, ValueType, metrics } from "@opentelemetry/api";
2
2
  import { Result, err } from "neverthrow";
3
3
  import { PeerState } from "../PeerState.js";
4
4
  import { RawCoValue } from "../coValue.js";
5
- import { ControlledAccountOrAgent, RawAccountID } from "../coValues/account.js";
5
+ import { ControlledAccountOrAgent } from "../coValues/account.js";
6
6
  import { RawGroup } from "../coValues/group.js";
7
- import { CO_VALUE_LOADING_CONFIG, MAX_RECOMMENDED_TX_SIZE } from "../config.js";
7
+ import { CO_VALUE_LOADING_CONFIG } from "../config.js";
8
8
  import { coreToCoValue } from "../coreToCoValue.js";
9
9
  import {
10
10
  CryptoProvider,
@@ -380,7 +380,7 @@ export class CoValueCore {
380
380
  }
381
381
 
382
382
  knownStateWithStreaming(): CoValueKnownState {
383
- if (this.isAvailable()) {
383
+ if (this.verified) {
384
384
  return this.verified.knownStateWithStreaming();
385
385
  } else {
386
386
  return emptyKnownState(this.id);
@@ -388,7 +388,7 @@ export class CoValueCore {
388
388
  }
389
389
 
390
390
  knownState(): CoValueKnownState {
391
- if (this.isAvailable()) {
391
+ if (this.verified) {
392
392
  return this.verified.knownState();
393
393
  } else {
394
394
  return emptyKnownState(this.id);
@@ -605,8 +605,17 @@ export class CoValueCore {
605
605
  )._unsafeUnwrap({ withStackTrace: true });
606
606
 
607
607
  if (success) {
608
+ const session = this.verified.sessions.get(sessionID);
609
+ const txIdx = session ? session.transactions.length - 1 : 0;
610
+
608
611
  this.node.syncManager.recordTransactionsSize([transaction], "local");
609
- void this.node.syncManager.requestCoValueSync(this);
612
+ this.node.syncManager.syncLocalTransaction(
613
+ this.verified,
614
+ transaction,
615
+ sessionID,
616
+ signature,
617
+ txIdx,
618
+ );
610
619
  }
611
620
 
612
621
  return success;
@@ -1,6 +1,10 @@
1
1
  import { Result, err, ok } from "neverthrow";
2
2
  import { AnyRawCoValue } from "../coValue.js";
3
- import { MAX_RECOMMENDED_TX_SIZE } from "../config.js";
3
+ import {
4
+ createContentMessage,
5
+ exceedsRecommendedSize,
6
+ getTransactionSize,
7
+ } from "../coValueContentMessage.js";
4
8
  import {
5
9
  CryptoProvider,
6
10
  Encrypted,
@@ -14,7 +18,6 @@ import { RawCoID, SessionID, TransactionID } from "../ids.js";
14
18
  import { Stringified } from "../jsonStringify.js";
15
19
  import { JsonObject, JsonValue } from "../jsonValue.js";
16
20
  import { PermissionsDef as RulesetDef } from "../permissions.js";
17
- import { getPriorityFromHeader } from "../priority.js";
18
21
  import { CoValueKnownState, NewContentMessage } from "../sync.js";
19
22
  import { InvalidHashError, InvalidSignatureError } from "./coValueCore.js";
20
23
  import { TryAddTransactionsError } from "./coValueCore.js";
@@ -151,6 +154,17 @@ export class VerifiedState {
151
154
  return ok(true as const);
152
155
  }
153
156
 
157
+ getLastSignatureCheckpoint(sessionID: SessionID): number {
158
+ const sessionLog = this.sessions.get(sessionID);
159
+
160
+ if (!sessionLog?.signatureAfter) return -1;
161
+
162
+ return Object.keys(sessionLog.signatureAfter).reduce(
163
+ (max, idx) => Math.max(max, parseInt(idx)),
164
+ -1,
165
+ );
166
+ }
167
+
154
168
  private doAddTransactions(
155
169
  sessionID: SessionID,
156
170
  newTransactions: Transaction[],
@@ -165,24 +179,14 @@ export class VerifiedState {
165
179
  }
166
180
 
167
181
  const signatureAfter = sessionLog?.signatureAfter ?? {};
168
-
169
- const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
170
- (max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
171
- -1,
172
- );
182
+ const lastInbetweenSignatureIdx =
183
+ this.getLastSignatureCheckpoint(sessionID);
173
184
 
174
185
  const sizeOfTxsSinceLastInbetweenSignature = transactions
175
186
  .slice(lastInbetweenSignatureIdx + 1)
176
- .reduce(
177
- (sum, tx) =>
178
- sum +
179
- (tx.privacy === "private"
180
- ? tx.encryptedChanges.length
181
- : tx.changes.length),
182
- 0,
183
- );
187
+ .reduce((sum, tx) => sum + getTransactionSize(tx), 0);
184
188
 
185
- if (sizeOfTxsSinceLastInbetweenSignature > MAX_RECOMMENDED_TX_SIZE) {
189
+ if (exceedsRecommendedSize(sizeOfTxsSinceLastInbetweenSignature)) {
186
190
  signatureAfter[transactions.length - 1] = newSignature;
187
191
  }
188
192
 
@@ -242,13 +246,11 @@ export class VerifiedState {
242
246
  return this._cachedNewContentSinceEmpty;
243
247
  }
244
248
 
245
- let currentPiece: NewContentMessage = {
246
- action: "content",
247
- id: this.id,
248
- header: knownState?.header ? undefined : this.header,
249
- priority: getPriorityFromHeader(this.header),
250
- new: {},
251
- };
249
+ let currentPiece: NewContentMessage = createContentMessage(
250
+ this.id,
251
+ this.header,
252
+ !knownState?.header,
253
+ );
252
254
 
253
255
  const pieces = [currentPiece];
254
256
 
@@ -299,25 +301,16 @@ export class VerifiedState {
299
301
  const oldPieceSize = pieceSize;
300
302
  for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
301
303
  const tx = log.transactions[txIdx]!;
302
- pieceSize +=
303
- tx.privacy === "private"
304
- ? tx.encryptedChanges.length
305
- : tx.changes.length;
304
+ pieceSize += getTransactionSize(tx);
306
305
  }
307
306
 
308
- if (pieceSize >= MAX_RECOMMENDED_TX_SIZE) {
307
+ if (exceedsRecommendedSize(pieceSize)) {
309
308
  if (!currentPiece.expectContentUntil && pieces.length === 1) {
310
309
  currentPiece.expectContentUntil =
311
310
  this.knownStateWithStreaming().sessions;
312
311
  }
313
312
 
314
- currentPiece = {
315
- action: "content",
316
- id: this.id,
317
- header: undefined,
318
- new: {},
319
- priority: getPriorityFromHeader(this.header),
320
- };
313
+ currentPiece = createContentMessage(this.id, this.header, false);
321
314
  pieces.push(currentPiece);
322
315
  pieceSize = pieceSize - oldPieceSize;
323
316
  }
@@ -669,19 +669,30 @@ export class RawGroup<
669
669
 
670
670
  /** Detect circular references in group inheritance */
671
671
  isSelfExtension(parent: RawGroup) {
672
- if (parent.id === this.id) {
673
- return true;
674
- }
672
+ const checkedGroups = new Set<string>();
673
+ const queue = [parent];
675
674
 
676
- const childGroups = this.getChildGroups();
675
+ while (true) {
676
+ const current = queue.pop();
677
677
 
678
- for (const child of childGroups) {
679
- if (child.isSelfExtension(parent)) {
678
+ if (!current) {
679
+ return false;
680
+ }
681
+
682
+ if (current.id === this.id) {
680
683
  return true;
681
684
  }
682
- }
683
685
 
684
- return false;
686
+ checkedGroups.add(current.id);
687
+
688
+ const parentGroups = current.getParentGroups();
689
+
690
+ for (const parent of parentGroups) {
691
+ if (!checkedGroups.has(parent.id)) {
692
+ queue.push(parent);
693
+ }
694
+ }
695
+ }
685
696
  }
686
697
 
687
698
  extend(
@@ -700,8 +711,8 @@ export class RawGroup<
700
711
 
701
712
  const value = role === "inherit" ? "extend" : role;
702
713
 
703
- this.set(`parent_${parent.id}`, value, "trusting");
704
714
  parent.set(`child_${this.id}`, "extend", "trusting");
715
+ this.set(`parent_${parent.id}`, value, "trusting");
705
716
 
706
717
  if (
707
718
  parent.myRole() !== "admin" &&
package/src/localNode.ts CHANGED
@@ -351,7 +351,7 @@ export class LocalNode {
351
351
  new VerifiedState(id, this.crypto, header, new Map()),
352
352
  );
353
353
 
354
- void this.syncManager.requestCoValueSync(coValue);
354
+ this.syncManager.syncHeader(coValue.verified);
355
355
 
356
356
  return coValue;
357
357
  }
@@ -738,9 +738,14 @@ export class LocalNode {
738
738
  }
739
739
  }
740
740
 
741
- gracefulShutdown() {
742
- this.storage?.close();
741
+ /**
742
+ * Closes all the peer connections, drains all the queues and closes the storage.
743
+ *
744
+ * @returns Promise of the current pending store operation, if any.
745
+ */
746
+ gracefulShutdown(): Promise<unknown> | undefined {
743
747
  this.syncManager.gracefulShutdown();
748
+ return this.storage?.close();
744
749
  }
745
750
  }
746
751
 
@@ -0,0 +1,96 @@
1
+ import {
2
+ addTransactionToContentMessage,
3
+ createContentMessage,
4
+ } from "../coValueContentMessage.js";
5
+ import { Transaction, VerifiedState } from "../coValueCore/verifiedState.js";
6
+ import { Signature } from "../crypto/crypto.js";
7
+ import { SessionID } from "../ids.js";
8
+ import { NewContentMessage } from "../sync.js";
9
+ import { LinkedList } from "./LinkedList.js";
10
+
11
+ /**
12
+ * This queue is used to batch the sync of local transactions while preserving the order of updates between CoValues.
13
+ *
14
+ * We need to preserve the order of updates between CoValues to keep the state always consistent in case of shutdown in the middle of a sync.
15
+ *
16
+ * Examples:
17
+ * 1. When we extend a Group we need to always ensure that the parent group is persisted before persisting the extension transaction.
18
+ * 2. If we do multiple updates on the same CoMap, the updates will be batched because it's safe to do so.
19
+ */
20
+ export class LocalTransactionsSyncQueue {
21
+ private readonly queue = new LinkedList<NewContentMessage>();
22
+
23
+ constructor(private readonly sync: (content: NewContentMessage) => void) {}
24
+
25
+ syncHeader = (coValue: VerifiedState) => {
26
+ const lastPendingSync = this.queue.tail?.value;
27
+
28
+ if (lastPendingSync?.id === coValue.id) {
29
+ return;
30
+ }
31
+
32
+ this.enqueue(createContentMessage(coValue.id, coValue.header));
33
+ };
34
+
35
+ syncTransaction = (
36
+ coValue: VerifiedState,
37
+ transaction: Transaction,
38
+ sessionID: SessionID,
39
+ signature: Signature,
40
+ txIdx: number,
41
+ ) => {
42
+ const lastPendingSync = this.queue.tail?.value;
43
+ const lastSignatureIdx = coValue.getLastSignatureCheckpoint(sessionID);
44
+ const isSignatureCheckpoint =
45
+ lastSignatureIdx > -1 && lastSignatureIdx === txIdx - 1;
46
+
47
+ if (lastPendingSync?.id === coValue.id && !isSignatureCheckpoint) {
48
+ addTransactionToContentMessage(
49
+ lastPendingSync,
50
+ transaction,
51
+ sessionID,
52
+ signature,
53
+ txIdx,
54
+ );
55
+
56
+ return;
57
+ }
58
+
59
+ const content = createContentMessage(coValue.id, coValue.header, false);
60
+
61
+ addTransactionToContentMessage(
62
+ content,
63
+ transaction,
64
+ sessionID,
65
+ signature,
66
+ txIdx,
67
+ );
68
+
69
+ this.enqueue(content);
70
+ };
71
+
72
+ enqueue(content: NewContentMessage) {
73
+ this.queue.push(content);
74
+
75
+ this.processPendingSyncs();
76
+ }
77
+
78
+ private processingSyncs = false;
79
+ processPendingSyncs() {
80
+ if (this.processingSyncs) return;
81
+
82
+ this.processingSyncs = true;
83
+
84
+ queueMicrotask(() => {
85
+ while (this.queue.head) {
86
+ const content = this.queue.head.value;
87
+
88
+ this.sync(content);
89
+
90
+ this.queue.shift();
91
+ }
92
+
93
+ this.processingSyncs = false;
94
+ });
95
+ }
96
+ }
@@ -1,19 +1,22 @@
1
+ import { CorrectionCallback } from "../exports.js";
1
2
  import { logger } from "../logger.js";
2
- import { CoValueKnownState, NewContentMessage } from "../sync.js";
3
+ import { NewContentMessage } from "../sync.js";
3
4
  import { LinkedList } from "./LinkedList.js";
4
5
 
5
6
  type StoreQueueEntry = {
6
- data: NewContentMessage[];
7
- correctionCallback: (data: CoValueKnownState) => void;
7
+ data: NewContentMessage;
8
+ correctionCallback: CorrectionCallback;
8
9
  };
9
10
 
10
11
  export class StoreQueue {
11
12
  private queue = new LinkedList<StoreQueueEntry>();
13
+ closed = false;
14
+
15
+ public push(data: NewContentMessage, correctionCallback: CorrectionCallback) {
16
+ if (this.closed) {
17
+ return;
18
+ }
12
19
 
13
- public push(
14
- data: NewContentMessage[],
15
- correctionCallback: (data: CoValueKnownState) => void,
16
- ) {
17
20
  this.queue.push({ data, correctionCallback });
18
21
  }
19
22
 
@@ -22,12 +25,13 @@ export class StoreQueue {
22
25
  }
23
26
 
24
27
  processing = false;
28
+ lastCallback: Promise<unknown> | undefined;
25
29
 
26
30
  async processQueue(
27
31
  callback: (
28
- data: NewContentMessage[],
29
- correctionCallback: (data: CoValueKnownState) => void,
30
- ) => Promise<void>,
32
+ data: NewContentMessage,
33
+ correctionCallback: CorrectionCallback,
34
+ ) => Promise<unknown>,
31
35
  ) {
32
36
  if (this.processing) {
33
37
  return;
@@ -41,16 +45,22 @@ export class StoreQueue {
41
45
  const { data, correctionCallback } = entry;
42
46
 
43
47
  try {
44
- await callback(data, correctionCallback);
48
+ this.lastCallback = callback(data, correctionCallback);
49
+ await this.lastCallback;
45
50
  } catch (err) {
46
51
  logger.error("Error processing message in store queue", { err });
47
52
  }
48
53
  }
49
54
 
55
+ this.lastCallback = undefined;
50
56
  this.processing = false;
51
57
  }
52
58
 
53
- drain() {
59
+ close() {
60
+ this.closed = true;
61
+
54
62
  while (this.pull()) {}
63
+
64
+ return this.lastCallback;
55
65
  }
56
66
  }
@@ -1,11 +1,15 @@
1
+ import {
2
+ createContentMessage,
3
+ exceedsRecommendedSize,
4
+ getTransactionSize,
5
+ } from "../coValueContentMessage.js";
1
6
  import {
2
7
  type CoValueCore,
3
- MAX_RECOMMENDED_TX_SIZE,
4
8
  type RawCoID,
5
9
  type SessionID,
6
10
  type StorageAPI,
11
+ logger,
7
12
  } from "../exports.js";
8
- import { getPriorityFromHeader } from "../priority.js";
9
13
  import { StoreQueue } from "../queue/StoreQueue.js";
10
14
  import {
11
15
  CoValueKnownState,
@@ -13,8 +17,13 @@ import {
13
17
  emptyKnownState,
14
18
  } from "../sync.js";
15
19
  import { StorageKnownState } from "./knownState.js";
16
- import { collectNewTxs, getDependedOnCoValues } from "./syncUtils.js";
20
+ import {
21
+ collectNewTxs,
22
+ getDependedOnCoValues,
23
+ getNewTransactionsSize,
24
+ } from "./syncUtils.js";
17
25
  import type {
26
+ CorrectionCallback,
18
27
  DBClientInterfaceAsync,
19
28
  SignatureAfterRow,
20
29
  StoredCoValueRow,
@@ -82,6 +91,7 @@ export class StorageApiAsync implements StorageAPI {
82
91
  );
83
92
 
84
93
  const knownState = this.knwonStates.getKnownState(coValueRow.id);
94
+ knownState.header = true;
85
95
 
86
96
  for (const sessionRow of allCoValueSessions) {
87
97
  knownState.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
@@ -89,13 +99,7 @@ export class StorageApiAsync implements StorageAPI {
89
99
 
90
100
  this.loadedCoValues.add(coValueRow.id);
91
101
 
92
- let contentMessage = {
93
- action: "content",
94
- id: coValueRow.id,
95
- header: coValueRow.header,
96
- new: {},
97
- priority: getPriorityFromHeader(coValueRow.header),
98
- } as NewContentMessage;
102
+ let contentMessage = createContentMessage(coValueRow.id, coValueRow.header);
99
103
 
100
104
  if (contentStreaming) {
101
105
  contentMessage.expectContentUntil = knownState["sessions"];
@@ -136,13 +140,10 @@ export class StorageApiAsync implements StorageAPI {
136
140
  contentMessage,
137
141
  callback,
138
142
  );
139
- contentMessage = {
140
- action: "content",
141
- id: coValueRow.id,
142
- header: coValueRow.header,
143
- new: {},
144
- priority: getPriorityFromHeader(coValueRow.header),
145
- } satisfies NewContentMessage;
143
+ contentMessage = createContentMessage(
144
+ coValueRow.id,
145
+ coValueRow.header,
146
+ );
146
147
  }
147
148
  }
148
149
  }
@@ -194,33 +195,64 @@ export class StorageApiAsync implements StorageAPI {
194
195
 
195
196
  storeQueue = new StoreQueue();
196
197
 
197
- async store(
198
- msgs: NewContentMessage[],
199
- correctionCallback: (data: CoValueKnownState) => void,
200
- ) {
198
+ async store(msg: NewContentMessage, correctionCallback: CorrectionCallback) {
201
199
  /**
202
200
  * The store operations must be done one by one, because we can't start a new transaction when there
203
201
  * is already a transaction open.
204
202
  */
205
- this.storeQueue.push(msgs, correctionCallback);
203
+ this.storeQueue.push(msg, correctionCallback);
206
204
 
207
205
  this.storeQueue.processQueue(async (data, correctionCallback) => {
208
- for (const msg of data) {
209
- const success = await this.storeSingle(msg, correctionCallback);
206
+ return this.storeSingle(data, correctionCallback);
207
+ });
208
+ }
210
209
 
211
- if (!success) {
212
- // Stop processing the messages for this entry, because the data is out of sync with storage
213
- // and the other transactions will be rejected anyway.
214
- break;
215
- }
210
+ /**
211
+ * This function is called when the storage lacks the information required to store the incoming content.
212
+ *
213
+ * It triggers a `correctionCallback` to ask the syncManager to provide the missing information.
214
+ *
215
+ * The correction is applied immediately, to ensure that, when applicable, the dependent content in the queue won't require additional corrections.
216
+ */
217
+ private async handleCorrection(
218
+ knownState: CoValueKnownState,
219
+ correctionCallback: CorrectionCallback,
220
+ ) {
221
+ const correction = correctionCallback(knownState);
222
+
223
+ if (!correction) {
224
+ logger.error("Correction callback returned undefined", {
225
+ knownState,
226
+ correction: correction ?? null,
227
+ });
228
+ return false;
229
+ }
230
+
231
+ for (const msg of correction) {
232
+ const success = await this.storeSingle(msg, (knownState) => {
233
+ logger.error("Double correction requested", {
234
+ msg,
235
+ knownState,
236
+ });
237
+ return undefined;
238
+ });
239
+
240
+ if (!success) {
241
+ return false;
216
242
  }
217
- });
243
+ }
244
+
245
+ return true;
218
246
  }
219
247
 
220
248
  private async storeSingle(
221
249
  msg: NewContentMessage,
222
- correctionCallback: (data: CoValueKnownState) => void,
250
+ correctionCallback: CorrectionCallback,
223
251
  ): Promise<boolean> {
252
+ if (this.storeQueue.closed) {
253
+ return false;
254
+ }
255
+
224
256
  const id = msg.id;
225
257
  const coValueRow = await this.dbClient.getCoValue(id);
226
258
 
@@ -231,8 +263,7 @@ export class StorageApiAsync implements StorageAPI {
231
263
  const knownState = emptyKnownState(id as RawCoID);
232
264
  this.knwonStates.setKnownState(id, knownState);
233
265
 
234
- correctionCallback(knownState);
235
- return false;
266
+ return this.handleCorrection(knownState, correctionCallback);
236
267
  }
237
268
 
238
269
  const storedCoValueRowID: number = coValueRow
@@ -276,8 +307,7 @@ export class StorageApiAsync implements StorageAPI {
276
307
  this.knwonStates.handleUpdate(id, knownState);
277
308
 
278
309
  if (invalidAssumptions) {
279
- correctionCallback(knownState);
280
- return false;
310
+ return this.handleCorrection(knownState, correctionCallback);
281
311
  }
282
312
 
283
313
  return true;
@@ -290,38 +320,31 @@ export class StorageApiAsync implements StorageAPI {
290
320
  storedCoValueRowID: number,
291
321
  ) {
292
322
  const newTransactions = msg.new[sessionID]?.newTransactions || [];
323
+ const lastIdx = sessionRow?.lastIdx || 0;
293
324
 
294
- const actuallyNewOffset =
295
- (sessionRow?.lastIdx || 0) - (msg.new[sessionID]?.after || 0);
325
+ const actuallyNewOffset = lastIdx - (msg.new[sessionID]?.after || 0);
296
326
 
297
327
  const actuallyNewTransactions = newTransactions.slice(actuallyNewOffset);
298
328
 
299
329
  if (actuallyNewTransactions.length === 0) {
300
- return sessionRow?.lastIdx || 0;
330
+ return lastIdx;
301
331
  }
302
332
 
303
- let newBytesSinceLastSignature =
304
- (sessionRow?.bytesSinceLastSignature || 0) +
305
- actuallyNewTransactions.reduce(
306
- (sum, tx) =>
307
- sum +
308
- (tx.privacy === "private"
309
- ? tx.encryptedChanges.length
310
- : tx.changes.length),
311
- 0,
312
- );
333
+ let bytesSinceLastSignature = sessionRow?.bytesSinceLastSignature || 0;
334
+ const newTransactionsSize = getNewTransactionsSize(actuallyNewTransactions);
313
335
 
314
- const newLastIdx =
315
- (sessionRow?.lastIdx || 0) + actuallyNewTransactions.length;
336
+ const newLastIdx = lastIdx + actuallyNewTransactions.length;
316
337
 
317
338
  let shouldWriteSignature = false;
318
339
 
319
- if (newBytesSinceLastSignature > MAX_RECOMMENDED_TX_SIZE) {
340
+ if (exceedsRecommendedSize(bytesSinceLastSignature, newTransactionsSize)) {
320
341
  shouldWriteSignature = true;
321
- newBytesSinceLastSignature = 0;
342
+ bytesSinceLastSignature = 0;
343
+ } else {
344
+ bytesSinceLastSignature += newTransactionsSize;
322
345
  }
323
346
 
324
- const nextIdx = sessionRow?.lastIdx || 0;
347
+ const nextIdx = lastIdx;
325
348
 
326
349
  if (!msg.new[sessionID]) throw new Error("Session ID not found");
327
350
 
@@ -330,7 +353,7 @@ export class StorageApiAsync implements StorageAPI {
330
353
  sessionID,
331
354
  lastIdx: newLastIdx,
332
355
  lastSignature: msg.new[sessionID].lastSignature,
333
- bytesSinceLastSignature: newBytesSinceLastSignature,
356
+ bytesSinceLastSignature,
334
357
  };
335
358
 
336
359
  const sessionRowID: number = await this.dbClient.addSessionUpdate({
@@ -360,7 +383,6 @@ export class StorageApiAsync implements StorageAPI {
360
383
  }
361
384
 
362
385
  close() {
363
- // Drain the store queue
364
- this.storeQueue.drain();
386
+ return this.storeQueue.close();
365
387
  }
366
388
  }