cojson 0.20.7 → 0.20.9

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 (209) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/dist/SyncStateManager.d.ts.map +1 -1
  4. package/dist/SyncStateManager.js +0 -2
  5. package/dist/SyncStateManager.js.map +1 -1
  6. package/dist/base64url.d.ts +15 -0
  7. package/dist/base64url.d.ts.map +1 -1
  8. package/dist/base64url.js +101 -5
  9. package/dist/base64url.js.map +1 -1
  10. package/dist/base64url.test.js +76 -1
  11. package/dist/base64url.test.js.map +1 -1
  12. package/dist/coValue.d.ts +2 -1
  13. package/dist/coValue.d.ts.map +1 -1
  14. package/dist/coValue.js.map +1 -1
  15. package/dist/coValueCore/coValueCore.d.ts +9 -11
  16. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  17. package/dist/coValueCore/coValueCore.js +92 -65
  18. package/dist/coValueCore/coValueCore.js.map +1 -1
  19. package/dist/coValueCore/verifiedState.d.ts +38 -7
  20. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  21. package/dist/coValueCore/verifiedState.js +226 -30
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/binaryCoStream.d.ts +63 -0
  24. package/dist/coValues/binaryCoStream.d.ts.map +1 -0
  25. package/dist/coValues/binaryCoStream.js +125 -0
  26. package/dist/coValues/binaryCoStream.js.map +1 -0
  27. package/dist/coValues/coList.d.ts +3 -1
  28. package/dist/coValues/coList.d.ts.map +1 -1
  29. package/dist/coValues/coList.js +15 -6
  30. package/dist/coValues/coList.js.map +1 -1
  31. package/dist/coValues/coMap.d.ts +1 -1
  32. package/dist/coValues/coMap.d.ts.map +1 -1
  33. package/dist/coValues/coMap.js +2 -2
  34. package/dist/coValues/coMap.js.map +1 -1
  35. package/dist/coValues/coStream.d.ts +0 -38
  36. package/dist/coValues/coStream.d.ts.map +1 -1
  37. package/dist/coValues/coStream.js +0 -86
  38. package/dist/coValues/coStream.js.map +1 -1
  39. package/dist/coValues/group.d.ts +44 -6
  40. package/dist/coValues/group.d.ts.map +1 -1
  41. package/dist/coValues/group.js +198 -17
  42. package/dist/coValues/group.js.map +1 -1
  43. package/dist/coreToCoValue.d.ts +2 -1
  44. package/dist/coreToCoValue.d.ts.map +1 -1
  45. package/dist/coreToCoValue.js +2 -1
  46. package/dist/coreToCoValue.js.map +1 -1
  47. package/dist/crypto/NapiCrypto.d.ts +18 -24
  48. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  49. package/dist/crypto/NapiCrypto.js +98 -60
  50. package/dist/crypto/NapiCrypto.js.map +1 -1
  51. package/dist/crypto/RNCrypto.d.ts +16 -3
  52. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  53. package/dist/crypto/RNCrypto.js +117 -54
  54. package/dist/crypto/RNCrypto.js.map +1 -1
  55. package/dist/crypto/WasmCrypto.d.ts +18 -24
  56. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  57. package/dist/crypto/WasmCrypto.js +100 -61
  58. package/dist/crypto/WasmCrypto.js.map +1 -1
  59. package/dist/crypto/crypto.d.ts +55 -19
  60. package/dist/crypto/crypto.d.ts.map +1 -1
  61. package/dist/crypto/crypto.js +14 -3
  62. package/dist/crypto/crypto.js.map +1 -1
  63. package/dist/exports.d.ts +7 -3
  64. package/dist/exports.d.ts.map +1 -1
  65. package/dist/exports.js +4 -2
  66. package/dist/exports.js.map +1 -1
  67. package/dist/localNode.d.ts +3 -1
  68. package/dist/localNode.d.ts.map +1 -1
  69. package/dist/localNode.js +10 -3
  70. package/dist/localNode.js.map +1 -1
  71. package/dist/media.d.ts +1 -1
  72. package/dist/media.d.ts.map +1 -1
  73. package/dist/permissions.d.ts +2 -1
  74. package/dist/permissions.d.ts.map +1 -1
  75. package/dist/permissions.js +19 -3
  76. package/dist/permissions.js.map +1 -1
  77. package/dist/storage/sqliteAsync/client.d.ts +24 -12
  78. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  79. package/dist/storage/sqliteAsync/client.js +70 -58
  80. package/dist/storage/sqliteAsync/client.js.map +1 -1
  81. package/dist/storage/sqliteAsync/types.d.ts +1 -1
  82. package/dist/storage/sqliteAsync/types.d.ts.map +1 -1
  83. package/dist/storage/types.d.ts +1 -0
  84. package/dist/storage/types.d.ts.map +1 -1
  85. package/dist/sync.d.ts.map +1 -1
  86. package/dist/sync.js +7 -1
  87. package/dist/sync.js.map +1 -1
  88. package/dist/tests/CojsonMessageChannel.test.js +2 -2
  89. package/dist/tests/SQLiteClientAsync.test.d.ts +2 -0
  90. package/dist/tests/SQLiteClientAsync.test.d.ts.map +1 -0
  91. package/dist/tests/SQLiteClientAsync.test.js +64 -0
  92. package/dist/tests/SQLiteClientAsync.test.js.map +1 -0
  93. package/dist/tests/StorageApiAsync.test.js +2 -8
  94. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  95. package/dist/tests/SyncStateManager.test.js +2 -2
  96. package/dist/tests/WasmCrypto.test.js +1 -15
  97. package/dist/tests/WasmCrypto.test.js.map +1 -1
  98. package/dist/tests/coList.test.js +24 -5
  99. package/dist/tests/coList.test.js.map +1 -1
  100. package/dist/tests/coStream.test.js +4 -3
  101. package/dist/tests/coStream.test.js.map +1 -1
  102. package/dist/tests/coValueCore.initTransaction.test.d.ts +2 -0
  103. package/dist/tests/coValueCore.initTransaction.test.d.ts.map +1 -0
  104. package/dist/tests/coValueCore.initTransaction.test.js +438 -0
  105. package/dist/tests/coValueCore.initTransaction.test.js.map +1 -0
  106. package/dist/tests/coValueCore.test.js +11 -19
  107. package/dist/tests/coValueCore.test.js.map +1 -1
  108. package/dist/tests/crypto.test.js +83 -0
  109. package/dist/tests/crypto.test.js.map +1 -1
  110. package/dist/tests/deleteCoValue.test.js +5 -5
  111. package/dist/tests/deleteCoValue.test.js.map +1 -1
  112. package/dist/tests/group.inheritance.test.js +11 -0
  113. package/dist/tests/group.inheritance.test.js.map +1 -1
  114. package/dist/tests/group.test.js +24 -1
  115. package/dist/tests/group.test.js.map +1 -1
  116. package/dist/tests/groupSealer.test.d.ts +2 -0
  117. package/dist/tests/groupSealer.test.d.ts.map +1 -0
  118. package/dist/tests/groupSealer.test.js +913 -0
  119. package/dist/tests/groupSealer.test.js.map +1 -0
  120. package/dist/tests/setup.js +5 -0
  121. package/dist/tests/setup.js.map +1 -1
  122. package/dist/tests/sync.auth.test.js +10 -10
  123. package/dist/tests/sync.concurrentLoad.test.js +12 -12
  124. package/dist/tests/sync.deleted.test.js +8 -8
  125. package/dist/tests/sync.garbageCollection.test.js +10 -10
  126. package/dist/tests/sync.invite.test.js +12 -12
  127. package/dist/tests/sync.known.test.js +2 -2
  128. package/dist/tests/sync.load.test.js +107 -107
  129. package/dist/tests/sync.mesh.test.js +164 -46
  130. package/dist/tests/sync.mesh.test.js.map +1 -1
  131. package/dist/tests/sync.multipleServers.test.js +43 -43
  132. package/dist/tests/sync.peerReconciliation.test.js +29 -29
  133. package/dist/tests/sync.sharding.test.js +3 -3
  134. package/dist/tests/sync.storage.test.js +104 -104
  135. package/dist/tests/sync.storage.test.js.map +1 -1
  136. package/dist/tests/sync.storageAsync.test.js +56 -56
  137. package/dist/tests/sync.upload.test.js +22 -22
  138. package/dist/tests/testStorage.d.ts +2 -0
  139. package/dist/tests/testStorage.d.ts.map +1 -1
  140. package/dist/tests/testStorage.js +30 -6
  141. package/dist/tests/testStorage.js.map +1 -1
  142. package/dist/typeUtils/isCoValue.js +1 -1
  143. package/dist/typeUtils/isCoValue.js.map +1 -1
  144. package/package.json +4 -4
  145. package/src/SyncStateManager.ts +0 -2
  146. package/src/base64url.test.ts +89 -1
  147. package/src/base64url.ts +134 -6
  148. package/src/coValue.ts +2 -1
  149. package/src/coValueCore/coValueCore.ts +126 -84
  150. package/src/coValueCore/verifiedState.ts +335 -53
  151. package/src/coValues/binaryCoStream.ts +217 -0
  152. package/src/coValues/coList.ts +21 -8
  153. package/src/coValues/coMap.ts +3 -0
  154. package/src/coValues/coStream.ts +0 -170
  155. package/src/coValues/group.ts +270 -21
  156. package/src/coreToCoValue.ts +2 -1
  157. package/src/crypto/NapiCrypto.ts +198 -95
  158. package/src/crypto/RNCrypto.ts +229 -102
  159. package/src/crypto/WasmCrypto.ts +201 -95
  160. package/src/crypto/crypto.ts +118 -45
  161. package/src/exports.ts +11 -5
  162. package/src/localNode.ts +17 -1
  163. package/src/media.ts +1 -1
  164. package/src/permissions.ts +30 -7
  165. package/src/storage/sqliteAsync/client.ts +136 -115
  166. package/src/storage/sqliteAsync/types.ts +3 -1
  167. package/src/storage/types.ts +4 -0
  168. package/src/sync.ts +10 -1
  169. package/src/tests/CojsonMessageChannel.test.ts +2 -2
  170. package/src/tests/SQLiteClientAsync.test.ts +75 -0
  171. package/src/tests/StorageApiAsync.test.ts +4 -9
  172. package/src/tests/SyncStateManager.test.ts +2 -2
  173. package/src/tests/WasmCrypto.test.ts +1 -25
  174. package/src/tests/coList.test.ts +39 -5
  175. package/src/tests/coStream.test.ts +4 -5
  176. package/src/tests/coValueCore.initTransaction.test.ts +836 -0
  177. package/src/tests/coValueCore.test.ts +11 -22
  178. package/src/tests/crypto.test.ts +107 -0
  179. package/src/tests/deleteCoValue.test.ts +5 -5
  180. package/src/tests/group.inheritance.test.ts +16 -0
  181. package/src/tests/group.test.ts +29 -1
  182. package/src/tests/groupSealer.test.ts +1473 -0
  183. package/src/tests/setup.ts +6 -0
  184. package/src/tests/sync.auth.test.ts +10 -10
  185. package/src/tests/sync.concurrentLoad.test.ts +12 -12
  186. package/src/tests/sync.deleted.test.ts +8 -8
  187. package/src/tests/sync.garbageCollection.test.ts +10 -10
  188. package/src/tests/sync.invite.test.ts +12 -12
  189. package/src/tests/sync.known.test.ts +2 -2
  190. package/src/tests/sync.load.test.ts +107 -107
  191. package/src/tests/sync.mesh.test.ts +189 -46
  192. package/src/tests/sync.multipleServers.test.ts +43 -43
  193. package/src/tests/sync.peerReconciliation.test.ts +29 -29
  194. package/src/tests/sync.sharding.test.ts +3 -3
  195. package/src/tests/sync.storage.test.ts +104 -104
  196. package/src/tests/sync.storageAsync.test.ts +56 -56
  197. package/src/tests/sync.upload.test.ts +22 -22
  198. package/src/tests/testStorage.ts +39 -9
  199. package/src/typeUtils/isCoValue.ts +1 -1
  200. package/dist/coValueCore/SessionMap.d.ts +0 -55
  201. package/dist/coValueCore/SessionMap.d.ts.map +0 -1
  202. package/dist/coValueCore/SessionMap.js +0 -206
  203. package/dist/coValueCore/SessionMap.js.map +0 -1
  204. package/dist/tests/coreWasm.test.d.ts +0 -2
  205. package/dist/tests/coreWasm.test.d.ts.map +0 -1
  206. package/dist/tests/coreWasm.test.js +0 -203
  207. package/dist/tests/coreWasm.test.js.map +0 -1
  208. package/src/coValueCore/SessionMap.ts +0 -394
  209. package/src/tests/coreWasm.test.ts +0 -452
