cojson 0.19.22 → 0.20.1

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 (223) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +66 -0
  3. package/dist/PeerState.d.ts +6 -1
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +18 -3
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/coValueContentMessage.d.ts +0 -2
  8. package/dist/coValueContentMessage.d.ts.map +1 -1
  9. package/dist/coValueContentMessage.js +0 -8
  10. package/dist/coValueContentMessage.js.map +1 -1
  11. package/dist/coValueCore/SessionMap.d.ts +4 -2
  12. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  13. package/dist/coValueCore/SessionMap.js +30 -0
  14. package/dist/coValueCore/SessionMap.js.map +1 -1
  15. package/dist/coValueCore/coValueCore.d.ts +70 -5
  16. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  17. package/dist/coValueCore/coValueCore.js +302 -31
  18. package/dist/coValueCore/coValueCore.js.map +1 -1
  19. package/dist/coValueCore/verifiedState.d.ts +6 -1
  20. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  21. package/dist/coValueCore/verifiedState.js +9 -0
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/coList.d.ts +4 -2
  24. package/dist/coValues/coList.d.ts.map +1 -1
  25. package/dist/coValues/coList.js +3 -0
  26. package/dist/coValues/coList.js.map +1 -1
  27. package/dist/coValues/group.d.ts.map +1 -1
  28. package/dist/coValues/group.js +3 -6
  29. package/dist/coValues/group.js.map +1 -1
  30. package/dist/config.d.ts +2 -8
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +4 -12
  33. package/dist/config.js.map +1 -1
  34. package/dist/crypto/NapiCrypto.d.ts +1 -2
  35. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  36. package/dist/crypto/NapiCrypto.js +19 -4
  37. package/dist/crypto/NapiCrypto.js.map +1 -1
  38. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  39. package/dist/crypto/RNCrypto.js +19 -4
  40. package/dist/crypto/RNCrypto.js.map +1 -1
  41. package/dist/crypto/WasmCrypto.d.ts +11 -4
  42. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  43. package/dist/crypto/WasmCrypto.js +52 -10
  44. package/dist/crypto/WasmCrypto.js.map +1 -1
  45. package/dist/crypto/WasmCryptoEdge.d.ts +1 -0
  46. package/dist/crypto/WasmCryptoEdge.d.ts.map +1 -1
  47. package/dist/crypto/WasmCryptoEdge.js +4 -1
  48. package/dist/crypto/WasmCryptoEdge.js.map +1 -1
  49. package/dist/crypto/crypto.d.ts +3 -3
  50. package/dist/crypto/crypto.d.ts.map +1 -1
  51. package/dist/crypto/crypto.js +6 -1
  52. package/dist/crypto/crypto.js.map +1 -1
  53. package/dist/exports.d.ts +5 -5
  54. package/dist/exports.d.ts.map +1 -1
  55. package/dist/exports.js +4 -3
  56. package/dist/exports.js.map +1 -1
  57. package/dist/ids.d.ts +4 -1
  58. package/dist/ids.d.ts.map +1 -1
  59. package/dist/ids.js +4 -0
  60. package/dist/ids.js.map +1 -1
  61. package/dist/knownState.d.ts +2 -0
  62. package/dist/knownState.d.ts.map +1 -1
  63. package/dist/localNode.d.ts +12 -0
  64. package/dist/localNode.d.ts.map +1 -1
  65. package/dist/localNode.js +14 -0
  66. package/dist/localNode.js.map +1 -1
  67. package/dist/platformUtils.d.ts +3 -0
  68. package/dist/platformUtils.d.ts.map +1 -0
  69. package/dist/platformUtils.js +24 -0
  70. package/dist/platformUtils.js.map +1 -0
  71. package/dist/queue/LinkedList.d.ts +9 -3
  72. package/dist/queue/LinkedList.d.ts.map +1 -1
  73. package/dist/queue/LinkedList.js +30 -1
  74. package/dist/queue/LinkedList.js.map +1 -1
  75. package/dist/queue/OutgoingLoadQueue.d.ts +95 -0
  76. package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -0
  77. package/dist/queue/OutgoingLoadQueue.js +240 -0
  78. package/dist/queue/OutgoingLoadQueue.js.map +1 -0
  79. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts +30 -0
  80. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts.map +1 -0
  81. package/dist/storage/DeletedCoValuesEraserScheduler.js +84 -0
  82. package/dist/storage/DeletedCoValuesEraserScheduler.js.map +1 -0
  83. package/dist/storage/sqlite/client.d.ts +3 -0
  84. package/dist/storage/sqlite/client.d.ts.map +1 -1
  85. package/dist/storage/sqlite/client.js +44 -0
  86. package/dist/storage/sqlite/client.js.map +1 -1
  87. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  88. package/dist/storage/sqlite/sqliteMigrations.js +7 -0
  89. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  90. package/dist/storage/sqliteAsync/client.d.ts +3 -0
  91. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  92. package/dist/storage/sqliteAsync/client.js +42 -0
  93. package/dist/storage/sqliteAsync/client.js.map +1 -1
  94. package/dist/storage/storageAsync.d.ts +7 -0
  95. package/dist/storage/storageAsync.d.ts.map +1 -1
  96. package/dist/storage/storageAsync.js +48 -0
  97. package/dist/storage/storageAsync.js.map +1 -1
  98. package/dist/storage/storageSync.d.ts +6 -0
  99. package/dist/storage/storageSync.d.ts.map +1 -1
  100. package/dist/storage/storageSync.js +42 -0
  101. package/dist/storage/storageSync.js.map +1 -1
  102. package/dist/storage/types.d.ts +59 -0
  103. package/dist/storage/types.d.ts.map +1 -1
  104. package/dist/storage/types.js +12 -1
  105. package/dist/storage/types.js.map +1 -1
  106. package/dist/sync.d.ts.map +1 -1
  107. package/dist/sync.js +66 -43
  108. package/dist/sync.js.map +1 -1
  109. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts +2 -0
  110. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts.map +1 -0
  111. package/dist/tests/DeletedCoValuesEraserScheduler.test.js +149 -0
  112. package/dist/tests/DeletedCoValuesEraserScheduler.test.js.map +1 -0
  113. package/dist/tests/GarbageCollector.test.js +5 -6
  114. package/dist/tests/GarbageCollector.test.js.map +1 -1
  115. package/dist/tests/LinkedList.test.js +90 -0
  116. package/dist/tests/LinkedList.test.js.map +1 -1
  117. package/dist/tests/OutgoingLoadQueue.test.d.ts +2 -0
  118. package/dist/tests/OutgoingLoadQueue.test.d.ts.map +1 -0
  119. package/dist/tests/OutgoingLoadQueue.test.js +814 -0
  120. package/dist/tests/OutgoingLoadQueue.test.js.map +1 -0
  121. package/dist/tests/StorageApiAsync.test.js +484 -152
  122. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  123. package/dist/tests/StorageApiSync.test.js +505 -136
  124. package/dist/tests/StorageApiSync.test.js.map +1 -1
  125. package/dist/tests/WasmCrypto.test.js +6 -3
  126. package/dist/tests/WasmCrypto.test.js.map +1 -1
  127. package/dist/tests/coValueCore.loadFromStorage.test.js +3 -0
  128. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  129. package/dist/tests/coValueCore.test.js +34 -13
  130. package/dist/tests/coValueCore.test.js.map +1 -1
  131. package/dist/tests/coreWasm.test.js +127 -4
  132. package/dist/tests/coreWasm.test.js.map +1 -1
  133. package/dist/tests/crypto.test.js +89 -93
  134. package/dist/tests/crypto.test.js.map +1 -1
  135. package/dist/tests/deleteCoValue.test.d.ts +2 -0
  136. package/dist/tests/deleteCoValue.test.d.ts.map +1 -0
  137. package/dist/tests/deleteCoValue.test.js +313 -0
  138. package/dist/tests/deleteCoValue.test.js.map +1 -0
  139. package/dist/tests/group.removeMember.test.js +18 -30
  140. package/dist/tests/group.removeMember.test.js.map +1 -1
  141. package/dist/tests/knownState.lazyLoading.test.js +3 -0
  142. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  143. package/dist/tests/sync.concurrentLoad.test.d.ts +2 -0
  144. package/dist/tests/sync.concurrentLoad.test.d.ts.map +1 -0
  145. package/dist/tests/sync.concurrentLoad.test.js +481 -0
  146. package/dist/tests/sync.concurrentLoad.test.js.map +1 -0
  147. package/dist/tests/sync.deleted.test.d.ts +2 -0
  148. package/dist/tests/sync.deleted.test.d.ts.map +1 -0
  149. package/dist/tests/sync.deleted.test.js +214 -0
  150. package/dist/tests/sync.deleted.test.js.map +1 -0
  151. package/dist/tests/sync.mesh.test.js +3 -2
  152. package/dist/tests/sync.mesh.test.js.map +1 -1
  153. package/dist/tests/sync.storage.test.js +4 -3
  154. package/dist/tests/sync.storage.test.js.map +1 -1
  155. package/dist/tests/sync.test.js +3 -2
  156. package/dist/tests/sync.test.js.map +1 -1
  157. package/dist/tests/testStorage.d.ts +3 -0
  158. package/dist/tests/testStorage.d.ts.map +1 -1
  159. package/dist/tests/testStorage.js +17 -1
  160. package/dist/tests/testStorage.js.map +1 -1
  161. package/dist/tests/testUtils.d.ts +7 -3
  162. package/dist/tests/testUtils.d.ts.map +1 -1
  163. package/dist/tests/testUtils.js +19 -4
  164. package/dist/tests/testUtils.js.map +1 -1
  165. package/package.json +6 -16
  166. package/src/PeerState.ts +26 -3
  167. package/src/coValueContentMessage.ts +0 -14
  168. package/src/coValueCore/SessionMap.ts +43 -1
  169. package/src/coValueCore/coValueCore.ts +415 -27
  170. package/src/coValueCore/verifiedState.ts +26 -3
  171. package/src/coValues/coList.ts +9 -3
  172. package/src/coValues/group.ts +5 -6
  173. package/src/config.ts +4 -13
  174. package/src/crypto/NapiCrypto.ts +29 -13
  175. package/src/crypto/RNCrypto.ts +29 -11
  176. package/src/crypto/WasmCrypto.ts +67 -20
  177. package/src/crypto/WasmCryptoEdge.ts +5 -1
  178. package/src/crypto/crypto.ts +16 -4
  179. package/src/exports.ts +4 -2
  180. package/src/ids.ts +11 -1
  181. package/src/localNode.ts +15 -0
  182. package/src/platformUtils.ts +26 -0
  183. package/src/queue/LinkedList.ts +34 -4
  184. package/src/queue/OutgoingLoadQueue.ts +307 -0
  185. package/src/storage/DeletedCoValuesEraserScheduler.ts +124 -0
  186. package/src/storage/sqlite/client.ts +77 -0
  187. package/src/storage/sqlite/sqliteMigrations.ts +7 -0
  188. package/src/storage/sqliteAsync/client.ts +75 -0
  189. package/src/storage/storageAsync.ts +62 -0
  190. package/src/storage/storageSync.ts +58 -0
  191. package/src/storage/types.ts +69 -0
  192. package/src/sync.ts +78 -46
  193. package/src/tests/DeletedCoValuesEraserScheduler.test.ts +185 -0
  194. package/src/tests/GarbageCollector.test.ts +6 -10
  195. package/src/tests/LinkedList.test.ts +111 -0
  196. package/src/tests/OutgoingLoadQueue.test.ts +1129 -0
  197. package/src/tests/StorageApiAsync.test.ts +572 -162
  198. package/src/tests/StorageApiSync.test.ts +580 -143
  199. package/src/tests/WasmCrypto.test.ts +8 -3
  200. package/src/tests/coValueCore.loadFromStorage.test.ts +6 -0
  201. package/src/tests/coValueCore.test.ts +49 -14
  202. package/src/tests/coreWasm.test.ts +319 -10
  203. package/src/tests/crypto.test.ts +141 -150
  204. package/src/tests/deleteCoValue.test.ts +528 -0
  205. package/src/tests/group.removeMember.test.ts +35 -35
  206. package/src/tests/knownState.lazyLoading.test.ts +6 -0
  207. package/src/tests/sync.concurrentLoad.test.ts +650 -0
  208. package/src/tests/sync.deleted.test.ts +294 -0
  209. package/src/tests/sync.mesh.test.ts +5 -2
  210. package/src/tests/sync.storage.test.ts +6 -3
  211. package/src/tests/sync.test.ts +5 -2
  212. package/src/tests/testStorage.ts +31 -2
  213. package/src/tests/testUtils.ts +31 -10
  214. package/dist/crypto/PureJSCrypto.d.ts +0 -77
  215. package/dist/crypto/PureJSCrypto.d.ts.map +0 -1
  216. package/dist/crypto/PureJSCrypto.js +0 -236
  217. package/dist/crypto/PureJSCrypto.js.map +0 -1
  218. package/dist/tests/PureJSCrypto.test.d.ts +0 -2
  219. package/dist/tests/PureJSCrypto.test.d.ts.map +0 -1
  220. package/dist/tests/PureJSCrypto.test.js +0 -145
  221. package/dist/tests/PureJSCrypto.test.js.map +0 -1
  222. package/src/crypto/PureJSCrypto.ts +0 -429
  223. package/src/tests/PureJSCrypto.test.ts +0 -217
