@evolu/common 5.4.8 → 6.0.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 (242) hide show
  1. package/README.md +30 -34
  2. package/dist/src/Array.d.ts +17 -0
  3. package/dist/src/Array.d.ts.map +1 -0
  4. package/dist/src/Array.js +12 -0
  5. package/dist/src/Assert.d.ts +68 -0
  6. package/dist/src/Assert.d.ts.map +1 -0
  7. package/dist/src/Assert.js +77 -0
  8. package/dist/src/BigInt.d.ts +20 -0
  9. package/dist/src/BigInt.d.ts.map +1 -0
  10. package/dist/src/BigInt.js +18 -0
  11. package/dist/src/Buffer.d.ts +92 -0
  12. package/dist/src/Buffer.d.ts.map +1 -0
  13. package/dist/src/Buffer.js +62 -0
  14. package/dist/src/Callbacks.d.ts +20 -0
  15. package/dist/src/Callbacks.d.ts.map +1 -0
  16. package/dist/src/Callbacks.js +18 -0
  17. package/dist/src/Console.d.ts +78 -0
  18. package/dist/src/Console.d.ts.map +1 -0
  19. package/dist/src/Console.js +103 -0
  20. package/dist/src/Crypto.d.ts +72 -39
  21. package/dist/src/Crypto.d.ts.map +1 -1
  22. package/dist/src/Crypto.js +89 -54
  23. package/dist/src/Eq.d.ts +97 -0
  24. package/dist/src/Eq.d.ts.map +1 -0
  25. package/dist/src/Eq.js +167 -0
  26. package/dist/src/Error.d.ts +14 -10
  27. package/dist/src/Error.d.ts.map +1 -1
  28. package/dist/src/Error.js +43 -11
  29. package/dist/src/Evolu/Config.d.ts +69 -0
  30. package/dist/src/Evolu/Config.d.ts.map +1 -0
  31. package/dist/src/Evolu/Config.js +9 -0
  32. package/dist/src/Evolu/Db.d.ts +126 -0
  33. package/dist/src/Evolu/Db.d.ts.map +1 -0
  34. package/dist/src/Evolu/Db.js +774 -0
  35. package/dist/src/Evolu/Diff.d.ts +43 -0
  36. package/dist/src/Evolu/Diff.d.ts.map +1 -0
  37. package/dist/src/Evolu/Diff.js +95 -0
  38. package/dist/src/Evolu/Evolu.d.ts +334 -0
  39. package/dist/src/Evolu/Evolu.d.ts.map +1 -0
  40. package/dist/src/Evolu/Evolu.js +434 -0
  41. package/dist/src/Evolu/Internal.d.ts +26 -0
  42. package/dist/src/Evolu/Internal.d.ts.map +1 -0
  43. package/dist/src/Evolu/Internal.js +25 -0
  44. package/dist/src/Evolu/Kysely.d.ts +6 -0
  45. package/dist/src/Evolu/Kysely.d.ts.map +1 -0
  46. package/dist/src/Evolu/Kysely.js +21 -0
  47. package/dist/src/Evolu/Owner.d.ts +155 -0
  48. package/dist/src/Evolu/Owner.d.ts.map +1 -0
  49. package/dist/src/Evolu/Owner.js +126 -0
  50. package/dist/src/Evolu/Platform.d.ts +23 -0
  51. package/dist/src/Evolu/Platform.d.ts.map +1 -0
  52. package/dist/src/Evolu/Platform.js +1 -0
  53. package/dist/src/Evolu/Protocol.d.ts +401 -0
  54. package/dist/src/Evolu/Protocol.d.ts.map +1 -0
  55. package/dist/src/Evolu/Protocol.js +1151 -0
  56. package/dist/src/Evolu/Public.d.ts +18 -0
  57. package/dist/src/Evolu/Public.d.ts.map +1 -0
  58. package/dist/src/Evolu/Public.js +11 -0
  59. package/dist/src/Evolu/PublicKysely.d.ts +148 -0
  60. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -0
  61. package/dist/src/Evolu/PublicKysely.js +185 -0
  62. package/dist/src/Evolu/Query.d.ts +63 -0
  63. package/dist/src/Evolu/Query.d.ts.map +1 -0
  64. package/dist/src/Evolu/Query.js +61 -0
  65. package/dist/src/Evolu/Relay.d.ts +13 -0
  66. package/dist/src/Evolu/Relay.d.ts.map +1 -0
  67. package/dist/src/Evolu/Relay.js +109 -0
  68. package/dist/src/Evolu/Schema.d.ts +201 -0
  69. package/dist/src/Evolu/Schema.d.ts.map +1 -0
  70. package/dist/src/Evolu/Schema.js +150 -0
  71. package/dist/src/Evolu/Storage.d.ts +49 -0
  72. package/dist/src/Evolu/Storage.d.ts.map +1 -0
  73. package/dist/src/Evolu/Storage.js +1111 -0
  74. package/dist/src/Evolu/Sync.d.ts +59 -0
  75. package/dist/src/Evolu/Sync.d.ts.map +1 -0
  76. package/dist/src/Evolu/Sync.js +29 -0
  77. package/dist/src/Evolu/Timestamp.d.ts +106 -0
  78. package/dist/src/Evolu/Timestamp.d.ts.map +1 -0
  79. package/dist/src/Evolu/Timestamp.js +179 -0
  80. package/dist/src/Function.d.ts +54 -0
  81. package/dist/src/Function.d.ts.map +1 -0
  82. package/dist/src/Function.js +38 -0
  83. package/dist/src/ManyToManyMap.d.ts +26 -0
  84. package/dist/src/ManyToManyMap.d.ts.map +1 -0
  85. package/dist/src/ManyToManyMap.js +92 -0
  86. package/dist/src/NanoId.d.ts +27 -0
  87. package/dist/src/NanoId.d.ts.map +1 -0
  88. package/dist/src/NanoId.js +6 -0
  89. package/dist/src/Number.d.ts +42 -0
  90. package/dist/src/Number.d.ts.map +1 -0
  91. package/dist/src/Number.js +55 -0
  92. package/dist/src/Object.d.ts +35 -0
  93. package/dist/src/Object.d.ts.map +1 -0
  94. package/dist/src/Object.js +36 -0
  95. package/dist/src/Order.d.ts +90 -0
  96. package/dist/src/Order.d.ts.map +1 -0
  97. package/dist/src/Order.js +85 -0
  98. package/dist/src/Promise.d.ts +180 -0
  99. package/dist/src/Promise.d.ts.map +1 -0
  100. package/dist/src/Promise.js +176 -0
  101. package/dist/src/Random.d.ts +52 -0
  102. package/dist/src/Random.d.ts.map +1 -0
  103. package/dist/src/Random.js +29 -0
  104. package/dist/src/Ref.d.ts +40 -0
  105. package/dist/src/Ref.d.ts.map +1 -0
  106. package/dist/src/Ref.js +13 -0
  107. package/dist/src/Result.d.ts +421 -0
  108. package/dist/src/Result.d.ts.map +1 -0
  109. package/dist/src/Result.js +357 -0
  110. package/dist/src/Skiplist.d.ts +23 -0
  111. package/dist/src/Skiplist.d.ts.map +1 -0
  112. package/dist/src/Skiplist.js +58 -0
  113. package/dist/src/Sqlite.d.ts +116 -52
  114. package/dist/src/Sqlite.d.ts.map +1 -1
  115. package/dist/src/Sqlite.js +183 -67
  116. package/dist/src/Store.d.ts +45 -8
  117. package/dist/src/Store.d.ts.map +1 -1
  118. package/dist/src/Store.js +33 -17
  119. package/dist/src/String.d.ts +2 -0
  120. package/dist/src/String.d.ts.map +1 -0
  121. package/dist/src/String.js +14 -0
  122. package/dist/src/Time.d.ts +20 -0
  123. package/dist/src/Time.d.ts.map +1 -0
  124. package/dist/src/Time.js +25 -0
  125. package/dist/src/Type.d.ts +1937 -0
  126. package/dist/src/Type.d.ts.map +1 -0
  127. package/dist/src/Type.js +2002 -0
  128. package/dist/src/Types.d.ts +188 -0
  129. package/dist/src/Types.d.ts.map +1 -0
  130. package/dist/src/Types.js +6 -0
  131. package/dist/src/WebSocket.d.ts +112 -0
  132. package/dist/src/WebSocket.d.ts.map +1 -0
  133. package/dist/src/WebSocket.js +139 -0
  134. package/dist/src/Worker.d.ts +44 -0
  135. package/dist/src/Worker.d.ts.map +1 -0
  136. package/dist/src/Worker.js +66 -0
  137. package/dist/src/index.d.ts +24 -11
  138. package/dist/src/index.d.ts.map +1 -1
  139. package/dist/src/index.js +24 -11
  140. package/package.json +29 -38
  141. package/src/Array.ts +39 -0
  142. package/src/Assert.ts +116 -0
  143. package/src/BigInt.ts +29 -0
  144. package/src/Buffer.ts +175 -0
  145. package/src/Callbacks.ts +43 -0
  146. package/src/Console.ts +159 -0
  147. package/src/Crypto.ts +169 -115
  148. package/src/Eq.ts +204 -0
  149. package/src/Error.ts +57 -20
  150. package/src/Evolu/Config.ts +83 -0
  151. package/src/Evolu/Db.ts +1275 -0
  152. package/src/Evolu/Diff.ts +142 -0
  153. package/src/Evolu/Evolu.ts +947 -0
  154. package/src/Evolu/Internal.ts +26 -0
  155. package/src/Evolu/Kysely.ts +38 -0
  156. package/src/Evolu/Owner.ts +296 -0
  157. package/src/Evolu/Platform.ts +27 -0
  158. package/src/Evolu/Protocol.ts +1857 -0
  159. package/src/Evolu/Public.ts +43 -0
  160. package/src/Evolu/PublicKysely.ts +240 -0
  161. package/src/Evolu/Query.ts +167 -0
  162. package/src/Evolu/Relay.ts +142 -0
  163. package/src/Evolu/Schema.ts +417 -0
  164. package/src/Evolu/Storage.ts +1281 -0
  165. package/src/Evolu/Sync.ts +105 -0
  166. package/src/Evolu/Timestamp.ts +311 -0
  167. package/src/Function.ts +58 -0
  168. package/src/ManyToManyMap.ts +140 -0
  169. package/src/NanoId.ts +39 -0
  170. package/src/Number.ts +90 -0
  171. package/src/Object.ts +64 -0
  172. package/src/Order.ts +113 -0
  173. package/src/Promise.ts +295 -0
  174. package/src/Random.ts +68 -0
  175. package/src/Ref.ts +63 -0
  176. package/src/Result.ts +453 -0
  177. package/src/Skiplist.ts +102 -0
  178. package/src/Sqlite.ts +366 -153
  179. package/src/Store.ts +79 -36
  180. package/src/String.ts +10 -0
  181. package/src/Time.ts +36 -0
  182. package/src/Type.ts +3978 -0
  183. package/src/Types.ts +209 -0
  184. package/src/WebSocket.ts +273 -0
  185. package/src/Worker.ts +129 -0
  186. package/src/index.ts +24 -11
  187. package/dist/src/Config.d.ts +0 -56
  188. package/dist/src/Config.d.ts.map +0 -1
  189. package/dist/src/Config.js +0 -39
  190. package/dist/src/Crdt.d.ts +0 -89
  191. package/dist/src/Crdt.d.ts.map +0 -1
  192. package/dist/src/Crdt.js +0 -181
  193. package/dist/src/Db.d.ts +0 -107
  194. package/dist/src/Db.d.ts.map +0 -1
  195. package/dist/src/Db.js +0 -443
  196. package/dist/src/Diff.d.ts +0 -27
  197. package/dist/src/Diff.d.ts.map +0 -1
  198. package/dist/src/Diff.js +0 -84
  199. package/dist/src/Evolu.d.ts +0 -426
  200. package/dist/src/Evolu.d.ts.map +0 -1
  201. package/dist/src/Evolu.js +0 -333
  202. package/dist/src/Model.d.ts +0 -141
  203. package/dist/src/Model.d.ts.map +0 -1
  204. package/dist/src/Model.js +0 -125
  205. package/dist/src/Murmurhash.d.ts +0 -2
  206. package/dist/src/Murmurhash.d.ts.map +0 -1
  207. package/dist/src/Murmurhash.js +0 -60
  208. package/dist/src/Owner.d.ts +0 -33
  209. package/dist/src/Owner.d.ts.map +0 -1
  210. package/dist/src/Owner.js +0 -26
  211. package/dist/src/Platform.d.ts +0 -37
  212. package/dist/src/Platform.d.ts.map +0 -1
  213. package/dist/src/Platform.js +0 -11
  214. package/dist/src/Protobuf.d.ts +0 -81
  215. package/dist/src/Protobuf.d.ts.map +0 -1
  216. package/dist/src/Protobuf.js +0 -92
  217. package/dist/src/Public.d.ts +0 -13
  218. package/dist/src/Public.d.ts.map +0 -1
  219. package/dist/src/Public.js +0 -6
  220. package/dist/src/Socket.d.ts +0 -8
  221. package/dist/src/Socket.d.ts.map +0 -1
  222. package/dist/src/Socket.js +0 -51
  223. package/dist/src/Sql.d.ts +0 -12
  224. package/dist/src/Sql.d.ts.map +0 -1
  225. package/dist/src/Sql.js +0 -30
  226. package/dist/src/Sync.d.ts +0 -70
  227. package/dist/src/Sync.d.ts.map +0 -1
  228. package/dist/src/Sync.js +0 -127
  229. package/src/Config.ts +0 -119
  230. package/src/Crdt.ts +0 -361
  231. package/src/Db.ts +0 -955
  232. package/src/Diff.ts +0 -114
  233. package/src/Evolu.ts +0 -1016
  234. package/src/Model.ts +0 -233
  235. package/src/Murmurhash.ts +0 -70
  236. package/src/Owner.ts +0 -69
  237. package/src/Platform.ts +0 -47
  238. package/src/Protobuf.ts +0 -155
  239. package/src/Public.ts +0 -12
  240. package/src/Socket.ts +0 -83
  241. package/src/Sql.ts +0 -41
  242. package/src/Sync.ts +0 -315
