cojson 0.16.5 → 0.16.6

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 (117) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +10 -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/queue/LocalTransactionsSyncQueue.js +1 -1
  41. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -1
  42. package/dist/queue/StoreQueue.d.ts +7 -1
  43. package/dist/queue/StoreQueue.d.ts.map +1 -1
  44. package/dist/queue/StoreQueue.js +35 -13
  45. package/dist/queue/StoreQueue.js.map +1 -1
  46. package/dist/storage/sqlite/client.d.ts +4 -4
  47. package/dist/storage/sqlite/client.d.ts.map +1 -1
  48. package/dist/storage/sqlite/client.js +13 -4
  49. package/dist/storage/sqlite/client.js.map +1 -1
  50. package/dist/storage/sqliteAsync/client.d.ts +3 -3
  51. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  52. package/dist/storage/sqliteAsync/client.js +12 -3
  53. package/dist/storage/sqliteAsync/client.js.map +1 -1
  54. package/dist/storage/storageAsync.d.ts.map +1 -1
  55. package/dist/storage/storageAsync.js +2 -7
  56. package/dist/storage/storageAsync.js.map +1 -1
  57. package/dist/storage/storageSync.d.ts.map +1 -1
  58. package/dist/storage/storageSync.js +2 -7
  59. package/dist/storage/storageSync.js.map +1 -1
  60. package/dist/storage/types.d.ts +2 -2
  61. package/dist/storage/types.d.ts.map +1 -1
  62. package/dist/sync.d.ts.map +1 -1
  63. package/dist/sync.js +17 -3
  64. package/dist/sync.js.map +1 -1
  65. package/dist/tests/GarbageCollector.test.d.ts +2 -0
  66. package/dist/tests/GarbageCollector.test.d.ts.map +1 -0
  67. package/dist/tests/GarbageCollector.test.js +85 -0
  68. package/dist/tests/GarbageCollector.test.js.map +1 -0
  69. package/dist/tests/coPlainText.test.js +142 -4
  70. package/dist/tests/coPlainText.test.js.map +1 -1
  71. package/dist/tests/coStream.test.js +3 -3
  72. package/dist/tests/coStream.test.js.map +1 -1
  73. package/dist/tests/sync.garbageCollection.test.d.ts +2 -0
  74. package/dist/tests/sync.garbageCollection.test.d.ts.map +1 -0
  75. package/dist/tests/sync.garbageCollection.test.js +133 -0
  76. package/dist/tests/sync.garbageCollection.test.js.map +1 -0
  77. package/dist/tests/sync.mesh.test.js +48 -34
  78. package/dist/tests/sync.mesh.test.js.map +1 -1
  79. package/dist/tests/sync.storage.test.js +31 -21
  80. package/dist/tests/sync.storage.test.js.map +1 -1
  81. package/dist/tests/sync.storageAsync.test.js +76 -29
  82. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  83. package/dist/tests/testStorage.d.ts +1 -0
  84. package/dist/tests/testStorage.d.ts.map +1 -1
  85. package/dist/tests/testStorage.js +1 -1
  86. package/dist/tests/testStorage.js.map +1 -1
  87. package/dist/tests/testUtils.d.ts +1 -0
  88. package/dist/tests/testUtils.d.ts.map +1 -1
  89. package/dist/tests/testUtils.js +1 -0
  90. package/dist/tests/testUtils.js.map +1 -1
  91. package/package.json +1 -1
  92. package/src/GarbageCollector.ts +48 -0
  93. package/src/coValueContentMessage.ts +16 -3
  94. package/src/coValueCore/coValueCore.ts +27 -10
  95. package/src/coValueCore/utils.ts +1 -0
  96. package/src/coValueCore/verifiedState.ts +1 -0
  97. package/src/coValues/coPlainText.ts +40 -8
  98. package/src/config.ts +20 -1
  99. package/src/exports.ts +13 -5
  100. package/src/localNode.ts +15 -1
  101. package/src/queue/LocalTransactionsSyncQueue.ts +1 -1
  102. package/src/queue/StoreQueue.ts +45 -12
  103. package/src/storage/sqlite/client.ts +24 -10
  104. package/src/storage/sqliteAsync/client.ts +26 -5
  105. package/src/storage/storageAsync.ts +5 -9
  106. package/src/storage/storageSync.ts +2 -9
  107. package/src/storage/types.ts +7 -4
  108. package/src/sync.ts +19 -3
  109. package/src/tests/GarbageCollector.test.ts +127 -0
  110. package/src/tests/coPlainText.test.ts +176 -4
  111. package/src/tests/coStream.test.ts +7 -3
  112. package/src/tests/sync.garbageCollection.test.ts +178 -0
  113. package/src/tests/sync.mesh.test.ts +49 -34
  114. package/src/tests/sync.storage.test.ts +31 -21
  115. package/src/tests/sync.storageAsync.test.ts +81 -29
  116. package/src/tests/testStorage.ts +11 -3
  117. package/src/tests/testUtils.ts +4 -1
