cojson 0.16.5 → 0.16.7

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 (121) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/dist/GarbageCollector.d.ts +12 -0
  4. package/dist/GarbageCollector.d.ts.map +1 -0
  5. package/dist/GarbageCollector.js +37 -0
  6. package/dist/GarbageCollector.js.map +1 -0
  7. package/dist/coValue.d.ts +1 -1
  8. package/dist/coValueContentMessage.d.ts +1 -0
  9. package/dist/coValueContentMessage.d.ts.map +1 -1
  10. package/dist/coValueContentMessage.js +11 -3
  11. package/dist/coValueContentMessage.js.map +1 -1
  12. package/dist/coValueCore/coValueCore.d.ts +2 -1
  13. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  14. package/dist/coValueCore/coValueCore.js +15 -0
  15. package/dist/coValueCore/coValueCore.js.map +1 -1
  16. package/dist/coValueCore/utils.d.ts.map +1 -1
  17. package/dist/coValueCore/utils.js.map +1 -1
  18. package/dist/coValueCore/verifiedState.d.ts +1 -0
  19. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  20. package/dist/coValueCore/verifiedState.js.map +1 -1
  21. package/dist/coValues/coMap.d.ts +3 -3
  22. package/dist/coValues/coPlainText.d.ts +1 -0
  23. package/dist/coValues/coPlainText.d.ts.map +1 -1
  24. package/dist/coValues/coPlainText.js +27 -8
  25. package/dist/coValues/coPlainText.js.map +1 -1
  26. package/dist/coValues/coStream.d.ts +2 -2
  27. package/dist/coValues/group.d.ts +1 -1
  28. package/dist/config.d.ts +10 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +16 -1
  31. package/dist/config.js.map +1 -1
  32. package/dist/exports.d.ts +11 -4
  33. package/dist/exports.d.ts.map +1 -1
  34. package/dist/exports.js +8 -3
  35. package/dist/exports.js.map +1 -1
  36. package/dist/localNode.d.ts +3 -0
  37. package/dist/localNode.d.ts.map +1 -1
  38. package/dist/localNode.js +11 -0
  39. package/dist/localNode.js.map +1 -1
  40. package/dist/permissions.d.ts.map +1 -1
  41. package/dist/permissions.js +3 -1
  42. package/dist/permissions.js.map +1 -1
  43. package/dist/queue/LocalTransactionsSyncQueue.js +1 -1
  44. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -1
  45. package/dist/queue/StoreQueue.d.ts +7 -1
  46. package/dist/queue/StoreQueue.d.ts.map +1 -1
  47. package/dist/queue/StoreQueue.js +35 -13
  48. package/dist/queue/StoreQueue.js.map +1 -1
  49. package/dist/storage/sqlite/client.d.ts +4 -4
  50. package/dist/storage/sqlite/client.d.ts.map +1 -1
  51. package/dist/storage/sqlite/client.js +13 -4
  52. package/dist/storage/sqlite/client.js.map +1 -1
  53. package/dist/storage/sqliteAsync/client.d.ts +3 -3
  54. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  55. package/dist/storage/sqliteAsync/client.js +12 -3
  56. package/dist/storage/sqliteAsync/client.js.map +1 -1
  57. package/dist/storage/storageAsync.d.ts.map +1 -1
  58. package/dist/storage/storageAsync.js +2 -7
  59. package/dist/storage/storageAsync.js.map +1 -1
  60. package/dist/storage/storageSync.d.ts.map +1 -1
  61. package/dist/storage/storageSync.js +2 -7
  62. package/dist/storage/storageSync.js.map +1 -1
  63. package/dist/storage/types.d.ts +2 -2
  64. package/dist/storage/types.d.ts.map +1 -1
  65. package/dist/sync.d.ts.map +1 -1
  66. package/dist/sync.js +17 -3
  67. package/dist/sync.js.map +1 -1
  68. package/dist/tests/GarbageCollector.test.d.ts +2 -0
  69. package/dist/tests/GarbageCollector.test.d.ts.map +1 -0
  70. package/dist/tests/GarbageCollector.test.js +85 -0
  71. package/dist/tests/GarbageCollector.test.js.map +1 -0
  72. package/dist/tests/coPlainText.test.js +142 -4
  73. package/dist/tests/coPlainText.test.js.map +1 -1
  74. package/dist/tests/coStream.test.js +3 -3
  75. package/dist/tests/coStream.test.js.map +1 -1
  76. package/dist/tests/sync.garbageCollection.test.d.ts +2 -0
  77. package/dist/tests/sync.garbageCollection.test.d.ts.map +1 -0
  78. package/dist/tests/sync.garbageCollection.test.js +133 -0
  79. package/dist/tests/sync.garbageCollection.test.js.map +1 -0
  80. package/dist/tests/sync.mesh.test.js +48 -34
  81. package/dist/tests/sync.mesh.test.js.map +1 -1
  82. package/dist/tests/sync.storage.test.js +31 -21
  83. package/dist/tests/sync.storage.test.js.map +1 -1
  84. package/dist/tests/sync.storageAsync.test.js +76 -29
  85. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  86. package/dist/tests/testStorage.d.ts +1 -0
  87. package/dist/tests/testStorage.d.ts.map +1 -1
  88. package/dist/tests/testStorage.js +1 -1
  89. package/dist/tests/testStorage.js.map +1 -1
  90. package/dist/tests/testUtils.d.ts +1 -0
  91. package/dist/tests/testUtils.d.ts.map +1 -1
  92. package/dist/tests/testUtils.js +1 -0
  93. package/dist/tests/testUtils.js.map +1 -1
  94. package/package.json +1 -1
  95. package/src/GarbageCollector.ts +48 -0
  96. package/src/coValueContentMessage.ts +16 -3
  97. package/src/coValueCore/coValueCore.ts +27 -10
  98. package/src/coValueCore/utils.ts +1 -0
  99. package/src/coValueCore/verifiedState.ts +1 -0
  100. package/src/coValues/coPlainText.ts +40 -8
  101. package/src/config.ts +20 -1
  102. package/src/exports.ts +13 -5
  103. package/src/localNode.ts +15 -1
  104. package/src/permissions.ts +3 -1
  105. package/src/queue/LocalTransactionsSyncQueue.ts +1 -1
  106. package/src/queue/StoreQueue.ts +45 -12
  107. package/src/storage/sqlite/client.ts +24 -10
  108. package/src/storage/sqliteAsync/client.ts +26 -5
  109. package/src/storage/storageAsync.ts +5 -9
  110. package/src/storage/storageSync.ts +2 -9
  111. package/src/storage/types.ts +7 -4
  112. package/src/sync.ts +19 -3
  113. package/src/tests/GarbageCollector.test.ts +127 -0
  114. package/src/tests/coPlainText.test.ts +176 -4
  115. package/src/tests/coStream.test.ts +7 -3
  116. package/src/tests/sync.garbageCollection.test.ts +178 -0
  117. package/src/tests/sync.mesh.test.ts +49 -34
  118. package/src/tests/sync.storage.test.ts +31 -21
  119. package/src/tests/sync.storageAsync.test.ts +81 -29
  120. package/src/tests/testStorage.ts +11 -3
  121. package/src/tests/testUtils.ts +4 -1
