@evolu/common 7.4.1 → 8.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +162 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +200 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +29 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +329 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2274 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1499 -348
  107. package/dist/src/Test.d.ts +150 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +97 -0
  110. package/dist/src/Time.d.ts +202 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +199 -88
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +587 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +370 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +130 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +274 -113
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +621 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +34 -49
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -177
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +54 -58
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +127 -179
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +37 -24
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +429 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +520 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3696 -740
  206. package/src/Test.ts +202 -0
  207. package/src/Time.ts +357 -147
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +866 -430
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +404 -167
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +867 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -824
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +441 -307
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +119 -273
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +211 -283
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/dist/src/Instances.d.ts +0 -34
  228. package/dist/src/Instances.d.ts.map +0 -1
  229. package/dist/src/Instances.js +0 -44
  230. package/dist/src/Resources.d.ts +0 -118
  231. package/dist/src/Resources.d.ts.map +0 -1
  232. package/dist/src/Resources.js +0 -197
  233. package/dist/src/local-first/Platform.d.ts +0 -25
  234. package/dist/src/local-first/Platform.d.ts.map +0 -1
  235. package/dist/src/local-first/Platform.js +0 -1
  236. package/dist/src/local-first/Public.d.ts +0 -22
  237. package/dist/src/local-first/Public.d.ts.map +0 -1
  238. package/dist/src/local-first/Public.js +0 -15
  239. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  240. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  241. package/dist/src/local-first/PublicKysely.js +0 -184
  242. package/dist/src/local-first/Sync.d.ts +0 -112
  243. package/dist/src/local-first/Sync.d.ts.map +0 -1
  244. package/dist/src/local-first/Sync.js +0 -526
  245. package/src/Instances.ts +0 -92
  246. package/src/Resources.ts +0 -367
  247. package/src/local-first/Platform.ts +0 -27
  248. package/src/local-first/Public.ts +0 -42
  249. package/src/local-first/PublicKysely.ts +0 -239
  250. package/src/local-first/Sync.ts +0 -949
@@ -20,7 +20,7 @@
20
20
  * transfer, ownership, real-time broadcasting, request-response semantics, and
21
21
  * error handling.
22
22
  *
23
- * ### Message structure
23
+ * ## Message structure
24
24
  *
25
25
  * | Field | Notes |
26
26
  * | :----------------------------- | :------------------------ |
@@ -43,7 +43,7 @@
43
43
  * | - {@link NonNegativeInt} | Number of ranges. |
44
44
  * | - {@link Range} | |
45
45
  *
46
- * ### WriteKey validation
46
+ * ## WriteKey validation
47
47
  *
48
48
  * The initiator sends a hasWriteKey flag and optionally a WriteKey. The
49
49
  * WriteKey is required when sending messages as a secure token proving the
@@ -52,7 +52,7 @@
52
52
  * validates the WriteKey immediately after parsing the initiator header, before
53
53
  * processing any messages or ranges.
54
54
  *
55
- * ### Synchronization
55
+ * ## Synchronization
56
56
  *
57
57
  * - **Messages**: Sends {@link EncryptedCrdtMessage}s in either direction.
58
58
  * - **Ranges**: Determines messages to sync. Usage varies by transport—e.g., sent
@@ -79,7 +79,7 @@
79
79
  * initiator. In relay-to-relay or P2P sync, both sides may require the
80
80
  * {@link OwnerWriteKey} depending on who is the initiator.
81
81
  *
82
- * ### Protocol errors
82
+ * ## Protocol errors
83
83
  *
84
84
  * The protocol uses error codes in the header to signal issues:
85
85
  *
@@ -94,7 +94,7 @@
94
94
  * All protocol errors except `ProtocolInvalidDataError` include the `OwnerId`
95
95
  * to allow clients to associate errors with the correct owner.
96
96
  *
97
- * ### Message size limit
97
+ * ## Message size limit
98
98
  *
99
99
  * The protocol enforces a strict maximum size for all messages, defined by
100
100
  * {@link ProtocolMessageMaxSize}. This ensures every {@link ProtocolMessage} is
@@ -107,7 +107,7 @@
107
107
  * message limit to ensure efficient sync with
108
108
  * {@link defaultProtocolMessageRangesMaxSize}.
109
109
  *
110
- * ### Why Binary?
110
+ * ## Why Binary?
111
111
  *
112
112
  * The protocol avoids JSON because:
113
113
  *
@@ -127,7 +127,7 @@
127
127
  * To avoid reinventing serialization where it’s unnecessary—like for JSON and
128
128
  * certain numbers—the Evolu Protocol relies on MessagePack.
129
129
  *
130
- * ### Versioning
130
+ * ## Versioning
131
131
  *
132
132
  * Evolu Protocol uses explicit versioning to ensure compatibility between
133
133
  * clients and relays (or peers). Each protocol message begins with a version
@@ -148,7 +148,7 @@
148
148
  * Version negotiation is per-owner, allowing Evolu Protocol to evolve safely
149
149
  * over time and provide clear feedback about version mismatches.
150
150
  *
151
- * ### Credible exit
151
+ * ## Credible exit
152
152
  *
153
153
  * The protocol specification is intentionally non-configurable to ensure
154
154
  * universal compatibility. This design allows applications (users) to switch
@@ -178,29 +178,36 @@
178
178
  */
179
179
 
180
180
  import { Packr } from "msgpackr";
181
- import { isNonEmptyReadonlyArray, NonEmptyReadonlyArray } from "../Array.js";
181
+ import { isNonEmptyArray, type NonEmptyReadonlyArray } from "../Array.js";
182
182
  import { assert } from "../Assert.js";
183
- import { Brand } from "../Brand.js";
183
+ import type { Brand } from "../Brand.js";
184
184
  import {
185
- Buffer,
185
+ type Buffer,
186
186
  bytesToHex,
187
187
  bytesToUtf8,
188
188
  createBuffer,
189
189
  hexToBytes,
190
190
  utf8ToBytes,
191
191
  } from "../Buffer.js";
192
+ import type { ConsoleDep } from "../Console.js";
192
193
  import {
193
194
  createPadmePadding,
195
+ decryptWithXChaCha20Poly1305,
196
+ type DecryptWithXChaCha20Poly1305Error,
194
197
  EncryptionKey,
195
- RandomBytesDep,
196
- SymmetricCryptoDecryptError,
197
- SymmetricCryptoDep,
198
+ encryptWithXChaCha20Poly1305,
199
+ Entropy24,
200
+ type RandomBytesDep,
201
+ XChaCha20Poly1305Ciphertext,
202
+ xChaCha20Poly1305NonceLength,
198
203
  } from "../Crypto.js";
199
204
  import { eqArrayNumber } from "../Eq.js";
200
205
  import { computeBalancedBuckets } from "../Number.js";
201
206
  import { createRecord, objectToEntries } from "../Object.js";
202
- import { err, ok, Result } from "../Result.js";
207
+ import { err, ok, type Result } from "../Result.js";
203
208
  import { SqliteValue } from "../Sqlite.js";
