@evolu/common 7.4.1 → 8.0.0-next.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 (251) 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 +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -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 +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -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 +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  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 +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -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 +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  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 +622 -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 +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  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 +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  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 +36 -26
  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 +415 -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 +465 -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 +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +851 -430
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -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 +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  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 +206 -277
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -526
  246. package/src/Instances.ts +0 -92
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. 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,11 +178,12 @@
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
+ import type { ConsoleDep } from "../Console.js";
184
185
  import {
185
- Buffer,
186
+ type Buffer,
186
187
  bytesToHex,
187
188
  bytesToUtf8,
188
189
  createBuffer,
@@ -191,16 +192,22 @@ import {
191
192
  } from "../Buffer.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,7 +547,7 @@ export const createProtocolMessageFromCrdtMessages =
514
547
 
515
548
  /** Creates a {@link ProtocolMessage} for sync. */
516
549
  export const createProtocolMessageForSync =
517
- (deps: StorageDep) =>
550
+ (deps: StorageDep & ConsoleDep) =>
518
551
  (
519
552
  ownerId: OwnerId,
520
553
  subscriptionFlag?: SubscriptionFlag,
@@ -526,12 +559,10 @@ export const createProtocolMessageForSync =
526
559
  const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
527
560
 
528
561
  const size = deps.storage.getSize(ownerIdBytes);
529
- // Errors are handled by the storage.
530
- if (size == null) return null;
531
562
 
532
563
  splitRange(deps)(
533
564
  ownerIdBytes,
534
- NonNegativeInt.orThrow(0),
565
+ zeroNonNegativeInt,
535
566
  size,
536
567
  InfiniteUpperBound,
537
568
  buffer,
@@ -687,9 +718,8 @@ export const createProtocolMessageBuffer = (
687
718
  assert(isWithinSizeLimits(), "the message is too big");
688
719
  },
689
720
 
690
- canSplitRange: () => {
691
- return getRangesSize() + safeMargins.splitRange <= rangesMaxSize;
692
- },
721
+ canSplitRange: () =>
722
+ getRangesSize() + safeMargins.splitRange <= rangesMaxSize,
693
723
 
694
724
  canAddTimestampsRangeAndMessage: (timestamps, message) => {
695
725
  const rangesNewSize =
@@ -790,7 +820,7 @@ export interface TimestampsBuffer {
790
820
  }
791
821
 
792
822
  export const createTimestampsBuffer = (): TimestampsBuffer => {
793
- let count = NonNegativeInt.orThrow(0);
823
+ let count = zeroNonNegativeInt;
794
824
  const countBuffer = createBuffer();
795
825
 
796
826
  const syncCount = () => {
@@ -857,9 +887,9 @@ const createRunLengthEncoder = <T>(
857
887
  encodeValue: (buffer: Buffer, value: T) => void,
858
888
  ): RunLengthEncoder<T> => {
859
889
  const buffer = createBuffer();
860
- let previousLength = NonNegativeInt.orThrow(0);
890
+ let previousLength = zeroNonNegativeInt;
861
891
  let previousValue = null as T | null;
862
- let runLength = NonNegativeInt.orThrow(0);
892
+ let runLength = zeroNonNegativeInt;
863
893
 
864
894
  return {
865
895
  add: (value) => {
@@ -882,7 +912,7 @@ const createRunLengthEncoder = <T>(
882
912
  };
883
913
 
884
914
  export interface ApplyProtocolMessageAsClientOptions {
885
- getWriteKey?: (ownerId: OwnerId) => OwnerWriteKey | null;
915
+ writeKey?: OwnerWriteKey;
886
916
 
887
917
  rangesMaxSize?: ProtocolMessageRangesMaxSize;
888
918
 
@@ -894,27 +924,35 @@ export interface ApplyProtocolMessageAsClientOptions {
894
924
  * Result type for {@link applyProtocolMessageAsClient} that distinguishes
895
925
  * between responses to client requests and broadcast messages.
896
926
  */
927
+ export interface ApplyProtocolMessageAsClientResponse extends Typed<"Response"> {
928
+ readonly message: ProtocolMessage;
929
+ }
930
+
931
+ export interface ApplyProtocolMessageAsClientNoResponse extends Typed<"NoResponse"> {}
932
+
933
+ export interface ApplyProtocolMessageAsClientBroadcast extends Typed<"Broadcast"> {}
934
+
897
935
  export type ApplyProtocolMessageAsClientResult =
898
- | { readonly type: "response"; readonly message: ProtocolMessage }
899
- | { readonly type: "no-response" }
900
- | { readonly type: "broadcast" };
936
+ | ApplyProtocolMessageAsClientResponse
937
+ | ApplyProtocolMessageAsClientNoResponse
938
+ | ApplyProtocolMessageAsClientBroadcast;
901
939
 
902
940
  export const applyProtocolMessageAsClient =
903
- (deps: StorageDep) =>
904
- async (
941
+ (
905
942
  inputMessage: Uint8Array,
906
943
  options: ApplyProtocolMessageAsClientOptions = {},
907
- ): Promise<
908
- Result<
909
- ApplyProtocolMessageAsClientResult,
910
- | ProtocolInvalidDataError
911
- | ProtocolSyncError
912
- | ProtocolVersionError
913
- | ProtocolWriteError
914
- | ProtocolWriteKeyError
915
- | ProtocolQuotaError
916
- >
917
- > => {
944
+ ): Task<
945
+ ApplyProtocolMessageAsClientResult,
946
+ | ProtocolInvalidDataError
947
+ | ProtocolSyncError
948
+ | ProtocolVersionError
949
+ | ProtocolWriteError
950
+ | ProtocolWriteKeyError
951
+ | ProtocolQuotaError,
952
+ StorageDep
953
+ > =>
954
+ async (run) => {
955
+ const { storage } = run.deps;
918
956
  try {
919
957
  const input = createBuffer(inputMessage);
920
958
  const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
@@ -971,10 +1009,17 @@ export const applyProtocolMessageAsClient =
971
1009
  const messages = decodeMessages(input);
972
1010
  const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
973
1011
 
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" });
1012
+ if (isNonEmptyArray(messages)) {
1013
+ try {
1014
+ const result = await run(
1015
+ storage.writeMessages(ownerIdBytes, messages),
1016
+ );
1017
+ // Quota errors are handled by Storage; protocol just stops syncing.
1018
+ if (!result.ok) return ok({ type: "NoResponse" });
1019
+ } catch (error) {
1020
+ run.deps.console.error(error);
1021
+ return ok({ type: "NoResponse" });
1022
+ }
978
1023
  }
979
1024
 
980
1025
  // Now: No writeKey, no sync.
@@ -982,19 +1027,19 @@ export const applyProtocolMessageAsClient =
982
1027
  // Without local changes, writeKey will not be required.
983
1028
  // With local changes, writeKey will be required and if not provided,
984
1029
  // the sync will stop.
985
- const writeKey = options.getWriteKey?.(ownerId);
1030
+ const writeKey = options.writeKey;
986
1031
  if (writeKey == null) {
987
- return ok({ type: "no-response" });
1032
+ return ok({ type: "NoResponse" });
988
1033
  }
989
1034
 
990
1035
  if (messageType === MessageType.Broadcast) {
991
- return ok({ type: "broadcast" });
1036
+ return ok({ type: "Broadcast" });
992
1037
  }
993
1038
 
994
1039
  const ranges = decodeRanges(input);
995
1040
 
996
- if (!isNonEmptyReadonlyArray(ranges)) {
997
- return ok({ type: "no-response" });
1041
+ if (!isNonEmptyArray(ranges)) {
1042
+ return ok({ type: "NoResponse" });
998
1043
  }
999
1044
 
1000
1045
  const output = createProtocolMessageBuffer(ownerId, {
@@ -1003,14 +1048,14 @@ export const applyProtocolMessageAsClient =
1003
1048
  rangesMaxSize: options.rangesMaxSize,
1004
1049
  });
1005
1050
 
1006
- const result = sync(deps)(ranges, output, ownerIdBytes);
1051
+ const result = sync(run.deps)(ranges, output, ownerIdBytes);
1007
1052
 
1008
1053
  // Client sync error (handled via Storage) or no changes.
1009
1054
  if (!result.ok || !result.value) {
1010
- return ok({ type: "no-response" });
1055
+ return ok({ type: "NoResponse" });
1011
1056
  }
1012
1057
 
1013
- return ok({ type: "response", message: output.unwrap() });
1058
+ return ok({ type: "Response", message: output.unwrap() });
1014
1059
  } catch (error) {
1015
1060
  return err<ProtocolInvalidDataError>({
1016
1061
  type: "ProtocolInvalidDataError",
@@ -1043,21 +1088,23 @@ export interface ApplyProtocolMessageAsRelayOptions {
1043
1088
  * to sync. Clients may choose not to respond in certain cases (like when they
1044
1089
  * receive broadcast messages or when they lack a write key for syncing).
1045
1090
  */
1046
- export interface ApplyProtocolMessageAsRelayResult {
1047
- readonly type: "response";
1091
+ export interface ApplyProtocolMessageAsRelayResult extends Typed<"Response"> {
1048
1092
  readonly message: ProtocolMessage;
1049
1093
  }
1050
1094
 
1051
1095
  export const applyProtocolMessageAsRelay =
1052
- (deps: StorageDep) =>
1053
- async (
1096
+ (
1054
1097
  inputMessage: Uint8Array,
1055
1098
  options: ApplyProtocolMessageAsRelayOptions = {},
1056
1099
  /** For tests only. */
1057
- version = protocolVersion,
1058
- ): Promise<
1059
- Result<ApplyProtocolMessageAsRelayResult, ProtocolInvalidDataError>
1060
- > => {
1100
+ version: NonNegativeInt = protocolVersion,
1101
+ ): Task<
1102
+ ApplyProtocolMessageAsRelayResult,
1103
+ ProtocolInvalidDataError,
1104
+ StorageDep
1105
+ > =>
1106
+ async (run) => {
1107
+ const { storage } = run.deps;
1061
1108
  try {
1062
1109
  const input = createBuffer(inputMessage);
1063
1110
  const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
@@ -1069,7 +1116,7 @@ export const applyProtocolMessageAsRelay =
1069
1116
  encodeNonNegativeInt(output, version);
1070
1117
  output.extend(ownerIdBytes);
1071
1118
  return ok({
1072
- type: "response",
1119
+ type: "Response",
1073
1120
  message: output.unwrap() as ProtocolMessage,
1074
1121
  });
1075
1122
  }
@@ -1098,10 +1145,10 @@ export const applyProtocolMessageAsRelay =
1098
1145
  }
1099
1146
 
1100
1147
  if (writeKey) {
1101
- const isValid = deps.storage.validateWriteKey(ownerIdBytes, writeKey);
1148
+ const isValid = storage.validateWriteKey(ownerIdBytes, writeKey);
1102
1149
  if (!isValid) {
1103
1150
  return ok({
1104
- type: "response",
1151
+ type: "Response",
1105
1152
  message: createProtocolMessageBuffer(ownerId, {
1106
1153
  messageType: MessageType.Response,
1107
1154
  errorCode: ProtocolErrorCode.WriteKeyError,
@@ -1112,10 +1159,10 @@ export const applyProtocolMessageAsRelay =
1112
1159
 
1113
1160
  const messages = decodeMessages(input);
1114
1161
 
1115
- if (isNonEmptyReadonlyArray(messages)) {
1162
+ if (isNonEmptyArray(messages)) {
1116
1163
  if (!writeKey) {
1117
1164
  return ok({
1118
- type: "response",
1165
+ type: "Response",
1119
1166
  message: createProtocolMessageBuffer(ownerId, {
1120
1167
  messageType: MessageType.Response,
1121
1168
  errorCode: ProtocolErrorCode.WriteKeyError,
@@ -1123,18 +1170,25 @@ export const applyProtocolMessageAsRelay =
1123
1170
  });
1124
1171
  }
1125
1172
 
1126
- const result = await deps.storage.writeMessages(ownerIdBytes, messages);
1173
+ try {
1174
+ const result = await run(
1175
+ storage.writeMessages(ownerIdBytes, messages),
1176
+ );
1127
1177
 
1128
- if (!result.ok) {
1129
- const errorCode =
1130
- result.error.type === "StorageWriteError"
1131
- ? ProtocolErrorCode.WriteError
1132
- : ProtocolErrorCode.QuotaError;
1178
+ if (!result.ok) {
1179
+ const message = createProtocolMessageBuffer(ownerId, {
1180
+ messageType: MessageType.Response,
1181
+ errorCode: ProtocolErrorCode.QuotaError,
1182
+ }).unwrap();
1183
+ return ok({ type: "Response", message });
1184
+ }
1185
+ } catch (error) {
1186
+ run.deps.console.error(error);
1133
1187
  const message = createProtocolMessageBuffer(ownerId, {
1134
1188
  messageType: MessageType.Response,
1135
- errorCode,
1189
+ errorCode: ProtocolErrorCode.WriteError,
1136
1190
  }).unwrap();
1137
- return ok({ type: "response", message });
1191
+ return ok({ type: "Response", message });
1138
1192
  }
1139
1193
 
1140
1194
  /**
@@ -1179,11 +1233,11 @@ export const applyProtocolMessageAsRelay =
1179
1233
 
1180
1234
  // Non-initiators always respond to provide sync completion feedback,
1181
1235
  // even when there's nothing to sync.
1182
- if (!isNonEmptyReadonlyArray(ranges)) {
1183
- return ok({ type: "response", message: output.unwrap() });
1236
+ if (!isNonEmptyArray(ranges)) {
1237
+ return ok({ type: "Response", message: output.unwrap() });
1184
1238
  }
1185
1239
 
1186
- const result = sync(deps)(ranges, output, ownerIdBytes);
1240
+ const result = sync(run.deps)(ranges, output, ownerIdBytes);
1187
1241
 
1188
1242
  const message = result.ok
1189
1243
  ? output.unwrap()
@@ -1193,7 +1247,7 @@ export const applyProtocolMessageAsRelay =
1193
1247
  }).unwrap();
1194
1248
 
1195
1249
  // Non-initiators always respond to provide sync completion feedback,
1196
- return ok({ type: "response", message });
1250
+ return ok({ type: "Response", message });
1197
1251
  } catch (error) {
1198
1252
  return err<ProtocolInvalidDataError>({
1199
1253
  type: "ProtocolInvalidDataError",
@@ -1213,6 +1267,38 @@ const decodeVersionAndOwner = (input: Buffer): [NonNegativeInt, OwnerId] => {
1213
1267
  return [version, ownerId];
1214
1268
  };
1215
1269
 
1270
+ const parseProtocolHeaderFromBuffer = (input: Buffer): ProtocolHeader => {
1271
+ const [version, ownerId] = decodeVersionAndOwner(input);
1272
+
1273
+ if (version !== protocolVersion) {
1274
+ throw new ProtocolDecodeError(`Unsupported protocol version: ${version}`);
1275
+ }
1276
+
1277
+ const messageTypeValue = input.shift();
1278
+ let messageType: MessageType;
1279
+
1280
+ switch (messageTypeValue) {
1281
+ case MessageType.Request:
1282
+ messageType = MessageType.Request;
1283
+ break;
1284
+ case MessageType.Response:
1285
+ messageType = MessageType.Response;
1286
+ break;
1287
+ case MessageType.Broadcast:
1288
+ messageType = MessageType.Broadcast;
1289
+ break;
1290
+ default:
1291
+ throw new ProtocolDecodeError("Invalid MessageType");
1292
+ }
1293
+
1294
+ return {
1295
+ type: "ProtocolHeader",
1296
+ version: 1,
1297
+ ownerId,
1298
+ messageType,
1299
+ };
1300
+ };
1301
+
1216
1302
  /**
1217
1303
  * Error thrown for internal protocol validation failures, such as invalid data
1218
1304
  * or type errors.
@@ -1231,30 +1317,35 @@ const decodeMessages = (
1231
1317
  ): ReadonlyArray<EncryptedCrdtMessage> => {
1232
1318
  const timestamps = decodeTimestamps(buffer);
1233
1319
 
1234
- const messages: Array<EncryptedCrdtMessage> = [];
1235
- for (const timestamp of timestamps) {
1320
+ const messages = new Array<EncryptedCrdtMessage>(timestamps.length);
1321
+ for (let i = 0; i < timestamps.length; i++) {
1322
+ const timestamp = timestamps[i];
1236
1323
  const changeLength = decodeLength(buffer);
1237
1324
  const change = buffer.shiftN(changeLength) as EncryptedDbChange;
1238
- messages.push({ timestamp, change });
1325
+ messages[i] = { timestamp, change };
1239
1326
  }
1240
1327
 
1241
1328
  return messages;
1242
1329
  };
1243
1330
 
1244
1331
  const sync =
1245
- (deps: StorageDep) =>
1332
+ (deps: StorageDep & ConsoleDep) =>
1246
1333
  (
1247
1334
  ranges: NonEmptyReadonlyArray<Range>,
1248
1335
  output: ProtocolMessageBuffer,
1249
1336
  ownerIdBytes: OwnerIdBytes,
1250
1337
  ): Result<boolean, typeof ProtocolErrorCode.SyncError> => {
1251
1338
  const outputInitialSize = output.getSize();
1252
-
1253
- const storageSize = deps.storage.getSize(ownerIdBytes);
1254
- if (storageSize == null) return err(ProtocolErrorCode.SyncError);
1339
+ let storageSize: NonNegativeInt;
1340
+ try {
1341
+ storageSize = deps.storage.getSize(ownerIdBytes);
1342
+ } catch (error) {
1343
+ deps.console.error(error);
1344
+ return err(ProtocolErrorCode.SyncError);
1345
+ }
1255
1346
 
1256
1347
  let prevUpperBound: RangeUpperBound | null = null;
1257
- let prevIndex = NonNegativeInt.orThrow(0);
1348
+ let prevIndex = zeroNonNegativeInt;
1258
1349
 
1259
1350
  let skip = false;
1260
1351
  let nonSkipRangeAdded = false;
@@ -1291,12 +1382,17 @@ const sync =
1291
1382
  const addFingerprintForRemainingRange = (
1292
1383
  begin: NonNegativeInt,
1293
1384
  ): boolean => {
1294
- const fingerprint = deps.storage.fingerprint(
1295
- ownerIdBytes,
1296
- begin,
1297
- storageSize,
1298
- );
1299
- if (!fingerprint) return false;
1385
+ let fingerprint: Fingerprint;
1386
+ try {
1387
+ fingerprint = deps.storage.fingerprint(
1388
+ ownerIdBytes,
1389
+ begin,
1390
+ storageSize,
1391
+ );
1392
+ } catch (error) {
1393
+ deps.console.error(error);
1394
+ return false;
1395
+ }
1300
1396
  // There is always a space for a ramaining range.
1301
1397
  output.addRange({
1302
1398
  type: RangeType.Fingerprint,
@@ -1310,13 +1406,18 @@ const sync =
1310
1406
  const currentUpperBound = range.upperBound;
1311
1407
 
1312
1408
  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);
1409
+ let upper: NonNegativeInt;
1410
+ try {
1411
+ upper = deps.storage.findLowerBound(
1412
+ ownerIdBytes,
1413
+ prevIndex,
1414
+ storageSize,
1415
+ currentUpperBound,
1416
+ );
1417
+ } catch (error) {
1418
+ deps.console.error(error);
1419
+ return err(ProtocolErrorCode.SyncError);
1420
+ }
1320
1421
 
1321
1422
  switch (range.type) {
1322
1423
  case RangeType.Skip: {
@@ -1325,12 +1426,17 @@ const sync =
1325
1426
  }
1326
1427
 
1327
1428
  case RangeType.Fingerprint: {
1328
- const ourFingerprint = deps.storage.fingerprint(
1329
- ownerIdBytes,
1330
- lower,
1331
- upper,
1332
- );
1333
- if (ourFingerprint == null) return err(ProtocolErrorCode.SyncError);
1429
+ let ourFingerprint: Fingerprint;
1430
+ try {
1431
+ ourFingerprint = deps.storage.fingerprint(
1432
+ ownerIdBytes,
1433
+ lower,
1434
+ upper,
1435
+ );
1436
+ } catch (error) {
1437
+ deps.console.error(error);
1438
+ return err(ProtocolErrorCode.SyncError);
1439
+ }
1334
1440
 
1335
1441
  if (eqArrayNumber(range.fingerprint, ourFingerprint)) {
1336
1442
  skipRange(range);
@@ -1361,52 +1467,61 @@ const sync =
1361
1467
  );
1362
1468
  const ourTimestamps = createTimestampsBuffer();
1363
1469
 
1364
- let cantReadDbChange = false as boolean;
1365
1470
  let exceeded = false as boolean;
1471
+ let iterateFailed = false as boolean;
1472
+
1473
+ try {
1474
+ deps.storage.iterate(
1475
+ ownerIdBytes,
1476
+ lower,
1477
+ upper,
1478
+ (timestamp, index) => {
1479
+ const timestampString = timestamp.join();
1480
+ const timestampBinary = timestampBytesToTimestamp(timestamp);
1481
+
1482
+ let message: EncryptedCrdtMessage | null = null;
1483
+
1484
+ if (timestampsWeNeed.has(timestampString)) {
1485
+ timestampsWeNeed.delete(timestampString);
1486
+ } else {
1487
+ try {
1488
+ message = {
1489
+ timestamp: timestampBinary,
1490
+ change: deps.storage.readDbChange(
1491
+ ownerIdBytes,
1492
+ timestamp,
1493
+ ),
1494
+ };
1495
+ } catch (error) {
1496
+ deps.console.error(error);
1497
+ iterateFailed = true;
1498
+ return false;
1499
+ }
1500
+ }
1366
1501
 
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;
1502
+ if (
1503
+ !output.canAddTimestampsRangeAndMessage(
1504
+ ourTimestamps,
1505
+ message,
1506
+ )
1507
+ ) {
1508
+ exceeded = true;
1509
+ endBound = timestamp;
1510
+ upper = index;
1386
1511
  return false;
1387
1512
  }
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
1513
 
1409
- if (cantReadDbChange) {
1514
+ ourTimestamps.add(timestampBinary);
1515
+ if (message) output.addMessage(message);
1516
+ return true;
1517
+ },
1518
+ );
1519
+ } catch (error) {
1520
+ deps.console.error(error);
1521
+ return err(ProtocolErrorCode.SyncError);
1522
+ }
1523
+
1524
+ if (iterateFailed) {
1410
1525
  return err(ProtocolErrorCode.SyncError);
1411
1526
  }
1412
1527
 
@@ -1449,7 +1564,7 @@ const sync =
1449
1564
  };
1450
1565
 
1451
1566
  const splitRange =
1452
- (deps: StorageDep) =>
1567
+ (deps: StorageDep & ConsoleDep) =>
1453
1568
  (
1454
1569
  ownerId: OwnerIdBytes,
1455
1570
  lower: NonNegativeInt,
@@ -1469,7 +1584,7 @@ const splitRange =
1469
1584
 
1470
1585
  deps.storage.iterate(
1471
1586
  ownerId,
1472
- NonNegativeInt.orThrow(0),
1587
+ zeroNonNegativeInt,
1473
1588
  itemCount,
1474
1589
  (timestamp) => {
1475
1590
  range.timestamps.add(timestampBytesToTimestamp(timestamp));
@@ -1490,13 +1605,17 @@ const splitRange =
1490
1605
  ...buckets.value.map((b) => NonNegativeInt.orThrow(b + lower)),
1491
1606
  ];
1492
1607
 
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;
1608
+ let fingerprintRanges: ReadonlyArray<FingerprintRange>;
1609
+ try {
1610
+ fingerprintRanges = deps.storage.fingerprintRanges(
1611
+ ownerId,
1612
+ fingerprintRangesBuckets,
1613
+ upperBound,
1614
+ );
1615
+ } catch (error) {
1616
+ deps.console.error(error);
1617
+ return;
1618
+ }
1500
1619
 
1501
1620
  const rangesToUse =
1502
1621
  lower > 0 ? fingerprintRanges.slice(1) : fingerprintRanges;
@@ -1514,7 +1633,8 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1514
1633
 
1515
1634
  const timestampsCount = NonNegativeInt.orThrow(rangesCount - 1);
1516
1635
  const timestamps = decodeTimestamps(buffer, timestampsCount);
1517
- const rangeTypes: Array<RangeType> = [];
1636
+
1637
+ const rangeTypes = new Array<RangeType>(rangesCount);
1518
1638
 
1519
1639
  for (let i = 0; i < rangesCount; i++) {
1520
1640
  const rangeType = decodeNonNegativeInt(buffer);
@@ -1522,14 +1642,14 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1522
1642
  case RangeType.Fingerprint:
1523
1643
  case RangeType.Skip:
1524
1644
  case RangeType.Timestamps:
1525
- rangeTypes.push(rangeType as RangeType);
1645
+ rangeTypes[i] = rangeType as RangeType;
1526
1646
  break;
1527
1647
  default:
1528
1648
  throw new ProtocolDecodeError(`Invalid RangeType: ${rangeType}`);
1529
1649
  }
1530
1650
  }
1531
1651
 
1532
- const ranges: Array<Range> = [];
1652
+ const ranges = new Array<Range>(rangesCount);
1533
1653
 
1534
1654
  for (let i = 0; i < rangesCount; i++) {
1535
1655
  const upperBound =
@@ -1541,16 +1661,16 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1541
1661
 
1542
1662
  switch (rangeType) {
1543
1663
  case RangeType.Skip:
1544
- ranges.push({ type: RangeType.Skip, upperBound });
1664
+ ranges[i] = { type: RangeType.Skip, upperBound };
1545
1665
  break;
1546
1666
 
1547
1667
  case RangeType.Fingerprint: {
1548
1668
  const fingerprint = buffer.shiftN(fingerprintSize) as Fingerprint;
1549
- ranges.push({
1669
+ ranges[i] = {
1550
1670
  type: RangeType.Fingerprint,
1551
1671
  upperBound,
1552
1672
  fingerprint,
1553
- });
1673
+ };
1554
1674
  break;
1555
1675
  }
1556
1676
 
@@ -1558,11 +1678,11 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1558
1678
  const timestamps = decodeTimestamps(buffer).map(
1559
1679
  timestampToTimestampBytes,
1560
1680
  );
1561
- ranges.push({
1681
+ ranges[i] = {
1562
1682
  type: RangeType.Timestamps,
1563
1683
  upperBound,
1564
1684
  timestamps,
1565
- });
1685
+ };
1566
1686
  break;
1567
1687
  }
1568
1688
  }
@@ -1571,7 +1691,6 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1571
1691
  return ranges;
1572
1692
  };
1573
1693
 
1574
- /** Decodes an array of sorted timestamps with delta-encoded millis. */
1575
1694
  const decodeTimestamps = (
1576
1695
  buffer: Buffer,
1577
1696
  length?: NonNegativeInt,
@@ -1580,50 +1699,68 @@ const decodeTimestamps = (
1580
1699
 
1581
1700
  let previousMillis = 0 as Millis;
1582
1701
 
1583
- const millises: Array<Millis> = [];
1702
+ const millises = new Array<Millis>(length);
1584
1703
  for (let i = 0; i < length; i++) {
1585
1704
  const deltaMillis = decodeNonNegativeInt(buffer);
1586
1705
  const millis = Millis.from(previousMillis + deltaMillis);
1587
1706
  if (!millis.ok) throw new ProtocolDecodeError(millis.error.type);
1588
- millises.push(millis.value);
1707
+ millises[i] = millis.value;
1589
1708
  previousMillis = millis.value;
1590
1709
  }
1591
1710
 
1592
- const counters: Array<Counter> = [];
1593
- let counterIndex = 0;
1594
- while (counterIndex < length) {
1711
+ const counters = decodeRle(buffer, length, (): Counter => {
1595
1712
  const counter = Counter.from(decodeNonNegativeInt(buffer));
1596
1713
  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
- }
1714
+ return counter.value;
1715
+ });
1603
1716
 
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
- }
1717
+ const nodeIds = decodeRle(buffer, length, (): NodeId => decodeNodeId(buffer));
1614
1718
 
1615
- const timestamps: Array<Timestamp> = [];
1719
+ const timestamps = new Array<Timestamp>(length);
1616
1720
  for (let i = 0; i < length; i++) {
1617
- timestamps.push({
1721
+ timestamps[i] = {
1618
1722
  millis: millises[i],
1619
1723
  counter: counters[i],
1620
1724
  nodeId: nodeIds[i],
1621
- });
1725
+ };
1622
1726
  }
1623
1727
 
1624
1728
  return timestamps;
1625
1729
  };
1626
1730
 
1731
+ export const decodeRle = <T>(
1732
+ buffer: Buffer,
1733
+ length: NonNegativeInt,
1734
+ decodeValue: () => T,
1735
+ ): ReadonlyArray<T> => {
1736
+ const values = new Array<T>(length);
1737
+ let index = 0;
1738
+ while (index < length) {
1739
+ const value = decodeValue();
1740
+ const runLength = decodeNonNegativeInt(buffer);
1741
+
1742
+ // Prevent infinite loop on malformed input.
1743
+ if (runLength === 0) {
1744
+ throw new ProtocolDecodeError(
1745
+ "Invalid RLE encoding: runLength must be positive",
1746
+ );
1747
+ }
1748
+ const remaining = length - index;
1749
+
1750
+ // Prevent CPU/memory amplification via oversized runLength.
1751
+ if (runLength > remaining) {
1752
+ throw new ProtocolDecodeError(
1753
+ `Invalid RLE encoding: runLength ${runLength} exceeds remaining ${remaining}`,
1754
+ );
1755
+ }
1756
+ for (let i = 0; i < runLength; i++) {
1757
+ values[index] = value;
1758
+ index++;
1759
+ }
1760
+ }
1761
+ return values;
1762
+ };
1763
+
1627
1764
  const decodeId = (buffer: Buffer): Id => {
1628
1765
  const bytes = buffer.shiftN(idBytesTypeValueLength);
1629
1766
  return idBytesToId(bytes as IdBytes);
@@ -1699,9 +1836,10 @@ export const decodeFlags = (
1699
1836
  count: PositiveInt,
1700
1837
  ): ReadonlyArray<boolean> => {
1701
1838
  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);
1839
+ const length = globalThis.Math.min(count, 8);
1840
+ const flags = new Array<boolean>(length);
1841
+ for (let i = 0; i < length; i++) {
1842
+ flags[i] = (byte & (1 << i)) !== 0;
1705
1843
  }
1706
1844
  return flags;
1707
1845
  };
@@ -1715,7 +1853,7 @@ export const decodeFlags = (
1715
1853
  * data.
1716
1854
  */
1717
1855
  export const encodeAndEncryptDbChange =
1718
- (deps: SymmetricCryptoDep) =>
1856
+ (deps: RandomBytesDep) =>
1719
1857
  (message: CrdtMessage, key: EncryptionKey): EncryptedDbChange => {
1720
1858
  const buffer = createBuffer();
1721
1859
 
@@ -1727,6 +1865,7 @@ export const encodeAndEncryptDbChange =
1727
1865
 
1728
1866
  encodeFlags(buffer, [
1729
1867
  message.change.isInsert,
1868
+ // Encode nullable boolean as two flags: presence + value.
1730
1869
  message.change.isDelete != null,
1731
1870
  message.change.isDelete ?? false,
1732
1871
  ]);
@@ -1745,7 +1884,7 @@ export const encodeAndEncryptDbChange =
1745
1884
  // Add PADMÉ padding (ignored during decoding)
1746
1885
  buffer.extend(createPadmePadding(buffer.getLength()));
1747
1886
 
1748
- const { nonce, ciphertext } = deps.symmetricCrypto.encrypt(
1887
+ const [ciphertext, nonce] = encryptWithXChaCha20Poly1305(deps)(
1749
1888
  buffer.unwrap(),
1750
1889
  key,
1751
1890
  );
@@ -1763,78 +1902,76 @@ export const encodeAndEncryptDbChange =
1763
1902
  * owner's encryption key. Verifies that the embedded timestamp matches the
1764
1903
  * expected timestamp to ensure message integrity.
1765
1904
  */
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);
1905
+ export const decryptAndDecodeDbChange = (
1906
+ message: EncryptedCrdtMessage,
1907
+ key: EncryptionKey,
1908
+ ): Result<
1909
+ DbChange,
1910
+ | DecryptWithXChaCha20Poly1305Error
1911
+ | ProtocolInvalidDataError
1912
+ | ProtocolTimestampMismatchError
1913
+ > => {
1914
+ try {
1915
+ const buffer = createBuffer(message.change);
1779
1916
 
1780
- const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
1781
- const ciphertext = buffer.shiftN(decodeLength(buffer));
1917
+ const nonce = buffer.shiftN(xChaCha20Poly1305NonceLength as NonNegativeInt);
1918
+ const ciphertext = buffer.shiftN(decodeLength(buffer));
1782
1919
 
1783
- const plaintextBytes = deps.symmetricCrypto.decrypt(
1784
- ciphertext,
1785
- key,
1786
- nonce,
1787
- );
1788
- if (!plaintextBytes.ok) return plaintextBytes;
1920
+ const plaintextBytes = decryptWithXChaCha20Poly1305(
1921
+ XChaCha20Poly1305Ciphertext.orThrow(ciphertext),
1922
+ Entropy24.orThrow(nonce),
1923
+ key,
1924
+ );
1925
+ if (!plaintextBytes.ok) return plaintextBytes;
1789
1926
 
1790
- buffer.reset();
1791
- buffer.extend(plaintextBytes.value);
1927
+ buffer.reset();
1928
+ buffer.extend(plaintextBytes.value);
1792
1929
 
1793
- // Decode version (for future compatibility, not need yet)
1794
- decodeNonNegativeInt(buffer);
1930
+ // Decode version (for future compatibility, not need yet)
1931
+ decodeNonNegativeInt(buffer);
1795
1932
 
1796
- const timestamp = timestampBytesToTimestamp(
1797
- buffer.shiftN(timestampBytesLength) as TimestampBytes,
1798
- );
1933
+ const timestamp = timestampBytesToTimestamp(
1934
+ TimestampBytes.orThrow(buffer.shiftN(timestampBytesLength)),
1935
+ );
1799
1936
 
1800
- if (!eqTimestamp(timestamp, message.timestamp)) {
1801
- return err<ProtocolTimestampMismatchError>({
1802
- type: "ProtocolTimestampMismatchError",
1803
- expected: message.timestamp,
1804
- timestamp,
1805
- });
1806
- }
1937
+ if (!eqTimestamp(timestamp, message.timestamp)) {
1938
+ return err<ProtocolTimestampMismatchError>({
1939
+ type: "ProtocolTimestampMismatchError",
1940
+ expected: message.timestamp,
1941
+ timestamp,
1942
+ });
1943
+ }
1807
1944
 
1808
- const flags = decodeFlags(buffer, PositiveInt.orThrow(3));
1809
- const table = decodeString(buffer);
1810
- const id = decodeId(buffer);
1945
+ const flags = decodeFlags(buffer, PositiveInt.orThrow(3));
1946
+ const table = decodeString(buffer);
1947
+ const id = decodeId(buffer);
1811
1948
 
1812
- const length = decodeLength(buffer);
1813
- const values = createRecord<string, SqliteValue>();
1949
+ const length = decodeLength(buffer);
1950
+ const values = createRecord<string, SqliteValue>();
1814
1951
 
1815
- for (let i = 0; i < length; i++) {
1816
- const column = decodeString(buffer);
1817
- const value = decodeSqliteValue(buffer);
1818
- values[column] = value;
1819
- }
1952
+ for (let i = 0; i < length; i++) {
1953
+ const column = decodeString(buffer);
1954
+ const value = decodeSqliteValue(buffer);
1955
+ values[column] = value;
1956
+ }
1820
1957
 
1821
- const dbChange = DbChange.orThrow({
1822
- table,
1823
- id,
1824
- values,
1825
- isInsert: flags[0],
1826
- isDelete: flags[1] ? flags[2] : null,
1827
- });
1958
+ const dbChange = DbChange.orThrow({
1959
+ table,
1960
+ id,
1961
+ values,
1962
+ isInsert: flags[0],
1963
+ isDelete: flags[1] ? flags[2] : null,
1964
+ });
1828
1965
 
1829
- return ok(dbChange);
1830
- } catch (error) {
1831
- return err<ProtocolInvalidDataError>({
1832
- type: "ProtocolInvalidDataError",
1833
- data: message.change,
1834
- error,
1835
- });
1836
- }
1837
- };
1966
+ return ok(dbChange);
1967
+ } catch (error) {
1968
+ return err<ProtocolInvalidDataError>({
1969
+ type: "ProtocolInvalidDataError",
1970
+ data: message.change,
1971
+ error,
1972
+ });
1973
+ }
1974
+ };
1838
1975
 
1839
1976
  /**
1840
1977
  * Encodes a non-negative integer into a variable-length integer format. It's
@@ -1926,26 +2063,26 @@ export const ProtocolValueType = {
1926
2063
  // 0-19 small ints
1927
2064
 
1928
2065
  // SQLite types
1929
- String: NonNegativeInt.orThrow(20),
1930
- Number: NonNegativeInt.orThrow(21),
1931
- Null: NonNegativeInt.orThrow(22),
1932
- Bytes: NonNegativeInt.orThrow(23),
2066
+ String: /*#__PURE__*/ NonNegativeInt.orThrow(20),
2067
+ Number: /*#__PURE__*/ NonNegativeInt.orThrow(21),
2068
+ Null: /*#__PURE__*/ NonNegativeInt.orThrow(22),
2069
+ Bytes: /*#__PURE__*/ NonNegativeInt.orThrow(23),
1933
2070
  // We can add more types for other DBs or anything else later.
1934
2071
 
1935
2072
  // Optimized types
1936
- NonNegativeInt: NonNegativeInt.orThrow(30),
2073
+ NonNegativeInt: /*#__PURE__*/ NonNegativeInt.orThrow(30),
1937
2074
 
1938
2075
  // 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),
2076
+ EmptyString: /*#__PURE__*/ NonNegativeInt.orThrow(31), // 1 byte vs 2 bytes (50% reduction)
2077
+ Base64Url: /*#__PURE__*/ NonNegativeInt.orThrow(32),
2078
+ Id: /*#__PURE__*/ NonNegativeInt.orThrow(33),
2079
+ Json: /*#__PURE__*/ NonNegativeInt.orThrow(34),
1943
2080
 
1944
2081
  // new Date().toISOString() - 24 bytes
1945
2082
  // encoded with fixed length - 8 bytes
1946
2083
  // encode as NonNegativeInt - 6 bytes (additional 25% reduction)
1947
- DateIsoWithNonNegativeTime: NonNegativeInt.orThrow(35),
1948
- DateIsoWithNegativeTime: NonNegativeInt.orThrow(36), // 9 bytes
2084
+ DateIsoWithNonNegativeTime: /*#__PURE__*/ NonNegativeInt.orThrow(35),
2085
+ DateIsoWithNegativeTime: /*#__PURE__*/ NonNegativeInt.orThrow(36), // 9 bytes
1949
2086
 
1950
2087
  // TODO: Operations (from 40)
1951
2088
  // Increment, Decrement, Patch, whatever.