cojson 0.19.21 → 0.20.0

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 (254) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +67 -0
  3. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts +42 -0
  4. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts.map +1 -0
  5. package/dist/CojsonMessageChannel/CojsonMessageChannel.js +261 -0
  6. package/dist/CojsonMessageChannel/CojsonMessageChannel.js.map +1 -0
  7. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts +18 -0
  8. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts.map +1 -0
  9. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js +37 -0
  10. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js.map +1 -0
  11. package/dist/CojsonMessageChannel/index.d.ts +3 -0
  12. package/dist/CojsonMessageChannel/index.d.ts.map +1 -0
  13. package/dist/CojsonMessageChannel/index.js +2 -0
  14. package/dist/CojsonMessageChannel/index.js.map +1 -0
  15. package/dist/CojsonMessageChannel/types.d.ts +149 -0
  16. package/dist/CojsonMessageChannel/types.d.ts.map +1 -0
  17. package/dist/CojsonMessageChannel/types.js +36 -0
  18. package/dist/CojsonMessageChannel/types.js.map +1 -0
  19. package/dist/GarbageCollector.d.ts +4 -2
  20. package/dist/GarbageCollector.d.ts.map +1 -1
  21. package/dist/GarbageCollector.js +5 -3
  22. package/dist/GarbageCollector.js.map +1 -1
  23. package/dist/SyncStateManager.d.ts +3 -3
  24. package/dist/SyncStateManager.d.ts.map +1 -1
  25. package/dist/SyncStateManager.js +4 -4
  26. package/dist/SyncStateManager.js.map +1 -1
  27. package/dist/coValueContentMessage.d.ts +0 -2
  28. package/dist/coValueContentMessage.d.ts.map +1 -1
  29. package/dist/coValueContentMessage.js +0 -8
  30. package/dist/coValueContentMessage.js.map +1 -1
  31. package/dist/coValueCore/SessionMap.d.ts +4 -2
  32. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  33. package/dist/coValueCore/SessionMap.js +30 -0
  34. package/dist/coValueCore/SessionMap.js.map +1 -1
  35. package/dist/coValueCore/coValueCore.d.ts +86 -4
  36. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  37. package/dist/coValueCore/coValueCore.js +318 -17
  38. package/dist/coValueCore/coValueCore.js.map +1 -1
  39. package/dist/coValueCore/verifiedState.d.ts +6 -1
  40. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  41. package/dist/coValueCore/verifiedState.js +9 -0
  42. package/dist/coValueCore/verifiedState.js.map +1 -1
  43. package/dist/coValues/coList.d.ts +3 -2
  44. package/dist/coValues/coList.d.ts.map +1 -1
  45. package/dist/coValues/coList.js.map +1 -1
  46. package/dist/coValues/group.d.ts.map +1 -1
  47. package/dist/coValues/group.js +3 -6
  48. package/dist/coValues/group.js.map +1 -1
  49. package/dist/config.d.ts +0 -6
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +0 -8
  52. package/dist/config.js.map +1 -1
  53. package/dist/crypto/NapiCrypto.d.ts +1 -2
  54. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  55. package/dist/crypto/NapiCrypto.js +19 -4
  56. package/dist/crypto/NapiCrypto.js.map +1 -1
  57. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  58. package/dist/crypto/RNCrypto.js +19 -4
  59. package/dist/crypto/RNCrypto.js.map +1 -1
  60. package/dist/crypto/WasmCrypto.d.ts +11 -4
  61. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  62. package/dist/crypto/WasmCrypto.js +52 -10
  63. package/dist/crypto/WasmCrypto.js.map +1 -1
  64. package/dist/crypto/WasmCryptoEdge.d.ts +1 -0
  65. package/dist/crypto/WasmCryptoEdge.d.ts.map +1 -1
  66. package/dist/crypto/WasmCryptoEdge.js +4 -1
  67. package/dist/crypto/WasmCryptoEdge.js.map +1 -1
  68. package/dist/crypto/crypto.d.ts +3 -3
  69. package/dist/crypto/crypto.d.ts.map +1 -1
  70. package/dist/crypto/crypto.js +6 -1
  71. package/dist/crypto/crypto.js.map +1 -1
  72. package/dist/exports.d.ts +3 -2
  73. package/dist/exports.d.ts.map +1 -1
  74. package/dist/exports.js +3 -1
  75. package/dist/exports.js.map +1 -1
  76. package/dist/ids.d.ts +4 -1
  77. package/dist/ids.d.ts.map +1 -1
  78. package/dist/ids.js +4 -0
  79. package/dist/ids.js.map +1 -1
  80. package/dist/knownState.d.ts +2 -0
  81. package/dist/knownState.d.ts.map +1 -1
  82. package/dist/localNode.d.ts +13 -3
  83. package/dist/localNode.d.ts.map +1 -1
  84. package/dist/localNode.js +17 -2
  85. package/dist/localNode.js.map +1 -1
  86. package/dist/platformUtils.d.ts +3 -0
  87. package/dist/platformUtils.d.ts.map +1 -0
  88. package/dist/platformUtils.js +24 -0
  89. package/dist/platformUtils.js.map +1 -0
  90. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts +30 -0
  91. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts.map +1 -0
  92. package/dist/storage/DeletedCoValuesEraserScheduler.js +84 -0
  93. package/dist/storage/DeletedCoValuesEraserScheduler.js.map +1 -0
  94. package/dist/storage/sqlite/client.d.ts +3 -0
  95. package/dist/storage/sqlite/client.d.ts.map +1 -1
  96. package/dist/storage/sqlite/client.js +44 -0
  97. package/dist/storage/sqlite/client.js.map +1 -1
  98. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  99. package/dist/storage/sqlite/sqliteMigrations.js +7 -0
  100. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  101. package/dist/storage/sqliteAsync/client.d.ts +3 -0
  102. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  103. package/dist/storage/sqliteAsync/client.js +42 -0
  104. package/dist/storage/sqliteAsync/client.js.map +1 -1
  105. package/dist/storage/storageAsync.d.ts +15 -3
  106. package/dist/storage/storageAsync.d.ts.map +1 -1
  107. package/dist/storage/storageAsync.js +60 -3
  108. package/dist/storage/storageAsync.js.map +1 -1
  109. package/dist/storage/storageSync.d.ts +14 -3
  110. package/dist/storage/storageSync.d.ts.map +1 -1
  111. package/dist/storage/storageSync.js +54 -3
  112. package/dist/storage/storageSync.js.map +1 -1
  113. package/dist/storage/types.d.ts +64 -0
  114. package/dist/storage/types.d.ts.map +1 -1
  115. package/dist/storage/types.js +12 -1
  116. package/dist/storage/types.js.map +1 -1
  117. package/dist/sync.d.ts +6 -0
  118. package/dist/sync.d.ts.map +1 -1
  119. package/dist/sync.js +69 -15
  120. package/dist/sync.js.map +1 -1
  121. package/dist/tests/CojsonMessageChannel.test.d.ts +2 -0
  122. package/dist/tests/CojsonMessageChannel.test.d.ts.map +1 -0
  123. package/dist/tests/CojsonMessageChannel.test.js +236 -0
  124. package/dist/tests/CojsonMessageChannel.test.js.map +1 -0
  125. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts +2 -0
  126. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts.map +1 -0
  127. package/dist/tests/DeletedCoValuesEraserScheduler.test.js +149 -0
  128. package/dist/tests/DeletedCoValuesEraserScheduler.test.js.map +1 -0
  129. package/dist/tests/GarbageCollector.test.js +91 -18
  130. package/dist/tests/GarbageCollector.test.js.map +1 -1
  131. package/dist/tests/StorageApiAsync.test.js +510 -146
  132. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  133. package/dist/tests/StorageApiSync.test.js +531 -130
  134. package/dist/tests/StorageApiSync.test.js.map +1 -1
  135. package/dist/tests/SyncManager.processQueues.test.js +1 -1
  136. package/dist/tests/SyncManager.processQueues.test.js.map +1 -1
  137. package/dist/tests/SyncStateManager.test.js +1 -1
  138. package/dist/tests/SyncStateManager.test.js.map +1 -1
  139. package/dist/tests/WasmCrypto.test.js +6 -3
  140. package/dist/tests/WasmCrypto.test.js.map +1 -1
  141. package/dist/tests/coPlainText.test.js +1 -1
  142. package/dist/tests/coPlainText.test.js.map +1 -1
  143. package/dist/tests/coValueCore.loadFromStorage.test.js +4 -0
  144. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  145. package/dist/tests/coValueCore.test.js +34 -13
  146. package/dist/tests/coValueCore.test.js.map +1 -1
  147. package/dist/tests/coreWasm.test.js +127 -4
  148. package/dist/tests/coreWasm.test.js.map +1 -1
  149. package/dist/tests/crypto.test.js +89 -93
  150. package/dist/tests/crypto.test.js.map +1 -1
  151. package/dist/tests/deleteCoValue.test.d.ts +2 -0
  152. package/dist/tests/deleteCoValue.test.d.ts.map +1 -0
  153. package/dist/tests/deleteCoValue.test.js +313 -0
  154. package/dist/tests/deleteCoValue.test.js.map +1 -0
  155. package/dist/tests/group.removeMember.test.js +18 -30
  156. package/dist/tests/group.removeMember.test.js.map +1 -1
  157. package/dist/tests/knownState.lazyLoading.test.js +4 -0
  158. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  159. package/dist/tests/sync.deleted.test.d.ts +2 -0
  160. package/dist/tests/sync.deleted.test.d.ts.map +1 -0
  161. package/dist/tests/sync.deleted.test.js +214 -0
  162. package/dist/tests/sync.deleted.test.js.map +1 -0
  163. package/dist/tests/sync.garbageCollection.test.js +56 -32
  164. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  165. package/dist/tests/sync.load.test.js +3 -5
  166. package/dist/tests/sync.load.test.js.map +1 -1
  167. package/dist/tests/sync.mesh.test.js +4 -3
  168. package/dist/tests/sync.mesh.test.js.map +1 -1
  169. package/dist/tests/sync.peerReconciliation.test.js +3 -3
  170. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  171. package/dist/tests/sync.storage.test.js +12 -11
  172. package/dist/tests/sync.storage.test.js.map +1 -1
  173. package/dist/tests/sync.storageAsync.test.js +7 -7
  174. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  175. package/dist/tests/sync.test.js +3 -2
  176. package/dist/tests/sync.test.js.map +1 -1
  177. package/dist/tests/sync.tracking.test.js +35 -4
  178. package/dist/tests/sync.tracking.test.js.map +1 -1
  179. package/dist/tests/testStorage.d.ts +3 -0
  180. package/dist/tests/testStorage.d.ts.map +1 -1
  181. package/dist/tests/testStorage.js +16 -2
  182. package/dist/tests/testStorage.js.map +1 -1
  183. package/dist/tests/testUtils.d.ts +29 -4
  184. package/dist/tests/testUtils.d.ts.map +1 -1
  185. package/dist/tests/testUtils.js +84 -9
  186. package/dist/tests/testUtils.js.map +1 -1
  187. package/package.json +6 -16
  188. package/src/CojsonMessageChannel/CojsonMessageChannel.ts +332 -0
  189. package/src/CojsonMessageChannel/MessagePortOutgoingChannel.ts +52 -0
  190. package/src/CojsonMessageChannel/index.ts +9 -0
  191. package/src/CojsonMessageChannel/types.ts +200 -0
  192. package/src/GarbageCollector.ts +5 -5
  193. package/src/SyncStateManager.ts +6 -6
  194. package/src/coValueContentMessage.ts +0 -14
  195. package/src/coValueCore/SessionMap.ts +43 -1
  196. package/src/coValueCore/coValueCore.ts +430 -15
  197. package/src/coValueCore/verifiedState.ts +26 -3
  198. package/src/coValues/coList.ts +5 -3
  199. package/src/coValues/group.ts +5 -6
  200. package/src/config.ts +0 -9
  201. package/src/crypto/NapiCrypto.ts +29 -13
  202. package/src/crypto/RNCrypto.ts +29 -11
  203. package/src/crypto/WasmCrypto.ts +67 -20
  204. package/src/crypto/WasmCryptoEdge.ts +5 -1
  205. package/src/crypto/crypto.ts +16 -4
  206. package/src/exports.ts +3 -0
  207. package/src/ids.ts +11 -1
  208. package/src/localNode.ts +18 -5
  209. package/src/platformUtils.ts +26 -0
  210. package/src/storage/DeletedCoValuesEraserScheduler.ts +124 -0
  211. package/src/storage/sqlite/client.ts +77 -0
  212. package/src/storage/sqlite/sqliteMigrations.ts +7 -0
  213. package/src/storage/sqliteAsync/client.ts +75 -0
  214. package/src/storage/storageAsync.ts +77 -4
  215. package/src/storage/storageSync.ts +73 -4
  216. package/src/storage/types.ts +75 -0
  217. package/src/sync.ts +84 -15
  218. package/src/tests/CojsonMessageChannel.test.ts +306 -0
  219. package/src/tests/DeletedCoValuesEraserScheduler.test.ts +185 -0
  220. package/src/tests/GarbageCollector.test.ts +119 -22
  221. package/src/tests/StorageApiAsync.test.ts +615 -156
  222. package/src/tests/StorageApiSync.test.ts +623 -137
  223. package/src/tests/SyncManager.processQueues.test.ts +1 -1
  224. package/src/tests/SyncStateManager.test.ts +1 -1
  225. package/src/tests/WasmCrypto.test.ts +8 -3
  226. package/src/tests/coPlainText.test.ts +1 -1
  227. package/src/tests/coValueCore.loadFromStorage.test.ts +8 -0
  228. package/src/tests/coValueCore.test.ts +49 -14
  229. package/src/tests/coreWasm.test.ts +319 -10
  230. package/src/tests/crypto.test.ts +141 -150
  231. package/src/tests/deleteCoValue.test.ts +528 -0
  232. package/src/tests/group.removeMember.test.ts +35 -35
  233. package/src/tests/knownState.lazyLoading.test.ts +8 -0
  234. package/src/tests/sync.deleted.test.ts +294 -0
  235. package/src/tests/sync.garbageCollection.test.ts +69 -36
  236. package/src/tests/sync.load.test.ts +3 -5
  237. package/src/tests/sync.mesh.test.ts +6 -3
  238. package/src/tests/sync.peerReconciliation.test.ts +3 -3
  239. package/src/tests/sync.storage.test.ts +14 -11
  240. package/src/tests/sync.storageAsync.test.ts +7 -7
  241. package/src/tests/sync.test.ts +5 -2
  242. package/src/tests/sync.tracking.test.ts +54 -4
  243. package/src/tests/testStorage.ts +30 -3
  244. package/src/tests/testUtils.ts +113 -15
  245. package/dist/crypto/PureJSCrypto.d.ts +0 -77
  246. package/dist/crypto/PureJSCrypto.d.ts.map +0 -1
  247. package/dist/crypto/PureJSCrypto.js +0 -236
  248. package/dist/crypto/PureJSCrypto.js.map +0 -1
  249. package/dist/tests/PureJSCrypto.test.d.ts +0 -2
  250. package/dist/tests/PureJSCrypto.test.d.ts.map +0 -1
  251. package/dist/tests/PureJSCrypto.test.js +0 -145
  252. package/dist/tests/PureJSCrypto.test.js.map +0 -1
  253. package/src/crypto/PureJSCrypto.ts +0 -429
  254. package/src/tests/PureJSCrypto.test.ts +0 -217
