cojson 0.19.22 → 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 (194) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +54 -0
  3. package/dist/coValueContentMessage.d.ts +0 -2
  4. package/dist/coValueContentMessage.d.ts.map +1 -1
  5. package/dist/coValueContentMessage.js +0 -8
  6. package/dist/coValueContentMessage.js.map +1 -1
  7. package/dist/coValueCore/SessionMap.d.ts +4 -2
  8. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  9. package/dist/coValueCore/SessionMap.js +30 -0
  10. package/dist/coValueCore/SessionMap.js.map +1 -1
  11. package/dist/coValueCore/coValueCore.d.ts +67 -3
  12. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  13. package/dist/coValueCore/coValueCore.js +289 -12
  14. package/dist/coValueCore/coValueCore.js.map +1 -1
  15. package/dist/coValueCore/verifiedState.d.ts +6 -1
  16. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  17. package/dist/coValueCore/verifiedState.js +9 -0
  18. package/dist/coValueCore/verifiedState.js.map +1 -1
  19. package/dist/coValues/coList.d.ts +3 -2
  20. package/dist/coValues/coList.d.ts.map +1 -1
  21. package/dist/coValues/coList.js.map +1 -1
  22. package/dist/coValues/group.d.ts.map +1 -1
  23. package/dist/coValues/group.js +3 -6
  24. package/dist/coValues/group.js.map +1 -1
  25. package/dist/config.d.ts +0 -6
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +0 -8
  28. package/dist/config.js.map +1 -1
  29. package/dist/crypto/NapiCrypto.d.ts +1 -2
  30. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  31. package/dist/crypto/NapiCrypto.js +19 -4
  32. package/dist/crypto/NapiCrypto.js.map +1 -1
  33. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  34. package/dist/crypto/RNCrypto.js +19 -4
  35. package/dist/crypto/RNCrypto.js.map +1 -1
  36. package/dist/crypto/WasmCrypto.d.ts +11 -4
  37. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  38. package/dist/crypto/WasmCrypto.js +52 -10
  39. package/dist/crypto/WasmCrypto.js.map +1 -1
  40. package/dist/crypto/WasmCryptoEdge.d.ts +1 -0
  41. package/dist/crypto/WasmCryptoEdge.d.ts.map +1 -1
  42. package/dist/crypto/WasmCryptoEdge.js +4 -1
  43. package/dist/crypto/WasmCryptoEdge.js.map +1 -1
  44. package/dist/crypto/crypto.d.ts +3 -3
  45. package/dist/crypto/crypto.d.ts.map +1 -1
  46. package/dist/crypto/crypto.js +6 -1
  47. package/dist/crypto/crypto.js.map +1 -1
  48. package/dist/exports.d.ts +2 -2
  49. package/dist/exports.d.ts.map +1 -1
  50. package/dist/exports.js +2 -1
  51. package/dist/exports.js.map +1 -1
  52. package/dist/ids.d.ts +4 -1
  53. package/dist/ids.d.ts.map +1 -1
  54. package/dist/ids.js +4 -0
  55. package/dist/ids.js.map +1 -1
  56. package/dist/knownState.d.ts +2 -0
  57. package/dist/knownState.d.ts.map +1 -1
  58. package/dist/localNode.d.ts +12 -0
  59. package/dist/localNode.d.ts.map +1 -1
  60. package/dist/localNode.js +14 -0
  61. package/dist/localNode.js.map +1 -1
  62. package/dist/platformUtils.d.ts +3 -0
  63. package/dist/platformUtils.d.ts.map +1 -0
  64. package/dist/platformUtils.js +24 -0
  65. package/dist/platformUtils.js.map +1 -0
  66. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts +30 -0
  67. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts.map +1 -0
  68. package/dist/storage/DeletedCoValuesEraserScheduler.js +84 -0
  69. package/dist/storage/DeletedCoValuesEraserScheduler.js.map +1 -0
  70. package/dist/storage/sqlite/client.d.ts +3 -0
  71. package/dist/storage/sqlite/client.d.ts.map +1 -1
  72. package/dist/storage/sqlite/client.js +44 -0
  73. package/dist/storage/sqlite/client.js.map +1 -1
  74. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  75. package/dist/storage/sqlite/sqliteMigrations.js +7 -0
  76. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  77. package/dist/storage/sqliteAsync/client.d.ts +3 -0
  78. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  79. package/dist/storage/sqliteAsync/client.js +42 -0
  80. package/dist/storage/sqliteAsync/client.js.map +1 -1
  81. package/dist/storage/storageAsync.d.ts +7 -0
  82. package/dist/storage/storageAsync.d.ts.map +1 -1
  83. package/dist/storage/storageAsync.js +48 -0
  84. package/dist/storage/storageAsync.js.map +1 -1
  85. package/dist/storage/storageSync.d.ts +6 -0
  86. package/dist/storage/storageSync.d.ts.map +1 -1
  87. package/dist/storage/storageSync.js +42 -0
  88. package/dist/storage/storageSync.js.map +1 -1
  89. package/dist/storage/types.d.ts +59 -0
  90. package/dist/storage/types.d.ts.map +1 -1
  91. package/dist/storage/types.js +12 -1
  92. package/dist/storage/types.js.map +1 -1
  93. package/dist/sync.d.ts.map +1 -1
  94. package/dist/sync.js +44 -11
  95. package/dist/sync.js.map +1 -1
  96. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts +2 -0
  97. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts.map +1 -0
  98. package/dist/tests/DeletedCoValuesEraserScheduler.test.js +149 -0
  99. package/dist/tests/DeletedCoValuesEraserScheduler.test.js.map +1 -0
  100. package/dist/tests/GarbageCollector.test.js +5 -6
  101. package/dist/tests/GarbageCollector.test.js.map +1 -1
  102. package/dist/tests/StorageApiAsync.test.js +484 -152
  103. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  104. package/dist/tests/StorageApiSync.test.js +505 -136
  105. package/dist/tests/StorageApiSync.test.js.map +1 -1
  106. package/dist/tests/WasmCrypto.test.js +6 -3
  107. package/dist/tests/WasmCrypto.test.js.map +1 -1
  108. package/dist/tests/coValueCore.loadFromStorage.test.js +3 -0
  109. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  110. package/dist/tests/coValueCore.test.js +34 -13
  111. package/dist/tests/coValueCore.test.js.map +1 -1
  112. package/dist/tests/coreWasm.test.js +127 -4
  113. package/dist/tests/coreWasm.test.js.map +1 -1
  114. package/dist/tests/crypto.test.js +89 -93
  115. package/dist/tests/crypto.test.js.map +1 -1
  116. package/dist/tests/deleteCoValue.test.d.ts +2 -0
  117. package/dist/tests/deleteCoValue.test.d.ts.map +1 -0
  118. package/dist/tests/deleteCoValue.test.js +313 -0
  119. package/dist/tests/deleteCoValue.test.js.map +1 -0
  120. package/dist/tests/group.removeMember.test.js +18 -30
  121. package/dist/tests/group.removeMember.test.js.map +1 -1
  122. package/dist/tests/knownState.lazyLoading.test.js +3 -0
  123. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  124. package/dist/tests/sync.deleted.test.d.ts +2 -0
  125. package/dist/tests/sync.deleted.test.d.ts.map +1 -0
  126. package/dist/tests/sync.deleted.test.js +214 -0
  127. package/dist/tests/sync.deleted.test.js.map +1 -0
  128. package/dist/tests/sync.mesh.test.js +3 -2
  129. package/dist/tests/sync.mesh.test.js.map +1 -1
  130. package/dist/tests/sync.storage.test.js +3 -2
  131. package/dist/tests/sync.storage.test.js.map +1 -1
  132. package/dist/tests/sync.test.js +3 -2
  133. package/dist/tests/sync.test.js.map +1 -1
  134. package/dist/tests/testStorage.d.ts +3 -0
  135. package/dist/tests/testStorage.d.ts.map +1 -1
  136. package/dist/tests/testStorage.js +14 -0
  137. package/dist/tests/testStorage.js.map +1 -1
  138. package/dist/tests/testUtils.d.ts +6 -3
  139. package/dist/tests/testUtils.d.ts.map +1 -1
  140. package/dist/tests/testUtils.js +17 -3
  141. package/dist/tests/testUtils.js.map +1 -1
  142. package/package.json +6 -16
  143. package/src/coValueContentMessage.ts +0 -14
  144. package/src/coValueCore/SessionMap.ts +43 -1
  145. package/src/coValueCore/coValueCore.ts +400 -8
  146. package/src/coValueCore/verifiedState.ts +26 -3
  147. package/src/coValues/coList.ts +5 -3
  148. package/src/coValues/group.ts +5 -6
  149. package/src/config.ts +0 -9
  150. package/src/crypto/NapiCrypto.ts +29 -13
  151. package/src/crypto/RNCrypto.ts +29 -11
  152. package/src/crypto/WasmCrypto.ts +67 -20
  153. package/src/crypto/WasmCryptoEdge.ts +5 -1
  154. package/src/crypto/crypto.ts +16 -4
  155. package/src/exports.ts +2 -0
  156. package/src/ids.ts +11 -1
  157. package/src/localNode.ts +15 -0
  158. package/src/platformUtils.ts +26 -0
  159. package/src/storage/DeletedCoValuesEraserScheduler.ts +124 -0
  160. package/src/storage/sqlite/client.ts +77 -0
  161. package/src/storage/sqlite/sqliteMigrations.ts +7 -0
  162. package/src/storage/sqliteAsync/client.ts +75 -0
  163. package/src/storage/storageAsync.ts +62 -0
  164. package/src/storage/storageSync.ts +58 -0
  165. package/src/storage/types.ts +69 -0
  166. package/src/sync.ts +51 -11
  167. package/src/tests/DeletedCoValuesEraserScheduler.test.ts +185 -0
  168. package/src/tests/GarbageCollector.test.ts +6 -10
  169. package/src/tests/StorageApiAsync.test.ts +572 -162
  170. package/src/tests/StorageApiSync.test.ts +580 -143
  171. package/src/tests/WasmCrypto.test.ts +8 -3
  172. package/src/tests/coValueCore.loadFromStorage.test.ts +6 -0
  173. package/src/tests/coValueCore.test.ts +49 -14
  174. package/src/tests/coreWasm.test.ts +319 -10
  175. package/src/tests/crypto.test.ts +141 -150
  176. package/src/tests/deleteCoValue.test.ts +528 -0
  177. package/src/tests/group.removeMember.test.ts +35 -35
  178. package/src/tests/knownState.lazyLoading.test.ts +6 -0
  179. package/src/tests/sync.deleted.test.ts +294 -0
  180. package/src/tests/sync.mesh.test.ts +5 -2
  181. package/src/tests/sync.storage.test.ts +5 -2
  182. package/src/tests/sync.test.ts +5 -2
  183. package/src/tests/testStorage.ts +28 -1
  184. package/src/tests/testUtils.ts +28 -9
  185. package/dist/crypto/PureJSCrypto.d.ts +0 -77
  186. package/dist/crypto/PureJSCrypto.d.ts.map +0 -1
  187. package/dist/crypto/PureJSCrypto.js +0 -236
  188. package/dist/crypto/PureJSCrypto.js.map +0 -1
  189. package/dist/tests/PureJSCrypto.test.d.ts +0 -2
  190. package/dist/tests/PureJSCrypto.test.d.ts.map +0 -1
  191. package/dist/tests/PureJSCrypto.test.js +0 -145
  192. package/dist/tests/PureJSCrypto.test.js.map +0 -1
  193. package/src/crypto/PureJSCrypto.ts +0 -429
  194. package/src/tests/PureJSCrypto.test.ts +0 -217