@@ -10,7 +10,7 @@ import {
10
10
  isInheritableRole,
11
11
  isSelfExtension,
12
12
  } from "./coValues/group.js";
13
- import { KeyID } from "./crypto/crypto.js";
13
+ import { KeyID, SealerID } from "./crypto/crypto.js";
14
14
  import {
15
15
  AgentID,
16
16
  ParentGroupReference,
@@ -273,6 +273,7 @@ function determineValidTransactionsForGroup(
273
273
  const change = changes[0] as
274
274
  | MapOpPayload<RawAccountID | AgentID | Everyone, Role>
275
275
  | MapOpPayload<"readKey", JsonValue>
276
+ | MapOpPayload<"groupSealer", SealerID>
276
277
  | MapOpPayload<"profile", CoID<RawProfile>>
277
278
  | MapOpPayload<"root", CoID<RawCoMap>>
278
279
  | MapOpPayload<`parent_${CoID<RawGroup>}`, CoID<RawGroup>>
@@ -294,6 +295,14 @@ function determineValidTransactionsForGroup(
294
295
  continue;
295
296
  }
296
297
 
298
+ transaction.markValid();
299
+ continue;
300
+ } else if (change.key === "groupSealer") {
301
+ if (!canAdmin(transactorRole)) {
302
+ transaction.markInvalid("Only admins can set groupSealer");
303
+ continue;
304
+ }
305
+
297
306
  transaction.markValid();
298
307
  continue;
299
308
  } else if (change.key === "profile") {
@@ -314,7 +323,8 @@ function determineValidTransactionsForGroup(
314
323
  continue;
315
324
  } else if (
316
325
  isKeyForKeyField(change.key) ||
317
- isKeyForAccountField(change.key)
326
+ isKeyForAccountField(change.key) ||
327
+ isKeySealedForGroupField(change.key)
318
328
  ) {
319
329
  if (
320
330
  transactorRole !== "admin" &&
@@ -596,6 +606,12 @@ export function isKeyForAccountField(
596
606
  );
597
607
  }
598
608
 
609
+ export function isKeySealedForGroupField(
610
+ co: string,
611
+ ): co is `${KeyID}_sealedFor_${SealerID}` {
612
+ return co.startsWith("key_") && co.includes("_sealedFor_sealer");
613
+ }
614
+
599
615
  function isParentExtension(key: string): key is `parent_${CoID<RawGroup>}` {
600
616
  return key.startsWith("parent_");
601
617
  }
@@ -605,15 +621,22 @@ function isChildExtension(key: string): key is `child_${CoID<RawGroup>}` {
605
621
  }
606
622
 
607
623
  function isOwnWriteKeyRevelation(
608
- key: `${KeyID}_for_${string}`,
624
+ key: `${KeyID}_for_${string}` | `${KeyID}_sealedFor_${SealerID}`,
609
625
  memberKey: RawAccountID | AgentID,
610
626
  writeOnlyKeys: Record<RawAccountID | AgentID, KeyID>,
611
- ): key is `${KeyID}_for_${RawAccountID | AgentID}` {
612
- if (Object.keys(writeOnlyKeys).length === 0) {
613
- return false;
627
+ ): key is
628
+ | `${KeyID}_for_${RawAccountID | AgentID}`
629
+ | `${KeyID}_sealedFor_${SealerID}` {
630
+ let i = key.indexOf("_for_");
631
+ if (i === -1) {
632
+ i = key.indexOf("_sealedFor_");
614
633
  }
615
634
 
616
- const keyID = key.slice(0, key.indexOf("_for_"));
635
+ const keyID = key.slice(0, i);
636
+
637
+ if (!keyID) {
638
+ return false;
639
+ }
617
640
 
618
641
  return writeOnlyKeys[memberKey] === keyID;
619
642
  }
@@ -38,15 +38,138 @@ export function getErrorMessage(error: unknown) {
38
38
  return error instanceof Error ? error.message : "Unknown error";
39
39
  }
40
40
 
41
- export class SQLiteClientAsync
42
- implements DBClientInterfaceAsync, DBTransactionInterfaceAsync
43
- {
41
+ /**
42
+ * Executes storage operations inside a single DB transaction.
43
+ */
44
+ export class SQLiteTransactionAsync implements DBTransactionInterfaceAsync {
45
+ constructor(private readonly tx: SQLiteDatabaseDriverAsync) {}
46
+
47
+ async getSingleCoValueSession(
48
+ coValueRowId: number,
49
+ sessionID: SessionID,
50
+ ): Promise<StoredSessionRow | undefined> {
51
+ return this.tx.get<StoredSessionRow>(
52
+ "SELECT * FROM sessions WHERE coValue = ? AND sessionID = ?",
53
+ [coValueRowId, sessionID],
54
+ );
55
+ }
56
+
57
+ async markCoValueAsDeleted(id: RawCoID): Promise<void> {
58
+ await this.tx.run(
59
+ `INSERT INTO deletedCoValues (coValueID) VALUES (?) ON CONFLICT(coValueID) DO NOTHING`,
60
+ [id],
61
+ );
62
+ }
63
+
64
+ async addSessionUpdate({
65
+ sessionUpdate,
66
+ }: {
67
+ sessionUpdate: SessionRow;
68
+ sessionRow?: StoredSessionRow;
69
+ }): Promise<number> {
70
+ const result = await this.tx.get<{ rowID: number }>(
71
+ `INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature, bytesSinceLastSignature) VALUES (?, ?, ?, ?, ?)
72
+ ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature, bytesSinceLastSignature=excluded.bytesSinceLastSignature
73
+ RETURNING rowID`,
74
+ [
75
+ sessionUpdate.coValue,
76
+ sessionUpdate.sessionID,
77
+ sessionUpdate.lastIdx,
78
+ sessionUpdate.lastSignature,
79
+ sessionUpdate.bytesSinceLastSignature,
80
+ ],
81
+ );
82
+
83
+ if (!result) {
84
+ throw new Error("Failed to add session update");
85
+ }
86
+
87
+ return result.rowID;
88
+ }
89
+
90
+ async addTransaction(
91
+ sessionRowID: number,
92
+ nextIdx: number,
93
+ newTransaction: Transaction,
94
+ ): Promise<void> {
95
+ await this.tx.run(
96
+ "INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)",
97
+ [sessionRowID, nextIdx, JSON.stringify(newTransaction)],
98
+ );
99
+ }
100
+
101
+ async addSignatureAfter({
102
+ sessionRowID,
103
+ idx,
104
+ signature,
105
+ }: {
106
+ sessionRowID: number;
107
+ idx: number;
108
+ signature: Signature;
109
+ }): Promise<void> {
110
+ await this.tx.run(
111
+ "INSERT INTO signatureAfter (ses, idx, signature) VALUES (?, ?, ?)",
112
+ [sessionRowID, idx, signature],
113
+ );
114
+ }
115
+
116
+ async deleteCoValueContent(
117
+ coValueRow: Pick<StoredCoValueRow, "rowID" | "id">,
118
+ ): Promise<void> {
119
+ await this.tx.run(
120
+ `DELETE FROM transactions
121
+ WHERE ses IN (
122
+ SELECT rowID FROM sessions
123
+ WHERE coValue = ?
124
+ AND sessionID NOT LIKE '%$'
125
+ )`,
126
+ [coValueRow.rowID],
127
+ );
128
+
129
+ await this.tx.run(
130
+ `DELETE FROM signatureAfter
131
+ WHERE ses IN (
132
+ SELECT rowID FROM sessions
133
+ WHERE coValue = ?
134
+ AND sessionID NOT LIKE '%$'
135
+ )`,
136
+ [coValueRow.rowID],
137
+ );
138
+
139
+ await this.tx.run(
140
+ `DELETE FROM sessions
141
+ WHERE coValue = ?
142
+ AND sessionID NOT LIKE '%$'`,
143
+ [coValueRow.rowID],
144
+ );
145
+
146
+ await this.tx.run(
147
+ `INSERT INTO deletedCoValues (coValueID, status) VALUES (?, ?)
148
+ ON CONFLICT(coValueID) DO UPDATE SET status=?`,
149
+ [
150
+ coValueRow.id,
151
+ DeletedCoValueDeletionStatus.Done,
152
+ DeletedCoValueDeletionStatus.Done,
153
+ ],
154
+ );
155
+ }
156
+ }
157
+
158
+ export class SQLiteClientAsync implements DBClientInterfaceAsync {
44
159
  private readonly db: SQLiteDatabaseDriverAsync;
160
+ /** Serialize transactions to avoid SQLITE_BUSY errors */
161
+ private txQueue = Promise.resolve() as Promise<unknown>;
45
162
 
46
163
  constructor(db: SQLiteDatabaseDriverAsync) {
47
164
  this.db = db;
48
165
  }
49
166
 
167
+ private enqueueTx<T>(fn: () => Promise<T>): Promise<T> {
168
+ const next = this.txQueue.then(fn, fn);
169
+ this.txQueue = next;
170
+ return next;
171
+ }
172
+
50
173
  async getCoValue(coValueId: RawCoID): Promise<StoredCoValueRow | undefined> {
51
174
  const coValueRow = await this.db.get<RawCoValueRow & { rowID: number }>(
52
175
  "SELECT * FROM coValues WHERE id = ?",
@@ -80,16 +203,6 @@ export class SQLiteClientAsync
80
203
  );
81
204
  }
82
205
 
83
- async getSingleCoValueSession(
84
- coValueRowId: number,
85
- sessionID: SessionID,
86
- ): Promise<StoredSessionRow | undefined> {
87
- return this.db.get<StoredSessionRow>(
88
- "SELECT * FROM sessions WHERE coValue = ? AND sessionID = ?",
89
- [coValueRowId, sessionID],
90
- );
91
- }
92
-
93
206
  async getNewTransactionInSession(
94
207
  sessionRowId: number,
95
208
  fromIdx: number,
@@ -151,15 +264,6 @@ export class SQLiteClientAsync
151
264
  return result.rowID;
152
265
  }
153
266
 
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
267
  async eraseCoValueButKeepTombstone(coValueId: RawCoID) {
164
268
  const coValueRow = await this.db.get<RawCoValueRow & { rowID: number }>(
165
269
  "SELECT * FROM coValues WHERE id = ?",
@@ -171,112 +275,29 @@ export class SQLiteClientAsync
171
275
  return;
172
276
  }
173
277
 
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
- );
278
+ await this.transaction(async (tx) => {
279
+ await tx.deleteCoValueContent(coValueRow);
211
280
  });
212
281
  }
213
282
 
214
283
  async getAllCoValuesWaitingForDelete(): Promise<RawCoID[]> {
215
284
  const rows = await this.db.query<DeletedCoValueQueueRow>(
216
285
  `SELECT coValueID as id
217
- FROM deletedCoValues
218
- WHERE status = ?`,
286
+ FROM deletedCoValues
287
+ WHERE status = ?`,
219
288
  [DeletedCoValueDeletionStatus.Pending],
220
289
  );
221
290
  return rows.map((r) => r.id);
222
291
  }
223
292
 
224
- async addSessionUpdate({
225
- sessionUpdate,
226
- }: {
227
- sessionUpdate: SessionRow;
228
- }): Promise<number> {
229
- const result = await this.db.get<{ rowID: number }>(
230
- `INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature, bytesSinceLastSignature) VALUES (?, ?, ?, ?, ?)
231
- ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature, bytesSinceLastSignature=excluded.bytesSinceLastSignature
232
- RETURNING rowID`,
233
- [
234
- sessionUpdate.coValue,
235
- sessionUpdate.sessionID,
236
- sessionUpdate.lastIdx,
237
- sessionUpdate.lastSignature,
238
- sessionUpdate.bytesSinceLastSignature,
239
- ],
240
- );
241
-
242
- if (!result) {
243
- throw new Error("Failed to add session update");
244
- }
245
-
246
- return result.rowID;
247
- }
248
-
249
- addTransaction(
250
- sessionRowID: number,
251
- nextIdx: number,
252
- newTransaction: Transaction,
253
- ) {
254
- this.db.run("INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)", [
255
- sessionRowID,
256
- nextIdx,
257
- JSON.stringify(newTransaction),
258
- ]);
259
- }
260
-
261
- async addSignatureAfter({
262
- sessionRowID,
263
- idx,
264
- signature,
265
- }: {
266
- sessionRowID: number;
267
- idx: number;
268
- signature: Signature;
269
- }) {
270
- this.db.run(
271
- "INSERT INTO signatureAfter (ses, idx, signature) VALUES (?, ?, ?)",
272
- [sessionRowID, idx, signature],
273
- );
274
- }
275
-
276
293
  async transaction(
277
294
  operationsCallback: (tx: DBTransactionInterfaceAsync) => Promise<unknown>,
278
- ) {
279
- return this.db.transaction(() => operationsCallback(this));
295
+ ): Promise<unknown> {
296
+ return this.enqueueTx(() =>
297
+ this.db.transaction((tx) =>
298
+ operationsCallback(new SQLiteTransactionAsync(tx)),
299
+ ),
300
+ );
280
301
  }
281
302
 
282
303
  async getUnsyncedCoValueIDs(): Promise<RawCoID[]> {
@@ -3,7 +3,9 @@ export interface SQLiteDatabaseDriverAsync {
3
3
  run(sql: string, params: unknown[]): Promise<void>;
4
4
  query<T>(sql: string, params: unknown[]): Promise<T[]>;
5
5
  get<T>(sql: string, params: unknown[]): Promise<T | undefined>;
6
- transaction(callback: () => unknown): Promise<unknown>;
6
+ transaction(
7
+ callback: (tx: SQLiteDatabaseDriverAsync) => unknown,
8
+ ): Promise<unknown>;
7
9
  closeDb(): Promise<unknown>;
8
10
  getMigrationVersion?(): Promise<number>;
9
11
  saveMigrationVersion?(version: number): Promise<void>;
@@ -172,6 +172,10 @@ export interface DBTransactionInterfaceAsync {
172
172
  idx: number;
173
173
  signature: Signature;
174
174
  }): Promise<unknown>;
175
+
176
+ deleteCoValueContent(
177
+ coValueRow: Pick<StoredCoValueRow, "rowID" | "id">,
178
+ ): Promise<unknown>;
175
179
  }
176
180
 
177
181
  export interface DBClientInterfaceAsync {
package/src/sync.ts CHANGED
@@ -626,6 +626,13 @@ export class SyncManager {
626
626
  action: "known",
627
627
  ...storageKnownState,
628
628
  });
629
+
630
+ // Subscribe to server peers (e.g., core) to receive future updates.
631
+ // Even though we responded with KNOWN (client has everything), we need
632
+ // to establish a subscription so that updates from core flow to us.
633
+ const serverPeers = this.getServerPeers(msg.id, peer.id);
634
+ coValue.loadFromPeers(serverPeers);
635
+
629
636
  return;
630
637
  }
631
638
 
@@ -888,6 +895,8 @@ export class SyncManager {
888
895
 
889
896
  let wasAlreadyDeleted = coValue.isDeleted;
890
897
 
898
+ const knownState = coValue.knownState();
899
+
891
900
  /**
892
901
  * The coValue is in memory, load the transactions from the content message
893
902
  */
@@ -901,7 +910,7 @@ export class SyncManager {
901
910
 
902
911
  const newTransactions = getNewTransactionsFromContentMessage(
903
912
  newContentForSession,
904
- coValue.knownState(),
913
+ knownState,
905
914
  sessionID,
906
915
  );
907
916
 
@@ -57,9 +57,9 @@ describe("CojsonMessageChannel", () => {
57
57
  ).toMatchInlineSnapshot(`
58
58
  [
59
59
  "server -> client | LOAD Map sessions: empty",
60
- "client -> server | CONTENT Group header: true new: After: 0 New: 5",
60
+ "client -> server | CONTENT Group header: true new: After: 0 New: 6",
61
61
  "client -> server | CONTENT Map header: true new: After: 0 New: 1",
62
- "server -> client | KNOWN Group sessions: header/5",
62
+ "server -> client | KNOWN Group sessions: header/6",
63
63
  "server -> client | KNOWN Map sessions: header/1",
64
64
  ]
65
65
  `);
@@ -0,0 +1,75 @@
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
+ import { getDbPath } from "./testStorage.js";
3
+ import { setupTestNode } from "./testUtils.js";
4
+ import { DBClientInterfaceAsync } from "../exports.js";
5
+
6
+ describe("SQLiteClientAsync", () => {
7
+ describe("transaction", () => {
8
+ let dbClient: DBClientInterfaceAsync;
9
+
10
+ beforeEach(async () => {
11
+ const node = setupTestNode();
12
+ const { storage } = await node.addAsyncStorage({
13
+ ourName: "test",
14
+ storageName: "test-storage",
15
+ filename: getDbPath(),
16
+ });
17
+ // @ts-expect-error - dbClient is private
18
+ dbClient = storage.dbClient;
19
+ });
20
+
21
+ test("serializes concurrent transactions to avoid SQLITE_BUSY errors", async () => {
22
+ const times = Array.from({ length: 10 });
23
+ await Promise.all(
24
+ times.map(async (_, i) => {
25
+ return dbClient.transaction(async (tx) => {
26
+ // Sleep between 0 and 100ms to force interleaving
27
+ await new Promise((r) => setTimeout(r, Math.random() * 100));
28
+ return tx.addSignatureAfter({
29
+ sessionRowID: 0,
30
+ idx: i,
31
+ signature: `signature_z${i}`,
32
+ });
33
+ });
34
+ }),
35
+ );
36
+
37
+ const signatures = await dbClient.getSignatures(0, 0);
38
+ expect(signatures.length).toBe(10);
39
+ signatures.forEach(async ({ signature }, i) => {
40
+ expect(signature).toBe(`signature_z${i}`);
41
+ });
42
+ });
43
+
44
+ test("continues to serialize transactions even if one fails", async () => {
45
+ // First transaction succeeds
46
+ await dbClient.transaction(async (tx) => {
47
+ return tx.addSignatureAfter({
48
+ sessionRowID: 0,
49
+ idx: 0,
50
+ signature: `signature_z0`,
51
+ });
52
+ });
53
+ // Second transaction fails (duplicate primary key)
54
+ await expect(
55
+ dbClient.transaction(async (tx) => {
56
+ return tx.addSignatureAfter({
57
+ sessionRowID: 0,
58
+ idx: 0,
59
+ signature: `signature_z0`,
60
+ });
61
+ }),
62
+ ).rejects.toThrow(
63
+ /UNIQUE constraint failed: signatureAfter\.ses, signatureAfter\.idx/,
64
+ );
65
+ // Third transaction succeeds
66
+ await dbClient.transaction(async (tx) => {
67
+ return tx.addSignatureAfter({
68
+ sessionRowID: 0,
69
+ idx: 1,
70
+ signature: `signature_z1`,
71
+ });
72
+ });
73
+ });
74
+ });
75
+ });
@@ -945,15 +945,10 @@ describe("StorageApiAsync", () => {
945
945
  return true;
946
946
  });
947
947
 
948
- let releaseBarrier!: () => void;
949
- const barrier = new Promise<void>((resolve) => {
950
- releaseBarrier = resolve;
951
- });
952
-
953
- let firstTxStartedResolve!: () => void;
954
- const firstTxStarted = new Promise<void>((resolve) => {
955
- firstTxStartedResolve = resolve;
956
- });
948
+ const { promise: barrier, resolve: releaseBarrier } =
949
+ Promise.withResolvers<void>();
950
+ const { promise: firstTxStarted, resolve: firstTxStartedResolve } =
951
+ Promise.withResolvers<void>();
957
952
 
958
953
  // @ts-expect-error - dbClient is private
959
954
  const dbClient = storage.dbClient;
@@ -307,8 +307,8 @@ describe("SyncStateManager", () => {
307
307
  [
308
308
  "server -> client | CONTENT Map header: true new: After: 0 New: 1",
309
309
  "client -> server | LOAD Group sessions: empty",
310
- "server -> client | CONTENT Group header: true new: After: 0 New: 3",
311
- "client -> server | KNOWN Group sessions: header/3",
310
+ "server -> client | CONTENT Group header: true new: After: 0 New: 4",
311
+ "client -> server | KNOWN Group sessions: header/4",
312
312
  "client -> server | KNOWN Map sessions: header/1",
313
313
  ]
314
314
  `);
@@ -183,7 +183,7 @@ describe("WasmCrypto", () => {
183
183
  const mapInOtherSession = await loadCoValueOrFail(session2.node, map.id);
184
184
 
185
185
  const transferredMeta = JSON.parse(
186
- mapInOtherSession.core.verified.sessions.get(client.node.currentSessionID)
186
+ mapInOtherSession.core.verified.getSession(client.node.currentSessionID)
187
187
  ?.transactions[0]?.meta!,
188
188
  );
189
189
 
@@ -193,28 +193,4 @@ describe("WasmCrypto", () => {
193
193
  },
194
194
  });
195
195
  });
196
-
197
- it("fails to verify signatures without a signer ID", async () => {
198
- const agentSecret = wasmCrypto.newRandomAgentSecret();
199
- const sessionID = wasmCrypto.newRandomSessionID(
200
- wasmCrypto.getAgentID(agentSecret),
201
- );
202
-
203
- const sessionLog = wasmCrypto.createSessionLog("co_z12345678", sessionID);
204
- expect(() =>
205
- sessionLog.tryAdd(
206
- [
207
- {
208
- privacy: "trusting",
209
- changes: JSON.stringify([
210
- { op: "set", key: "count", value: 1 },
211
- ]) as Stringified<JsonValue[]>,
212
- madeAt: Date.now(),
213
- },
214
- ],
215
- "signature_z12345678",
216
- false,
217
- ),
218
- ).toThrow(expect.stringContaining("Signature verification failed"));
219
- });
220
196
  });
@@ -210,6 +210,7 @@ test("init the list correctly", () => {
210
210
  "universe",
211
211
  "hello",
212
212
  ]);
213
+ expect(content.core.verified.header.createdAt).toBeDefined();
213
214
  });