@@ -16,6 +16,7 @@ import type {
16
16
  StoredSessionRow,
17
17
  TransactionRow,
18
18
  } from "../types.js";
19
+ import { DeletedCoValueDeletionStatus } from "../types.js";
19
20
  import type { SQLiteDatabaseDriver } from "./types.js";
20
21
 
21
22
  export type RawCoValueRow = {
@@ -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
  }
@@ -143,6 +148,78 @@ export class SQLiteClient
143
148
  return result.rowID;
144
149
  }
145
150
 
151
+ markCoValueAsDeleted(id: RawCoID) {
152
+ // Work queue entry. Table only stores the coValueID.
153
+ // Idempotent by design.
154
+ this.db.run(
155
+ `INSERT INTO deletedCoValues (coValueID) VALUES (?) ON CONFLICT(coValueID) DO NOTHING`,
156
+ [id],
157
+ );
158
+ }
159
+
160
+ eraseCoValueButKeepTombstone(coValueId: RawCoID) {
161
+ const coValueRow = this.db.get<{ rowID: number }>(
162
+ "SELECT rowID FROM coValues WHERE id = ?",
163
+ [coValueId],
164
+ );
165
+
166
+ if (!coValueRow) {
167
+ logger.warn(`CoValue ${coValueId} not found, skipping deletion`);
168
+ return;
169
+ }
170
+
171
+ this.transaction(() => {
172
+ this.db.run(
173
+ `DELETE FROM transactions
174
+ WHERE ses IN (
175
+ SELECT rowID FROM sessions
176
+ WHERE coValue = ?
177
+ AND sessionID NOT LIKE '%$'
178
+ )`,
179
+ [coValueRow.rowID],
180
+ );
181
+
182
+ this.db.run(
183
+ `DELETE FROM signatureAfter
184
+ WHERE ses IN (
185
+ SELECT rowID FROM sessions
186
+ WHERE coValue = ?
187
+ AND sessionID NOT LIKE '%$'
188
+ )`,
189
+ [coValueRow.rowID],
190
+ );
191
+
192
+ this.db.run(
193
+ `DELETE FROM sessions
194
+ WHERE coValue = ?
195
+ AND sessionID NOT LIKE '%$'`,
196
+ [coValueRow.rowID],
197
+ );
198
+
199
+ // Mark the delete as done
200
+ this.db.run(
201
+ `INSERT INTO deletedCoValues (coValueID, status) VALUES (?, ?)
202
+ ON CONFLICT(coValueID) DO UPDATE SET status=?`,
203
+ [
204
+ coValueId,
205
+ DeletedCoValueDeletionStatus.Done,
206
+ DeletedCoValueDeletionStatus.Done,
207
+ ],
208
+ );
209
+ });
210
+ }
211
+
212
+ getAllCoValuesWaitingForDelete(): RawCoID[] {
213
+ return this.db
214
+ .query<DeletedCoValueQueueRow>(
215
+ `SELECT coValueID as id
216
+ FROM deletedCoValues
217
+ WHERE status = ?`,
218
+ [DeletedCoValueDeletionStatus.Pending],
219
+ )
220
+ .map((r) => r.id);
221
+ }
222
+
146
223
  addSessionUpdate({ sessionUpdate }: { sessionUpdate: SessionRow }): number {
147
224
  const result = this.db.get<{ rowID: number }>(
148
225
  `INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature, bytesSinceLastSignature) VALUES (?, ?, ?, ?, ?)
@@ -40,6 +40,13 @@ export const migrations: Record<number, string[]> = {
40
40
  );`,
41
41
  "CREATE INDEX IF NOT EXISTS idx_unsynced_covalues_co_value_id ON unsynced_covalues(co_value_id);",
42
42
  ],
43
+ 5: [
44
+ `CREATE TABLE IF NOT EXISTS deletedCoValues (
45
+ coValueID TEXT PRIMARY KEY,
46
+ status INTEGER NOT NULL DEFAULT 0
47
+ ) WITHOUT ROWID;`,
48
+ "CREATE INDEX IF NOT EXISTS deletedCoValuesByStatus ON deletedCoValues (status);",
49
+ ],
43
50
  };