@@ -15,6 +15,7 @@ import type {
15
15
  StoredSessionRow,
16
16
  TransactionRow,
17
17
  } from "../types.js";
18
+ import { DeletedCoValueDeletionStatus } from "../types.js";
18
19
  import type { SQLiteDatabaseDriverAsync } from "./types.js";
19
20
  import type { PeerID } from "../../sync.js";
20
21
 
@@ -29,6 +30,10 @@ export type RawTransactionRow = {
29
30
  tx: string;
30
31
  };
31
32
 
33
+ type DeletedCoValueQueueRow = {
34
+ id: RawCoID;
35
+ };
36
+
32
37
  export function getErrorMessage(error: unknown) {
33
38
  return error instanceof Error ? error.message : "Unknown error";
34
39
  }
@@ -146,6 +151,76 @@ export class SQLiteClientAsync
146
151
  return result.rowID;
147
152
  }
148
153
 
154
+ async markCoValueAsDeleted(id: RawCoID) {
155
+ // Work queue entry. Table only stores the coValueID.
156
+ // Idempotent by design.
157
+ await this.db.run(
158
+ `INSERT INTO deletedCoValues (coValueID) VALUES (?) ON CONFLICT(coValueID) DO NOTHING`,
159
+ [id],
160
+ );
161
+ }
162
+
163
+ async eraseCoValueButKeepTombstone(coValueId: RawCoID) {
164
+ const coValueRow = await this.db.get<RawCoValueRow & { rowID: number }>(
165
+ "SELECT * FROM coValues WHERE id = ?",
166
+ [coValueId],
167
+ );
168
+
169
+ if (!coValueRow) {
170
+ logger.warn(`CoValue ${coValueId} not found, skipping deletion`);
171
+ return;
172
+ }
173
+
174
+ await this.transaction(async () => {
175
+ await this.db.run(
176
+ `DELETE FROM transactions
177
+ WHERE ses IN (
178
+ SELECT rowID FROM sessions
179
+ WHERE coValue = ?
180
+ AND sessionID NOT LIKE '%$'
181
+ )`,
182
+ [coValueRow.rowID],
183
+ );
184
+
185
+ await this.db.run(
186
+ `DELETE FROM signatureAfter
187
+ WHERE ses IN (
188
+ SELECT rowID FROM sessions
189
+ WHERE coValue = ?
190
+ AND sessionID NOT LIKE '%$'
191
+ )`,
192
+ [coValueRow.rowID],
193
+ );
194
+
195
+ await this.db.run(
196
+ `DELETE FROM sessions
197
+ WHERE coValue = ?
198
+ AND sessionID NOT LIKE '%$'`,
199
+ [coValueRow.rowID],
200
+ );
201
+
202
+ await this.db.run(
203
+ `INSERT INTO deletedCoValues (coValueID, status) VALUES (?, ?)
204
+ ON CONFLICT(coValueID) DO UPDATE SET status=?`,
205
+ [
206
+ coValueId,
207
+ DeletedCoValueDeletionStatus.Done,
208
+ DeletedCoValueDeletionStatus.Done,
209
+ ],
210
+ );
211
+ });
212
+ }
213
+
214
+ async getAllCoValuesWaitingForDelete(): Promise<RawCoID[]> {
215
+ const rows = await this.db.query<DeletedCoValueQueueRow>(
216
+ `SELECT coValueID as id
217
+ FROM deletedCoValues
218
+ WHERE status = ?`,
219
+ [DeletedCoValueDeletionStatus.Pending],
220
+ );
221
+ return rows.map((r) => r.id);
222
+ }
223
+
149
224
  async addSessionUpdate({
150
225
  sessionUpdate,
151
226
  }: {
@@ -17,6 +17,7 @@ import {
17
17
  setSessionCounter,
18
18
  } from "../knownState.js";
19
19
  import { StorageKnownState } from "./knownState.js";
20
+ import { DeletedCoValuesEraserScheduler } from "./DeletedCoValuesEraserScheduler.js";
20
21
  import {
21
22
  collectNewTxs,
22
23
  getDependedOnCoValues,
@@ -30,10 +31,15 @@ import type {
30
31
  StoredCoValueRow,
31
32
  StoredSessionRow,
32
33
  } from "./types.js";
34
+ import { isDeleteSessionID } from "../ids.js";
33
35
 
34
36
  export class StorageApiAsync implements StorageAPI {
35
37
  private readonly dbClient: DBClientInterfaceAsync;
36
38
 
39
+ private deletedCoValuesEraserScheduler:
40
+ | DeletedCoValuesEraserScheduler
41
+ | undefined;
42
+ private eraserController: AbortController | undefined;
37
43
  /**
38
44
  * Keeps track of CoValues that are in memory, to avoid reloading them from storage
39
45
  * when it isn't necessary
@@ -114,6 +120,7 @@ export class StorageApiAsync implements StorageAPI {
114
120
  callback: (data: NewContentMessage) => void,
115
121
  done: (found: boolean) => void,
116
122
  ) {
123
+ this.interruptEraser("load");
117
124
  const coValueRow = await this.dbClient.getCoValue(id);
118
125
 
119
126
  if (!coValueRow) {
@@ -267,10 +274,35 @@ export class StorageApiAsync implements StorageAPI {
267
274
  this.storeQueue.push(msg, correctionCallback);
268
275
 
269
276
  this.storeQueue.processQueue(async (data, correctionCallback) => {
277
+ this.interruptEraser("store");
270
278
  return this.storeSingle(data, correctionCallback);
271
279
  });
272
280
  }
273
281
 
282
+ private interruptEraser(reason: string) {
283
+ // Cooperative cancellation: a DB transaction already in progress will complete,
284
+ // but the eraser loop will stop starting further work at its next abort check.
285
+ if (this.eraserController) {
286
+ this.eraserController.abort(reason);
287
+ this.eraserController = undefined;
288
+ }
289
+ }
290
+
291
+ async eraseAllDeletedCoValues() {
292
+ const ids = await this.dbClient.getAllCoValuesWaitingForDelete();
293
+
294
+ this.eraserController = new AbortController();
295
+ const signal = this.eraserController.signal;
296
+
297
+ for (const id of ids) {
298
+ if (signal.aborted) {
299
+ return;
300
+ }
301
+
302
+ await this.dbClient.eraseCoValueButKeepTombstone(id);
303
+ }
304
+ }
305
+
274
306
  /**
275
307
  * This function is called when the storage lacks the information required to store the incoming content.
276
308
  *
@@ -313,6 +345,7 @@ export class StorageApiAsync implements StorageAPI {
313
345
  msg: NewContentMessage,
314
346
  correctionCallback: CorrectionCallback,
315
347
  ): Promise<boolean> {
348
+ this.interruptEraser("store");
316
349
  if (this.storeQueue.closed) {
317
350
  return false;
318
351
  }
@@ -342,6 +375,10 @@ export class StorageApiAsync implements StorageAPI {
342
375
  sessionID,
343
376
  );
344
377
 
378
+ if (this.deletedValues.has(id) && isDeleteSessionID(sessionID)) {
379
+ await tx.markCoValueAsDeleted(id);
380
+ }
381
+
345
382
  if (sessionRow) {
346
383
  setSessionCounter(
347
384
  knownState.sessions,
@@ -445,6 +482,30 @@ export class StorageApiAsync implements StorageAPI {
445
482
  return newLastIdx;
446
483
  }
447
484
 
485
+ deletedValues = new Set<RawCoID>();
486
+
487
+ markDeleteAsValid(id: RawCoID) {
488
+ this.deletedValues.add(id);
489
+
490
+ if (this.deletedCoValuesEraserScheduler) {
491
+ this.deletedCoValuesEraserScheduler.onEnqueueDeletedCoValue();
492
+ }
493
+ }
494
+
495
+ enableDeletedCoValuesErasure() {
496
+ if (this.deletedCoValuesEraserScheduler) return;
497
+
498
+ this.deletedCoValuesEraserScheduler = new DeletedCoValuesEraserScheduler({
499
+ run: async () => {
500
+ // Async storage: no max-time budgeting; drain to completion when scheduled.
501
+ await this.eraseAllDeletedCoValues();
502
+ const remaining = await this.dbClient.getAllCoValuesWaitingForDelete();
503
+ return { hasMore: remaining.length > 0 };
504
+ },
505
+ });
506
+ this.deletedCoValuesEraserScheduler.scheduleStartupDrain();
507
+ }
508
+
448
509
  waitForSync(id: string, coValue: CoValueCore) {
449
510
  return this.knownStates.waitForSync(id, coValue);
450
511
  }
@@ -471,6 +532,7 @@ export class StorageApiAsync implements StorageAPI {
471
532
  }
472
533
 
473
534
  close() {
535
+ this.deletedCoValuesEraserScheduler?.dispose();
474
536
  this.inMemoryCoValues.clear();
475
537
  return this.storeQueue.close();
476
538
  }
@@ -16,6 +16,7 @@ import {
16
16
  emptyKnownState,
17
17
  setSessionCounter,
18
18
  } from "../knownState.js";
19
+ import { isDeleteSessionID } from "../ids.js";
19
20
  import {
20
21
  collectNewTxs,
21
22
  getDependedOnCoValues,
@@ -29,14 +30,21 @@ import type {
29
30
  StoredCoValueRow,
30
31
  StoredSessionRow,
31
32
  } from "./types.js";
33
+ import { DeletedCoValuesEraserScheduler } from "./DeletedCoValuesEraserScheduler.js";
32
34
  import {
33
35
  ContentCallback,
34
36
  StorageStreamingQueue,
35
37
  } from "../queue/StorageStreamingQueue.js";
36
38
  import { getPriorityFromHeader } from "../priority.js";
37
39
 
40
+ const MAX_DELETE_SCHEDULE_DURATION_MS = 100;
41
+
38
42
  export class StorageApiSync implements StorageAPI {
39
43
  private readonly dbClient: DBClientInterfaceSync;
44
+
45
+ private deletedCoValuesEraserScheduler:
46
+ | DeletedCoValuesEraserScheduler
47
+ | undefined;
40
48
  /**
41
49
  * Keeps track of CoValues that are in memory, to avoid reloading them from storage
42
50
  * when it isn't necessary
@@ -329,6 +337,10 @@ export class StorageApiSync implements StorageAPI {
329
337
 
330
338
  for (const sessionID of Object.keys(msg.new) as SessionID[]) {
331
339
  this.dbClient.transaction((tx) => {
340
+ if (this.deletedValues.has(id) && isDeleteSessionID(sessionID)) {
341
+ tx.markCoValueAsDeleted(id);
342
+ }
343
+
332
344
  const sessionRow = tx.getSingleCoValueSession(
333
345
  storedCoValueRowID,
334
346
  sessionID,
@@ -433,6 +445,51 @@ export class StorageApiSync implements StorageAPI {
433
445
  return newLastIdx;
434
446
  }
435
447
 
448
+ deletedValues = new Set<RawCoID>();
449
+
450
+ markDeleteAsValid(id: RawCoID) {
451
+ this.deletedValues.add(id);
452
+
453
+ if (this.deletedCoValuesEraserScheduler) {
454
+ this.deletedCoValuesEraserScheduler.onEnqueueDeletedCoValue();
455
+ }
456
+ }
457
+
458
+ async eraseAllDeletedCoValues(): Promise<void> {
459
+ const ids = this.dbClient.getAllCoValuesWaitingForDelete();
460
+
461
+ for (const id of ids) {
462
+ this.dbClient.eraseCoValueButKeepTombstone(id);
463
+ }
464
+ }
465
+
466
+ enableDeletedCoValuesErasure() {
467
+ if (this.deletedCoValuesEraserScheduler) return;
468
+ this.deletedCoValuesEraserScheduler = new DeletedCoValuesEraserScheduler({
469
+ run: async () =>
470
+ this.eraseDeletedCoValuesOnceBudgeted(MAX_DELETE_SCHEDULE_DURATION_MS),
471
+ });
472
+ this.deletedCoValuesEraserScheduler.scheduleStartupDrain();
473
+ }
474
+
475
+ private eraseDeletedCoValuesOnceBudgeted(budgetMs?: number) {
476
+ const startedAt = Date.now();
477
+ const ids = this.dbClient.getAllCoValuesWaitingForDelete();
478
+
479
+ for (const id of ids) {
480
+ // Strict time budget for sync storage to avoid blocking.
481
+ if (budgetMs && Date.now() - startedAt >= budgetMs) {
482
+ break;
483
+ }
484
+
485
+ this.dbClient.eraseCoValueButKeepTombstone(id);
486
+ }
487
+
488
+ return {
489
+ hasMore: this.dbClient.getAllCoValuesWaitingForDelete().length > 0,
490
+ };
491
+ }
492
+
436
493
  waitForSync(id: string, coValue: CoValueCore) {
437
494
  return this.knownStates.waitForSync(id, coValue);
438
495
  }
@@ -461,6 +518,7 @@ export class StorageApiSync implements StorageAPI {
461
518
  }
462
519
 
463
520
  close() {
521
+ this.deletedCoValuesEraserScheduler?.dispose();
464
522
  this.inMemoryCoValues.clear();
465
523
  return undefined;
466
524
  }
@@ -13,12 +13,43 @@ export type CorrectionCallback = (
13
13
  correction: CoValueKnownState,
14
14
  ) => NewContentMessage[] | undefined;
15
15
 
16
+ /**
17
+ * Deletion work queue status for `deletedCoValues` (SQLite).
18
+ *
19
+ * Stored as an INTEGER in SQLite:
20
+ * - 0 = pending
21
+ * - 1 = done
22
+ */
23
+ export enum DeletedCoValueDeletionStatus {
24
+ Pending = 0,
25
+ Done = 1,
26
+ }
27
+
16
28
  /**
17
29
  * The StorageAPI is the interface that the StorageSync and StorageAsync classes implement.
18
30
  *
19
31
  * It uses callbacks instead of promises to have no overhead when using the StorageSync and less overhead when using the StorageAsync.
20
32
  */
21
33
  export interface StorageAPI {
34
+ /**
35
+ * Flags that the coValue delete is valid.
36
+ *
37
+ * When the delete tx is stored, the storage will mark the coValue as deleted.
38
+ */
39
+ markDeleteAsValid(id: RawCoID): void;
40
+
41
+ /**
42
+ * Enable the background erasure scheduler that drains the `deletedCoValues` work queue.
43
+ * This is intentionally opt-in and should be activated by `LocalNode`.
44
+ */
45
+ enableDeletedCoValuesErasure(): void;
46
+
47
+ /**
48
+ * Batch physical deletion for coValues queued in `deletedCoValues` with status `Pending`.
49
+ * Must preserve tombstones (header + delete session(s) + their tx/signatures).
50
+ */
51
+ eraseAllDeletedCoValues(): Promise<void>;
52
+
22
53
  load(
23
54
  id: string,
24
55
  // This callback is fired when data is found, might be called multiple times if the content requires streaming (e.g when loading files)
@@ -111,6 +142,13 @@ export interface DBTransactionInterfaceAsync {
111
142
  sessionID: SessionID,
112
143
  ): Promise<StoredSessionRow | undefined>;
113
144
 
145
+ /**
146
+ * Persist a "deleted coValue" marker in storage (work queue entry).
147
+ * This is an enqueue signal: implementations should set status to `Pending`.
148
+ * This is expected to be idempotent (safe to call repeatedly).
149
+ */
150
+ markCoValueAsDeleted(id: RawCoID): Promise<unknown>;
151
+
114
152
  addSessionUpdate({
115
153
  sessionUpdate,
116
154
  sessionRow,
@@ -146,6 +184,11 @@ export interface DBClientInterfaceAsync {
146
184
  header?: CoValueHeader,
147
185
  ): Promise<number | undefined>;
148
186
 
187
+ /**
188
+ * Enumerate all coValue IDs currently pending in the "deleted coValues" work queue.
189
+ */
190
+ getAllCoValuesWaitingForDelete(): Promise<RawCoID[]>;
191
+
149
192
  getCoValueSessions(coValueRowId: number): Promise<StoredSessionRow[]>;
150
193
 
151
194
  getNewTransactionInSession(
@@ -171,6 +214,13 @@ export interface DBClientInterfaceAsync {
171
214
 
172
215
  stopTrackingSyncState(id: RawCoID): Promise<void>;
173
216
 
217
+ /**
218
+ * Physical deletion primitive: erase all persisted history for a deleted coValue,
219
+ * while preserving the tombstone (header + delete session(s)).
220
+ * Must run inside a single storage transaction.
221
+ */
222
+ eraseCoValueButKeepTombstone(coValueID: RawCoID): Promise<unknown>;
223
+
174
224
  /**
175
225
  * Get the knownState for a CoValue without loading transactions.
176
226
  * Returns undefined if the CoValue doesn't exist.
@@ -186,6 +236,13 @@ export interface DBTransactionInterfaceSync {
186
236
  sessionID: SessionID,
187
237
  ): StoredSessionRow | undefined;
188
238
 
239
+ /**
240
+ * Persist a "deleted coValue" marker in storage (work queue entry).
241
+ * This is an enqueue signal: implementations should set status to `"pending"`.
242
+ * This is expected to be idempotent (safe to call repeatedly).
243
+ */
244
+ markCoValueAsDeleted(id: RawCoID): unknown;
245
+
189
246
  addSessionUpdate({
190
247
  sessionUpdate,
191
248
  sessionRow,
@@ -216,6 +273,11 @@ export interface DBClientInterfaceSync {
216
273
 
217
274
  upsertCoValue(id: string, header?: CoValueHeader): number | undefined;
218
275
 
276
+ /**
277
+ * Enumerate all coValue IDs currently pending in the "deleted coValues" work queue.
278
+ */
279
+ getAllCoValuesWaitingForDelete(): RawCoID[];
280
+
219
281
  getCoValueSessions(coValueRowId: number): StoredSessionRow[];
220
282
 
221
283
  getNewTransactionInSession(
@@ -239,6 +301,13 @@ export interface DBClientInterfaceSync {
239
301
 
240
302
  stopTrackingSyncState(id: RawCoID): void;
241
303
 
304
+ /**
305
+ * Physical deletion primitive: erase all persisted history for a deleted coValue,
306
+ * while preserving the tombstone (header + delete session(s)).
307
+ * Must run inside a single storage transaction.
308
+ */
309
+ eraseCoValueButKeepTombstone(coValueID: RawCoID): unknown;
310
+
242
311
  /**
243
312
  * Get the knownState for a CoValue without loading transactions.
244
313
  * Returns undefined if the CoValue doesn't exist.
package/src/sync.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  import { CoValueCore } from "./coValueCore/coValueCore.js";
15
15
  import { CoValueHeader, Transaction } from "./coValueCore/verifiedState.js";
16
16
  import { Signature } from "./crypto/crypto.js";
17
- import { RawCoID, SessionID, isRawCoID } from "./ids.js";
17
+ import { isDeleteSessionID, RawCoID, SessionID, isRawCoID } from "./ids.js";
18
18
  import { LocalNode } from "./localNode.js";
19
19
  import { logger } from "./logger.js";
20
20
  import { CoValuePriority } from "./priority.js";
@@ -213,7 +213,6 @@ export class SyncManager {
213
213
  return;
214
214
  }
215
215
 
216
- // TODO: validate
217
216
  switch (msg.action) {
218
217
  case "load":
219
218
  return this.handleLoad(msg, peer);
@@ -265,10 +264,18 @@ export class SyncManager {
265
264
 
266
265
  peer.combineOptimisticWith(id, coValue.knownState());
267
266
  } else if (!peer.toldKnownState.has(id)) {
268
- this.trySendToPeer(peer, {
269
- action: "known",
270
- ...coValue.knownStateWithStreaming(),
271
- });
267
+ if (coValue.isDeleted) {
268
+ // This way we make the peer believe that we've always ingested all the content they sent, even though we skipped it because the coValue is deleted
269
+ this.trySendToPeer(
270
+ peer,
271
+ coValue.stopSyncingKnownStateMessage(peer.getKnownState(id)),
272
+ );
273
+ } else {
274
+ this.trySendToPeer(peer, {
275
+ action: "known",
276
+ ...coValue.knownStateWithStreaming(),
277
+ });
278
+ }
272
279
  }
273
280
 
274
281
  peer.trackToldKnownState(id);
@@ -398,13 +405,7 @@ export class SyncManager {
398
405
  // If the coValue is unavailable and we never tried this peer
399
406
  // we try to load it from the peer
400
407
  if (!peer.loadRequestSent.has(coValue.id)) {
401
- peer.trackLoadRequestSent(coValue.id);
402
- this.trySendToPeer(peer, {
403
- action: "load",
404
- header: false,
405
- id: coValue.id,
406
- sessions: {},
407
- });
408
+ peer.sendLoadRequest(coValue, "low-priority");
408
409
  }
409
410
  } else {
410
411
  // Build the list of coValues ordered by dependency
@@ -424,12 +425,11 @@ export class SyncManager {
424
425
  * - Subscribe to the coValue updates
425
426
  * - Start the sync process in case we or the other peer
426
427
  * lacks some transactions
428
+ *
429
+ * Use low priority for reconciliation loads so that user-initiated
430
+ * loads take precedence.
427
431
  */
428
- peer.trackLoadRequestSent(coValue.id);
429
- this.trySendToPeer(peer, {
430
- action: "load",
431
- ...coValue.knownState(),
432
- });
432
+ peer.sendLoadRequest(coValue, "low-priority");
433
433
  }
434
434
  }
435
435
 
@@ -508,7 +508,7 @@ export class SyncManager {
508
508
  currentTimer - lastTimer >
509
509
  SYNC_SCHEDULER_CONFIG.INCOMING_MESSAGES_TIME_BUDGET
510
510
  ) {
511
- await new Promise<void>((resolve) => setTimeout(resolve));
511
+ await waitForNextTick();
512
512
  lastTimer = performance.now();
513
513
  }
514
514
  }
@@ -726,6 +726,8 @@ export class SyncManager {
726
726
  if (coValue.isAvailable()) {
727
727
  this.sendNewContent(msg.id, peer);
728
728
  }
729
+
730
+ peer.trackLoadRequestComplete(coValue);
729
731
  }
730
732
 
731
733
  recordTransactionsSize(newTransactions: Transaction[], source: string) {
@@ -744,6 +746,7 @@ export class SyncManager {
744
746
  ) {
745
747
  const coValue = this.local.getCoValue(msg.id);
746
748
  const peer = from === "storage" || from === "import" ? undefined : from;
749
+
747
750
  const sourceRole =
748
751
  from === "storage"
749
752
  ? "storage"
@@ -760,6 +763,7 @@ export class SyncManager {
760
763
  };
761
764
  }
762
765
 
766
+ peer?.trackLoadRequestUpdate(coValue);
763
767
  coValue.addDependenciesFromContentMessage(msg);
764
768
 
765
769
  // If some of the dependencies are missing, we wait for them to be available
@@ -779,7 +783,12 @@ export class SyncManager {
779
783
  peers.push(peer);
780
784
  }
781
785
 
782
- dependencyCoValue.load(peers);
786
+ // Use immediate mode to bypass the concurrency limit for dependencies
787
+ // We do this to avoid that the dependency load is blocked
788
+ // by the pending dependendant load
789
+ // Also these should be done with the highest priority, because we need to
790
+ // unblock the coValue wait
791
+ dependencyCoValue.load(peers, "immediate");
783
792
  }
784
793
  }
785
794
 
@@ -875,6 +884,8 @@ export class SyncManager {
875
884
  new: {},
876
885
  };
877
886
 
887
+ let wasAlreadyDeleted = coValue.isDeleted;
888
+
878
889
  /**
879
890
  * The coValue is in memory, load the transactions from the content message
880
891
  */
@@ -882,6 +893,10 @@ export class SyncManager {
882
893
  sessionID,
883
894
  newContentForSession,
884
895
  ] of getSessionEntriesFromContentMessage(msg)) {
896
+ if (wasAlreadyDeleted && !isDeleteSessionID(sessionID)) {
897
+ continue;
898
+ }
899
+
885
900
  const newTransactions = getNewTransactionsFromContentMessage(
886
901
  newContentForSession,
887
902
  coValue.knownState(),
@@ -936,12 +951,25 @@ export class SyncManager {
936
951
  this.recordTransactionsSize(newTransactions, sourceRole);
937
952
  }
938
953
 
954
+ // We reset the new content for the deleted coValue
955
+ // because we want to store only the delete session/transaction
956
+ if (!wasAlreadyDeleted && coValue.isDeleted) {
957
+ wasAlreadyDeleted = true;
958
+ validNewContent.new = {};
959
+ }
960
+
939
961
  // The new content for this session has been verified, so we can store it
940
962
  validNewContent.new[sessionID] = newContentForSession;
941
963
  }
942
964
 
943
965
  if (peer) {
944
- peer.combineWith(msg.id, knownStateFromContent(validNewContent));
966
+ if (coValue.isDeleted) {
967
+ // In case of deleted coValues, we combine the known state with the content message
968
+ // to avoid that clients that don't support deleted coValues try to sync their own content indefinitely
969
+ peer.combineWith(msg.id, knownStateFromContent(msg));
970
+ } else {
971
+ peer.combineWith(msg.id, knownStateFromContent(validNewContent));
972
+ }
945
973
  }
946
974
 
947
975
  /**
@@ -973,15 +1001,21 @@ export class SyncManager {
973
1001
  * This way the sender knows that the content has been received and applied
974
1002
  * and can update their peer's knownState accordingly.
975
1003
  */
976
- this.trySendToPeer(peer, {
977
- action: "known",
978
- ...coValue.knownState(),
979
- });
1004
+ if (coValue.isDeleted) {
1005
+ // This way we make the peer believe that we've ingested all the content, even though we skipped it because the coValue is deleted
1006
+ this.trySendToPeer(
1007
+ peer,
1008
+ coValue.stopSyncingKnownStateMessage(peer.getKnownState(msg.id)),
1009
+ );
1010
+ } else {
1011
+ this.trySendToPeer(peer, {
1012
+ action: "known",
1013
+ ...coValue.knownState(),
1014
+ });
1015
+ }
980
1016
  peer.trackToldKnownState(msg.id);
981
1017
  }
982
1018
 
983
- const syncedPeers = [];
984
-
985
1019
  /**
986
1020
  * Store the content and propagate it to the server peers and the subscribed client peers
987
1021
  */
@@ -995,6 +1029,8 @@ export class SyncManager {
995
1029
  }
996
1030
  }
997
1031
 
1032
+ peer?.trackLoadRequestComplete(coValue);
1033
+
998
1034
  for (const peer of this.getPeers(coValue.id)) {
999
1035
  /**
1000
1036
  * We sync the content against the source peer if it is a client or server peers
@@ -1008,25 +1044,8 @@ export class SyncManager {
1008
1044
  // We directly forward the new content to peers that have an active subscription
1009
1045
  if (peer.isCoValueSubscribedToPeer(coValue.id)) {
1010
1046
  this.sendNewContent(coValue.id, peer);
1011
- syncedPeers.push(peer);
1012
- } else if (
1013
- peer.role === "server" &&
1014
- !peer.loadRequestSent.has(coValue.id)
1015
- ) {
1016
- const state = coValue.getLoadingStateForPeer(peer.id);
1017
-
1018
- // Check if there is a inflight load operation and we
1019
- // are waiting for other peers to send the load request
1020
- if (state === "unknown") {
1021
- // Sending a load message to the peer to get to know how much content is missing
1022
- // before sending the new content
1023
- this.trySendToPeer(peer, {
1024
- action: "load",
1025
- ...coValue.knownStateWithStreaming(),
1026
- });
1027
- peer.trackLoadRequestSent(coValue.id);
1028
- syncedPeers.push(peer);
1029
- }
1047
+ } else if (peer.role === "server") {
1048
+ peer.sendLoadRequest(coValue);
1030
1049
  }
1031
1050
  }
1032
1051
  }
@@ -1124,6 +1143,12 @@ export class SyncManager {
1124
1143
 
1125
1144
  const value = this.local.getCoValue(content.id);
1126
1145
 
1146
+ if (value.isDeleted) {
1147
+ // This doesn't persist the delete flag, it only signals the storage
1148
+ // API that the delete transaction is valid
1149
+ storage.markDeleteAsValid(value.id);
1150
+ }
1151
+
1127
1152
  // Try to store the content as-is for performance
1128
1153
  // In case that some transactions are missing, a correction will be requested, but it's an edge case
1129
1154
  storage.store(content, (correction) => {
@@ -1304,3 +1329,10 @@ export function hwrServerPeerSelector(n: number): ServerPeerSelector {
1304
1329
  .map((wp) => wp.peer);
1305
1330
  };
1306
1331
  }
1332
+
1333
+ let waitForNextTick = () =>
1334
+ new Promise<void>((resolve) => queueMicrotask(resolve));
1335
+
1336
+ if (typeof setImmediate === "function") {
1337
+ waitForNextTick = () => new Promise<void>((resolve) => setImmediate(resolve));
1338
+ }