@@ -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
  }
@@ -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
 
@@ -65,6 +65,11 @@ export interface DBClientInterfaceAsync {
65
65
  coValueId: string,
66
66
  ): Promise<StoredCoValueRow | undefined> | undefined;
67
67
 
68
+ upsertCoValue(
69
+ id: string,
70
+ header?: CoValueHeader,
71
+ ): Promise<number | undefined>;
72
+
68
73
  getCoValueSessions(coValueRowId: number): Promise<StoredSessionRow[]>;
69
74
 
70
75
  getSingleCoValueSession(
@@ -83,8 +88,6 @@ export interface DBClientInterfaceAsync {
83
88
  firstNewTxIdx: number,
84
89
  ): Promise<SignatureAfterRow[]>;
85
90
 
86
- addCoValue(msg: NewContentMessage): Promise<number>;
87
-
88
91
  addSessionUpdate({
89
92
  sessionUpdate,
90
93
  sessionRow,
@@ -115,6 +118,8 @@ export interface DBClientInterfaceAsync {
115
118
  export interface DBClientInterfaceSync {
116
119
  getCoValue(coValueId: string): StoredCoValueRow | undefined;
117
120
 
121
+ upsertCoValue(id: string, header?: CoValueHeader): number | undefined;
122
+
118
123
  getCoValueSessions(coValueRowId: number): StoredSessionRow[];
119
124
 
120
125
  getSingleCoValueSession(
@@ -133,8 +138,6 @@ export interface DBClientInterfaceSync {
133
138
  firstNewTxIdx: number,
134
139
  ): Pick<SignatureAfterRow, "idx" | "signature">[];
135
140
 
136
- addCoValue(msg: NewContentMessage): number;
137
-
138
141
  addSessionUpdate({
139
142
  sessionUpdate,
140
143
  sessionRow,
package/src/sync.ts CHANGED
@@ -461,6 +461,22 @@ export class SyncManager {
461
461
 
462
462
  if (!coValue.hasVerifiedContent()) {
463
463
  if (!msg.header) {
464
+ const storageKnownState = this.local.storage?.getKnownState(msg.id);
465
+
466
+ if (storageKnownState?.header) {
467
+ // If the CoValue has been garbage collected, we load it from the storage before handling the new content
468
+ coValue.loadFromStorage((found) => {
469
+ if (found) {
470
+ this.handleNewContent(msg, from);
471
+ } else {
472
+ logger.error("Known CoValue not found in storage", {
473
+ id: msg.id,
474
+ });
475
+ }
476
+ });
477
+ return;
478
+ }
479
+
464
480
  if (peer) {
465
481
  this.trySendToPeer(peer, {
466
482
  action: "known",
@@ -782,12 +798,12 @@ export class SyncManager {
782
798
 
783
799
  if (!storage) return;
784
800
 
801
+ const value = this.local.getCoValue(content.id);
802
+
785
803
  // Try to store the content as-is for performance
786
804
  // In case that some transactions are missing, a correction will be requested, but it's an edge case
787
805
  storage.store(content, (correction) => {
788
- return this.local
789
- .getCoValue(content.id)
790
- .verified?.newContentSince(correction);
806
+ return value.verified?.newContentSince(correction);
791
807
  });
792
808
  }
793
809
 
@@ -0,0 +1,127 @@
1
+ import { assert, beforeEach, describe, expect, test, vi } from "vitest";
2
+
3
+ import { setGarbageCollectorMaxAge } from "../config";
4
+ import { TEST_NODE_CONFIG, setupTestAccount, setupTestNode } from "./testUtils";
5
+
6
+ // We want to simulate a real world communication that happens asynchronously
7
+ TEST_NODE_CONFIG.withAsyncPeers = true;
8
+
9
+ beforeEach(() => {
10
+ // We want to test what happens when the garbage collector kicks in and removes a coValue
11
+ // We set the max age to -1 to make it remove everything
12
+ setGarbageCollectorMaxAge(-1);
13
+ });
14
+
15
+ describe("garbage collector", () => {
16
+ test("coValues are garbage collected when maxAge is reached", async () => {
17
+ const client = setupTestNode();
18
+
19
+ client.addStorage({
20
+ ourName: "client",
21
+ });
22
+ client.node.enableGarbageCollector();
23
+
24
+ const group = client.node.createGroup();
25
+ const map = group.createMap();
26
+ map.set("hello", "world", "trusting");
27
+
28
+ await new Promise((resolve) => setTimeout(resolve, 10));
29
+
30
+ client.node.garbageCollector?.collect();
31
+
32
+ const coValue = client.node.getCoValue(map.id);
33
+
34
+ expect(coValue.isAvailable()).toBe(false);
35
+ });
36
+
37
+ test("coValues are not garbage collected if they have listeners", async () => {
38
+ const client = setupTestNode();
39
+
40
+ client.addStorage({
41
+ ourName: "client",
42
+ });
43
+ client.node.enableGarbageCollector();
44
+
45
+ const group = client.node.createGroup();
46
+ const map = group.createMap();
47
+ map.set("hello", "world", "trusting");
48
+
49
+ // Add a listener to the map
50
+ const unsubscribe = map.subscribe(() => {
51
+ // This listener keeps the coValue alive
52
+ });
53
+
54
+ await new Promise((resolve) => setTimeout(resolve, 10));
55
+
56
+ client.node.garbageCollector?.collect();
57
+
58
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(true);
59
+
60
+ // Clean up the listener
61
+ unsubscribe();
62
+
63
+ // The coValue should be collected after the listener is removed
64
+ client.node.garbageCollector?.collect();
65
+
66
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(false);
67
+ });
68
+
69
+ test("coValues are not garbage collected if they are a group or account", async () => {
70
+ const client = await setupTestAccount();
71
+
72
+ client.addStorage({
73
+ ourName: "client",
74
+ });
75
+ client.node.enableGarbageCollector();
76
+
77
+ const group = client.node.createGroup();
78
+
79
+ await new Promise((resolve) => setTimeout(resolve, 10));
80
+
81
+ client.node.garbageCollector?.collect();
82
+
83
+ expect(client.node.getCoValue(group.id).isAvailable()).toBe(true);
84
+ expect(client.node.getCoValue(client.accountID).isAvailable()).toBe(true);
85
+ });
86
+
87
+ test("coValues are not garbage collected if the maxAge is not reached", async () => {
88
+ setGarbageCollectorMaxAge(1000);
89
+
90
+ const client = setupTestNode();
91
+
92
+ client.addStorage({
93
+ ourName: "client",
94
+ });
95
+ client.node.enableGarbageCollector();
96
+
97
+ const garbageCollector = client.node.garbageCollector;
98
+
99
+ assert(garbageCollector);
100
+
101
+ const getCurrentTime = vi.spyOn(garbageCollector, "getCurrentTime");
102
+
103
+ getCurrentTime.mockReturnValue(1);
104
+
105
+ const group = client.node.createGroup();
106
+ const map1 = group.createMap();
107
+ const map2 = group.createMap();
108
+
109
+ await new Promise((resolve) => setTimeout(resolve, 10));
110
+
111
+ map1.set("hello", "world", "trusting");
112
+
113
+ getCurrentTime.mockReturnValue(2000);
114
+
115
+ await new Promise((resolve) => setTimeout(resolve, 10));
116
+
117
+ garbageCollector.collect();
118
+
119
+ const coValue = client.node.getCoValue(map1.id);
120
+
121
+ expect(coValue.isAvailable()).toBe(true);
122
+
123
+ const coValue2 = client.node.getCoValue(map2.id);
124
+
125
+ expect(coValue2.isAvailable()).toBe(false);
126
+ });
127
+ });