209
+ import type { Task } from "../Task.js";
210
+ import { Millis } from "../Time.js";
204
211
  import {
205
212
  Base64Url,
206
213
  base64UrlToUint8Array,
@@ -216,13 +223,16 @@ import {
216
223
  jsonToJsonValue,
217
224
  NonNegativeInt,
218
225
  Number,
226
+ onePositiveInt,
219
227
  PositiveInt,
228
+ type Typed,
220
229
  uint8ArrayToBase64Url,
230
+ zeroNonNegativeInt,
221
231
  } from "../Type.js";
222
- import { Predicate } from "../Types.js";
232
+ import type { Predicate } from "../Types.js";
223
233
  import {
224
- Owner,
225
- OwnerError,
234
+ type Owner,
235
+ type OwnerError,
226
236
  OwnerId,
227
237
  OwnerIdBytes,
228
238
  ownerIdToOwnerIdBytes,
@@ -230,26 +240,25 @@ import {
230
240
  ownerWriteKeyLength,
231
241
  } from "./Owner.js";
232
242
  import {
233
- BaseRange,
234
- CrdtMessage,
243
+ type BaseRange,
244
+ type CrdtMessage,
235
245
  DbChange,
236
- EncryptedCrdtMessage,
237
- EncryptedDbChange,
238
- Fingerprint,
239
- FingerprintRange,
246
+ type EncryptedCrdtMessage,
247
+ type EncryptedDbChange,
248
+ type Fingerprint,
249
+ type FingerprintRange,
240
250
  fingerprintSize,
241
251
  InfiniteUpperBound,
242
- Range,
252
+ type Range,
243
253
  RangeType,
244
- RangeUpperBound,
245
- SkipRange,
246
- StorageDep,
247
- TimestampsRange,
254
+ type RangeUpperBound,
255
+ type SkipRange,
256
+ type StorageDep,
257
+ type TimestampsRange,
248
258
  } from "./Storage.js";
249
259
  import {
250
260
  Counter,
251
261
  eqTimestamp,
252
- Millis,
253
262
  NodeId,
254
263
  Timestamp,
255
264
  TimestampBytes,
@@ -285,7 +294,7 @@ const maxProtocolMessageMaxSize = 100_000_000;
285
294
  * Increasing this value on the client side would break compatibility with
286
295
  * relays that enforce smaller limits.
287
296
  */
288
- export const ProtocolMessageMaxSize = between(
297
+ export const ProtocolMessageMaxSize = /*#__PURE__*/ between(
289
298
  minProtocolMessageMaxSize,
290
299
  maxProtocolMessageMaxSize,
291
300
  )(Int);
@@ -312,7 +321,10 @@ export const defaultProtocolMessageMaxSize =
312
321
  * {@link defaultProtocolMessageMaxSize}, maintaining compatibility between all
313
322
  * clients and relays.
314
323
  */
315
- export const ProtocolMessageRangesMaxSize = between(3_000, 100_000)(Int);
324
+ export const ProtocolMessageRangesMaxSize = /*#__PURE__*/ between(
325
+ 3_000,
326
+ 100_000,
327
+ )(Int);
316
328
  export type ProtocolMessageRangesMaxSize =
317
329
  typeof ProtocolMessageRangesMaxSize.Type;
318
330
 
@@ -329,7 +341,7 @@ export const defaultProtocolMessageRangesMaxSize =
329
341
  export type ProtocolMessage = Uint8Array & Brand<"ProtocolMessage">;
330
342
 
331
343
  /** Evolu Protocol version. */
332
- export const protocolVersion = NonNegativeInt.orThrow(1);
344
+ export const protocolVersion = onePositiveInt;
333
345
 
334
346
  export const MessageType = {
335
347
  /** Request message from initiator (client) to non-initiator (relay). */
@@ -342,6 +354,33 @@ export const MessageType = {
342
354
 
343
355
  export type MessageType = (typeof MessageType)[keyof typeof MessageType];
344
356
 
357
+ /** Parsed protocol header for supported protocol messages. */
358
+ export interface ProtocolHeader extends Typed<"ProtocolHeader"> {
359
+ readonly version: 1;
360
+ readonly ownerId: OwnerId;
361
+ readonly messageType: MessageType;
362
+ }
363
+
364
+ /**
365
+ * Parses the protocol header needed for routing.
366
+ *
367
+ * Every protocol message begins with `protocolVersion`, `ownerId`, and
368
+ * `messageType`.
369
+ */
370
+ export const parseProtocolHeader = (
371
+ inputMessage: Uint8Array,
372
+ ): Result<ProtocolHeader, ProtocolInvalidDataError> => {
373
+ try {
374
+ return ok(parseProtocolHeaderFromBuffer(createBuffer(inputMessage)));
375
+ } catch (error) {
376
+ return err<ProtocolInvalidDataError>({
377
+ type: "ProtocolInvalidDataError",
378
+ data: inputMessage,
379
+ error,
380
+ });
381
+ }
382
+ };
383
+
345
384
  export const SubscriptionFlags = {
346
385
  /** No subscription changes for this owner. */
347
386
  None: 0,
@@ -382,32 +421,29 @@ export type ProtocolError =
382
421
  * Represents a version mismatch in the Evolu Protocol. Occurs when the
383
422
  * initiator and non-initiator are using incompatible protocol versions.
384
423
  */
385
- export interface ProtocolVersionError extends OwnerError {
386
- readonly type: "ProtocolVersionError";
424
+ export interface ProtocolVersionError
425
+ extends OwnerError, Typed<"ProtocolVersionError"> {
387
426
  readonly version: NonNegativeInt;
388
427
  /** Indicates which side is obsolete and should update. */
389
428
  readonly isInitiator: boolean;
390
429
  }
391
430
 
392
431
  /** Error for invalid or corrupted protocol message data. */
393
- export interface ProtocolInvalidDataError {
394
- readonly type: "ProtocolInvalidDataError";
432
+ export interface ProtocolInvalidDataError extends Typed<"ProtocolInvalidDataError"> {
395
433
  readonly data: globalThis.Uint8Array;
396
434
  readonly error: unknown;
397
435
  }
398
436
 
399
437
  /** Error when a {@link OwnerWriteKey} is invalid, missing, or fails validation. */
400
- export interface ProtocolWriteKeyError extends OwnerError {
401
- readonly type: "ProtocolWriteKeyError";
402
- }
438
+ export interface ProtocolWriteKeyError
439
+ extends OwnerError, Typed<"ProtocolWriteKeyError"> {}
403
440
 
404
441
  /**
405
442
  * Error indicating a serious relay-side write failure. Clients should log this
406
443
  * error and show a generic sync error to the user.
407
444
  */
408
- export interface ProtocolWriteError extends OwnerError {
409
- readonly type: "ProtocolWriteError";
410
- }
445
+ export interface ProtocolWriteError
446
+ extends OwnerError, Typed<"ProtocolWriteError"> {}
411
447
 
412
448
  /**
413
449
  * Error when storage or billing quota is exceeded.
@@ -422,25 +458,22 @@ export interface ProtocolWriteError extends OwnerError {
422
458
  * plan. Quota monitoring and management is the relay provider's
423
459
  * responsibility.
424
460
  */
425
- export interface ProtocolQuotaError extends OwnerError {
426
- readonly type: "ProtocolQuotaError";
427
- }
461
+ export interface ProtocolQuotaError
462
+ extends OwnerError, Typed<"ProtocolQuotaError"> {}
428
463
 
429
464
  /**
430
465
  * Error indicating a serious relay-side synchronization failure. Clients should
431
466
  * log this error and show a generic sync error to the user.
432
467
  */
433
- export interface ProtocolSyncError extends OwnerError {
434
- readonly type: "ProtocolSyncError";
435
- }
468
+ export interface ProtocolSyncError
469
+ extends OwnerError, Typed<"ProtocolSyncError"> {}
436
470
 
437
471
  /**
438
472
  * Error when embedded timestamp doesn't match expected timestamp in
439
473
  * EncryptedDbChange. Indicates potential tampering or corruption of CRDT
440
474
  * messages.
441
475
  */
442
- export interface ProtocolTimestampMismatchError {
443
- readonly type: "ProtocolTimestampMismatchError";
476
+ export interface ProtocolTimestampMismatchError extends Typed<"ProtocolTimestampMismatchError"> {
444
477
  readonly expected: Timestamp;
445
478
  readonly timestamp: Timestamp;
446
479
  }
@@ -453,7 +486,7 @@ export interface ProtocolTimestampMismatchError {
453
486
  * unidirectional and stateless transports.
454
487
  */
455
488
  export const createProtocolMessageFromCrdtMessages =
456
- (deps: RandomBytesDep & SymmetricCryptoDep) =>
489
+ (deps: RandomBytesDep) =>
457
490
  (
458
491
  owner: Owner,
459
492
  messages: NonEmptyReadonlyArray<CrdtMessage>,
@@ -514,11 +547,8 @@ export const createProtocolMessageFromCrdtMessages =
514
547
 
515
548
  /** Creates a {@link ProtocolMessage} for sync. */
516
549
  export const createProtocolMessageForSync =
517
- (deps: StorageDep) =>
518
- (
519
- ownerId: OwnerId,
520
- subscriptionFlag?: SubscriptionFlag,
521
- ): ProtocolMessage | null => {
550
+ (deps: StorageDep & ConsoleDep) =>
551
+ (ownerId: OwnerId, subscriptionFlag?: SubscriptionFlag): ProtocolMessage => {
522
552
  const buffer = createProtocolMessageBuffer(ownerId, {
523
553
  messageType: MessageType.Request,
524
554
  subscriptionFlag: subscriptionFlag ?? SubscriptionFlags.None,
@@ -526,12 +556,10 @@ export const createProtocolMessageForSync =
526
556
  const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
527
557
 
528
558
  const size = deps.storage.getSize(ownerIdBytes);
529
- // Errors are handled by the storage.
530
- if (size == null) return null;
531
559
 
532
560
  splitRange(deps)(
533
561
  ownerIdBytes,
534
- NonNegativeInt.orThrow(0),
562
+ zeroNonNegativeInt,
535
563
  size,
536
564
  InfiniteUpperBound,
537
565
  buffer,
@@ -687,9 +715,8 @@ export const createProtocolMessageBuffer = (
687
715
  assert(isWithinSizeLimits(), "the message is too big");
688
716
  },
689
717
 
690
- canSplitRange: () => {
691
- return getRangesSize() + safeMargins.splitRange <= rangesMaxSize;
692
- },
718
+ canSplitRange: () =>
719
+ getRangesSize() + safeMargins.splitRange <= rangesMaxSize,
693
720
 
694
721
  canAddTimestampsRangeAndMessage: (timestamps, message) => {
695
722
  const rangesNewSize =
@@ -790,7 +817,7 @@ export interface TimestampsBuffer {
790
817
  }
791
818
 
792
819
  export const createTimestampsBuffer = (): TimestampsBuffer => {
793
- let count = NonNegativeInt.orThrow(0);
820
+ let count = zeroNonNegativeInt;
794
821
  const countBuffer = createBuffer();
795
822
 
796
823
  const syncCount = () => {
@@ -857,9 +884,9 @@ const createRunLengthEncoder = <T>(
857
884
  encodeValue: (buffer: Buffer, value: T) => void,
858
885
  ): RunLengthEncoder<T> => {
859
886
  const buffer = createBuffer();
860
- let previousLength = NonNegativeInt.orThrow(0);
887
+ let previousLength = zeroNonNegativeInt;
861
888
  let previousValue = null as T | null;
862
- let runLength = NonNegativeInt.orThrow(0);
889
+ let runLength = zeroNonNegativeInt;
863
890
 
864
891
  return {
865
892
  add: (value) => {
@@ -882,7 +909,7 @@ const createRunLengthEncoder = <T>(
882
909
  };
883
910
 
884
911
  export interface ApplyProtocolMessageAsClientOptions {
885
- getWriteKey?: (ownerId: OwnerId) => OwnerWriteKey | null;
912
+ writeKey?: OwnerWriteKey;
886
913
 
887
914
  rangesMaxSize?: ProtocolMessageRangesMaxSize;
888
915
 
@@ -894,27 +921,35 @@ export interface ApplyProtocolMessageAsClientOptions {
894
921
  * Result type for {@link applyProtocolMessageAsClient} that distinguishes
895
922
  * between responses to client requests and broadcast messages.
896
923
  */
924
+ export interface ApplyProtocolMessageAsClientResponse extends Typed<"Response"> {
925
+ readonly message: ProtocolMessage;
926
+ }
927
+
928
+ export interface ApplyProtocolMessageAsClientNoResponse extends Typed<"NoResponse"> {}
929
+
930
+ export interface ApplyProtocolMessageAsClientBroadcast extends Typed<"Broadcast"> {}
931
+
897
932
  export type ApplyProtocolMessageAsClientResult =
898
- | { readonly type: "response"; readonly message: ProtocolMessage }
899
- | { readonly type: "no-response" }
900
- | { readonly type: "broadcast" };
933
+ | ApplyProtocolMessageAsClientResponse
934
+ | ApplyProtocolMessageAsClientNoResponse
935
+ | ApplyProtocolMessageAsClientBroadcast;
901
936
 
902
937
  export const applyProtocolMessageAsClient =
903
- (deps: StorageDep) =>
904
- async (
938
+ (
905
939
  inputMessage: Uint8Array,
906
940
  options: ApplyProtocolMessageAsClientOptions = {},
907
- ): Promise<
908
- Result<
909
- ApplyProtocolMessageAsClientResult,
910
- | ProtocolInvalidDataError
911
- | ProtocolSyncError
912
- | ProtocolVersionError
913
- | ProtocolWriteError
914
- | ProtocolWriteKeyError
915
- | ProtocolQuotaError
916
- >
917
- > => {
941
+ ): Task<
942
+ ApplyProtocolMessageAsClientResult,
943
+ | ProtocolInvalidDataError
944
+ | ProtocolSyncError
945
+ | ProtocolVersionError
946
+ | ProtocolWriteError
947
+ | ProtocolWriteKeyError
948
+ | ProtocolQuotaError,
949
+ StorageDep
950
+ > =>
951
+ async (run) => {
952
+ const { storage } = run.deps;
918
953
  try {
919
954
  const input = createBuffer(inputMessage);
920
955
  const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
@@ -971,10 +1006,17 @@ export const applyProtocolMessageAsClient =
971
1006
  const messages = decodeMessages(input);
972
1007
  const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
973
1008
 
974
- if (isNonEmptyReadonlyArray(messages)) {
975
- const result = await deps.storage.writeMessages(ownerIdBytes, messages);
976
- // Errors are handled by the Storage. Here we just stop syncing.
977
- if (!result.ok) return ok({ type: "no-response" });
1009
+ if (isNonEmptyArray(messages)) {
1010
+ try {
1011
+ const result = await run(
1012
+ storage.writeMessages(ownerIdBytes, messages),
1013
+ );
1014
+ // Quota errors are handled by Storage; protocol just stops syncing.
1015
+ if (!result.ok) return ok({ type: "NoResponse" });
1016
+ } catch (error) {
1017
+ run.deps.console.error(error);
1018
+ return ok({ type: "NoResponse" });
1019
+ }
978
1020
  }
979
1021
 
980
1022
  // Now: No writeKey, no sync.
@@ -982,19 +1024,19 @@ export const applyProtocolMessageAsClient =
982
1024
  // Without local changes, writeKey will not be required.
983
1025
  // With local changes, writeKey will be required and if not provided,
984
1026
  // the sync will stop.
985
- const writeKey = options.getWriteKey?.(ownerId);
1027
+ const writeKey = options.writeKey;
986
1028
  if (writeKey == null) {
987
- return ok({ type: "no-response" });
1029
+ return ok({ type: "NoResponse" });
988
1030
  }
989
1031
 
990
1032
  if (messageType === MessageType.Broadcast) {
991
- return ok({ type: "broadcast" });
1033
+ return ok({ type: "Broadcast" });
992
1034
  }
993
1035
 
994
1036
  const ranges = decodeRanges(input);
995
1037
 
996
- if (!isNonEmptyReadonlyArray(ranges)) {
997
- return ok({ type: "no-response" });
1038
+ if (!isNonEmptyArray(ranges)) {
1039
+ return ok({ type: "NoResponse" });
998
1040
  }
999
1041
 
1000
1042
  const output = createProtocolMessageBuffer(ownerId, {
@@ -1003,14 +1045,14 @@ export const applyProtocolMessageAsClient =
1003
1045
  rangesMaxSize: options.rangesMaxSize,
1004
1046
  });
1005
1047
 
1006
- const result = sync(deps)(ranges, output, ownerIdBytes);
1048
+ const result = sync(run.deps)(ranges, output, ownerIdBytes);
1007
1049
 
1008
1050
  // Client sync error (handled via Storage) or no changes.
1009
1051
  if (!result.ok || !result.value) {
1010
- return ok({ type: "no-response" });
1052
+ return ok({ type: "NoResponse" });
1011
1053
  }
1012
1054
 
1013
- return ok({ type: "response", message: output.unwrap() });
1055
+ return ok({ type: "Response", message: output.unwrap() });
1014
1056
  } catch (error) {
1015
1057
  return err<ProtocolInvalidDataError>({
1016
1058
  type: "ProtocolInvalidDataError",
@@ -1043,21 +1085,23 @@ export interface ApplyProtocolMessageAsRelayOptions {
1043
1085
  * to sync. Clients may choose not to respond in certain cases (like when they
1044
1086
  * receive broadcast messages or when they lack a write key for syncing).
1045
1087
  */
1046
- export interface ApplyProtocolMessageAsRelayResult {
1047
- readonly type: "response";
1088
+ export interface ApplyProtocolMessageAsRelayResult extends Typed<"Response"> {
1048
1089
  readonly message: ProtocolMessage;
1049
1090
  }
1050
1091
 
1051
1092
  export const applyProtocolMessageAsRelay =
1052
- (deps: StorageDep) =>
1053
- async (
1093
+ (
1054
1094
  inputMessage: Uint8Array,
1055
1095
  options: ApplyProtocolMessageAsRelayOptions = {},
1056
1096
  /** For tests only. */
1057
- version = protocolVersion,
1058
- ): Promise<
1059
- Result<ApplyProtocolMessageAsRelayResult, ProtocolInvalidDataError>
1060
- > => {
1097
+ version: NonNegativeInt = protocolVersion,
1098
+ ): Task<
1099
+ ApplyProtocolMessageAsRelayResult,
1100
+ ProtocolInvalidDataError,
1101
+ StorageDep
1102
+ > =>
1103
+ async (run) => {
1104
+ const { storage } = run.deps;
1061
1105
  try {
1062
1106
  const input = createBuffer(inputMessage);
1063
1107
  const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
@@ -1069,7 +1113,7 @@ export const applyProtocolMessageAsRelay =
1069
1113
  encodeNonNegativeInt(output, version);
1070
1114
  output.extend(ownerIdBytes);
1071
1115
  return ok({
1072
- type: "response",
1116
+ type: "Response",
1073
1117
  message: output.unwrap() as ProtocolMessage,
1074
1118
  });
1075
1119
  }
@@ -1098,10 +1142,10 @@ export const applyProtocolMessageAsRelay =
1098
1142
  }
1099
1143
 
1100
1144
  if (writeKey) {
1101
- const isValid = deps.storage.validateWriteKey(ownerIdBytes, writeKey);
1145
+ const isValid = storage.validateWriteKey(ownerIdBytes, writeKey);
1102
1146
  if (!isValid) {
1103
1147
  return ok({
1104
- type: "response",
1148
+ type: "Response",
1105
1149
  message: createProtocolMessageBuffer(ownerId, {
1106
1150
  messageType: MessageType.Response,
1107
1151
  errorCode: ProtocolErrorCode.WriteKeyError,
@@ -1112,10 +1156,10 @@ export const applyProtocolMessageAsRelay =
1112
1156
 
1113
1157
  const messages = decodeMessages(input);
1114
1158
 
1115
- if (isNonEmptyReadonlyArray(messages)) {
1159
+ if (isNonEmptyArray(messages)) {
1116
1160
  if (!writeKey) {
1117
1161
  return ok({
1118
- type: "response",
1162
+ type: "Response",
1119
1163
  message: createProtocolMessageBuffer(ownerId, {
1120
1164
  messageType: MessageType.Response,
1121
1165
  errorCode: ProtocolErrorCode.WriteKeyError,
@@ -1123,18 +1167,25 @@ export const applyProtocolMessageAsRelay =
1123
1167
  });
1124
1168
  }
1125
1169
 
1126
- const result = await deps.storage.writeMessages(ownerIdBytes, messages);
1170
+ try {
1171
+ const result = await run(
1172
+ storage.writeMessages(ownerIdBytes, messages),
1173
+ );
1127
1174
 
1128
- if (!result.ok) {
1129
- const errorCode =
1130
- result.error.type === "StorageWriteError"
1131
- ? ProtocolErrorCode.WriteError
1132
- : ProtocolErrorCode.QuotaError;
1175
+ if (!result.ok) {
1176
+ const message = createProtocolMessageBuffer(ownerId, {
1177
+ messageType: MessageType.Response,
1178
+ errorCode: ProtocolErrorCode.QuotaError,
1179
+ }).unwrap();
1180
+ return ok({ type: "Response", message });
1181
+ }
1182
+ } catch (error) {
1183
+ run.deps.console.error(error);
1133
1184
  const message = createProtocolMessageBuffer(ownerId, {
1134
1185
  messageType: MessageType.Response,
1135
- errorCode,
1186
+ errorCode: ProtocolErrorCode.WriteError,
1136
1187
  }).unwrap();
1137
- return ok({ type: "response", message });
1188
+ return ok({ type: "Response", message });
1138
1189
  }
1139
1190
 
1140
1191
  /**
@@ -1179,11 +1230,11 @@ export const applyProtocolMessageAsRelay =
1179
1230
 
1180
1231
  // Non-initiators always respond to provide sync completion feedback,
1181
1232
  // even when there's nothing to sync.
1182
- if (!isNonEmptyReadonlyArray(ranges)) {
1183
- return ok({ type: "response", message: output.unwrap() });
1233
+ if (!isNonEmptyArray(ranges)) {
1234
+ return ok({ type: "Response", message: output.unwrap() });
1184
1235
  }
1185
1236
 
1186
- const result = sync(deps)(ranges, output, ownerIdBytes);
1237
+ const result = sync(run.deps)(ranges, output, ownerIdBytes);
1187
1238
 
1188
1239
  const message = result.ok
1189
1240
  ? output.unwrap()
@@ -1193,7 +1244,7 @@ export const applyProtocolMessageAsRelay =
1193
1244
  }).unwrap();
1194
1245
 
1195
1246
  // Non-initiators always respond to provide sync completion feedback,
1196
- return ok({ type: "response", message });
1247
+ return ok({ type: "Response", message });
1197
1248
  } catch (error) {
1198
1249
  return err<ProtocolInvalidDataError>({
1199
1250
  type: "ProtocolInvalidDataError",
@@ -1213,6 +1264,38 @@ const decodeVersionAndOwner = (input: Buffer): [NonNegativeInt, OwnerId] => {
1213
1264
  return [version, ownerId];
1214
1265
  };
1215
1266
 
1267
+ const parseProtocolHeaderFromBuffer = (input: Buffer): ProtocolHeader => {
1268
+ const [version, ownerId] = decodeVersionAndOwner(input);
1269
+
1270
+ if (version !== protocolVersion) {
1271
+ throw new ProtocolDecodeError(`Unsupported protocol version: ${version}`);
1272
+ }
1273
+
1274
+ const messageTypeValue = input.shift();
1275
+ let messageType: MessageType;
1276
+
1277
+ switch (messageTypeValue) {
1278
+ case MessageType.Request:
1279
+ messageType = MessageType.Request;
1280
+ break;
1281
+ case MessageType.Response:
1282
+ messageType = MessageType.Response;
1283
+ break;
1284
+ case MessageType.Broadcast:
1285
+ messageType = MessageType.Broadcast;
1286
+ break;
1287
+ default:
1288
+ throw new ProtocolDecodeError("Invalid MessageType");
1289
+ }
1290
+
1291
+ return {
1292
+ type: "ProtocolHeader",
1293
+ version: 1,
1294
+ ownerId,
1295
+ messageType,
1296
+ };
1297
+ };
1298
+
1216
1299
  /**
1217
1300
  * Error thrown for internal protocol validation failures, such as invalid data
1218
1301
  * or type errors.
@@ -1231,30 +1314,35 @@ const decodeMessages = (
1231
1314
  ): ReadonlyArray<EncryptedCrdtMessage> => {
1232
1315
  const timestamps = decodeTimestamps(buffer);
1233
1316
 
1234
- const messages: Array<EncryptedCrdtMessage> = [];
1235
- for (const timestamp of timestamps) {
1317
+ const messages = new Array<EncryptedCrdtMessage>(timestamps.length);
1318
+ for (let i = 0; i < timestamps.length; i++) {
1319
+ const timestamp = timestamps[i];
1236
1320
  const changeLength = decodeLength(buffer);
1237
1321
  const change = buffer.shiftN(changeLength) as EncryptedDbChange;
1238
- messages.push({ timestamp, change });
1322
+ messages[i] = { timestamp, change };
1239
1323
  }
1240
1324
 
1241
1325
  return messages;
1242
1326
  };
1243
1327
 
1244
1328
  const sync =
1245
- (deps: StorageDep) =>
1329
+ (deps: StorageDep & ConsoleDep) =>
1246
1330
  (
1247
1331
  ranges: NonEmptyReadonlyArray<Range>,
1248
1332
  output: ProtocolMessageBuffer,
1249
1333
  ownerIdBytes: OwnerIdBytes,
1250
1334
  ): Result<boolean, typeof ProtocolErrorCode.SyncError> => {
1251
1335
  const outputInitialSize = output.getSize();
1252
-
1253
- const storageSize = deps.storage.getSize(ownerIdBytes);
1254
- if (storageSize == null) return err(ProtocolErrorCode.SyncError);
1336
+ let storageSize: NonNegativeInt;
1337
+ try {
1338
+ storageSize = deps.storage.getSize(ownerIdBytes);
1339
+ } catch (error) {
1340
+ deps.console.error(error);
1341
+ return err(ProtocolErrorCode.SyncError);
1342
+ }
1255
1343
 
1256
1344
  let prevUpperBound: RangeUpperBound | null = null;
1257
- let prevIndex = NonNegativeInt.orThrow(0);
1345
+ let prevIndex = zeroNonNegativeInt;
1258
1346
 
1259
1347
  let skip = false;
1260
1348
  let nonSkipRangeAdded = false;
@@ -1291,12 +1379,17 @@ const sync =
1291
1379
  const addFingerprintForRemainingRange = (
1292
1380
  begin: NonNegativeInt,
1293
1381
  ): boolean => {
1294
- const fingerprint = deps.storage.fingerprint(
1295
- ownerIdBytes,
1296
- begin,
1297
- storageSize,
1298
- );
1299
- if (!fingerprint) return false;
1382
+ let fingerprint: Fingerprint;
1383
+ try {
1384
+ fingerprint = deps.storage.fingerprint(
1385
+ ownerIdBytes,
1386
+ begin,
1387
+ storageSize,
1388
+ );
1389
+ } catch (error) {
1390
+ deps.console.error(error);
1391
+ return false;
1392
+ }
1300
1393
  // There is always a space for a ramaining range.
1301
1394
  output.addRange({
1302
1395
  type: RangeType.Fingerprint,
@@ -1310,13 +1403,18 @@ const sync =
1310
1403
  const currentUpperBound = range.upperBound;
1311
1404
 
1312
1405
  const lower = prevIndex;
1313
- let upper = deps.storage.findLowerBound(
1314
- ownerIdBytes,
1315
- prevIndex,
1316
- storageSize,
1317
- currentUpperBound,
1318
- );
1319
- if (upper == null) return err(ProtocolErrorCode.SyncError);
1406
+ let upper: NonNegativeInt;
1407
+ try {
1408
+ upper = deps.storage.findLowerBound(
1409
+ ownerIdBytes,
1410
+ prevIndex,
1411
+ storageSize,
1412
+ currentUpperBound,
1413
+ );
1414
+ } catch (error) {
1415
+ deps.console.error(error);
1416
+ return err(ProtocolErrorCode.SyncError);
1417
+ }
1320
1418
 
1321
1419
  switch (range.type) {
1322
1420
  case RangeType.Skip: {
@@ -1325,12 +1423,17 @@ const sync =
1325
1423
  }
1326
1424
 
1327
1425
  case RangeType.Fingerprint: {
1328
- const ourFingerprint = deps.storage.fingerprint(
1329
- ownerIdBytes,
1330
- lower,
1331
- upper,
1332
- );
1333
- if (ourFingerprint == null) return err(ProtocolErrorCode.SyncError);
1426
+ let ourFingerprint: Fingerprint;
1427
+ try {
1428
+ ourFingerprint = deps.storage.fingerprint(
1429
+ ownerIdBytes,
1430
+ lower,
1431
+ upper,
1432
+ );
1433
+ } catch (error) {
1434
+ deps.console.error(error);
1435
+ return err(ProtocolErrorCode.SyncError);
1436
+ }
1334
1437
 
1335
1438
  if (eqArrayNumber(range.fingerprint, ourFingerprint)) {
1336
1439
  skipRange(range);
@@ -1361,52 +1464,61 @@ const sync =
1361
1464
  );
1362
1465
  const ourTimestamps = createTimestampsBuffer();
1363
1466
 
1364
- let cantReadDbChange = false as boolean;
1365
1467
  let exceeded = false as boolean;
1468
+ let iterateFailed = false as boolean;
1469
+
1470
+ try {
1471
+ deps.storage.iterate(
1472
+ ownerIdBytes,
1473
+ lower,
1474
+ upper,
1475
+ (timestamp, index) => {
1476
+ const timestampString = timestamp.join();
1477
+ const timestampBinary = timestampBytesToTimestamp(timestamp);
1478
+
1479
+ let message: EncryptedCrdtMessage | null = null;
1480
+
1481
+ if (timestampsWeNeed.has(timestampString)) {
1482
+ timestampsWeNeed.delete(timestampString);
1483
+ } else {
1484
+ try {
1485
+ message = {
1486
+ timestamp: timestampBinary,
1487
+ change: deps.storage.readDbChange(
1488
+ ownerIdBytes,
1489
+ timestamp,
1490
+ ),
1491
+ };
1492
+ } catch (error) {
1493
+ deps.console.error(error);
1494
+ iterateFailed = true;
1495
+ return false;
1496
+ }
1497
+ }
1366
1498
 
1367
- deps.storage.iterate(
1368
- ownerIdBytes,
1369
- lower,
1370
- upper,
1371
- (timestamp, index) => {
1372
- const timestampString = timestamp.join();
1373
- const timestampBinary = timestampBytesToTimestamp(timestamp);
1374
-
1375
- let message: EncryptedCrdtMessage | null = null;
1376
-
1377
- if (timestampsWeNeed.has(timestampString)) {
1378
- timestampsWeNeed.delete(timestampString);
1379
- } else {
1380
- const dbChange = deps.storage.readDbChange(
1381
- ownerIdBytes,
1382
- timestamp,
1383
- );
1384
- if (dbChange == null) {
1385
- cantReadDbChange = true;
1499
+ if (
1500
+ !output.canAddTimestampsRangeAndMessage(
1501
+ ourTimestamps,
1502
+ message,
1503
+ )
1504
+ ) {
1505
+ exceeded = true;
1506
+ endBound = timestamp;
1507
+ upper = index;
1386
1508
  return false;
1387
1509
  }
1388
- message = {
1389
- timestamp: timestampBinary,
1390
- change: dbChange,
1391
- };
1392
- }
1393
-
1394
- if (
1395
- !output.canAddTimestampsRangeAndMessage(ourTimestamps, message)
1396
- ) {
1397
- exceeded = true;
1398
- endBound = timestamp;
1399
- upper = index;
1400
- return false;
1401
- }
1402
-
1403
- ourTimestamps.add(timestampBinary);
1404
- if (message) output.addMessage(message);
1405
- return true;
1406
- },
1407
- );
1408
1510
 
1409
- if (cantReadDbChange) {
1511
+ ourTimestamps.add(timestampBinary);
1512
+ if (message) output.addMessage(message);
1513
+ return true;
1514
+ },
1515
+ );
1516
+ } catch (error) {
1517
+ deps.console.error(error);
1518
+ return err(ProtocolErrorCode.SyncError);
1519
+ }
1520
+
1521
+ if (iterateFailed) {
1410
1522
  return err(ProtocolErrorCode.SyncError);
1411
1523
  }
1412
1524
 
@@ -1449,7 +1561,7 @@ const sync =
1449
1561
  };
1450
1562
 
1451
1563
  const splitRange =
1452
- (deps: StorageDep) =>
1564
+ (deps: StorageDep & ConsoleDep) =>
1453
1565
  (
1454
1566
  ownerId: OwnerIdBytes,
1455
1567
  lower: NonNegativeInt,
@@ -1469,7 +1581,7 @@ const splitRange =
1469
1581
 
1470
1582
  deps.storage.iterate(
1471
1583
  ownerId,
1472
- NonNegativeInt.orThrow(0),
1584
+ zeroNonNegativeInt,
1473
1585
  itemCount,
1474
1586
  (timestamp) => {
1475
1587
  range.timestamps.add(timestampBytesToTimestamp(timestamp));
@@ -1490,13 +1602,17 @@ const splitRange =
1490
1602
  ...buckets.value.map((b) => NonNegativeInt.orThrow(b + lower)),
1491
1603
  ];
1492
1604
 
1493
- const fingerprintRanges = deps.storage.fingerprintRanges(
1494
- ownerId,
1495
- fingerprintRangesBuckets,
1496
- upperBound,
1497
- );
1498
- // Errors are handled by the storage.
1499
- if (fingerprintRanges == null) return;
1605
+ let fingerprintRanges: ReadonlyArray<FingerprintRange>;
1606
+ try {
1607
+ fingerprintRanges = deps.storage.fingerprintRanges(
1608
+ ownerId,
1609
+ fingerprintRangesBuckets,
1610
+ upperBound,
1611
+ );
1612
+ } catch (error) {
1613
+ deps.console.error(error);
1614
+ return;
1615
+ }
1500
1616
 
1501
1617
  const rangesToUse =
1502
1618
  lower > 0 ? fingerprintRanges.slice(1) : fingerprintRanges;
@@ -1514,7 +1630,8 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1514
1630
 
1515
1631
  const timestampsCount = NonNegativeInt.orThrow(rangesCount - 1);
1516
1632
  const timestamps = decodeTimestamps(buffer, timestampsCount);
1517
- const rangeTypes: Array<RangeType> = [];
1633
+
1634
+ const rangeTypes = new Array<RangeType>(rangesCount);
1518
1635
 
1519
1636
  for (let i = 0; i < rangesCount; i++) {
1520
1637
  const rangeType = decodeNonNegativeInt(buffer);
@@ -1522,14 +1639,14 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1522
1639
  case RangeType.Fingerprint:
1523
1640
  case RangeType.Skip:
1524
1641
  case RangeType.Timestamps:
1525
- rangeTypes.push(rangeType as RangeType);
1642
+ rangeTypes[i] = rangeType as RangeType;
1526
1643
  break;
1527
1644
  default:
1528
1645
  throw new ProtocolDecodeError(`Invalid RangeType: ${rangeType}`);
1529
1646
  }
1530
1647
  }
1531
1648
 
1532
- const ranges: Array<Range> = [];
1649
+ const ranges = new Array<Range>(rangesCount);
1533
1650
 
1534
1651
  for (let i = 0; i < rangesCount; i++) {
1535
1652
  const upperBound =
@@ -1541,16 +1658,16 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1541
1658
 
1542
1659
  switch (rangeType) {
1543
1660
  case RangeType.Skip:
1544
- ranges.push({ type: RangeType.Skip, upperBound });
1661
+ ranges[i] = { type: RangeType.Skip, upperBound };
1545
1662
  break;
1546
1663
 
1547
1664
  case RangeType.Fingerprint: {
1548
1665
  const fingerprint = buffer.shiftN(fingerprintSize) as Fingerprint;
1549
- ranges.push({
1666
+ ranges[i] = {
1550
1667
  type: RangeType.Fingerprint,
1551
1668
  upperBound,
1552
1669
  fingerprint,
1553
- });
1670
+ };
1554
1671
  break;
1555
1672
  }
1556
1673
 
@@ -1558,11 +1675,11 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1558
1675
  const timestamps = decodeTimestamps(buffer).map(
1559
1676
  timestampToTimestampBytes,
1560
1677
  );
1561
- ranges.push({
1678
+ ranges[i] = {
1562
1679
  type: RangeType.Timestamps,
1563
1680
  upperBound,
1564
1681
  timestamps,
1565
- });
1682
+ };
1566
1683
  break;
1567
1684
  }
1568
1685
  }
@@ -1571,7 +1688,6 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1571
1688
  return ranges;
1572
1689
  };
1573
1690
 
1574
- /** Decodes an array of sorted timestamps with delta-encoded millis. */
1575
1691
  const decodeTimestamps = (
1576
1692
  buffer: Buffer,
1577
1693
  length?: NonNegativeInt,
@@ -1580,50 +1696,68 @@ const decodeTimestamps = (
1580
1696
 
1581
1697
  let previousMillis = 0 as Millis;
1582
1698
 
1583
- const millises: Array<Millis> = [];
1699
+ const millises = new Array<Millis>(length);
1584
1700
  for (let i = 0; i < length; i++) {
1585
1701
  const deltaMillis = decodeNonNegativeInt(buffer);
1586
1702
  const millis = Millis.from(previousMillis + deltaMillis);
1587
1703
  if (!millis.ok) throw new ProtocolDecodeError(millis.error.type);
1588
- millises.push(millis.value);
1704
+ millises[i] = millis.value;
1589
1705
  previousMillis = millis.value;
1590
1706
  }
1591
1707
 
1592
- const counters: Array<Counter> = [];
1593
- let counterIndex = 0;
1594
- while (counterIndex < length) {
1708
+ const counters = decodeRle(buffer, length, (): Counter => {
1595
1709
  const counter = Counter.from(decodeNonNegativeInt(buffer));
1596
1710
  if (!counter.ok) throw new ProtocolDecodeError(counter.error.type);
1597
- const runLength = decodeNonNegativeInt(buffer);
1598
- for (let i = 0; i < runLength; i++) {
1599
- counters.push(counter.value);
1600
- counterIndex++;
1601
- }
1602
- }
1711
+ return counter.value;
1712
+ });
1603
1713
 
1604
- const nodeIds: Array<NodeId> = [];
1605
- let nodeIdIndex = 0;
1606
- while (nodeIdIndex < length) {
1607
- const nodeId = decodeNodeId(buffer);
1608
- const runLength = decodeNonNegativeInt(buffer);
1609
- for (let i = 0; i < runLength; i++) {
1610
- nodeIds.push(nodeId);
1611
- nodeIdIndex++;
1612
- }
1613
- }
1714
+ const nodeIds = decodeRle(buffer, length, (): NodeId => decodeNodeId(buffer));
1614
1715
 
1615
- const timestamps: Array<Timestamp> = [];
1716
+ const timestamps = new Array<Timestamp>(length);
1616
1717
  for (let i = 0; i < length; i++) {
1617
- timestamps.push({
1718
+ timestamps[i] = {
1618
1719
  millis: millises[i],
1619
1720
  counter: counters[i],
1620
1721
  nodeId: nodeIds[i],
1621
- });
1722
+ };
1622
1723
  }
1623
1724
 
1624
1725
  return timestamps;
1625
1726
  };
1626
1727
 
1728
+ export const decodeRle = <T>(
1729
+ buffer: Buffer,
1730
+ length: NonNegativeInt,
1731
+ decodeValue: () => T,
1732
+ ): ReadonlyArray<T> => {
1733
+ const values = new Array<T>(length);
1734
+ let index = 0;
1735
+ while (index < length) {
1736
+ const value = decodeValue();
1737
+ const runLength = decodeNonNegativeInt(buffer);
1738
+
1739
+ // Prevent infinite loop on malformed input.
1740
+ if (runLength === 0) {
1741
+ throw new ProtocolDecodeError(
1742
+ "Invalid RLE encoding: runLength must be positive",
1743
+ );
1744
+ }
1745
+ const remaining = length - index;
1746
+
1747
+ // Prevent CPU/memory amplification via oversized runLength.
1748
+ if (runLength > remaining) {
1749
+ throw new ProtocolDecodeError(
1750
+ `Invalid RLE encoding: runLength ${runLength} exceeds remaining ${remaining}`,
1751
+ );
1752
+ }
1753
+ for (let i = 0; i < runLength; i++) {
1754
+ values[index] = value;
1755
+ index++;
1756
+ }
1757
+ }
1758
+ return values;
1759
+ };
1760
+
1627
1761
  const decodeId = (buffer: Buffer): Id => {
1628
1762
  const bytes = buffer.shiftN(idBytesTypeValueLength);
1629
1763
  return idBytesToId(bytes as IdBytes);
@@ -1699,9 +1833,10 @@ export const decodeFlags = (
1699
1833
  count: PositiveInt,
1700
1834
  ): ReadonlyArray<boolean> => {
1701
1835
  const byte = buffer.shift();
1702
- const flags: Array<boolean> = [];
1703
- for (let i = 0; i < count && i < 8; i++) {
1704
- flags.push((byte & (1 << i)) !== 0);
1836
+ const length = globalThis.Math.min(count, 8);
1837
+ const flags = new Array<boolean>(length);
1838
+ for (let i = 0; i < length; i++) {
1839
+ flags[i] = (byte & (1 << i)) !== 0;
1705
1840
  }
1706
1841
  return flags;
1707
1842
  };
@@ -1715,7 +1850,7 @@ export const decodeFlags = (
1715
1850
  * data.
1716
1851
  */
1717
1852
  export const encodeAndEncryptDbChange =
1718
- (deps: SymmetricCryptoDep) =>
1853
+ (deps: RandomBytesDep) =>
1719
1854
  (message: CrdtMessage, key: EncryptionKey): EncryptedDbChange => {
1720
1855
  const buffer = createBuffer();
1721
1856
 
@@ -1727,6 +1862,7 @@ export const encodeAndEncryptDbChange =
1727
1862
 
1728
1863
  encodeFlags(buffer, [
1729
1864
  message.change.isInsert,
1865
+ // Encode nullable boolean as two flags: presence + value.
1730
1866
  message.change.isDelete != null,
1731
1867
  message.change.isDelete ?? false,
1732
1868
  ]);
@@ -1745,7 +1881,7 @@ export const encodeAndEncryptDbChange =
1745
1881
  // Add PADMÉ padding (ignored during decoding)
1746
1882
  buffer.extend(createPadmePadding(buffer.getLength()));
1747
1883
 
1748
- const { nonce, ciphertext } = deps.symmetricCrypto.encrypt(
1884
+ const [ciphertext, nonce] = encryptWithXChaCha20Poly1305(deps)(
1749
1885
  buffer.unwrap(),
1750
1886
  key,
1751
1887
  );
@@ -1763,78 +1899,76 @@ export const encodeAndEncryptDbChange =
1763
1899
  * owner's encryption key. Verifies that the embedded timestamp matches the
1764
1900
  * expected timestamp to ensure message integrity.
1765
1901
  */
1766
- export const decryptAndDecodeDbChange =
1767
- (deps: SymmetricCryptoDep) =>
1768
- (
1769
- message: EncryptedCrdtMessage,
1770
- key: EncryptionKey,
1771
- ): Result<
1772
- DbChange,
1773
- | SymmetricCryptoDecryptError
1774
- | ProtocolInvalidDataError
1775
- | ProtocolTimestampMismatchError
1776
- > => {
1777
- try {
1778
- const buffer = createBuffer(message.change);
1902
+ export const decryptAndDecodeDbChange = (
1903
+ message: EncryptedCrdtMessage,
1904
+ key: EncryptionKey,
1905
+ ): Result<
1906
+ DbChange,
1907
+ | DecryptWithXChaCha20Poly1305Error
1908
+ | ProtocolInvalidDataError
1909
+ | ProtocolTimestampMismatchError
1910
+ > => {
1911
+ try {
1912
+ const buffer = createBuffer(message.change);
1779
1913
 
1780
- const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
1781
- const ciphertext = buffer.shiftN(decodeLength(buffer));
1914
+ const nonce = buffer.shiftN(xChaCha20Poly1305NonceLength as NonNegativeInt);
1915
+ const ciphertext = buffer.shiftN(decodeLength(buffer));
1782
1916
 
1783
- const plaintextBytes = deps.symmetricCrypto.decrypt(
1784
- ciphertext,
1785
- key,
1786
- nonce,
1787
- );
1788
- if (!plaintextBytes.ok) return plaintextBytes;
1917
+ const plaintextBytes = decryptWithXChaCha20Poly1305(
1918
+ XChaCha20Poly1305Ciphertext.orThrow(ciphertext),
1919
+ Entropy24.orThrow(nonce),
1920
+ key,
1921
+ );
1922
+ if (!plaintextBytes.ok) return plaintextBytes;
1789
1923
 
1790
- buffer.reset();
1791
- buffer.extend(plaintextBytes.value);
1924
+ buffer.reset();
1925
+ buffer.extend(plaintextBytes.value);
1792
1926
 
1793
- // Decode version (for future compatibility, not need yet)
1794
- decodeNonNegativeInt(buffer);
1927
+ // Decode version (for future compatibility, not need yet)
1928
+ decodeNonNegativeInt(buffer);
1795
1929
 
1796
- const timestamp = timestampBytesToTimestamp(
1797
- buffer.shiftN(timestampBytesLength) as TimestampBytes,
1798
- );
1930
+ const timestamp = timestampBytesToTimestamp(
1931
+ TimestampBytes.orThrow(buffer.shiftN(timestampBytesLength)),
1932
+ );
1799
1933
 
1800
- if (!eqTimestamp(timestamp, message.timestamp)) {
1801
- return err<ProtocolTimestampMismatchError>({
1802
- type: "ProtocolTimestampMismatchError",
1803
- expected: message.timestamp,
1804
- timestamp,
1805
- });
1806
- }
1934
+ if (!eqTimestamp(timestamp, message.timestamp)) {
1935
+ return err<ProtocolTimestampMismatchError>({
1936
+ type: "ProtocolTimestampMismatchError",
1937
+ expected: message.timestamp,
1938
+ timestamp,
1939
+ });
1940
+ }
1807
1941
 
1808
- const flags = decodeFlags(buffer, PositiveInt.orThrow(3));
1809
- const table = decodeString(buffer);
1810
- const id = decodeId(buffer);
1942
+ const flags = decodeFlags(buffer, PositiveInt.orThrow(3));
1943
+ const table = decodeString(buffer);
1944
+ const id = decodeId(buffer);
1811
1945
 
1812
- const length = decodeLength(buffer);
1813
- const values = createRecord<string, SqliteValue>();
1946
+ const length = decodeLength(buffer);
1947
+ const values = createRecord<string, SqliteValue>();
1814
1948
 
1815
- for (let i = 0; i < length; i++) {
1816
- const column = decodeString(buffer);
1817
- const value = decodeSqliteValue(buffer);
1818
- values[column] = value;
1819
- }
1949
+ for (let i = 0; i < length; i++) {
1950
+ const column = decodeString(buffer);
1951
+ const value = decodeSqliteValue(buffer);
1952
+ values[column] = value;
1953
+ }
1820
1954
 
1821
- const dbChange = DbChange.orThrow({
1822
- table,
1823
- id,
1824
- values,
1825
- isInsert: flags[0],
1826
- isDelete: flags[1] ? flags[2] : null,
1827
- });
1955
+ const dbChange = DbChange.orThrow({
1956
+ table,
1957
+ id,
1958
+ values,
1959
+ isInsert: flags[0],
1960
+ isDelete: flags[1] ? flags[2] : null,
1961
+ });
1828
1962
 
1829
- return ok(dbChange);
1830
- } catch (error) {
1831
- return err<ProtocolInvalidDataError>({
1832
- type: "ProtocolInvalidDataError",
1833
- data: message.change,
1834
- error,
1835
- });
1836
- }
1837
- };
1963
+ return ok(dbChange);
1964
+ } catch (error) {
1965
+ return err<ProtocolInvalidDataError>({
1966
+ type: "ProtocolInvalidDataError",
1967
+ data: message.change,
1968
+ error,
1969
+ });
1970
+ }
1971
+ };
1838
1972
 
1839
1973
  /**
1840
1974
  * Encodes a non-negative integer into a variable-length integer format. It's
@@ -1926,26 +2060,26 @@ export const ProtocolValueType = {
1926
2060
  // 0-19 small ints
1927
2061
 
1928
2062
  // SQLite types
1929
- String: NonNegativeInt.orThrow(20),
1930
- Number: NonNegativeInt.orThrow(21),
1931
- Null: NonNegativeInt.orThrow(22),
1932
- Bytes: NonNegativeInt.orThrow(23),
2063
+ String: /*#__PURE__*/ NonNegativeInt.orThrow(20),
2064
+ Number: /*#__PURE__*/ NonNegativeInt.orThrow(21),
2065
+ Null: /*#__PURE__*/ NonNegativeInt.orThrow(22),
2066
+ Bytes: /*#__PURE__*/ NonNegativeInt.orThrow(23),
1933
2067
  // We can add more types for other DBs or anything else later.
1934
2068
 
1935
2069
  // Optimized types
1936
- NonNegativeInt: NonNegativeInt.orThrow(30),
2070
+ NonNegativeInt: /*#__PURE__*/ NonNegativeInt.orThrow(30),
1937
2071
 
1938
2072
  // String optimizations
1939
- EmptyString: NonNegativeInt.orThrow(31), // 1 byte vs 2 bytes (50% reduction)
1940
- Base64Url: NonNegativeInt.orThrow(32),
1941
- Id: NonNegativeInt.orThrow(33),
1942
- Json: NonNegativeInt.orThrow(34),
2073
+ EmptyString: /*#__PURE__*/ NonNegativeInt.orThrow(31), // 1 byte vs 2 bytes (50% reduction)
2074
+ Base64Url: /*#__PURE__*/ NonNegativeInt.orThrow(32),
2075
+ Id: /*#__PURE__*/ NonNegativeInt.orThrow(33),
2076
+ Json: /*#__PURE__*/ NonNegativeInt.orThrow(34),
1943
2077
 
1944
2078
  // new Date().toISOString() - 24 bytes
1945
2079
  // encoded with fixed length - 8 bytes
1946
2080
  // encode as NonNegativeInt - 6 bytes (additional 25% reduction)
1947
- DateIsoWithNonNegativeTime: NonNegativeInt.orThrow(35),
1948
- DateIsoWithNegativeTime: NonNegativeInt.orThrow(36), // 9 bytes
2081
+ DateIsoWithNonNegativeTime: /*#__PURE__*/ NonNegativeInt.orThrow(35),
2082
+ DateIsoWithNegativeTime: /*#__PURE__*/ NonNegativeInt.orThrow(36), // 9 bytes
1949
2083
 
1950
2084
  // TODO: Operations (from 40)
1951
2085
  // Increment, Decrement, Patch, whatever.