44
51
 
45
52
  type Migration = {
@@ -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,11 +31,20 @@ 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
 
37
- private loadedCoValues = new Set<RawCoID>();
39
+ private deletedCoValuesEraserScheduler:
40
+ | DeletedCoValuesEraserScheduler
41
+ | undefined;
42
+ private eraserController: AbortController | undefined;
43
+ /**
44
+ * Keeps track of CoValues that are in memory, to avoid reloading them from storage
45
+ * when it isn't necessary
46
+ */
47
+ private inMemoryCoValues = new Set<RawCoID>();
38
48
 
39
49
  // Track pending loads to deduplicate concurrent requests
40
50
  private pendingKnownStateLoads = new Map<
@@ -110,6 +120,7 @@ export class StorageApiAsync implements StorageAPI {
110
120
  callback: (data: NewContentMessage) => void,
111
121
  done: (found: boolean) => void,
112
122
  ) {
123
+ this.interruptEraser("load");
113
124
  const coValueRow = await this.dbClient.getCoValue(id);
114
125
 
115
126
  if (!coValueRow) {
@@ -153,7 +164,7 @@ export class StorageApiAsync implements StorageAPI {
153
164
  );
154
165
  }
155
166
 
156
- this.loadedCoValues.add(coValueRow.id);
167
+ this.inMemoryCoValues.add(coValueRow.id);
157
168
 
158
169
  let contentMessage = createContentMessage(coValueRow.id, coValueRow.header);
159
170
 
@@ -224,7 +235,7 @@ export class StorageApiAsync implements StorageAPI {
224
235
  done?.(true);
225
236
  }
226
237
 
227
- async pushContentWithDependencies(
238
+ private async pushContentWithDependencies(
228
239
  coValueRow: StoredCoValueRow,
229
240
  contentMessage: NewContentMessage,
230
241
  pushCallback: (data: NewContentMessage) => void,
@@ -237,7 +248,7 @@ export class StorageApiAsync implements StorageAPI {
237
248
  const promises = [];
238
249
 
239
250
  for (const dependedOnCoValue of dependedOnCoValuesList) {
240
- if (this.loadedCoValues.has(dependedOnCoValue)) {
251
+ if (this.inMemoryCoValues.has(dependedOnCoValue)) {
241
252
  continue;
242
253
  }
243
254
 
@@ -263,10 +274,35 @@ export class StorageApiAsync implements StorageAPI {
263
274
  this.storeQueue.push(msg, correctionCallback);
264
275
 
265
276
  this.storeQueue.processQueue(async (data, correctionCallback) => {
277
+ this.interruptEraser("store");
266
278
  return this.storeSingle(data, correctionCallback);
267
279
  });
268
280
  }
269
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
+
270
306
  /**
271
307
  * This function is called when the storage lacks the information required to store the incoming content.
272
308
  *
@@ -309,6 +345,7 @@ export class StorageApiAsync implements StorageAPI {
309
345
  msg: NewContentMessage,
310
346
  correctionCallback: CorrectionCallback,
311
347
  ): Promise<boolean> {
348
+ this.interruptEraser("store");
312
349
  if (this.storeQueue.closed) {
313
350
  return false;
314
351
  }
@@ -338,6 +375,10 @@ export class StorageApiAsync implements StorageAPI {
338
375
  sessionID,
339
376
  );
340
377
 
378
+ if (this.deletedValues.has(id) && isDeleteSessionID(sessionID)) {
379
+ await tx.markCoValueAsDeleted(id);
380
+ }
381
+
341
382
  if (sessionRow) {
342
383
  setSessionCounter(
343
384
  knownState.sessions,
@@ -364,6 +405,8 @@ export class StorageApiAsync implements StorageAPI {
364
405
  });
365
406
  }
366
407
 
408
+ this.inMemoryCoValues.add(id);
409
+
367
410
  this.knownStates.handleUpdate(id, knownState);
368
411
 
369
412
  if (invalidAssumptions) {
@@ -439,6 +482,30 @@ export class StorageApiAsync implements StorageAPI {
439
482
  return newLastIdx;
440
483
  }
441
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
+
442
509
  waitForSync(id: string, coValue: CoValueCore) {
443
510
  return this.knownStates.waitForSync(id, coValue);
444
511
  }
@@ -460,7 +527,13 @@ export class StorageApiAsync implements StorageAPI {
460
527
  this.dbClient.stopTrackingSyncState(id);
461
528
  }
462
529
 
530
+ onCoValueUnmounted(id: RawCoID): void {
531
+ this.inMemoryCoValues.delete(id);
532
+ }
533
+
463
534
  close() {
535
+ this.deletedCoValuesEraserScheduler?.dispose();
536
+ this.inMemoryCoValues.clear();
464
537
  return this.storeQueue.close();
465
538
  }
466
539
  }
@@ -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,15 +30,26 @@ 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;
40
- private loadedCoValues = new Set<RawCoID>();
44
+
45
+ private deletedCoValuesEraserScheduler:
46
+ | DeletedCoValuesEraserScheduler
47
+ | undefined;
48
+ /**
49
+ * Keeps track of CoValues that are in memory, to avoid reloading them from storage
50
+ * when it isn't necessary
51
+ */
52
+ private inMemoryCoValues = new Set<RawCoID>();
41
53
 
42
54
  /**
43
55
  * Queue for streaming content that will be pulled by SyncManager.
@@ -138,7 +150,7 @@ export class StorageApiSync implements StorageAPI {
138
150
  );
139
151
  }
140
152
 
141
- this.loadedCoValues.add(coValueRow.id);
153
+ this.inMemoryCoValues.add(coValueRow.id);
142
154
 
143
155
  const priority = getPriorityFromHeader(coValueRow.header);
144
156
  const contentMessage = createContentMessage(
@@ -244,7 +256,7 @@ export class StorageApiSync implements StorageAPI {
244
256
  });
245
257
  }
246
258
 
247
- async pushContentWithDependencies(
259
+ private async pushContentWithDependencies(
248
260
  coValueRow: StoredCoValueRow,
249
261
  contentMessage: NewContentMessage,
250
262
  pushCallback: (data: NewContentMessage) => void,
@@ -255,7 +267,7 @@ export class StorageApiSync implements StorageAPI {
255
267
  );
256
268
 
257
269
  for (const dependedOnCoValue of dependedOnCoValuesList) {
258
- if (this.loadedCoValues.has(dependedOnCoValue)) {
270
+ if (this.inMemoryCoValues.has(dependedOnCoValue)) {
259
271
  continue;
260
272
  }
261
273
 
@@ -325,6 +337,10 @@ export class StorageApiSync implements StorageAPI {
325
337
 
326
338
  for (const sessionID of Object.keys(msg.new) as SessionID[]) {
327
339
  this.dbClient.transaction((tx) => {
340
+ if (this.deletedValues.has(id) && isDeleteSessionID(sessionID)) {
341
+ tx.markCoValueAsDeleted(id);
342
+ }
343
+
328
344
  const sessionRow = tx.getSingleCoValueSession(
329
345
  storedCoValueRowID,
330
346
  sessionID,
@@ -353,6 +369,8 @@ export class StorageApiSync implements StorageAPI {
353
369
  });
354
370
  }
355
371
 
372
+ this.inMemoryCoValues.add(id);
373
+
356
374
  this.knownStates.handleUpdate(id, knownState);
357
375
 
358
376
  if (invalidAssumptions) {
@@ -427,6 +445,51 @@ export class StorageApiSync implements StorageAPI {
427
445
  return newLastIdx;
428
446
  }
429
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
+
430
493
  waitForSync(id: string, coValue: CoValueCore) {
431
494
  return this.knownStates.waitForSync(id, coValue);
432
495
  }
@@ -450,7 +513,13 @@ export class StorageApiSync implements StorageAPI {
450
513
  this.dbClient.stopTrackingSyncState(id);
451
514
  }
452
515
 
516
+ onCoValueUnmounted(id: RawCoID): void {
517
+ this.inMemoryCoValues.delete(id);
518
+ }
519
+
453
520
  close() {
521
+ this.deletedCoValuesEraserScheduler?.dispose();
522
+ this.inMemoryCoValues.clear();
454
523
  return undefined;
455
524
  }
456
525
  }
@@ -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)
@@ -67,6 +98,12 @@ export interface StorageAPI {
67
98
  callback: (knownState: CoValueKnownState | undefined) => void,
68
99
  ): void;
69
100
 
101
+ /**
102
+ * Called when a CoValue is unmounted from memory.
103
+ * Used to clean up the metadata associated with that CoValue.
104
+ */
105
+ onCoValueUnmounted(id: RawCoID): void;
106
+
70
107
  close(): Promise<unknown> | undefined;
71
108
  }
72
109
 
@@ -105,6 +142,13 @@ export interface DBTransactionInterfaceAsync {
105
142
  sessionID: SessionID,
106
143
  ): Promise<StoredSessionRow | undefined>;
107
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
+
108
152
  addSessionUpdate({
109
153
  sessionUpdate,
110
154
  sessionRow,
@@ -140,6 +184,11 @@ export interface DBClientInterfaceAsync {
140
184
  header?: CoValueHeader,
141
185
  ): Promise<number | undefined>;
142
186
 
187
+ /**
188
+ * Enumerate all coValue IDs currently pending in the "deleted coValues" work queue.
189
+ */
190
+ getAllCoValuesWaitingForDelete(): Promise<RawCoID[]>;
191
+
143
192
  getCoValueSessions(coValueRowId: number): Promise<StoredSessionRow[]>;
144
193
 
145
194
  getNewTransactionInSession(
@@ -165,6 +214,13 @@ export interface DBClientInterfaceAsync {
165
214
 
166
215
  stopTrackingSyncState(id: RawCoID): Promise<void>;
167
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
+
168
224
  /**
169
225
  * Get the knownState for a CoValue without loading transactions.
170
226
  * Returns undefined if the CoValue doesn't exist.
@@ -180,6 +236,13 @@ export interface DBTransactionInterfaceSync {
180
236
  sessionID: SessionID,
181
237
  ): StoredSessionRow | undefined;
182
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
+
183
246
  addSessionUpdate({
184
247
  sessionUpdate,
185
248
  sessionRow,
@@ -210,6 +273,11 @@ export interface DBClientInterfaceSync {
210
273
 
211
274
  upsertCoValue(id: string, header?: CoValueHeader): number | undefined;
212
275
 
276
+ /**
277
+ * Enumerate all coValue IDs currently pending in the "deleted coValues" work queue.
278
+ */
279
+ getAllCoValuesWaitingForDelete(): RawCoID[];
280
+
213
281
  getCoValueSessions(coValueRowId: number): StoredSessionRow[];
214
282
 
215
283
  getNewTransactionInSession(
@@ -233,6 +301,13 @@ export interface DBClientInterfaceSync {
233
301
 
234
302
  stopTrackingSyncState(id: RawCoID): void;
235
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
+
236
311
  /**
237
312
  * Get the knownState for a CoValue without loading transactions.
238
313
  * Returns undefined if the CoValue doesn't exist.