@@ -19,7 +19,6 @@ import { RawCoID, SessionID, TransactionID } from "../ids.js";
19
19
  import { Stringified, stableStringify } from "../jsonStringify.js";
20
20
  import { JsonObject, JsonValue } from "../jsonValue.js";
21
21
  import { logger } from "../logger.js";
22
- import { PureJSCrypto } from "./PureJSCrypto.js";
23
22
  import {
24
23
  CryptoProvider,
25
24
  Encrypted,
@@ -58,7 +57,7 @@ export class NapiCrypto extends CryptoProvider<Blake3State> {
58
57
  super();
59
58
  }
60
59
 
61
- static async create(): Promise<NapiCrypto | PureJSCrypto | WasmCrypto> {
60
+ static async create(): Promise<NapiCrypto | WasmCrypto> {
62
61
  return new NapiCrypto();
63
62
  }
64
63
 
@@ -190,11 +189,24 @@ class SessionLogAdapter {
190
189
  newSignature: Signature,
191
190
  skipVerify: boolean,
192
191
  ): void {
193
- this.sessionLog.tryAdd(
194
- transactions.map((tx) => stableStringify(tx)),
195
- newSignature,
196
- skipVerify,
197
- );
192
+ // Use direct calls instead of JSON.stringify for better performance
193
+ for (const tx of transactions) {
194
+ if (tx.privacy === "private") {
195
+ this.sessionLog.addExistingPrivateTransaction(
196
+ tx.encryptedChanges,
197
+ tx.keyUsed,
198
+ tx.madeAt,
199
+ tx.meta,
200
+ );
201
+ } else {
202
+ this.sessionLog.addExistingTrustingTransaction(
203
+ tx.changes,
204
+ tx.madeAt,
205
+ tx.meta,
206
+ );
207
+ }
208
+ }
209
+ this.sessionLog.commitTransactions(newSignature, skipVerify);
198
210
  }
199
211
 
200
212
  addNewPrivateTransaction(
@@ -206,12 +218,14 @@ class SessionLogAdapter {
206
218
  meta: JsonObject | undefined,
207
219
  ): { signature: Signature; transaction: PrivateTransaction } {
208
220
  const output = this.sessionLog.addNewPrivateTransaction(
209
- stableStringify(changes),
221
+ // We can avoid stableStringify because it will be encrypted.
222
+ JSON.stringify(changes),
210
223
  signerAgent.currentSignerSecret(),
211
224
  keySecret,
212
225
  keyID,
213
226
  madeAt,
214
- meta ? stableStringify(meta) : undefined,
227
+ // We can avoid stableStringify because it will be encrypted.
228
+ meta ? JSON.stringify(meta) : undefined,
215
229
  );
216
230
  const parsedOutput = JSON.parse(output);
217
231
  const transaction: PrivateTransaction = {
@@ -230,8 +244,10 @@ class SessionLogAdapter {
230
244
  madeAt: number,
231
245
  meta: JsonObject | undefined,
232
246
  ): { signature: Signature; transaction: TrustingTransaction } {
233
- const stringifiedChanges = stableStringify(changes);
234
- const stringifiedMeta = meta ? stableStringify(meta) : undefined;
247
+ // We can avoid stableStringify because the changes will be in a string format already.
248
+ const stringifiedChanges = JSON.stringify(changes);
249
+ // We can avoid stableStringify because the meta will be in a string format already.
250
+ const stringifiedMeta = meta ? JSON.stringify(meta) : undefined;
235
251
  const output = this.sessionLog.addNewTrustingTransaction(
236
252
  stringifiedChanges,
237
253
  signerAgent.currentSignerSecret(),
@@ -241,8 +257,8 @@ class SessionLogAdapter {
241
257
  const transaction: TrustingTransaction = {
242
258
  privacy: "trusting",
243
259
  madeAt,
244
- changes: stringifiedChanges,
245
- meta: stringifiedMeta,
260
+ changes: stringifiedChanges as Stringified<JsonValue[]>,
261
+ meta: stringifiedMeta as Stringified<JsonObject> | undefined,
246
262
  };
247
263
  return { signature: output as Signature, transaction };
248
264
  }
@@ -47,6 +47,7 @@ import {
47
47
  Blake3Hasher,
48
48
  SessionLog,
49
49
  } from "cojson-core-rn";
50
+ import { WasmCrypto } from "./WasmCrypto.js";
50
51
 
51
52
  type Blake3State = Blake3Hasher;
52
53
 
@@ -221,11 +222,24 @@ class SessionLogAdapter implements SessionLogImpl {
221
222
  newSignature: Signature,
222
223
  skipVerify: boolean,
223
224
  ): void {
224
- this.sessionLog.tryAdd(
225
- transactions.map((tx) => stableStringify(tx)),
226
- newSignature,
227
- skipVerify,
228
- );
225
+ // Use direct calls instead of JSON.stringify for better performance
226
+ for (const tx of transactions) {
227
+ if (tx.privacy === "private") {
228
+ this.sessionLog.addExistingPrivateTransaction(
229
+ tx.encryptedChanges,
230
+ tx.keyUsed,
231
+ tx.madeAt,
232
+ tx.meta,
233
+ );
234
+ } else {
235
+ this.sessionLog.addExistingTrustingTransaction(
236
+ tx.changes,
237
+ tx.madeAt,
238
+ tx.meta,
239
+ );
240
+ }
241
+ }
242
+ this.sessionLog.commitTransactions(newSignature, skipVerify);
229
243
  }
230
244
 
231
245
  addNewPrivateTransaction(
@@ -237,12 +251,14 @@ class SessionLogAdapter implements SessionLogImpl {
237
251
  meta: JsonObject | undefined,
238
252
  ) {
239
253
  const output = this.sessionLog.addNewPrivateTransaction(
240
- stableStringify(changes),
254
+ // We can avoid stableStringify because it will be encrypted.
255
+ JSON.stringify(changes),
241
256
  signerAgent.currentSignerSecret(),
242
257
  keySecret,
243
258
  keyID,
244
259
  madeAt,
245
- meta ? stableStringify(meta) : undefined,
260
+ // We can avoid stableStringify because it will be encrypted.
261
+ meta ? JSON.stringify(meta) : undefined,
246
262
  );
247
263
  const parsedOutput = JSON.parse(output);
248
264
  const transaction: PrivateTransaction = {
@@ -261,8 +277,10 @@ class SessionLogAdapter implements SessionLogImpl {
261
277
  madeAt: number,
262
278
  meta: JsonObject | undefined,
263
279
  ) {
264
- const stringifiedChanges = stableStringify(changes);
265
- const stringifiedMeta = meta ? stableStringify(meta) : undefined;
280
+ // We can avoid stableStringify because the changes will be in a string format already.
281
+ const stringifiedChanges = JSON.stringify(changes);
282
+ // We can avoid stableStringify because the meta will be in a string format already.
283
+ const stringifiedMeta = meta ? JSON.stringify(meta) : undefined;
266
284
  const output = this.sessionLog.addNewTrustingTransaction(
267
285
  stringifiedChanges,
268
286
  signerAgent.currentSignerSecret(),
@@ -272,8 +290,8 @@ class SessionLogAdapter implements SessionLogImpl {
272
290
  const transaction: TrustingTransaction = {
273
291
  privacy: "trusting",
274
292
  madeAt,
275
- changes: stringifiedChanges,
276
- meta: stringifiedMeta,
293
+ changes: stringifiedChanges as Stringified<JsonValue[]>,
294
+ meta: stringifiedMeta as Stringified<JsonObject> | undefined,
277
295
  };
278
296
  return { signature: output as Signature, transaction };
279
297
  }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  SessionLog,
3
3
  initialize,
4
+ initializeSync,
4
5
  Blake3Hasher,
5
6
  blake3HashOnce,
6
7
  blake3HashOnceWithContext,
@@ -20,7 +21,6 @@ import { RawCoID, SessionID, TransactionID } from "../ids.js";
20
21
  import { Stringified, stableStringify } from "../jsonStringify.js";
21
22
  import { JsonObject, JsonValue } from "../jsonValue.js";
22
23
  import { logger } from "../logger.js";
23
- import { PureJSCrypto } from "./PureJSCrypto.js";
24
24
  import {
25
25
  CryptoProvider,
26
26
  Encrypted,
@@ -41,10 +41,34 @@ import {
41
41
  Transaction,
42
42
  TrustingTransaction,
43
43
  } from "../coValueCore/verifiedState.js";
44
+ import { isCloudflare, isEvalAllowed } from "../platformUtils.js";
44
45
 
45
46
  type Blake3State = Blake3Hasher;
46
47
 
47
48
  let wasmInit = initialize;
49
+ let wasmInitSync = initializeSync;
50
+
51
+ const wasmCryptoErrorMessage = (
52
+ e: unknown,
53
+ ) => `Critical Error: Failed to load WASM module
54
+
55
+ ${!isEvalAllowed() ? `You need to add \`import "jazz-tools/load-edge-wasm";\` on top of your entry module to make Jazz work with ${isCloudflare() ? "Cloudflare workers" : "this runtime"}` : (e as Error).message}
56
+
57
+ A native crypto module is required for Jazz to work. See https://jazz.tools/docs/react/reference/performance#use-the-best-crypto-implementation-for-your-platform for possible alternatives.`;
58
+
59
+ /**
60
+ * Initializes the WasmCrypto module. This function can be used to initialize the WasmCrypto module in a worker or a browser.
61
+ * if you are using SSR and you want to initialize WASM crypto asynchronously you can use this function.
62
+ * @returns A promise that resolves when the WasmCrypto module is successfully initialized.
63
+ */
64
+ export async function initWasmCrypto() {
65
+ try {
66
+ await wasmInit();
67
+ } catch (e) {
68
+ throw new Error(wasmCryptoErrorMessage(e), { cause: e });
69
+ }
70
+ }
71
+
48
72
  /**
49
73
  * WebAssembly implementation of the CryptoProvider interface using cojson-core-wasm.
50
74
  * This provides the primary implementation using WebAssembly for optimal performance, offering:
@@ -54,7 +78,7 @@ let wasmInit = initialize;
54
78
  * - Hashing (BLAKE3)
55
79
  */
56
80
  export class WasmCrypto extends CryptoProvider<Blake3State> {
57
- private constructor() {
81
+ protected constructor() {
58
82
  super();
59
83
  }
60
84
 
@@ -62,17 +86,23 @@ export class WasmCrypto extends CryptoProvider<Blake3State> {
62
86
  wasmInit = value;
63
87
  }
64
88
 
65
- static async create(): Promise<WasmCrypto | PureJSCrypto> {
89
+ static setInitSync(value: typeof initializeSync) {
90
+ wasmInitSync = value;
91
+ }
92
+
93
+ static createSync(): WasmCrypto {
66
94
  try {
67
- await wasmInit();
95
+ wasmInitSync();
68
96
  } catch (e) {
69
- logger.warn(
70
- "Failed to initialize WasmCrypto, falling back to PureJSCrypto",
71
- { err: e },
72
- );
73
- return new PureJSCrypto();
97
+ throw new Error(wasmCryptoErrorMessage(e), { cause: e });
74
98
  }
99
+ return new WasmCrypto();
100
+ }
75
101
 
102
+ // TODO: Remove this method and use createSync instead, this is not necessary since we can use createSync in the browser and in the worker.
103
+ // @deprecated
104
+ static async create(): Promise<WasmCrypto> {
105
+ await initWasmCrypto();
76
106
  return new WasmCrypto();
77
107
  }
78
108
 
@@ -204,11 +234,24 @@ class SessionLogAdapter {
204
234
  newSignature: Signature,
205
235
  skipVerify: boolean,
206
236
  ): void {
207
- this.sessionLog.tryAdd(
208
- transactions.map((tx) => stableStringify(tx)),
209
- newSignature,
210
- skipVerify,
211
- );
237
+ // Use direct calls instead of JSON.stringify for better performance
238
+ for (const tx of transactions) {
239
+ if (tx.privacy === "private") {
240
+ this.sessionLog.addExistingPrivateTransaction(
241
+ tx.encryptedChanges,
242
+ tx.keyUsed,
243
+ tx.madeAt,
244
+ tx.meta,
245
+ );
246
+ } else {
247
+ this.sessionLog.addExistingTrustingTransaction(
248
+ tx.changes,
249
+ tx.madeAt,
250
+ tx.meta,
251
+ );
252
+ }
253
+ }
254
+ this.sessionLog.commitTransactions(newSignature, skipVerify);
212
255
  }
213
256
 
214
257
  addNewPrivateTransaction(
@@ -220,12 +263,14 @@ class SessionLogAdapter {
220
263
  meta: JsonObject | undefined,
221
264
  ): { signature: Signature; transaction: PrivateTransaction } {
222
265
  const output = this.sessionLog.addNewPrivateTransaction(
223
- stableStringify(changes),
266
+ // We can avoid stableStringify because it will be encrypted.
267
+ JSON.stringify(changes),
224
268
  signerAgent.currentSignerSecret(),
225
269
  keySecret,
226
270
  keyID,
227
271
  madeAt,
228
- meta ? stableStringify(meta) : undefined,
272
+ // We can avoid stableStringify because it will be encrypted.
273
+ meta ? JSON.stringify(meta) : undefined,
229
274
  );
230
275
  const parsedOutput = JSON.parse(output);
231
276
  const transaction: PrivateTransaction = {
@@ -244,8 +289,10 @@ class SessionLogAdapter {
244
289
  madeAt: number,
245
290
  meta: JsonObject | undefined,
246
291
  ): { signature: Signature; transaction: TrustingTransaction } {
247
- const stringifiedChanges = stableStringify(changes);
248
- const stringifiedMeta = meta ? stableStringify(meta) : undefined;
292
+ // We can avoid stableStringify because the changes will be in a string format already.
293
+ const stringifiedChanges = JSON.stringify(changes);
294
+ // We can avoid stableStringify because the meta will be in a string format already.
295
+ const stringifiedMeta = meta ? JSON.stringify(meta) : undefined;
249
296
  const output = this.sessionLog.addNewTrustingTransaction(
250
297
  stringifiedChanges,
251
298
  signerAgent.currentSignerSecret(),
@@ -255,8 +302,8 @@ class SessionLogAdapter {
255
302
  const transaction: TrustingTransaction = {
256
303
  privacy: "trusting",
257
304
  madeAt,
258
- changes: stringifiedChanges,
259
- meta: stringifiedMeta,
305
+ changes: stringifiedChanges as Stringified<JsonValue[]>,
306
+ meta: stringifiedMeta as Stringified<JsonObject> | undefined,
260
307
  };
261
308
  return { signature: output as Signature, transaction };
262
309
  }
@@ -1,4 +1,4 @@
1
- import { initialize } from "cojson-core-wasm/edge-lite";
1
+ import { initialize, initializeSync } from "cojson-core-wasm/edge-lite";
2
2
  import { WasmCrypto } from "./WasmCrypto.js";
3
3
 
4
4
  /**
@@ -12,3 +12,7 @@ import { WasmCrypto } from "./WasmCrypto.js";
12
12
  export const init = async () => {
13
13
  return initialize();
14
14
  };
15
+
16
+ export const initSync = () => {
17
+ return initializeSync();
18
+ };
@@ -1,7 +1,13 @@
1
1
  import { base58 } from "@scure/base";
2
2
  import { ControlledAccountOrAgent, RawAccountID } from "../coValues/account.js";
3
- import { AgentID, RawCoID, TransactionID } from "../ids.js";
4
- import { SessionID } from "../ids.js";
3
+ import {
4
+ AgentID,
5
+ RawCoID,
6
+ TransactionID,
7
+ SessionID,
8
+ ActiveSessionID,
9
+ DeleteSessionID,
10
+ } from "../ids.js";
5
11
  import { Stringified, parseJSON, stableStringify } from "../jsonStringify.js";
6
12
  import { JsonObject, JsonValue } from "../jsonValue.js";
7
13
  import { logger } from "../logger.js";
@@ -246,8 +252,14 @@ export abstract class CryptoProvider<Blake3State = any> {
246
252
  )}`;
247
253
  }
248
254
 
249
- newRandomSessionID(accountID: RawAccountID | AgentID): SessionID {
250
- return `${accountID}_session_z${base58.encode(this.randomBytes(8))}`;
255
+ newRandomSessionID(accountID: RawAccountID | AgentID): ActiveSessionID {
256
+ const randomPart = base58.encode(this.randomBytes(8));
257
+ return `${accountID}_session_z${randomPart}`;
258
+ }
259
+
260
+ newDeleteSessionID(accountID: RawAccountID | AgentID): DeleteSessionID {
261
+ const randomPart = base58.encode(this.randomBytes(7));
262
+ return `${accountID}_session_d${randomPart}$`;
251
263
  }
252
264
 
253
265
  abstract createSessionLog(
package/src/exports.ts CHANGED
@@ -77,6 +77,7 @@ import { emptyKnownState } from "./knownState.js";
77
77
  import {
78
78
  getContentMessageSize,
79
79
  getTransactionSize,
80
+ knownStateFromContent,
80
81
  } from "./coValueContentMessage.js";
81
82
  import { getDependedOnCoValuesFromRawData } from "./coValueCore/utils.js";
82
83
  import {
@@ -102,6 +103,7 @@ type Value = JsonValue | AnyRawCoValue;
102
103
  export { PriorityBasedMessageQueue } from "./queue/PriorityBasedMessageQueue.js";
103
104
  /** @hidden */
104
105
  export const cojsonInternals = {
106
+ knownStateFromContent,
105
107
  connectedPeers,
106
108
  rawCoIDtoBytes,
107
109
  rawCoIDfromBytes,
package/src/ids.ts CHANGED
@@ -36,7 +36,17 @@ export function isAgentID(id: unknown): id is AgentID {
36
36
  );
37
37
  }
38
38
 
39
- export type SessionID = `${RawAccountID | AgentID}_session_z${string}`;
39
+ export type ActiveSessionID = `${RawAccountID | AgentID}_session_z${string}`;
40
+ export type DeleteSessionID = `${RawAccountID | AgentID}_session_d${string}$`;
41
+ export type SessionID = ActiveSessionID | DeleteSessionID;
42
+
43
+ const CHAR_DOLLAR = "$".charCodeAt(0);
44
+
45
+ export function isDeleteSessionID(
46
+ sessionID: SessionID,
47
+ ): sessionID is DeleteSessionID {
48
+ return sessionID.charCodeAt(sessionID.length - 1) === CHAR_DOLLAR;
49
+ }
40
50
 
41
51
  export function isParentGroupReference(
42
52
  key: string,
package/src/localNode.ts CHANGED
@@ -101,6 +101,21 @@ export class LocalNode {
101
101
  this.syncManager.removeStorage();
102
102
  }
103
103
 
104
+ /**
105
+ * Enable background erasure of deleted coValues (space reclamation).
106
+ *
107
+ * Deleted coValues are immediately blocked from syncing via tombstones; this feature
108
+ * only reclaims local storage space by deleting historical content while preserving
109
+ * the tombstone (header + delete session).
110
+ *
111
+ * This is opt-in and affects only the currently configured storage (if any)
112
+ *
113
+ * @category 3. Low-level
114
+ */
115
+ enableDeletedCoValuesErasure() {
116
+ this.storage?.enableDeletedCoValuesErasure();
117
+ }
118
+
104
119
  hasCoValue(id: RawCoID) {
105
120
  const coValue = this.coValues.get(id);
106
121
 
@@ -0,0 +1,26 @@
1
+ export function isCloudflare() {
2
+ if (
3
+ // @ts-ignore
4
+ typeof navigator !== "undefined" &&
5
+ // @ts-ignore
6
+ navigator?.userAgent?.includes("Cloudflare")
7
+ ) {
8
+ return true;
9
+ }
10
+
11
+ return false;
12
+ }
13
+
14
+ export const isEvalAllowed = () => {
15
+ if (isCloudflare()) {
16
+ return false;
17
+ }
18
+
19
+ try {
20
+ const F = Function;
21
+ new F("");
22
+ return true;
23
+ } catch (_) {
24
+ return false;
25
+ }
26
+ };
@@ -0,0 +1,124 @@
1
+ import { logger } from "../logger.js";
2
+
3
+ export type DeletedCoValuesEraserSchedulerRunResult = {
4
+ hasMore: boolean;
5
+ };
6
+
7
+ export type DeletedCoValuesEraserSchedulerOpts = {
8
+ throttleMs: number;
9
+ startupDelayMs: number;
10
+ followUpDelayMs: number;
11
+ };
12
+
13
+ type SchedulerState =
14
+ | "idle"
15
+ | "startup_scheduled"
16
+ | "throttle_scheduled"
17
+ | "followup_scheduled"
18
+ | "running"
19
+ | "disposed";
20
+
21
+ export const DEFAULT_DELETE_SCHEDULE_OPTS = {
22
+ throttleMs: 60_000,
23
+ startupDelayMs: 1_000,
24
+ followUpDelayMs: 1_000,
25
+ } satisfies DeletedCoValuesEraserSchedulerOpts;
26
+
27
+ export class DeletedCoValuesEraserScheduler {
28
+ private readonly runCallback: () => Promise<DeletedCoValuesEraserSchedulerRunResult>;
29
+ private readonly opts: DeletedCoValuesEraserSchedulerOpts;
30
+
31
+ private state: SchedulerState = "idle";
32
+
33
+ private isDisposed(): boolean {
34
+ return this.state === "disposed";
35
+ }
36
+
37
+ private scheduledTimeout: ReturnType<typeof setTimeout> | undefined;
38
+
39
+ constructor({
40
+ run,
41
+ opts,
42
+ }: {
43
+ run: () => Promise<DeletedCoValuesEraserSchedulerRunResult>;
44
+ opts?: DeletedCoValuesEraserSchedulerOpts;
45
+ }) {
46
+ this.runCallback = run;
47
+ this.opts = opts || DEFAULT_DELETE_SCHEDULE_OPTS;
48
+ }
49
+
50
+ scheduleStartupDrain() {
51
+ if (this.isDisposed()) return;
52
+
53
+ // Only schedule startup drain if nothing is already scheduled/running.
54
+ if (this.state !== "idle") return;
55
+
56
+ this.scheduleTimer("startup_scheduled", this.opts.startupDelayMs);
57
+ }
58
+
59
+ onEnqueueDeletedCoValue() {
60
+ if (this.isDisposed()) return;
61
+
62
+ // While we're already draining (or have a follow-up scheduled), ignore enqueue
63
+ // to avoid overlapping phases. The active drain loop will pick up new work.
64
+ if (this.state !== "idle") return;
65
+
66
+ // Only idle reaches here.
67
+ this.scheduleTimer("throttle_scheduled", this.opts.throttleMs);
68
+ }
69
+
70
+ dispose() {
71
+ if (this.isDisposed()) return;
72
+ this.state = "disposed";
73
+
74
+ if (this.scheduledTimeout) clearTimeout(this.scheduledTimeout);
75
+ this.scheduledTimeout = undefined;
76
+ }
77
+
78
+ private scheduleTimer(
79
+ state: Exclude<SchedulerState, "idle" | "running" | "disposed">,
80
+ delayMs: number,
81
+ ) {
82
+ if (this.isDisposed()) return;
83
+ if (this.scheduledTimeout) return;
84
+
85
+ this.state = state;
86
+ this.scheduledTimeout = setTimeout(() => {
87
+ this.scheduledTimeout = undefined;
88
+ void this.run();
89
+ }, delayMs);
90
+ }
91
+
92
+ private async run() {
93
+ if (this.isDisposed()) return;
94
+
95
+ // Clear any pre-run scheduled state and enter running state.
96
+ this.state = "running";
97
+
98
+ let result: DeletedCoValuesEraserSchedulerRunResult;
99
+ try {
100
+ result = await this.runCallback();
101
+ } catch (error) {
102
+ logger.error("Error running deleted co values eraser scheduler", {
103
+ err: error,
104
+ });
105
+ // If the run callback fails, recover to idle so future enqueues/startup drains
106
+ // can retry instead of getting stuck in "running".
107
+ if (!this.isDisposed()) {
108
+ this.state = "idle";
109
+ }
110
+ return;
111
+ }
112
+
113
+ if (this.isDisposed()) return;
114
+
115
+ if (result.hasMore) {
116
+ // One follow-up phase at a time. Further enqueues while follow-up is scheduled
117
+ // are ignored.
118
+ this.scheduleTimer("followup_scheduled", this.opts.followUpDelayMs);
119
+ return;
120
+ }
121
+
122
+ this.state = "idle";
123
+ }
124
+ }
@@ -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 (?, ?, ?, ?, ?)