@@ -0,0 +1,48 @@
1
+ import { CoValueCore } from "./coValueCore/coValueCore.js";
2
+ import { GARBAGE_COLLECTOR_CONFIG } from "./config.js";
3
+ import { RawCoID } from "./ids.js";
4
+
5
+ export class GarbageCollector {
6
+ private readonly interval: ReturnType<typeof setInterval>;
7
+
8
+ constructor(private readonly coValues: Map<RawCoID, CoValueCore>) {
9
+ this.interval = setInterval(() => {
10
+ this.collect();
11
+ }, GARBAGE_COLLECTOR_CONFIG.INTERVAL);
12
+ }
13
+
14
+ getCurrentTime() {
15
+ return performance.now();
16
+ }
17
+
18
+ trackCoValueAccess({ verified }: CoValueCore) {
19
+ if (verified) {
20
+ verified.lastAccessed = this.getCurrentTime();
21
+ }
22
+ }
23
+
24
+ collect() {
25
+ const currentTime = this.getCurrentTime();
26
+ for (const coValue of this.coValues.values()) {
27
+ const { verified } = coValue;
28
+
29
+ if (!verified?.lastAccessed) {
30
+ continue;
31
+ }
32
+
33
+ const timeSinceLastAccessed = currentTime - verified.lastAccessed;
34
+
35
+ if (timeSinceLastAccessed > GARBAGE_COLLECTOR_CONFIG.MAX_AGE) {
36
+ const unmounted = coValue.unmount();
37
+
38
+ if (unmounted) {
39
+ this.coValues.delete(coValue.id);
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ stop() {
46
+ clearInterval(this.interval);
47
+ }
48
+ }
@@ -3,7 +3,7 @@ import {
3
3
  Transaction,
4
4
  VerifiedState,
5
5
  } from "./coValueCore/verifiedState.js";
6
- import { MAX_RECOMMENDED_TX_SIZE } from "./config.js";
6
+ import { TRANSACTION_CONFIG } from "./config.js";
7
7
  import { Signature } from "./crypto/crypto.js";
8
8
  import { RawCoID, SessionID } from "./ids.js";
9
9
  import { getPriorityFromHeader } from "./priority.js";
@@ -55,10 +55,12 @@ export function exceedsRecommendedSize(
55
55
  transactionSize?: number,
56
56
  ) {
57
57
  if (transactionSize === undefined) {
58
- return baseSize > MAX_RECOMMENDED_TX_SIZE;
58
+ return baseSize > TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE;
59
59
  }
60
60
 
61
- return baseSize + transactionSize > MAX_RECOMMENDED_TX_SIZE;
61
+ return (
62
+ baseSize + transactionSize > TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE
63
+ );
62
64
  }
63
65
 
64
66
  export function knownStateFromContent(content: NewContentMessage) {
@@ -71,3 +73,14 @@ export function knownStateFromContent(content: NewContentMessage) {
71
73
 
72
74
  return knownState;
73
75
  }
76
+
77
+ export function getContentMessageSize(msg: NewContentMessage) {
78
+ return Object.values(msg.new).reduce((acc, sessionNewContent) => {
79
+ return (
80
+ acc +
81
+ sessionNewContent.newTransactions.reduce((acc, tx) => {
82
+ return acc + getTransactionSize(tx);
83
+ }, 0)
84
+ );
85
+ }, 0);
86
+ }
@@ -69,7 +69,9 @@ export class CoValueCore {
69
69
  }
70
70
  private readonly peers = new Map<
71
71
  PeerID,
72
- | { type: "unknown" | "pending" | "available" | "unavailable" }
72
+ | {
73
+ type: "unknown" | "pending" | "available" | "unavailable";
74
+ }
73
75
  | {
74
76
  type: "errored";
75
77
  error: TryAddTransactionsError;
@@ -78,9 +80,8 @@ export class CoValueCore {
78
80
 
79
81
  // cached state and listeners
80
82
  private _cachedContent?: RawCoValue;
81
- private readonly listeners: Set<
82
- (core: CoValueCore, unsub: () => void) => void
83
- > = new Set();
83
+ readonly listeners: Set<(core: CoValueCore, unsub: () => void) => void> =
84
+ new Set();
84
85
  private readonly _decryptionCache: {
85
86
  [key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined;
86
87
  } = {};
@@ -201,6 +202,26 @@ export class CoValueCore {
201
202
  }
202
203
  }
203
204
 
205
+ unmount() {
206
+ // For simplicity, we don't unmount groups and accounts
207
+ if (this.verified?.header.ruleset.type === "group") {
208
+ return false;
209
+ }
210
+
211
+ if (this.listeners.size > 0) {
212
+ return false; // The coValue is still in use
213
+ }
214
+
215
+ this.counter.add(-1, { state: this.loadingState });
216
+
217
+ if (this.groupInvalidationSubscription) {
218
+ this.groupInvalidationSubscription();
219
+ this.groupInvalidationSubscription = undefined;
220
+ }
221
+
222
+ return true;
223
+ }
224
+
204
225
  markNotFoundInPeer(peerId: PeerID) {
205
226
  const previousState = this.loadingState;
206
227
  this.peers.set(peerId, { type: "unavailable" });
@@ -609,9 +630,7 @@ export class CoValueCore {
609
630
  return success;
610
631
  }
611
632
 
612
- getCurrentContent(options?: {
613
- ignorePrivateTransactions: true;
614
- }): RawCoValue {
633
+ getCurrentContent(options?: { ignorePrivateTransactions: true }): RawCoValue {
615
634
  if (!this.verified) {
616
635
  throw new Error(
617
636
  "CoValueCore: getCurrentContent called on coValue without verified state",
@@ -851,9 +870,7 @@ export class CoValueCore {
851
870
  }
852
871
  }
853
872
 
854
- waitForSync(options?: {
855
- timeout?: number;
856
- }) {
873
+ waitForSync(options?: { timeout?: number }) {
857
874
  return this.node.syncManager.waitForSync(this.id, options?.timeout);
858
875
  }
859
876
 
@@ -2,6 +2,7 @@ import { getGroupDependentKey } from "../ids.js";
2
2
  import { RawCoID, SessionID } from "../ids.js";
3
3
  import { Stringified, parseJSON } from "../jsonStringify.js";
4
4
  import { JsonValue } from "../jsonValue.js";
5
+ import { NewContentMessage } from "../sync.js";
5
6
  import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
6
7
  import { isAccountID } from "../typeUtils/isAccountID.js";
7
8
  import { CoValueHeader, Transaction } from "./verifiedState.js";
@@ -65,6 +65,7 @@ export class VerifiedState {
65
65
  private _cachedKnownState?: CoValueKnownState;
66
66
  private _cachedNewContentSinceEmpty: NewContentMessage[] | undefined;
67
67
  private streamingKnownState?: CoValueKnownState["sessions"];
68
+ public lastAccessed: number | undefined;
68
69
 
69
70
  constructor(
70
71
  id: RawCoID,
@@ -1,5 +1,6 @@
1
1
  import { splitGraphemes } from "unicode-segmenter/grapheme";
2
2
  import { AvailableCoValueCore } from "../coValueCore/coValueCore.js";
3
+ import { TRANSACTION_CONFIG } from "../config.js";
3
4
  import { JsonObject } from "../jsonValue.js";
4
5
  import { DeletionOpPayload, OpID, RawCoList } from "./coList.js";
5
6
 
@@ -110,16 +111,34 @@ export class RawCoPlainText<
110
111
  text: string,
111
112
  privacy: "private" | "trusting" = "private",
112
113
  ) {
113
- const graphemes = [...splitGraphemes(text)];
114
+ const graphemes = Array.from(splitGraphemes(text));
114
115
 
115
116
  if (idx === 0) {
116
- // For insertions at start, prepend each character in reverse
117
- for (const grapheme of graphemes.reverse()) {
118
- this.prepend(grapheme, 0, privacy);
117
+ // For insertions at start, prepend the first char and append the rest
118
+ const firstChar = graphemes[0];
119
+
120
+ if (firstChar) {
121
+ this.prepend(firstChar, 0, privacy);
122
+ }
123
+
124
+ if (graphemes.length > 1) {
125
+ this.appendChars(graphemes.slice(1), 0, privacy);
119
126
  }
120
127
  } else {
121
128
  // For other insertions, append after the previous character
122
- this.appendItems(graphemes, idx - 1, privacy);
129
+ this.appendChars(graphemes, idx - 1, privacy);
130
+ }
131
+ }
132
+
133
+ appendChars(
134
+ text: string[],
135
+ position: number,
136
+ privacy: "private" | "trusting" = "private",
137
+ ) {
138
+ const chunks = splitIntoChunks(text);
139
+ for (const chunk of chunks) {
140
+ this.appendItems(chunk, position, privacy);
141
+ position += chunk.length;
123
142
  }
124
143
  }
125
144
 
@@ -136,11 +155,12 @@ export class RawCoPlainText<
136
155
  text: string,
137
156
  privacy: "private" | "trusting" = "private",
138
157
  ) {
139
- const graphemes = [...splitGraphemes(text)];
158
+ const graphemes = Array.from(splitGraphemes(text));
159
+
140
160
  if (idx >= this.entries().length) {
141
- this.appendItems(graphemes, idx - 1, privacy);
161
+ this.appendChars(graphemes, idx - 1, privacy);
142
162
  } else {
143
- this.appendItems(graphemes, idx, privacy);
163
+ this.appendChars(graphemes, idx, privacy);
144
164
  }
145
165
  }
146
166
 
@@ -178,3 +198,15 @@ export class RawCoPlainText<
178
198
  return graphemes.join("");
179
199
  }
180
200
  }
201
+
202
+ function splitIntoChunks(text: string[]) {
203
+ const chunks: string[][] = [];
204
+ for (
205
+ let i = 0;
206
+ i < text.length;
207
+ i += TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE
208
+ ) {
209
+ chunks.push(text.slice(i, i + TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE));
210
+ }
211
+ return chunks;
212
+ }
package/src/config.ts CHANGED
@@ -5,7 +5,13 @@
5
5
  This also means that we want to keep signatures roughly after each MAX_RECOMMENDED_TX size chunk,
6
6
  to be able to verify partially loaded CoValues or CoValues that are still being created (like a video live stream).
7
7
  **/
8
- export const MAX_RECOMMENDED_TX_SIZE = 100 * 1024;
8
+ export const TRANSACTION_CONFIG = {
9
+ MAX_RECOMMENDED_TX_SIZE: 100 * 1024,
10
+ };
11
+
12
+ export function setMaxRecommendedTxSize(size: number) {
13
+ TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE = size;
14
+ }
9
15
 
10
16
  export const CO_VALUE_LOADING_CONFIG = {
11
17
  MAX_RETRIES: 1,
@@ -32,3 +38,16 @@ export const SYNC_SCHEDULER_CONFIG = {
32
38
  export function setIncomingMessagesTimeBudget(budget: number) {
33
39
  SYNC_SCHEDULER_CONFIG.INCOMING_MESSAGES_TIME_BUDGET = budget;
34
40
  }
41
+
42
+ export const GARBAGE_COLLECTOR_CONFIG = {
43
+ MAX_AGE: 1000 * 60 * 10, // 10 minutes
44
+ INTERVAL: 1000 * 60 * 5, // 5 minutes
45
+ };
46
+
47
+ export function setGarbageCollectorMaxAge(maxAge: number) {
48
+ GARBAGE_COLLECTOR_CONFIG.MAX_AGE = maxAge;
49
+ }
50
+
51
+ export function setGarbageCollectorInterval(interval: number) {
52
+ GARBAGE_COLLECTOR_CONFIG.INTERVAL = interval;
53
+ }
package/src/exports.ts CHANGED
@@ -61,20 +61,25 @@ import { disablePermissionErrors } from "./permissions.js";
61
61
  import type { Peer, SyncMessage } from "./sync.js";
62
62
  import { DisconnectedError, SyncManager, emptyKnownState } from "./sync.js";
63
63
 
64
- type Value = JsonValue | AnyRawCoValue;
65
-
66
- export { PriorityBasedMessageQueue } from "./queue/PriorityBasedMessageQueue.js";
64
+ import {
65
+ getContentMessageSize,
66
+ getTransactionSize,
67
+ } from "./coValueContentMessage.js";
67
68
  import { getDependedOnCoValuesFromRawData } from "./coValueCore/utils.js";
68
69
  import {
69
70
  CO_VALUE_LOADING_CONFIG,
70
- MAX_RECOMMENDED_TX_SIZE,
71
+ TRANSACTION_CONFIG,
71
72
  setCoValueLoadingRetryDelay,
72
73
  setIncomingMessagesTimeBudget,
74
+ setMaxRecommendedTxSize,
73
75
  } from "./config.js";
74
76
  import { LogLevel, logger } from "./logger.js";
75
77
  import { CO_VALUE_PRIORITY, getPriorityFromHeader } from "./priority.js";
76
78
  import { getDependedOnCoValues } from "./storage/syncUtils.js";
77
79
 
80
+ type Value = JsonValue | AnyRawCoValue;
81
+
82
+ export { PriorityBasedMessageQueue } from "./queue/PriorityBasedMessageQueue.js";
78
83
  /** @hidden */
79
84
  export const cojsonInternals = {
80
85
  connectedPeers,
@@ -106,6 +111,10 @@ export const cojsonInternals = {
106
111
  ConnectedPeerChannel,
107
112
  textEncoder,
108
113
  textDecoder,
114
+ getTransactionSize,
115
+ getContentMessageSize,
116
+ TRANSACTION_CONFIG,
117
+ setMaxRecommendedTxSize,
109
118
  };
110
119
 
111
120
  export {
@@ -132,7 +141,6 @@ export {
132
141
  Media,
133
142
  CoValueCore,
134
143
  ControlledAgent,
135
- MAX_RECOMMENDED_TX_SIZE,
136
144
  JsonObject,
137
145
  JsonValue,
138
146
  Peer,
package/src/localNode.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Result, err, ok } from "neverthrow";
2
+ import { GarbageCollector } from "./GarbageCollector.js";
2
3
  import type { CoID } from "./coValue.js";
3
4
  import type { RawCoValue } from "./coValue.js";
4
5
  import {
@@ -30,7 +31,7 @@ import {
30
31
  type RawGroup,
31
32
  secretSeedFromInviteSecret,
32
33
  } from "./coValues/group.js";
33
- import { CO_VALUE_LOADING_CONFIG } from "./config.js";
34
+ import { CO_VALUE_LOADING_CONFIG, GARBAGE_COLLECTOR_CONFIG } from "./config.js";
34
35
  import { AgentSecret, CryptoProvider } from "./crypto/crypto.js";
35
36
  import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
36
37
  import { logger } from "./logger.js";
@@ -63,6 +64,7 @@ export class LocalNode {
63
64
  /** @category 3. Low-level */
64
65
  syncManager = new SyncManager(this);
65
66
 
67
+ garbageCollector: GarbageCollector | undefined = undefined;
66
68
  crashed: Error | undefined = undefined;
67
69
 
68
70
  storage?: StorageAPI;
@@ -78,6 +80,14 @@ export class LocalNode {
78
80
  this.crypto = crypto;
79
81
  }
80
82
 
83
+ enableGarbageCollector() {
84
+ if (this.garbageCollector) {
85
+ return;
86
+ }
87
+
88
+ this.garbageCollector = new GarbageCollector(this.coValues);
89
+ }
90
+
81
91
  setStorage(storage: StorageAPI) {
82
92
  this.storage = storage;
83
93
  }
@@ -95,6 +105,8 @@ export class LocalNode {
95
105
  this.coValues.set(id, entry);
96
106
  }
97
107
 
108
+ this.garbageCollector?.trackCoValueAccess(entry);
109
+
98
110
  return entry;
99
111
  }
100
112
 
@@ -351,6 +363,7 @@ export class LocalNode {
351
363
  new VerifiedState(id, this.crypto, header, new Map()),
352
364
  );
353
365
 
366
+ this.garbageCollector?.trackCoValueAccess(coValue);
354
367
  this.syncManager.syncHeader(coValue.verified);
355
368
 
356
369
  return coValue;
@@ -745,6 +758,7 @@ export class LocalNode {
745
758
  */
746
759
  gracefulShutdown(): Promise<unknown> | undefined {
747
760
  this.syncManager.gracefulShutdown();
761
+ this.garbageCollector?.stop();
748
762
  return this.storage?.close();
749
763
  }
750
764
  }
@@ -434,7 +434,9 @@ function determineValidTransactionsForGroup(
434
434
  const currentAccountId = coValue.node.getCurrentAccountOrAgentID();
435
435
 
436
436
  const isSelfRevoke =
437
- currentAccountId === change.key && change.value === "revoked";
437
+ currentAccountId === change.key &&
438
+ transactor === currentAccountId &&
439
+ change.value === "revoked";
438
440
 
439
441
  if (!isFirstSelfAppointment && !isSelfRevoke) {
440
442
  if (memberState[transactor] === "admin") {
@@ -42,7 +42,7 @@ export class LocalTransactionsSyncQueue {
42
42
  const lastPendingSync = this.queue.tail?.value;
43
43
  const lastSignatureIdx = coValue.getLastSignatureCheckpoint(sessionID);
44
44
  const isSignatureCheckpoint =
45
- lastSignatureIdx > -1 && lastSignatureIdx === txIdx - 1;
45
+ lastSignatureIdx > -1 && lastSignatureIdx === txIdx;
46
46
 
47
47
  if (lastPendingSync?.id === coValue.id && !isSignatureCheckpoint) {
48
48
  addTransactionToContentMessage(
@@ -8,7 +8,38 @@ type StoreQueueEntry = {
8
8
  correctionCallback: CorrectionCallback;
9
9
  };
10
10
 
11
+ class StoreQueueManager {
12
+ private backlog = new LinkedList<{
13
+ queue: StoreQueue;
14
+ callback: () => Promise<unknown>;
15
+ }>();
16
+
17
+ private processing = false;
18
+
19
+ async schedule(queue: StoreQueue, callback: () => Promise<unknown>) {
20
+ this.backlog.push({ queue, callback });
21
+
22
+ if (this.processing) {
23
+ return;
24
+ }
25
+
26
+ this.processing = true;
27
+
28
+ while (this.backlog.head) {
29
+ const entry = this.backlog.head;
30
+
31
+ await entry.value.callback();
32
+
33
+ this.backlog.shift();
34
+ }
35
+
36
+ this.processing = false;
37
+ }
38
+ }
39
+
11
40
  export class StoreQueue {
41
+ static manager = new StoreQueueManager();
42
+
12
43
  private queue = new LinkedList<StoreQueueEntry>();
13
44
  closed = false;
14
45
 
@@ -27,7 +58,7 @@ export class StoreQueue {
27
58
  processing = false;
28
59
  lastCallback: Promise<unknown> | undefined;
29
60
 
30
- async processQueue(
61
+ processQueue(
31
62
  callback: (
32
63
  data: NewContentMessage,
33
64
  correctionCallback: CorrectionCallback,
@@ -39,21 +70,23 @@ export class StoreQueue {
39
70
 
40
71
  this.processing = true;
41
72
 
42
- let entry: StoreQueueEntry | undefined;
73
+ return StoreQueue.manager.schedule(this, async () => {
74
+ let entry: StoreQueueEntry | undefined;
43
75
 
44
- while ((entry = this.pull())) {
45
- const { data, correctionCallback } = entry;
76
+ while ((entry = this.pull())) {
77
+ const { data, correctionCallback } = entry;
46
78
 
47
- try {
48
- this.lastCallback = callback(data, correctionCallback);
49
- await this.lastCallback;
50
- } catch (err) {
51
- logger.error("Error processing message in store queue", { err });
79
+ try {
80
+ this.lastCallback = callback(data, correctionCallback);
81
+ await this.lastCallback;
82
+ } catch (err) {
83
+ logger.error("Error processing message in store queue", { err });
84
+ }
52
85
  }
53
- }
54
86
 
55
- this.lastCallback = undefined;
56
- this.processing = false;
87
+ this.lastCallback = undefined;
88
+ this.processing = false;
89
+ });
57
90
  }
58
91
 
59
92
  close() {
@@ -112,24 +112,34 @@ export class SQLiteClient implements DBClientInterfaceSync {
112
112
  ) as SignatureAfterRow[];
113
113
  }
114
114
 
115
- addCoValue(msg: NewContentMessage): number {
115
+ getCoValueRowID(id: RawCoID): number | undefined {
116
+ const row = this.db.get<{ rowID: number }>(
117
+ "SELECT rowID FROM coValues WHERE id = ?",
118
+ [id],
119
+ );
120
+ return row?.rowID;
121
+ }
122
+
123
+ upsertCoValue(id: RawCoID, header?: CoValueHeader): number | undefined {
124
+ if (!header) {
125
+ return this.getCoValueRowID(id);
126
+ }
127
+
116
128
  const result = this.db.get<{ rowID: number }>(
117
- "INSERT INTO coValues (id, header) VALUES (?, ?) RETURNING rowID",
118
- [msg.id, JSON.stringify(msg.header)],
129
+ `INSERT INTO coValues (id, header) VALUES (?, ?)
130
+ ON CONFLICT(id) DO NOTHING
131
+ RETURNING rowID`,
132
+ [id, JSON.stringify(header)],
119
133
  );
120
134
 
121
135
  if (!result) {
122
- throw new Error("Failed to add coValue");
136
+ return this.getCoValueRowID(id);
123
137
  }
124
138
 
125
139
  return result.rowID;
126
140
  }
127
141
 
128
- addSessionUpdate({
129
- sessionUpdate,
130
- }: {
131
- sessionUpdate: SessionRow;
132
- }): number {
142
+ addSessionUpdate({ sessionUpdate }: { sessionUpdate: SessionRow }): number {
133
143
  const result = this.db.get<{ rowID: number }>(
134
144
  `INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature, bytesSinceLastSignature) VALUES (?, ?, ?, ?, ?)
135
145
  ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature, bytesSinceLastSignature=excluded.bytesSinceLastSignature
@@ -166,7 +176,11 @@ export class SQLiteClient implements DBClientInterfaceSync {
166
176
  sessionRowID,
167
177
  idx,
168
178
  signature,
169
- }: { sessionRowID: number; idx: number; signature: Signature }) {
179
+ }: {
180
+ sessionRowID: number;
181
+ idx: number;
182
+ signature: Signature;
183
+ }) {
170
184
  this.db.run(
171
185
  "INSERT INTO signatureAfter (ses, idx, signature) VALUES (?, ?, ?)",
172
186
  [sessionRowID, idx, signature],
@@ -112,14 +112,31 @@ export class SQLiteClientAsync implements DBClientInterfaceAsync {
112
112
  );
113
113
  }
114
114
 
115
- async addCoValue(msg: NewContentMessage): Promise<number> {
115
+ async getCoValueRowID(id: RawCoID): Promise<number | undefined> {
116
+ const row = await this.db.get<{ rowID: number }>(
117
+ "SELECT rowID FROM coValues WHERE id = ?",
118
+ [id],
119
+ );
120
+ return row?.rowID;
121
+ }
122
+
123
+ async upsertCoValue(
124
+ id: RawCoID,
125
+ header?: CoValueHeader,
126
+ ): Promise<number | undefined> {
127
+ if (!header) {
128
+ return this.getCoValueRowID(id);
129
+ }
130
+
116
131
  const result = await this.db.get<{ rowID: number }>(
117
- "INSERT INTO coValues (id, header) VALUES (?, ?) RETURNING rowID",
118
- [msg.id, JSON.stringify(msg.header)],
132
+ `INSERT INTO coValues (id, header) VALUES (?, ?)
133
+ ON CONFLICT(id) DO NOTHING
134
+ RETURNING rowID`,
135
+ [id, JSON.stringify(header)],
119
136
  );
120
137
 
121
138
  if (!result) {
122
- throw new Error("Failed to add coValue");
139
+ return this.getCoValueRowID(id);
123
140
  }
124
141
 
125
142
  return result.rowID;
@@ -166,7 +183,11 @@ export class SQLiteClientAsync implements DBClientInterfaceAsync {
166
183
  sessionRowID,
167
184
  idx,
168
185
  signature,
169
- }: { sessionRowID: number; idx: number; signature: Signature }) {
186
+ }: {
187
+ sessionRowID: number;
188
+ idx: number;
189
+ signature: Signature;
190
+ }) {
170
191
  this.db.run(
171
192
  "INSERT INTO signatureAfter (ses, idx, signature) VALUES (?, ?, ?)",
172
193
  [sessionRowID, idx, signature],
@@ -254,22 +254,18 @@ export class StorageApiAsync implements StorageAPI {
254
254
  }
255
255
 
256
256
  const id = msg.id;
257
- const coValueRow = await this.dbClient.getCoValue(id);
258
-
259
- // We have no info about coValue header
260
- const invalidAssumptionOnHeaderPresence = !msg.header && !coValueRow;
257
+ const storedCoValueRowID = await this.dbClient.upsertCoValue(
258
+ id,
259
+ msg.header,
260
+ );
261
261
 
262
- if (invalidAssumptionOnHeaderPresence) {
262
+ if (!storedCoValueRowID) {
263
263
  const knownState = emptyKnownState(id as RawCoID);
264
264
  this.knwonStates.setKnownState(id, knownState);
265
265
 
266
266
  return this.handleCorrection(knownState, correctionCallback);
267
267
  }
268
268
 
269
- const storedCoValueRowID: number = coValueRow
270
- ? coValueRow.rowID
271
- : await this.dbClient.addCoValue(msg);
272
-
273
269
  const knownState = this.knwonStates.getKnownState(id);
274
270
  knownState.header = true;
275
271
 
@@ -235,22 +235,15 @@ export class StorageApiSync implements StorageAPI {
235
235
  correctionCallback: CorrectionCallback,
236
236
  ): boolean {
237
237
  const id = msg.id;
238
- const coValueRow = this.dbClient.getCoValue(id);
239
-
240
- // We have no info about coValue header
241
- const invalidAssumptionOnHeaderPresence = !msg.header && !coValueRow;
238
+ const storedCoValueRowID = this.dbClient.upsertCoValue(id, msg.header);
242
239
 
243
- if (invalidAssumptionOnHeaderPresence) {
240
+ if (!storedCoValueRowID) {
244
241
  const knownState = emptyKnownState(id as RawCoID);
245
242
  this.knwonStates.setKnownState(id, knownState);
246
243
 
247
244
  return this.handleCorrection(knownState, correctionCallback);
248
245
  }
249
246
 
250
- const storedCoValueRowID: number = coValueRow
251
- ? coValueRow.rowID
252
- : this.dbClient.addCoValue(msg);
253
-
254
247
  const knownState = this.knwonStates.getKnownState(id);
255
248
  knownState.header = true;
256
249