214
215
 
215
216
  test("Items prepended to start appear with latest first", () => {
@@ -904,6 +905,8 @@ describe("CoList Branching", () => {
904
905
  // Client1 adds items to the branch
905
906
  aliceBranch.append("eggs", undefined, "trusting");
906
907
 
908
+ await new Promise((resolve) => setTimeout(resolve, 10));
909
+
907
910
  // Client2 loads the branch from a different session
908
911
  const branchOnClient2 = await loadCoValueOrFail(
909
912
  client2.node,
@@ -917,11 +920,11 @@ describe("CoList Branching", () => {
917
920
  "trusting",
918
921
  );
919
922
 
920
- // Merge the branch back to source
921
923
  branchOnClient2.core.mergeBranch();
922
924
 
923
- // Wait for sync
924
- await groceryList.core.waitForSync();
925
+ // Wait for all coValues to sync on both nodes
926
+ await client2.node.syncManager.waitForAllCoValuesSync();
927
+ await client1.node.syncManager.waitForAllCoValuesSync();
925
928
 
926
929
  // Source list should contain the final state
927
930
  expect(groceryList.toJSON()).toEqual(["milk", "eggs", "cheese"]);
@@ -960,6 +963,8 @@ describe("CoList Branching", () => {
960
963
  // Client2 adds different items to second branch
961
964
  bobBranch.append("eggs", undefined, "trusting");
962
965
 
966
+ await new Promise((resolve) => setTimeout(resolve, 10));
967
+
963
968
  // Client2 loads first branch and modifies it
964
969
  const aliceBranchOnClient2 = await loadCoValueOrFail(
965
970
  client2.node,
@@ -975,8 +980,9 @@ describe("CoList Branching", () => {
975
980
 
976
981
  bobBranch.core.mergeBranch();
977
982
 
978
- // Wait for sync
979
- await groceryList.core.waitForSync();
983
+ // Wait for all coValues to sync on both nodes
984
+ await client2.node.syncManager.waitForAllCoValuesSync();
985
+ await client1.node.syncManager.waitForAllCoValuesSync();
980
986
 
981
987
  // Source list should contain all changes
982
988
  expect(groceryList.toJSON()).toMatchInlineSnapshot(`
@@ -1017,3 +1023,31 @@ test("the list should rebuild when the group permissions change", async () => {
1017
1023
  expect(listOnBob.version).toEqual(1);
1018
1024
  expect(listOnBob.totalValidTransactions).toEqual(1);
1019
1025
  });
1026
+
1027
+ test("items appended after a losing init transaction are preserved", async () => {
1028
+ const alice = setupTestNode({ connected: true });
1029
+ const bob = setupTestNode({ connected: true });
1030
+
1031
+ const group = alice.node.createGroup();
1032
+ group.addMember("everyone", "writer");
1033
+
1034
+ const list = group.createList(
1035
+ ["alice-init"],
1036
+ undefined,
1037
+ "trusting",
1038
+ undefined,
1039
+ { fww: "init" },
1040
+ );
1041
+
1042
+ await new Promise((resolve) => setTimeout(resolve, 5));
1043
+
1044
+ const listOnBob = await loadCoValueOrFail(bob.node, list.id);
1045
+
1046
+ listOnBob.appendItems(["bob-init"], undefined, "trusting", { fww: "init" });
1047
+ listOnBob.appendItems(["bob-update"], undefined, "trusting");
1048
+
1049
+ await waitFor(() => {
1050
+ expect(listOnBob.toJSON()).toEqual(["alice-init", "bob-update"]);
1051
+ expect(list.toJSON()).toEqual(["alice-init", "bob-update"]);
1052
+ });
1053
+ });