@@ -0,0 +1,1857 @@
1
+ /**
2
+ * Evolu Protocol
3
+ *
4
+ * Evolu Protocol is a local-first, end-to-end encrypted binary synchronization
5
+ * protocol optimized for minimal size and maximum speed. It enables data sync
6
+ * between a client and a relay, clients in a peer-to-peer (P2P) setup, or
7
+ * relays with each other.
8
+ *
9
+ * Evolu Protocol is designed for SQLite but can be extended to any database. It
10
+ * implements [Range-Based Set Reconciliation](https://arxiv.org/abs/2212.13567)
11
+ * by Aljoscha Meyer.
12
+ *
13
+ * To learn how RBSR works, check
14
+ * [Negentropy](https://logperiodic.com/rbsr.html). Evolu Protocol is similar to
15
+ * Negentropy but uses different encoding and also provides data transfer and
16
+ * ownership.
17
+ *
18
+ * ### Message Structure
19
+ *
20
+ * | Field | Notes |
21
+ * | :----------------------------- | :------------------------- |
22
+ * | **Header** | |
23
+ * | - {@link protocolVersion} | |
24
+ * | - {@link OwnerId} | |
25
+ * | - {@link ProtocolErrorCode} | In non-initiator response. |
26
+ * | **Messages** | |
27
+ * | - {@link NonNegativeInt} | A number of messages. |
28
+ * | - {@link EncryptedCrdtMessage} | |
29
+ * | - {@link WriteKey} | In initiator request. |
30
+ * | **Ranges** | |
31
+ * | - {@link NonNegativeInt} | Number of ranges. |
32
+ * | - {@link Range} | |
33
+ *
34
+ * Every protocol message belongs to an owner.
35
+ *
36
+ * ### Synchronization
37
+ *
38
+ * - **Messages**: Sends {@link EncryptedCrdtMessage}s in either direction.
39
+ * - **Ranges**: Determines messages to sync. Usage varies by transport—e.g., sent
40
+ * only on WebSocket connection open or with every fetch request.
41
+ *
42
+ * Synchronization involves an initiator and a non-initiator. The **initiator**
43
+ * is typically a client, and the **non-initiator** is typically a relay. Each
44
+ * side processes the received message and responds with a new `ProtocolMessage`
45
+ * if further sync is needed or possible, continuing until both sides are
46
+ * synchronized.
47
+ *
48
+ * Both **Messages** and **Ranges** are optional, allowing each side to send,
49
+ * sync, or only subscribe data as needed.
50
+ *
51
+ * When the initiator sends data, the {@link WriteKey} is **required** in
52
+ * **Messages** as a secure token proving the initiator can write changes. The
53
+ * non-initiator responds without a {@link WriteKey}, since the initiator’s
54
+ * request already signals it wants data. If the non-initiator detects an issue
55
+ * (e.g., an invalid {@link WriteKey} causing a {@link ProtocolWriteKeyError}, or
56
+ * a write failure causing a {@link ProtocolWriteError}), it sends an error code
57
+ * via the `Error` field in the header back to the initiator. In relay-to-relay
58
+ * or P2P sync, both sides may require the {@link WriteKey} depending on who is
59
+ * the initiator.
60
+ *
61
+ * ### Message Size Limit
62
+ *
63
+ * The protocol enforces a strict maximum size for all messages, defined by
64
+ * {@link maxProtocolMessageSize}. This ensures every `ProtocolMessage` is less
65
+ * than or equal to this limit, eliminating the need for applications to
66
+ * fragment and reconstruct messages during transmission.
67
+ *
68
+ * ### Why Binary?
69
+ *
70
+ * The protocol avoids JSON because:
71
+ *
72
+ * - Encrypted data doesn’t compress well, unlike plain JSON.
73
+ * - Message size must be controlled during creation.
74
+ * - Sequential byte reading is faster than parsing and can avoid conversions.
75
+ *
76
+ * It uses structure-aware encoding, significantly outperforming generic binary
77
+ * serialization formats with the following optimizations:
78
+ *
79
+ * - **NonNegativeInt:** Up to 33% smaller than MessagePack.
80
+ * - **Base64Url Strings:** Up to 25% size reduction.
81
+ * - **DateIso:** Up to 75% smaller.
82
+ * - **Timestamp Encoding:** Delta encoding for milliseconds and run-length
83
+ * encoding (RLE) for counters and NodeIds.
84
+ * - **Small Integers (0 to 19):** Reduces size by 1 byte per integer.
85
+ *
86
+ * To avoid reinventing serialization where it’s unnecessary—like for JSON and
87
+ * certain numbers—the Evolu Protocol relies on MessagePack.
88
+ *
89
+ * ### Versioning
90
+ *
91
+ * The initiator sends a versioned `ProtocolMessage`. If the non-initiator uses
92
+ * a different version, it responds with a message containing only its protocol
93
+ * version—**without an `ownerId`**. This allows the initiator to check protocol
94
+ * compatibility, for example, by sending version-only messages to multiple
95
+ * relays before starting synchronization.
96
+ *
97
+ * @module
98
+ */
99
+
100
+ import { sha256 } from "@noble/hashes/sha2";
101
+ import { pack, unpack, unpackMultiple } from "msgpackr";
102
+ import { isNonEmptyReadonlyArray, NonEmptyReadonlyArray } from "../Array.js";
103
+ import { assert } from "../Assert.js";
104
+ import {
105
+ Buffer,
106
+ BufferError,
107
+ bytesToHex,
108
+ bytesToUtf8,
109
+ concatBytes,
110
+ createBuffer,
111
+ hexToBytes,
112
+ utf8ToBytes,
113
+ } from "../Buffer.js";
114
+ import {
115
+ CreateRandomBytesDep,
116
+ EncryptionKey,
117
+ padmePaddingLength,
118
+ SymmetricCryptoDecryptError,
119
+ SymmetricCryptoDep,
120
+ } from "../Crypto.js";
121
+ import { eqArrayNumber } from "../Eq.js";
122
+ import { computeBalancedBuckets } from "../Number.js";
123
+ import { objectToEntries, ReadonlyRecord } from "../Object.js";
124
+ import { err, ok, Result } from "../Result.js";
125
+ import { SqliteValue } from "../Sqlite.js";
126
+ import {
127
+ Base64Url,
128
+ DateIsoString,
129
+ Id,
130
+ idTypeValueLength,
131
+ JsonValueFromString,
132
+ maxLength,
133
+ NanoId,
134
+ NonNegativeInt,
135
+ Number,
136
+ PositiveInt,
137
+ } from "../Type.js";
138
+ import { Brand, Predicate } from "../Types.js";
139
+ import {
140
+ OwnerId,
141
+ OwnerWithWriteAccess,
142
+ WriteKey,
143
+ writeKeyLength,
144
+ } from "./Owner.js";
145
+ import {
146
+ BinaryTimestamp,
147
+ binaryTimestampToTimestamp,
148
+ Counter,
149
+ Millis,
150
+ NodeId,
151
+ Timestamp,
152
+ timestampToBinaryTimestamp,
153
+ } from "./Timestamp.js";
154
+
155
+ /** Maximum size of the entire protocol message in bytes. */
156
+ export const maxProtocolMessageSize = 1_000_000 as PositiveInt;
157
+
158
+ /** Maximum size of the ranges in bytes. */
159
+ export const maxProtocolMessageRangesSize = 30_000 as PositiveInt;
160
+
161
+ /** Evolu Protocol Message. */
162
+ export type ProtocolMessage = Uint8Array & Brand<"ProtocolMessage">;
163
+
164
+ /** Evolu Protocol version. */
165
+ export const protocolVersion = 0 as NonNegativeInt;
166
+
167
+ export const ProtocolErrorCode = {
168
+ NoError: 0,
169
+ /** A code for {@link ProtocolWriteKeyError}. */
170
+ WriteKeyError: 1,
171
+ /** A code for {@link ProtocolWriteError}. */
172
+ WriteError: 2,
173
+ /** A code for {@link ProtocolSyncError}. */
174
+ SyncError: 3,
175
+ } as const;
176
+
177
+ type ProtocolErrorCode =
178
+ (typeof ProtocolErrorCode)[keyof typeof ProtocolErrorCode];
179
+
180
+ /**
181
+ * Evolu Protocol Storage
182
+ *
183
+ * The protocol is agnostic to storage implementation details—any storage can be
184
+ * plugged in, as long as it implements this interface. Implementations must
185
+ * handle their own errors; return values only indicates overall success or
186
+ * failure.
187
+ */
188
+ export interface Storage {
189
+ readonly getSize: (ownerId: BinaryOwnerId) => NonNegativeInt | null;
190
+
191
+ readonly fingerprint: (
192
+ ownerId: BinaryOwnerId,
193
+ begin: NonNegativeInt,
194
+ end: NonNegativeInt,
195
+ ) => Fingerprint | null;
196
+
197
+ /**
198
+ * Computes fingerprints with their upper bounds in one call.
199
+ *
200
+ * This function can be replaced with many fingerprint/findLowerBound calls,
201
+ * but implementations can leverage it for batching and more efficient
202
+ * fingerprint computation.
203
+ */
204
+ readonly fingerprintRanges: (
205
+ ownerId: BinaryOwnerId,
206
+ buckets: ReadonlyArray<NonNegativeInt>,
207
+ upperBound?: RangeUpperBound,
208
+ ) => ReadonlyArray<FingerprintRange> | null;
209
+
210
+ readonly findLowerBound: (
211
+ ownerId: BinaryOwnerId,
212
+ begin: NonNegativeInt,
213
+ end: NonNegativeInt,
214
+ upperBound: RangeUpperBound,
215
+ ) => NonNegativeInt | null;
216
+
217
+ readonly iterate: (
218
+ ownerId: BinaryOwnerId,
219
+ begin: NonNegativeInt,
220
+ end: NonNegativeInt,
221
+ callback: (timestamp: BinaryTimestamp, index: NonNegativeInt) => boolean,
222
+ ) => void;
223
+
224
+ /**
225
+ * Authorizes the initiator's {@link WriteKey} for the given
226
+ * {@link BinaryOwnerId}.
227
+ *
228
+ * For a client that does not expect foreign writes, return `false`.
229
+ */
230
+ readonly validateWriteKey: (
231
+ ownerId: BinaryOwnerId,
232
+ writeKey: WriteKey,
233
+ ) => boolean;
234
+
235
+ /** Write encrypted {@link CrdtMessage}s to storage. */
236
+ readonly writeMessages: (
237
+ ownerId: BinaryOwnerId,
238
+ messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
239
+ ) => boolean;
240
+
241
+ /** Read encrypted {@link DbChange}s from storage. */
242
+ readonly readDbChange: (
243
+ ownerId: BinaryOwnerId,
244
+ timestamp: BinaryTimestamp,
245
+ ) => EncryptedDbChange | null;
246
+ }
247
+
248
+ export interface StorageDep {
249
+ readonly storage: Storage;
250
+ }
251
+
252
+ /** An encrypted {@link CrdtMessage}. */
253
+ export interface EncryptedCrdtMessage {
254
+ readonly timestamp: Timestamp;
255
+ readonly change: EncryptedDbChange;
256
+ }
257
+
258
+ /** Encrypted DbChange */
259
+ export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
260
+
261
+ /**
262
+ * A CRDT message that combines a unique {@link Timestamp} with a
263
+ * {@link DbChange}.
264
+ */
265
+ export interface CrdtMessage {
266
+ readonly timestamp: Timestamp;
267
+ readonly change: DbChange;
268
+ }
269
+
270
+ /**
271
+ * A DbChange is a change to a table row. Together with a unique
272
+ * {@link Timestamp}, it forms a {@link CrdtMessage}.
273
+ */
274
+ export interface DbChange {
275
+ readonly table: Base64Url256;
276
+ readonly id: Id;
277
+ readonly values: ReadonlyRecord<Base64Url256, SqliteValue>;
278
+ }
279
+
280
+ export const RangeType = {
281
+ Fingerprint: 1,
282
+ Skip: 0,
283
+ Timestamps: 2,
284
+ } as const;
285
+
286
+ export type RangeType = (typeof RangeType)[keyof typeof RangeType];
287
+
288
+ export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
289
+ export type InfiniteUpperBound = typeof InfiniteUpperBound;
290
+
291
+ /**
292
+ * Union type for Range's upperBound: either a {@link BinaryTimestamp} or
293
+ * {@link InfiniteUpperBound}.
294
+ */
295
+ export type RangeUpperBound = BinaryTimestamp | InfiniteUpperBound;
296
+
297
+ interface BaseRange {
298
+ readonly upperBound: RangeUpperBound;
299
+ }
300
+
301
+ export interface SkipRange extends BaseRange {
302
+ readonly type: typeof RangeType.Skip;
303
+ }
304
+
305
+ export interface FingerprintRange extends BaseRange {
306
+ readonly type: typeof RangeType.Fingerprint;
307
+ readonly fingerprint: Fingerprint;
308
+ }
309
+
310
+ /**
311
+ * A cryptographic hash used for efficiently comparing collections of
312
+ * {@link BinaryTimestamp}s.
313
+ *
314
+ * It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
315
+ * one or more timestamps.
316
+ */
317
+ export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
318
+
319
+ export const fingerprintSize = 12 as NonNegativeInt;
320
+
321
+ /** A fingerprint of an empty range. */
322
+ export const zeroFingerprint = new Uint8Array(fingerprintSize) as Fingerprint;
323
+
324
+ export interface TimestampsRange extends BaseRange {
325
+ readonly type: typeof RangeType.Timestamps;
326
+ readonly timestamps: ReadonlyArray<BinaryTimestamp>;
327
+ }
328
+
329
+ export interface TimestampsRangeWithTimestampsBuffer extends BaseRange {
330
+ readonly type: typeof RangeType.Timestamps;
331
+ readonly timestamps: TimestampsBuffer;
332
+ }
333
+
334
+ export type Range = SkipRange | FingerprintRange | TimestampsRange;
335
+
336
+ export type ProtocolError =
337
+ | ProtocolUnsupportedVersionError
338
+ | ProtocolInvalidDataError
339
+ | ProtocolWriteKeyError
340
+ | ProtocolWriteError
341
+ | ProtocolSyncError;
342
+
343
+ /**
344
+ * Represents a version mismatch in the Evolu Protocol. Occurs when the
345
+ * initiator and non-initiator are using incompatible protocol versions.
346
+ */
347
+ export interface ProtocolUnsupportedVersionError {
348
+ readonly type: "ProtocolUnsupportedVersionError";
349
+ readonly unsupportedVersion: NonNegativeInt;
350
+ /** Indicates which side is obsolete and should update. */
351
+ readonly isInitiator: boolean;
352
+ }
353
+
354
+ /** Error for invalid or corrupted protocol message data. */
355
+ export interface ProtocolInvalidDataError {
356
+ readonly type: "ProtocolInvalidDataError";
357
+ readonly data: globalThis.Uint8Array;
358
+ readonly error: unknown;
359
+ }
360
+
361
+ /** Error when a {@link WriteKey} is invalid, missing, or fails validation. */
362
+ export interface ProtocolWriteKeyError {
363
+ readonly type: "ProtocolWriteKeyError";
364
+ }
365
+
366
+ /**
367
+ * Error when a write fails due to storage limits or billing requirements.
368
+ * Indicates the need to expand capacity or resolve payment issues.
369
+ */
370
+ export interface ProtocolWriteError {
371
+ readonly type: "ProtocolWriteError";
372
+ }
373
+
374
+ /**
375
+ * Error indicating a synchronization failure during the protocol exchange. Used
376
+ * for unexpected or generic sync errors not covered by other error types.
377
+ */
378
+ export interface ProtocolSyncError {
379
+ readonly type: "ProtocolSyncError";
380
+ }
381
+
382
+ /**
383
+ * Creates a {@link ProtocolMessage} from CRDT messages.
384
+ *
385
+ * If the message size would exceed {@link maxProtocolMessageSize}, the protocol
386
+ * ensures all messages will be sent in the next round(s) even over
387
+ * unidirectional and stateless transports.
388
+ */
389
+ export const createProtocolMessageFromCrdtMessages =
390
+ (deps: SymmetricCryptoDep & CreateRandomBytesDep) =>
391
+ (
392
+ owner: OwnerWithWriteAccess,
393
+ messages: NonEmptyReadonlyArray<CrdtMessage>,
394
+ maxSize?: PositiveInt,
395
+ ): ProtocolMessage => {
396
+ const buffer = createProtocolMessageBuffer(owner.id, {
397
+ totalMaxSize: maxSize ?? maxProtocolMessageSize,
398
+ writeKey: owner.writeKey,
399
+ });
400
+
401
+ let notAllMessagesSent = false;
402
+
403
+ for (const message of messages) {
404
+ const change = encodeAndEncryptDbChange(deps)(
405
+ message.change,
406
+ owner.encryptionKey,
407
+ );
408
+ const encryptedCrdtMessage = { timestamp: message.timestamp, change };
409
+ if (buffer.canAddMessage(encryptedCrdtMessage)) {
410
+ buffer.addMessage(encryptedCrdtMessage);
411
+ } else {
412
+ notAllMessagesSent = true;
413
+ break;
414
+ }
415
+ }
416
+
417
+ if (notAllMessagesSent) {
418
+ /**
419
+ * DEV: If not all messages fit due to size limits, we trigger a sync
420
+ * continuation by appending a Range with a random fingerprint. This
421
+ * ensures the receiver always responds with ranges, prompting another
422
+ * sync round.
423
+ *
424
+ * The ideal approach would be to send three ranges (skip, fingerprint,
425
+ * skip) where the fingerprint of unsent messages would act as narrow sync
426
+ * probe. I think we can send {@link zeroFingerprint} which can be
427
+ * interpreted as an indication that the other side should reply with
428
+ * {@link TimestampsRange}, so no need to restart syncing.
429
+ *
430
+ * For now, using a random fingerprint avoids extra complexity and is good
431
+ * enough for this case.
432
+ */
433
+ const randomFingerprint = deps.createRandomBytes(
434
+ fingerprintSize,
435
+ ) as unknown as Fingerprint;
436
+
437
+ // There is always a space for Fingerprint with InfiniteUpperBound.
438
+ buffer.addRange({
439
+ type: RangeType.Fingerprint,
440
+ upperBound: InfiniteUpperBound,
441
+ fingerprint: randomFingerprint,
442
+ });
443
+ }
444
+
445
+ return buffer.unwrap();
446
+ };
447
+
448
+ /** Creates a {@link ProtocolMessage} for sync. */
449
+ export const createProtocolMessageForSync =
450
+ (deps: StorageDep) =>
451
+ (ownerId: OwnerId): ProtocolMessage | null => {
452
+ const buffer = createProtocolMessageBuffer(ownerId);
453
+ const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
454
+
455
+ const size = deps.storage.getSize(binaryOwnerId);
456
+ // Errors are handled by the storage.
457
+ if (size == null) return null;
458
+
459
+ splitRange(deps)(
460
+ binaryOwnerId,
461
+ 0 as NonNegativeInt,
462
+ size,
463
+ InfiniteUpperBound,
464
+ buffer,
465
+ );
466
+
467
+ return buffer.unwrap();
468
+ };
469
+
470
+ /**
471
+ * Mutable builder for constructing {@link ProtocolMessage} respecting size
472
+ * limits.
473
+ */
474
+ export interface ProtocolMessageBuffer {
475
+ readonly canAddMessage: (message: EncryptedCrdtMessage) => boolean;
476
+
477
+ readonly addMessage: (message: EncryptedCrdtMessage) => void;
478
+
479
+ readonly canSplitRange: () => boolean;
480
+
481
+ readonly canAddTimestampsRangeAndMessage: (
482
+ timestamps: TimestampsBuffer,
483
+ message: EncryptedCrdtMessage | null,
484
+ ) => boolean;
485
+
486
+ readonly addRange: (
487
+ range: SkipRange | FingerprintRange | TimestampsRangeWithTimestampsBuffer,
488
+ ) => void;
489
+
490
+ readonly unwrap: () => ProtocolMessage;
491
+ readonly getSize: () => PositiveInt;
492
+ }
493
+
494
+ export const createProtocolMessageBuffer = (
495
+ ownerId: OwnerId,
496
+ options: {
497
+ readonly errorCode?: ProtocolErrorCode;
498
+ readonly writeKey?: WriteKey;
499
+ readonly totalMaxSize?: PositiveInt | undefined;
500
+ readonly rangesMaxSize?: PositiveInt | undefined;
501
+ readonly version?: NonNegativeInt;
502
+ } = {},
503
+ ): ProtocolMessageBuffer => {
504
+ const {
505
+ errorCode,
506
+ writeKey,
507
+ totalMaxSize = maxProtocolMessageSize,
508
+ rangesMaxSize = maxProtocolMessageRangesSize,
509
+ version = protocolVersion,
510
+ } = options;
511
+
512
+ const buffers = {
513
+ header: createBuffer(),
514
+ messages: {
515
+ timestamps: createTimestampsBuffer(),
516
+ dbChanges: createBuffer(),
517
+ },
518
+ ranges: {
519
+ timestamps: createTimestampsBuffer(),
520
+ types: createBuffer(),
521
+ payloads: createBuffer(),
522
+ },
523
+ };
524
+
525
+ encodeNonNegativeInt(buffers.header, version);
526
+ buffers.header.extend(ownerIdToBinaryOwnerId(ownerId));
527
+ if (errorCode != null) buffers.header.extend([errorCode]);
528
+
529
+ let isLastRangeInfinite = false;
530
+
531
+ const isWithinSizeLimits = () => getSize() <= totalMaxSize;
532
+
533
+ const getSize = () =>
534
+ (getHeaderAndMessagesSize() + getRangesSize()) as PositiveInt;
535
+
536
+ const getHeaderAndMessagesSize = () =>
537
+ buffers.header.getLength() +
538
+ buffers.messages.timestamps.getLength() +
539
+ buffers.messages.dbChanges.getLength() +
540
+ (buffers.messages.timestamps.getCount() > 0 && writeKey
541
+ ? writeKeyLength
542
+ : 0);
543
+
544
+ const getRangesSize = () =>
545
+ buffers.ranges.timestamps.getCount() > 0
546
+ ? buffers.ranges.timestamps.getLength() +
547
+ buffers.ranges.types.getLength() +
548
+ buffers.ranges.payloads.getLength() +
549
+ safeMargins.remainingRange
550
+ : 0;
551
+
552
+ /**
553
+ * We calculated worst-case sizes as closely as possible and added a small
554
+ * safety margin, since computing exact worst cases is difficult due to
555
+ * variable-length, run-length, and delta encoding.
556
+ *
557
+ * Runtime assertions (`assert`) are used to guarantee that size limits are
558
+ * never exceeded. If a limit is exceeded, the assertion will fail at the
559
+ * precise location, making it easy to identify and fix the issue.
560
+ *
561
+ * While it would be possible to avoid the safety margin by snapshotting
562
+ * buffer states and rolling back changes, this would likely impact
563
+ * performance. If someone has time and wants to experiment with this
564
+ * approach, contributions are welcome.
565
+ */
566
+ const safeMargins = {
567
+ remainingRange: fingerprintSize + 10, // bytes: range type + possible increased count varint
568
+ timestamp: 30, // bytes: max millis + max count + NodeId
569
+ dbChangeLength: 8, // bytes: maximum encoded DbChange length varint
570
+ splitRange: 800, // bytes: worst case is around 650 bytes
571
+ timestampsRange: 50, // bytes: range type + its upperBound + possible increased count varint
572
+ };
573
+
574
+ const addMessageSafeMargin =
575
+ safeMargins.timestamp +
576
+ safeMargins.dbChangeLength +
577
+ safeMargins.remainingRange;
578
+
579
+ return {
580
+ canAddMessage: (message) =>
581
+ getSize() + addMessageSafeMargin + message.change.length <= totalMaxSize,
582
+
583
+ addMessage: (message) => {
584
+ buffers.messages.timestamps.add(message.timestamp);
585
+ encodeLength(buffers.messages.dbChanges, message.change);
586
+ buffers.messages.dbChanges.extend(message.change);
587
+ assert(isWithinSizeLimits(), "the message is too big");
588
+ },
589
+
590
+ canSplitRange: () => {
591
+ return getRangesSize() + safeMargins.splitRange <= rangesMaxSize;
592
+ },
593
+
594
+ canAddTimestampsRangeAndMessage: (timestamps, message) => {
595
+ const rangesNewSize =
596
+ getRangesSize() + timestamps.getLength() + safeMargins.timestampsRange;
597
+
598
+ return (
599
+ rangesNewSize <= rangesMaxSize &&
600
+ (message
601
+ ? getHeaderAndMessagesSize() +
602
+ rangesNewSize +
603
+ addMessageSafeMargin +
604
+ message.change.length <=
605
+ totalMaxSize
606
+ : true)
607
+ );
608
+ },
609
+
610
+ addRange: (range) => {
611
+ assert(
612
+ !isLastRangeInfinite,
613
+ "Cannot add a range after an InfiniteUpperBound range",
614
+ );
615
+ isLastRangeInfinite = range.upperBound === InfiniteUpperBound;
616
+
617
+ /**
618
+ * We don't have to encode InfiniteUpperBound timestamp since it's always
619
+ * the last because ranges cover the whole universe. For partial sync, we
620
+ * use SkipRange.
621
+ */
622
+ if (range.upperBound !== InfiniteUpperBound)
623
+ buffers.ranges.timestamps.add(
624
+ binaryTimestampToTimestamp(range.upperBound),
625
+ );
626
+ else {
627
+ buffers.ranges.timestamps.addInfinite();
628
+ }
629
+
630
+ encodeNonNegativeInt(buffers.ranges.types, range.type as NonNegativeInt);
631
+
632
+ switch (range.type) {
633
+ case RangeType.Skip:
634
+ break;
635
+ case RangeType.Fingerprint:
636
+ buffers.ranges.payloads.extend(range.fingerprint);
637
+ break;
638
+ case RangeType.Timestamps: {
639
+ range.timestamps.append(buffers.ranges.payloads);
640
+ break;
641
+ }
642
+ }
643
+
644
+ assert(isWithinSizeLimits(), `the range ${range.type} is too big`);
645
+ },
646
+
647
+ unwrap: () => {
648
+ if (buffers.ranges.timestamps.getCount() > 0) {
649
+ assert(
650
+ isLastRangeInfinite,
651
+ "The last range's upperBound must be InfiniteUpperBound",
652
+ );
653
+ }
654
+
655
+ buffers.messages.timestamps.append(buffers.header);
656
+ buffers.header.extend(buffers.messages.dbChanges.unwrap());
657
+ if (buffers.messages.timestamps.getCount() > 0 && writeKey)
658
+ buffers.header.extend(writeKey);
659
+
660
+ if (buffers.ranges.timestamps.getCount() > 0) {
661
+ buffers.ranges.timestamps.append(buffers.header);
662
+ buffers.header.extend(buffers.ranges.types.unwrap());
663
+ buffers.header.extend(buffers.ranges.payloads.unwrap());
664
+ }
665
+
666
+ return buffers.header.unwrap() as ProtocolMessage;
667
+ },
668
+
669
+ getSize,
670
+ };
671
+ };
672
+
673
+ export interface TimestampsBuffer {
674
+ readonly add: (timestamp: Timestamp) => void;
675
+ readonly addInfinite: () => void;
676
+ readonly getCount: () => NonNegativeInt;
677
+ readonly getLength: () => number;
678
+ readonly append: (buffer: Buffer) => void;
679
+ }
680
+
681
+ export const createTimestampsBuffer = (): TimestampsBuffer => {
682
+ let count = 0 as NonNegativeInt;
683
+ const countBuffer = createBuffer();
684
+
685
+ const syncCount = () => {
686
+ countBuffer.reset();
687
+ encodeNonNegativeInt(countBuffer, count);
688
+ };
689
+
690
+ syncCount();
691
+
692
+ const millisBuffer = createBuffer();
693
+ let previousMillis = 0 as Millis;
694
+
695
+ const counterEncoder = createRunLengthEncoder<Counter>((buffer, value) => {
696
+ encodeNonNegativeInt(buffer, value);
697
+ });
698
+ const nodeIdEncoder = createRunLengthEncoder<NodeId>((buffer, value) => {
699
+ encodeNodeId(buffer, value);
700
+ });
701
+
702
+ return {
703
+ add: (timestamp) => {
704
+ const delta = timestamp.millis - previousMillis;
705
+ assert(NonNegativeInt.is(delta), "The delta must be NonNegativeInt");
706
+
707
+ count++;
708
+ syncCount();
709
+
710
+ previousMillis = timestamp.millis;
711
+ encodeNonNegativeInt(millisBuffer, delta);
712
+
713
+ counterEncoder.add(timestamp.counter);
714
+ nodeIdEncoder.add(timestamp.nodeId);
715
+ },
716
+
717
+ addInfinite: () => {
718
+ count++;
719
+ syncCount();
720
+ },
721
+
722
+ getCount: () => count,
723
+
724
+ getLength: () =>
725
+ countBuffer.getLength() +
726
+ millisBuffer.getLength() +
727
+ counterEncoder.getLength() +
728
+ nodeIdEncoder.getLength(),
729
+
730
+ append: (buffer) => {
731
+ buffer.extend(countBuffer.unwrap());
732
+ buffer.extend(millisBuffer.unwrap());
733
+ buffer.extend(counterEncoder.unwrap());
734
+ buffer.extend(nodeIdEncoder.unwrap());
735
+ },
736
+ };
737
+ };
738
+
739
+ interface RunLengthEncoder<T> {
740
+ add: (value: T) => void;
741
+ getLength: () => NonNegativeInt;
742
+ unwrap: () => Uint8Array;
743
+ }
744
+
745
+ const createRunLengthEncoder = <T>(
746
+ encodeValue: (buffer: Buffer, value: T) => void,
747
+ ): RunLengthEncoder<T> => {
748
+ const buffer = createBuffer();
749
+ let previousLength = 0 as NonNegativeInt;
750
+ let previousValue = null as T | null;
751
+ let runLength = 0 as NonNegativeInt;
752
+
753
+ return {
754
+ add: (value) => {
755
+ if (value === previousValue) {
756
+ runLength++;
757
+ buffer.truncate(previousLength);
758
+ } else {
759
+ previousValue = value;
760
+ runLength = 1 as NonNegativeInt;
761
+ }
762
+ previousLength = buffer.getLength();
763
+ encodeValue(buffer, value);
764
+ encodeNonNegativeInt(buffer, runLength);
765
+ },
766
+
767
+ getLength: () => buffer.getLength(),
768
+
769
+ unwrap: () => buffer.unwrap(),
770
+ };
771
+ };
772
+
773
+ export interface ApplyProtocolMessageAsClientOptions {
774
+ getWriteKey?: (ownerId: OwnerId) => WriteKey | null;
775
+
776
+ /** For testing purposes only; should not be used in production. */
777
+ version?: NonNegativeInt;
778
+
779
+ totalMaxSize?: PositiveInt;
780
+ rangesMaxSize?: PositiveInt;
781
+ }
782
+
783
+ export const applyProtocolMessageAsClient =
784
+ (deps: StorageDep) =>
785
+ (
786
+ inputMessage: Uint8Array,
787
+ {
788
+ getWriteKey,
789
+ version = protocolVersion,
790
+ totalMaxSize,
791
+ rangesMaxSize,
792
+ }: ApplyProtocolMessageAsClientOptions = {},
793
+ ): Result<ProtocolMessage | null, ProtocolError> =>
794
+ tryDecodeProtocolData<ProtocolMessage | null, ProtocolError>(
795
+ inputMessage,
796
+ (input) => {
797
+ const requestedVersion = decodeNonNegativeInt(input);
798
+
799
+ if (requestedVersion !== version) {
800
+ return err<ProtocolUnsupportedVersionError>({
801
+ type: "ProtocolUnsupportedVersionError",
802
+ unsupportedVersion: requestedVersion,
803
+ isInitiator: version < requestedVersion,
804
+ });
805
+ }
806
+
807
+ const ownerId = decodeOwnerId(input);
808
+ const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
809
+
810
+ const errorCode = input.shift() as ProtocolErrorCode;
811
+ if (errorCode !== ProtocolErrorCode.NoError) {
812
+ switch (errorCode) {
813
+ case ProtocolErrorCode.WriteKeyError:
814
+ return err<ProtocolWriteKeyError>({
815
+ type: "ProtocolWriteKeyError",
816
+ });
817
+ case ProtocolErrorCode.WriteError:
818
+ return err<ProtocolWriteError>({
819
+ type: "ProtocolWriteError",
820
+ });
821
+ case ProtocolErrorCode.SyncError:
822
+ return err<ProtocolSyncError>({
823
+ type: "ProtocolSyncError",
824
+ });
825
+ default:
826
+ throw new ProtocolDecodeError(
827
+ `Invalid ProtocolErrorCode: ${errorCode}`,
828
+ );
829
+ }
830
+ }
831
+
832
+ const messages = decodeMessages(input);
833
+
834
+ if (
835
+ isNonEmptyReadonlyArray(messages) &&
836
+ !deps.storage.writeMessages(binaryOwnerId, messages)
837
+ ) {
838
+ return ok(null);
839
+ }
840
+
841
+ if (!getWriteKey) return ok(null);
842
+ const writeKey = getWriteKey(ownerId);
843
+ if (writeKey == null) return ok(null);
844
+
845
+ const output = createProtocolMessageBuffer(ownerId, {
846
+ writeKey,
847
+ totalMaxSize,
848
+ rangesMaxSize,
849
+ });
850
+
851
+ return sync(deps)("initiator", input, output, binaryOwnerId);
852
+ },
853
+ );
854
+
855
+ export interface ApplyProtocolMessageAsRelayOptions {
856
+ /** To subscribe an owner for broadcasting. */
857
+ subscribe?: (ownerId: OwnerId) => void;
858
+
859
+ /** To broadcast a protocol message to all subscribers. */
860
+ broadcast?: (ownerId: OwnerId, message: ProtocolMessage) => void;
861
+
862
+ totalMaxSize?: PositiveInt;
863
+ rangesMaxSize?: PositiveInt;
864
+ }
865
+
866
+ export const applyProtocolMessageAsRelay =
867
+ (deps: StorageDep) =>
868
+ (
869
+ inputMessage: Uint8Array,
870
+ {
871
+ subscribe,
872
+ broadcast,
873
+ totalMaxSize,
874
+ rangesMaxSize,
875
+ }: ApplyProtocolMessageAsRelayOptions = {},
876
+ /** For testing purposes only; should not be used in production. */
877
+ version = protocolVersion,
878
+ ): Result<ProtocolMessage | null, ProtocolInvalidDataError> =>
879
+ tryDecodeProtocolData(inputMessage, (input) => {
880
+ const requestedVersion = decodeNonNegativeInt(input);
881
+
882
+ if (requestedVersion !== version) {
883
+ // Non-initiator responds with its version.
884
+ const output = createBuffer();
885
+ encodeNonNegativeInt(output, version);
886
+ return ok(output.unwrap() as ProtocolMessage);
887
+ }
888
+
889
+ const ownerId = decodeOwnerId(input);
890
+ const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
891
+
892
+ subscribe?.(ownerId);
893
+
894
+ const messages = decodeMessages(input);
895
+
896
+ if (isNonEmptyReadonlyArray(messages)) {
897
+ const messagesEnd = inputMessage.length - input.getLength();
898
+ const writeKey = input.shiftN(writeKeyLength) as WriteKey;
899
+
900
+ const writeKeyIsValid = deps.storage.validateWriteKey(
901
+ binaryOwnerId,
902
+ writeKey,
903
+ );
904
+
905
+ if (!writeKeyIsValid)
906
+ return ok(
907
+ createProtocolMessageBuffer(ownerId, {
908
+ errorCode: ProtocolErrorCode.WriteKeyError,
909
+ }).unwrap(),
910
+ );
911
+
912
+ if (broadcast) {
913
+ // Instead of encoding a new protocol message, we reuse the inputMessage.
914
+ const broadcastMessage = concatBytes(
915
+ inputMessage.slice(0, 17),
916
+ new Uint8Array([ProtocolErrorCode.NoError]),
917
+ inputMessage.slice(17, messagesEnd),
918
+ ) as ProtocolMessage;
919
+
920
+ broadcast(ownerId, broadcastMessage);
921
+ }
922
+
923
+ if (!deps.storage.writeMessages(binaryOwnerId, messages))
924
+ return ok(
925
+ createProtocolMessageBuffer(ownerId, {
926
+ errorCode: ProtocolErrorCode.WriteError,
927
+ }).unwrap(),
928
+ );
929
+ }
930
+
931
+ const output = createProtocolMessageBuffer(ownerId, {
932
+ errorCode: ProtocolErrorCode.NoError,
933
+ totalMaxSize,
934
+ rangesMaxSize,
935
+ });
936
+
937
+ return sync(deps)("non-initiator", input, output, binaryOwnerId);
938
+ });
939
+
940
+ /**
941
+ * Wraps Evolu Protocol decoding functions, which use exceptions instead of
942
+ * {@link Result} to provide stack traces for debugging and reduce allocation
943
+ * overhead in success cases.
944
+ */
945
+ const tryDecodeProtocolData = <T, E>(
946
+ data: Uint8Array,
947
+ callback: (buffer: Buffer) => Result<T, E | ProtocolInvalidDataError>,
948
+ ) => {
949
+ try {
950
+ return callback(createBuffer(data));
951
+ } catch (error: unknown) {
952
+ if (error instanceof ProtocolDecodeError || error instanceof BufferError)
953
+ return err<ProtocolInvalidDataError>({
954
+ type: "ProtocolInvalidDataError",
955
+ data,
956
+ error,
957
+ });
958
+
959
+ throw error;
960
+ }
961
+ };
962
+
963
+ /**
964
+ * Error thrown for internal protocol validation failures, such as invalid data
965
+ * or type errors.
966
+ */
967
+ class ProtocolDecodeError extends Error {
968
+ constructor(message: string) {
969
+ super(message);
970
+ this.name = this.constructor.name;
971
+
972
+ Error.captureStackTrace(this, this.constructor);
973
+ }
974
+ }
975
+
976
+ const decodeMessages = (
977
+ buffer: Buffer,
978
+ ): ReadonlyArray<EncryptedCrdtMessage> => {
979
+ const timestamps = decodeTimestamps(buffer);
980
+
981
+ const messages: Array<EncryptedCrdtMessage> = [];
982
+ for (const timestamp of timestamps) {
983
+ const changeLength = decodeLength(buffer);
984
+ const change = buffer.shiftN(changeLength) as EncryptedDbChange;
985
+ messages.push({ timestamp, change });
986
+ }
987
+
988
+ return messages;
989
+ };
990
+
991
+ const sync =
992
+ (deps: StorageDep) =>
993
+ (
994
+ role: "initiator" | "non-initiator",
995
+ input: Buffer,
996
+ output: ProtocolMessageBuffer,
997
+ ownerId: BinaryOwnerId,
998
+ ): Result<ProtocolMessage | null, never> => {
999
+ const ranges = decodeRanges(input);
1000
+
1001
+ if (!isNonEmptyReadonlyArray(ranges)) {
1002
+ // Nothing to sync.
1003
+ return ok(null);
1004
+ }
1005
+
1006
+ const binaryOwnerId = binaryOwnerIdToOwnerId(ownerId);
1007
+ const outputInitialSize = output.getSize();
1008
+
1009
+ const syncFail = () => {
1010
+ // Only the relay (non-initiator) reports sync errors, not the client (initiator).
1011
+ if (role === "initiator") {
1012
+ return ok(null);
1013
+ }
1014
+ const message = createProtocolMessageBuffer(binaryOwnerId, {
1015
+ errorCode: ProtocolErrorCode.SyncError,
1016
+ });
1017
+ return ok(message.unwrap());
1018
+ };
1019
+
1020
+ const storageSize = deps.storage.getSize(ownerId);
1021
+ if (storageSize == null) return syncFail();
1022
+
1023
+ let prevUpperBound: RangeUpperBound | null = null;
1024
+ let prevIndex = 0 as NonNegativeInt;
1025
+
1026
+ let skip = false;
1027
+ let nonSkipRangeAdded = false;
1028
+
1029
+ const skipRange = (
1030
+ range: SkipRange | FingerprintRange | TimestampsRange,
1031
+ ) => {
1032
+ // The last range, if any non skip was added, must have InfiniteUpperBound.
1033
+ if (nonSkipRangeAdded && range.upperBound === InfiniteUpperBound) {
1034
+ output.addRange({
1035
+ type: RangeType.Skip,
1036
+ upperBound: InfiniteUpperBound,
1037
+ });
1038
+ } else {
1039
+ skip = true;
1040
+ }
1041
+ };
1042
+
1043
+ const coalesceSkipsBeforeAdd = () => {
1044
+ // Set to true because we are going to add a non skip range.
1045
+ nonSkipRangeAdded = true;
1046
+ if (skip) {
1047
+ skip = false;
1048
+ assert(prevUpperBound != null, "prevUpperBound is null");
1049
+ // There is always a space for a skip range before adding.
1050
+ output.addRange({
1051
+ type: RangeType.Skip,
1052
+ upperBound: prevUpperBound,
1053
+ });
1054
+ }
1055
+ };
1056
+
1057
+ // When we don't have a space...
1058
+ const addFingerprintForRemainingRange = (
1059
+ begin: NonNegativeInt,
1060
+ ): boolean => {
1061
+ const fingerprint = deps.storage.fingerprint(ownerId, begin, storageSize);
1062
+ if (!fingerprint) return false;
1063
+ // There is always a space for a ramaining range.
1064
+ output.addRange({
1065
+ type: RangeType.Fingerprint,
1066
+ upperBound: InfiniteUpperBound,
1067
+ fingerprint,
1068
+ });
1069
+ return true;
1070
+ };
1071
+
1072
+ for (const range of ranges) {
1073
+ const currentUpperBound = range.upperBound;
1074
+
1075
+ const lower = prevIndex;
1076
+ let upper = deps.storage.findLowerBound(
1077
+ ownerId,
1078
+ prevIndex,
1079
+ storageSize,
1080
+ currentUpperBound,
1081
+ );
1082
+ if (upper == null) return syncFail();
1083
+
1084
+ switch (range.type) {
1085
+ case RangeType.Skip: {
1086
+ skipRange(range);
1087
+ break;
1088
+ }
1089
+
1090
+ case RangeType.Fingerprint: {
1091
+ const ourFingerprint = deps.storage.fingerprint(
1092
+ ownerId,
1093
+ lower,
1094
+ upper,
1095
+ );
1096
+ if (ourFingerprint == null) return syncFail();
1097
+
1098
+ if (eqArrayNumber(range.fingerprint, ourFingerprint)) {
1099
+ skipRange(range);
1100
+ } else {
1101
+ if (output.canSplitRange()) {
1102
+ coalesceSkipsBeforeAdd();
1103
+ splitRange(deps)(
1104
+ ownerId,
1105
+ lower,
1106
+ upper,
1107
+ currentUpperBound,
1108
+ output,
1109
+ );
1110
+ } else {
1111
+ if (!addFingerprintForRemainingRange(upper)) return syncFail();
1112
+ return ok(output.unwrap());
1113
+ }
1114
+ }
1115
+ break;
1116
+ }
1117
+
1118
+ case RangeType.Timestamps: {
1119
+ let endBound = currentUpperBound;
1120
+
1121
+ const timestampsWeNeed = new Map(
1122
+ range.timestamps.map((t) => [t.join(), true]),
1123
+ );
1124
+ const ourTimestamps = createTimestampsBuffer();
1125
+
1126
+ let storageError = false as boolean;
1127
+ let exceeded = false as boolean;
1128
+
1129
+ deps.storage.iterate(ownerId, lower, upper, (timestamp, index) => {
1130
+ const timestampString = timestamp.join();
1131
+ const timestampBinary = binaryTimestampToTimestamp(timestamp);
1132
+
1133
+ let message: EncryptedCrdtMessage | null = null;
1134
+
1135
+ if (timestampsWeNeed.has(timestampString)) {
1136
+ timestampsWeNeed.delete(timestampString);
1137
+ } else {
1138
+ const dbChange = deps.storage.readDbChange(ownerId, timestamp);
1139
+ if (dbChange == null) {
1140
+ storageError = true;
1141
+ return false;
1142
+ }
1143
+ message = {
1144
+ timestamp: timestampBinary,
1145
+ change: dbChange,
1146
+ };
1147
+ }
1148
+
1149
+ if (
1150
+ !output.canAddTimestampsRangeAndMessage(ourTimestamps, message)
1151
+ ) {
1152
+ exceeded = true;
1153
+ endBound = timestamp;
1154
+ upper = index;
1155
+ return false;
1156
+ }
1157
+
1158
+ ourTimestamps.add(timestampBinary);
1159
+ if (message) output.addMessage(message);
1160
+ return true;
1161
+ });
1162
+
1163
+ if (storageError) {
1164
+ return syncFail();
1165
+ }
1166
+
1167
+ const addRange = () => {
1168
+ coalesceSkipsBeforeAdd();
1169
+ output.addRange({
1170
+ type: RangeType.Timestamps,
1171
+ upperBound: endBound,
1172
+ timestamps: ourTimestamps,
1173
+ });
1174
+ };
1175
+
1176
+ if (exceeded) {
1177
+ addRange();
1178
+ if (!addFingerprintForRemainingRange(upper)) {
1179
+ return syncFail();
1180
+ }
1181
+ return ok(output.unwrap());
1182
+ }
1183
+
1184
+ // If we need something, we have to respond with our timestamps.
1185
+ if (timestampsWeNeed.size > 0) {
1186
+ addRange();
1187
+ } else {
1188
+ skipRange(range);
1189
+ }
1190
+
1191
+ break;
1192
+ }
1193
+ }
1194
+
1195
+ prevIndex = upper;
1196
+ prevUpperBound = currentUpperBound;
1197
+ }
1198
+
1199
+ // If all ranges were skipped, there are no changes and sync is complete.
1200
+ const hasChange = output.getSize() > outputInitialSize;
1201
+ return ok(hasChange ? output.unwrap() : null);
1202
+ };
1203
+
1204
+ const splitRange =
1205
+ (deps: StorageDep) =>
1206
+ (
1207
+ ownerId: BinaryOwnerId,
1208
+ lower: NonNegativeInt,
1209
+ upper: NonNegativeInt,
1210
+ upperBound: RangeUpperBound,
1211
+ buffer: ProtocolMessageBuffer,
1212
+ ): void => {
1213
+ const itemCount = (upper - lower) as NonNegativeInt;
1214
+ const buckets = computeBalancedBuckets(itemCount);
1215
+
1216
+ if (!buckets.ok) {
1217
+ const range: TimestampsRangeWithTimestampsBuffer = {
1218
+ type: RangeType.Timestamps,
1219
+ upperBound,
1220
+ timestamps: createTimestampsBuffer(),
1221
+ };
1222
+
1223
+ deps.storage.iterate(
1224
+ ownerId,
1225
+ 0 as NonNegativeInt,
1226
+ itemCount,
1227
+ (timestamp) => {
1228
+ range.timestamps.add(binaryTimestampToTimestamp(timestamp));
1229
+ return true;
1230
+ },
1231
+ );
1232
+
1233
+ buffer.addRange(range);
1234
+ return;
1235
+ }
1236
+
1237
+ // Check Storage.ts `fingerprint` and `fingerprintRanges` docs.
1238
+ const fingerprintRangesBuckets =
1239
+ lower === 0
1240
+ ? buckets.value
1241
+ : [lower, ...buckets.value.map((b) => (b + lower) as NonNegativeInt)];
1242
+
1243
+ const fingerprintRanges = deps.storage.fingerprintRanges(
1244
+ ownerId,
1245
+ fingerprintRangesBuckets,
1246
+ upperBound,
1247
+ );
1248
+ // Errors are handled by the storage.
1249
+ if (fingerprintRanges == null) return;
1250
+
1251
+ const rangesToUse =
1252
+ lower > 0 ? fingerprintRanges.slice(1) : fingerprintRanges;
1253
+
1254
+ for (const range of rangesToUse) {
1255
+ buffer.addRange(range);
1256
+ }
1257
+ };
1258
+
1259
+ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1260
+ if (buffer.getLength() === 0) return [];
1261
+
1262
+ const rangesCount = decodeNonNegativeInt(buffer);
1263
+ if (rangesCount === 0) return [];
1264
+
1265
+ const timestampsCount = (rangesCount - 1) as NonNegativeInt;
1266
+ const timestamps = decodeTimestamps(buffer, timestampsCount);
1267
+ const rangeTypes: Array<RangeType> = [];
1268
+
1269
+ for (let i = 0; i < rangesCount; i++) {
1270
+ const rangeType = decodeNonNegativeInt(buffer);
1271
+ switch (rangeType) {
1272
+ case RangeType.Fingerprint:
1273
+ case RangeType.Skip:
1274
+ case RangeType.Timestamps:
1275
+ rangeTypes.push(rangeType as RangeType);
1276
+ break;
1277
+ default:
1278
+ throw new ProtocolDecodeError(`Invalid RangeType: ${rangeType}`);
1279
+ }
1280
+ }
1281
+
1282
+ const ranges: Array<Range> = [];
1283
+
1284
+ for (let i = 0; i < rangesCount; i++) {
1285
+ const upperBound =
1286
+ i < timestampsCount
1287
+ ? timestampToBinaryTimestamp(timestamps[i])
1288
+ : InfiniteUpperBound;
1289
+
1290
+ const rangeType = rangeTypes[i];
1291
+
1292
+ switch (rangeType) {
1293
+ case RangeType.Skip:
1294
+ ranges.push({ type: RangeType.Skip, upperBound });
1295
+ break;
1296
+
1297
+ case RangeType.Fingerprint: {
1298
+ const fingerprint = buffer.shiftN(fingerprintSize) as Fingerprint;
1299
+ ranges.push({
1300
+ type: RangeType.Fingerprint,
1301
+ upperBound,
1302
+ fingerprint,
1303
+ });
1304
+ break;
1305
+ }
1306
+
1307
+ case RangeType.Timestamps: {
1308
+ const timestamps = decodeTimestamps(buffer).map(
1309
+ timestampToBinaryTimestamp,
1310
+ );
1311
+ ranges.push({
1312
+ type: RangeType.Timestamps,
1313
+ upperBound,
1314
+ timestamps,
1315
+ });
1316
+ break;
1317
+ }
1318
+ }
1319
+ }
1320
+
1321
+ return ranges;
1322
+ };
1323
+
1324
+ /** Decodes an array of sorted timestamps with delta-encoded millis. */
1325
+ const decodeTimestamps = (
1326
+ buffer: Buffer,
1327
+ length?: NonNegativeInt,
1328
+ ): ReadonlyArray<Timestamp> => {
1329
+ length ??= decodeNonNegativeInt(buffer);
1330
+
1331
+ let previousMillis = 0 as Millis;
1332
+
1333
+ const millises: Array<Millis> = [];
1334
+ for (let i = 0; i < length; i++) {
1335
+ const deltaMillis = decodeNonNegativeInt(buffer);
1336
+ const millis = Millis.from(previousMillis + deltaMillis);
1337
+ if (!millis.ok) throw new Error(millis.error.type);
1338
+ millises.push(millis.value);
1339
+ previousMillis = millis.value;
1340
+ }
1341
+
1342
+ const counters: Array<Counter> = [];
1343
+ let counterIndex = 0;
1344
+ while (counterIndex < length) {
1345
+ const counter = Counter.from(decodeNonNegativeInt(buffer));
1346
+ if (!counter.ok) throw new Error(counter.error.type);
1347
+ const runLength = decodeNonNegativeInt(buffer);
1348
+ for (let i = 0; i < runLength; i++) {
1349
+ counters.push(counter.value);
1350
+ counterIndex++;
1351
+ }
1352
+ }
1353
+
1354
+ const nodeIds: Array<NodeId> = [];
1355
+ let nodeIdIndex = 0;
1356
+ while (nodeIdIndex < length) {
1357
+ const nodeId = decodeNodeId(buffer);
1358
+ const runLength = decodeNonNegativeInt(buffer);
1359
+ for (let i = 0; i < runLength; i++) {
1360
+ nodeIds.push(nodeId);
1361
+ nodeIdIndex++;
1362
+ }
1363
+ }
1364
+
1365
+ const timestamps: Array<Timestamp> = [];
1366
+ for (let i = 0; i < length; i++) {
1367
+ timestamps.push({
1368
+ millis: millises[i],
1369
+ counter: counters[i],
1370
+ nodeId: nodeIds[i],
1371
+ });
1372
+ }
1373
+
1374
+ return timestamps;
1375
+ };
1376
+
1377
+ /** Binary representation of {@link Id}. */
1378
+ export type BinaryId = Uint8Array & Brand<"BinaryId">;
1379
+
1380
+ export const binaryIdLength = 16 as NonNegativeInt;
1381
+
1382
+ export const idToBinaryId = (id: Id): BinaryId =>
1383
+ base64Url256ToBytes(id) as BinaryId;
1384
+
1385
+ export const binaryIdToId = (binaryId: BinaryId): Id =>
1386
+ decodeId(createBuffer(binaryId));
1387
+
1388
+ /** Binary representation of {@link OwnerId}. */
1389
+ export type BinaryOwnerId = Uint8Array & Brand<"BinaryOwnerId">;
1390
+
1391
+ export const ownerIdToBinaryOwnerId = (ownerId: OwnerId): BinaryOwnerId =>
1392
+ base64Url256ToBytes(ownerId) as BinaryOwnerId;
1393
+
1394
+ export const binaryOwnerIdToOwnerId = (binaryOwnerId: BinaryOwnerId): OwnerId =>
1395
+ decodeOwnerId(createBuffer(binaryOwnerId));
1396
+
1397
+ /**
1398
+ * Base64Url string with maximum length of 256 characters. Encoding strings as
1399
+ * Base64UrlString saves up to 25% in size compared to regular strings.
1400
+ */
1401
+ export const Base64Url256 = maxLength(256)(Base64Url);
1402
+ export type Base64Url256 = typeof Base64Url256.Type;
1403
+
1404
+ /**
1405
+ * Union type for all variants of Base64Url strings with limited length. All
1406
+ * these types use Base64Url alphabet and are < 256 characters.
1407
+ */
1408
+ export type Base64Url256Variant = Base64Url256 | Id | NanoId | OwnerId;
1409
+
1410
+ /**
1411
+ * Alphabet used for Base64Url encoding. This is copied from the `nanoid`
1412
+ * library to avoid dependency on a specific version of `nanoid`.
1413
+ */
1414
+ const urlAlphabet =
1415
+ "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
1416
+
1417
+ /**
1418
+ * Converts a Base64Url string to a Uint8Array for binary storage. This encoding
1419
+ * is more space-efficient than UTF-8 for Base64Url strings.
1420
+ */
1421
+ export const base64Url256ToBytes = (
1422
+ string: Base64Url256Variant,
1423
+ ): globalThis.Uint8Array => {
1424
+ const totalBits = string.length * 6; // 6 bits per character
1425
+ const byteLength = Math.ceil(totalBits / 8);
1426
+ const value = new globalThis.Uint8Array(byteLength);
1427
+
1428
+ let bitBuffer = 0;
1429
+ let bitsInBuffer = 0;
1430
+ let byteIndex = 0;
1431
+
1432
+ for (const char of string) {
1433
+ const charValue = urlAlphabet.indexOf(char);
1434
+ bitBuffer = (bitBuffer << 6) | charValue;
1435
+ bitsInBuffer += 6;
1436
+ while (bitsInBuffer >= 8) {
1437
+ bitsInBuffer -= 8;
1438
+ value[byteIndex++] = (bitBuffer >> bitsInBuffer) & 0xff;
1439
+ }
1440
+ }
1441
+
1442
+ if (bitsInBuffer > 0 && byteIndex < byteLength) {
1443
+ value[byteIndex] = (bitBuffer << (8 - bitsInBuffer)) & 0xff;
1444
+ }
1445
+
1446
+ return value;
1447
+ };
1448
+
1449
+ export const decodeBase64Url256 = (
1450
+ buffer: Buffer,
1451
+ stringLength: number,
1452
+ ): Base64Url256Variant => {
1453
+ const bytes = buffer.shiftN(
1454
+ Math.ceil((stringLength * 6) / 8) as NonNegativeInt,
1455
+ );
1456
+
1457
+ let bitBuffer = 0;
1458
+ let bitsInBuffer = 0;
1459
+ let string = "";
1460
+
1461
+ for (const byte of bytes) {
1462
+ bitBuffer = (bitBuffer << 8) | byte;
1463
+ bitsInBuffer += 8;
1464
+ while (bitsInBuffer >= 6) {
1465
+ bitsInBuffer -= 6;
1466
+ if (string.length < stringLength) {
1467
+ const charValue = (bitBuffer >> bitsInBuffer) & 0x3f;
1468
+ if (charValue < 0 || charValue >= urlAlphabet.length) {
1469
+ throw new ProtocolDecodeError("invalid charValue");
1470
+ }
1471
+ string += urlAlphabet[charValue];
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ const result = Base64Url256.from(string);
1477
+ if (!result.ok) throw new ProtocolDecodeError(result.error.type);
1478
+
1479
+ return result.value;
1480
+ };
1481
+
1482
+ const decodeId = (buffer: Buffer): Id =>
1483
+ decodeBase64Url256(buffer, idTypeValueLength) as Id;
1484
+
1485
+ /** Not all 16 bytes are valid {@link OwnerId}. */
1486
+ const decodeOwnerId = (buffer: Buffer): OwnerId => decodeId(buffer) as OwnerId;
1487
+
1488
+ /**
1489
+ * Evolu uses MessagePack to handle all number variants except for
1490
+ * NonNegativeInt. For NonNegativeInt, Evolu provides more efficient encoding.
1491
+ */
1492
+ export const encodeNumber = (buffer: Buffer, number: number): void => {
1493
+ buffer.extend(pack(number));
1494
+ };
1495
+
1496
+ export const decodeNumber = (buffer: Buffer): number => {
1497
+ let number: unknown;
1498
+ let end: unknown;
1499
+
1500
+ unpackMultiple(buffer.unwrap(), (n, _, e) => {
1501
+ number = n;
1502
+ end = e;
1503
+ return false;
1504
+ });
1505
+
1506
+ const endResult = NonNegativeInt.fromUnknown(end);
1507
+ if (!endResult.ok) throw new ProtocolDecodeError(endResult.error.type);
1508
+
1509
+ const numberResult = Number.fromUnknown(number);
1510
+ if (!numberResult.ok) throw new ProtocolDecodeError(numberResult.error.type);
1511
+
1512
+ buffer.shiftN(endResult.value);
1513
+ return numberResult.value;
1514
+ };
1515
+
1516
+ export const binaryTimestampToFingerprint = (
1517
+ timestamp: BinaryTimestamp,
1518
+ ): Fingerprint => {
1519
+ const hash = sha256(timestamp).slice(0, fingerprintSize);
1520
+ return hash as Fingerprint;
1521
+ };
1522
+
1523
+ /**
1524
+ * Encodes and encrypts a {@link DbChange} using the provided owner's encryption
1525
+ * key. Returns an encrypted binary representation as {@link EncryptedDbChange}.
1526
+ */
1527
+ export const encodeAndEncryptDbChange =
1528
+ (deps: SymmetricCryptoDep) =>
1529
+ (change: DbChange, key: EncryptionKey): EncryptedDbChange => {
1530
+ const buffer = createBuffer();
1531
+
1532
+ encodeBase64Url256(buffer, change.table);
1533
+
1534
+ buffer.extend(idToBinaryId(change.id));
1535
+
1536
+ const entries = objectToEntries(change.values).map(
1537
+ ([column, value]): [Base64Url256, SqliteValue] => {
1538
+ return [column, value];
1539
+ },
1540
+ );
1541
+
1542
+ encodeLength(buffer, entries);
1543
+
1544
+ for (const [column, value] of entries) {
1545
+ encodeBase64Url256(buffer, column);
1546
+ encodeSqliteValue(buffer, value);
1547
+ }
1548
+
1549
+ const paddingLength = padmePaddingLength(buffer.getLength());
1550
+ // Add zero bytes as PADMÉ padding - these will be ignored during decoding.
1551
+ buffer.extend(new Uint8Array(paddingLength));
1552
+
1553
+ const { nonce, ciphertext } = deps.symmetricCrypto.encrypt(
1554
+ buffer.unwrap(),
1555
+ key,
1556
+ );
1557
+
1558
+ buffer.reset();
1559
+ buffer.extend(nonce);
1560
+ encodeLength(buffer, ciphertext);
1561
+ buffer.extend(ciphertext);
1562
+
1563
+ return buffer.unwrap() as EncryptedDbChange;
1564
+ };
1565
+
1566
+ /**
1567
+ * Decrypts and decodes an {@link EncryptedDbChange} using the provided owner's
1568
+ * encryption key.
1569
+ */
1570
+ export const decryptAndDecodeDbChange =
1571
+ (deps: SymmetricCryptoDep) =>
1572
+ (
1573
+ change: EncryptedDbChange,
1574
+ key: EncryptionKey,
1575
+ ): Result<DbChange, SymmetricCryptoDecryptError | ProtocolInvalidDataError> =>
1576
+ tryDecodeProtocolData<DbChange, SymmetricCryptoDecryptError>(
1577
+ change,
1578
+ (buffer) => {
1579
+ const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
1580
+
1581
+ const ciphertextLength = decodeLength(buffer);
1582
+ const ciphertext = buffer.shiftN(ciphertextLength);
1583
+
1584
+ const plaintextBytes = deps.symmetricCrypto.decrypt(
1585
+ ciphertext,
1586
+ key,
1587
+ nonce,
1588
+ );
1589
+ if (!plaintextBytes.ok) return plaintextBytes;
1590
+
1591
+ buffer.reset();
1592
+ buffer.extend(plaintextBytes.value);
1593
+
1594
+ const table = decodeBase64Url256WithLength(buffer);
1595
+ const id = decodeId(buffer);
1596
+
1597
+ const length = decodeLength(buffer);
1598
+ const values = Object.create(null) as Record<string, SqliteValue>;
1599
+
1600
+ for (let i = 0; i < length; i++) {
1601
+ const column = decodeBase64Url256WithLength(buffer);
1602
+ const value = decodeSqliteValue(buffer);
1603
+ values[column] = value;
1604
+ }
1605
+
1606
+ const dbChange = { table, id, values };
1607
+
1608
+ return ok(dbChange);
1609
+ },
1610
+ );
1611
+
1612
+ /**
1613
+ * Encodes a non-negative integer into a variable-length integer format. It's
1614
+ * more efficient than encoding via {@link encodeNumber}.
1615
+ *
1616
+ * https://en.wikipedia.org/wiki/Variable-length_quantity
1617
+ */
1618
+ export const encodeNonNegativeInt = (
1619
+ buffer: Buffer,
1620
+ int: NonNegativeInt,
1621
+ ): void => {
1622
+ if (int === 0) {
1623
+ buffer.extend([0]);
1624
+ return;
1625
+ }
1626
+
1627
+ let remaining = BigInt(int);
1628
+ const bytes: Array<number> = [];
1629
+
1630
+ while (remaining !== 0n) {
1631
+ const byte = globalThis.Number(remaining & 127n);
1632
+ bytes.push(byte);
1633
+ remaining >>= 7n;
1634
+ }
1635
+
1636
+ for (let i = 0; i < bytes.length - 1; i++) {
1637
+ bytes[i] |= 128;
1638
+ }
1639
+
1640
+ buffer.extend(bytes);
1641
+ };
1642
+
1643
+ /**
1644
+ * Decodes a non-negative integer from a variable-length integer format.
1645
+ *
1646
+ * https://en.wikipedia.org/wiki/Variable-length_quantity
1647
+ */
1648
+ export const decodeNonNegativeInt = (buffer: Buffer): NonNegativeInt => {
1649
+ let result = 0n;
1650
+ let shift = 0n;
1651
+ let byte;
1652
+
1653
+ // 8 is the smallest required count
1654
+ for (let byteCount = 0; byteCount < 8; byteCount++) {
1655
+ byte = buffer.shift();
1656
+ result |= BigInt(byte & 127) << shift;
1657
+ if ((byte & 128) === 0) break;
1658
+ shift += 7n;
1659
+ }
1660
+
1661
+ const int = NonNegativeInt.from(globalThis.Number(result));
1662
+ if (!int.ok) throw new ProtocolDecodeError(int.error.type);
1663
+
1664
+ return int.value;
1665
+ };
1666
+
1667
+ export const encodeLength = (buffer: Buffer, value: ArrayLike<any>): void => {
1668
+ encodeNonNegativeInt(buffer, value.length as NonNegativeInt);
1669
+ };
1670
+
1671
+ export const decodeLength = decodeNonNegativeInt;
1672
+
1673
+ export const encodeString = (buffer: Buffer, value: string): void => {
1674
+ const bytes = utf8ToBytes(value);
1675
+ encodeLength(buffer, bytes);
1676
+ buffer.extend(bytes);
1677
+ };
1678
+
1679
+ export const decodeString = (buffer: Buffer): string => {
1680
+ const length = decodeLength(buffer);
1681
+ const bytes = buffer.shiftN(length);
1682
+ return bytesToUtf8(bytes);
1683
+ };
1684
+
1685
+ export const encodeNodeId = (buffer: Buffer, nodeId: NodeId): void => {
1686
+ buffer.extend(hexToBytes(nodeId));
1687
+ };
1688
+
1689
+ export const decodeNodeId = (buffer: Buffer): NodeId => {
1690
+ const bytes = buffer.shiftN(8 as NonNegativeInt);
1691
+ return bytesToHex(bytes) as NodeId;
1692
+ };
1693
+
1694
+ export const encodeBase64Url256 = (
1695
+ buffer: Buffer,
1696
+ string: Base64Url256Variant,
1697
+ ): void => {
1698
+ encodeLength(buffer, string);
1699
+ buffer.extend(base64Url256ToBytes(string));
1700
+ };
1701
+
1702
+ export const decodeBase64Url256WithLength = (buffer: Buffer): Base64Url256 => {
1703
+ const length = decodeLength(buffer);
1704
+ return decodeBase64Url256(buffer, length) as Base64Url256;
1705
+ };
1706
+
1707
+ // Small ints are encoded into ProtocolValueType, saving one byte per int.
1708
+ const isSmallInt: Predicate<number> = (value: number) =>
1709
+ value >= 0 && value < 20;
1710
+
1711
+ export const ProtocolValueType = {
1712
+ // 0-19 small ints
1713
+
1714
+ // SQLite types
1715
+ String: 20 as NonNegativeInt,
1716
+ Number: 21 as NonNegativeInt,
1717
+ Null: 22 as NonNegativeInt,
1718
+ Binary: 23 as NonNegativeInt,
1719
+ // We can add more types for other DBs or anything else later.
1720
+
1721
+ // Optimized types
1722
+ Id: 30 as NonNegativeInt,
1723
+ Base64Url256: 31 as NonNegativeInt,
1724
+ NonNegativeInt: 32 as NonNegativeInt,
1725
+ Json: 33 as NonNegativeInt,
1726
+
1727
+ // new Date().toISOString() - 24 bytes
1728
+ // encoded with fixed length - 8 bytes
1729
+ // encode as NonNegativeInt - 6 bytes (additional 25% reduction)
1730
+ DateIsoWithNonNegativeTime: 34 as NonNegativeInt,
1731
+ DateIsoWithNegativeTime: 35 as NonNegativeInt, // 9 bytes
1732
+
1733
+ // TODO: Operations (from 40)
1734
+ // Increment, Decrement, Patch, whatever.
1735
+ } as const;
1736
+
1737
+ export const encodeSqliteValue = (buffer: Buffer, value: SqliteValue): void => {
1738
+ if (value === null) {
1739
+ encodeNonNegativeInt(buffer, ProtocolValueType.Null);
1740
+ return;
1741
+ }
1742
+
1743
+ switch (typeof value) {
1744
+ case "string": {
1745
+ const dateIsoString = DateIsoString.from(value);
1746
+ if (dateIsoString.ok) {
1747
+ const time = new Date(dateIsoString.value).getTime();
1748
+ if (NonNegativeInt.is(time)) {
1749
+ encodeNonNegativeInt(
1750
+ buffer,
1751
+ ProtocolValueType.DateIsoWithNonNegativeTime,
1752
+ );
1753
+ encodeNonNegativeInt(buffer, time);
1754
+ } else {
1755
+ encodeNonNegativeInt(
1756
+ buffer,
1757
+ ProtocolValueType.DateIsoWithNegativeTime,
1758
+ );
1759
+ encodeNumber(buffer, time);
1760
+ }
1761
+ return;
1762
+ }
1763
+
1764
+ const base64Url256 = Base64Url256.from(value);
1765
+ if (base64Url256.ok) {
1766
+ if (base64Url256.value.length === idTypeValueLength) {
1767
+ encodeNonNegativeInt(buffer, ProtocolValueType.Id);
1768
+ buffer.extend(base64Url256ToBytes(base64Url256.value));
1769
+ return;
1770
+ }
1771
+ encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url256);
1772
+ encodeBase64Url256(buffer, base64Url256.value);
1773
+ return;
1774
+ }
1775
+
1776
+ const jsonValue = JsonValueFromString.fromParent(value);
1777
+ if (jsonValue.ok) {
1778
+ const jsonBytes = pack(jsonValue.value);
1779
+ encodeNonNegativeInt(buffer, ProtocolValueType.Json);
1780
+ encodeLength(buffer, jsonBytes);
1781
+ buffer.extend(jsonBytes);
1782
+ return;
1783
+ }
1784
+
1785
+ encodeNonNegativeInt(buffer, ProtocolValueType.String);
1786
+ encodeString(buffer, value);
1787
+ return;
1788
+ }
1789
+
1790
+ case "number": {
1791
+ if (NonNegativeInt.is(value)) {
1792
+ if (isSmallInt(value)) {
1793
+ encodeNonNegativeInt(buffer, value);
1794
+ return;
1795
+ }
1796
+ encodeNonNegativeInt(buffer, ProtocolValueType.NonNegativeInt);
1797
+ encodeNonNegativeInt(buffer, value);
1798
+ return;
1799
+ }
1800
+ encodeNonNegativeInt(buffer, ProtocolValueType.Number);
1801
+ encodeNumber(buffer, value);
1802
+ return;
1803
+ }
1804
+ }
1805
+
1806
+ encodeNonNegativeInt(buffer, ProtocolValueType.Binary);
1807
+ encodeLength(buffer, value);
1808
+ buffer.extend(value);
1809
+ };
1810
+
1811
+ export const decodeSqliteValue = (buffer: Buffer): SqliteValue => {
1812
+ const type = decodeNonNegativeInt(buffer);
1813
+
1814
+ if (isSmallInt(type)) {
1815
+ return type;
1816
+ }
1817
+
1818
+ switch (type) {
1819
+ case ProtocolValueType.String:
1820
+ return decodeString(buffer);
1821
+ case ProtocolValueType.Number:
1822
+ return decodeNumber(buffer);
1823
+ case ProtocolValueType.Null:
1824
+ return null;
1825
+ case ProtocolValueType.Binary: {
1826
+ const length = decodeLength(buffer);
1827
+ return buffer.shiftN(length);
1828
+ }
1829
+ case ProtocolValueType.Id: {
1830
+ return decodeId(buffer);
1831
+ }
1832
+ case ProtocolValueType.Base64Url256:
1833
+ return decodeBase64Url256WithLength(buffer);
1834
+ case ProtocolValueType.NonNegativeInt:
1835
+ return decodeNonNegativeInt(buffer);
1836
+ case ProtocolValueType.Json: {
1837
+ const length = decodeLength(buffer);
1838
+ const bytes = buffer.shiftN(length);
1839
+ return JSON.stringify(unpack(bytes));
1840
+ }
1841
+ case ProtocolValueType.DateIsoWithNonNegativeTime:
1842
+ case ProtocolValueType.DateIsoWithNegativeTime: {
1843
+ const time =
1844
+ type === ProtocolValueType.DateIsoWithNonNegativeTime
1845
+ ? decodeNonNegativeInt(buffer)
1846
+ : decodeNumber(buffer);
1847
+ const dateIsoString = DateIsoString.fromParent(
1848
+ new Date(time).toISOString(),
1849
+ );
1850
+ if (!dateIsoString.ok)
1851
+ throw new ProtocolDecodeError(dateIsoString.error.type);
1852
+ return dateIsoString.value;
1853
+ }
1854
+ default:
1855
+ throw new ProtocolDecodeError("invalid ProtocolValueType");
1856
+ }
1857
+ };