@evolu/common 5.4.7 → 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,1151 @@
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
+ import { sha256 } from "@noble/hashes/sha2";
100
+ import { pack, unpack, unpackMultiple } from "msgpackr";
101
+ import { isNonEmptyReadonlyArray } from "../Array.js";
102
+ import { assert } from "../Assert.js";
103
+ import { BufferError, bytesToHex, bytesToUtf8, concatBytes, createBuffer, hexToBytes, utf8ToBytes, } from "../Buffer.js";
104
+ import { padmePaddingLength, } from "../Crypto.js";
105
+ import { eqArrayNumber } from "../Eq.js";
106
+ import { computeBalancedBuckets } from "../Number.js";
107
+ import { objectToEntries } from "../Object.js";
108
+ import { err, ok } from "../Result.js";
109
+ import { Base64Url, DateIsoString, idTypeValueLength, JsonValueFromString, maxLength, NonNegativeInt, Number, } from "../Type.js";
110
+ import { writeKeyLength, } from "./Owner.js";
111
+ import { binaryTimestampToTimestamp, Counter, Millis, timestampToBinaryTimestamp, } from "./Timestamp.js";
112
+ /** Maximum size of the entire protocol message in bytes. */
113
+ export const maxProtocolMessageSize = 1_000_000;
114
+ /** Maximum size of the ranges in bytes. */
115
+ export const maxProtocolMessageRangesSize = 30_000;
116
+ /** Evolu Protocol version. */
117
+ export const protocolVersion = 0;
118
+ export const ProtocolErrorCode = {
119
+ NoError: 0,
120
+ /** A code for {@link ProtocolWriteKeyError}. */
121
+ WriteKeyError: 1,
122
+ /** A code for {@link ProtocolWriteError}. */
123
+ WriteError: 2,
124
+ /** A code for {@link ProtocolSyncError}. */
125
+ SyncError: 3,
126
+ };
127
+ export const RangeType = {
128
+ Fingerprint: 1,
129
+ Skip: 0,
130
+ Timestamps: 2,
131
+ };
132
+ export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
133
+ export const fingerprintSize = 12;
134
+ /** A fingerprint of an empty range. */
135
+ export const zeroFingerprint = new Uint8Array(fingerprintSize);
136
+ /**
137
+ * Creates a {@link ProtocolMessage} from CRDT messages.
138
+ *
139
+ * If the message size would exceed {@link maxProtocolMessageSize}, the protocol
140
+ * ensures all messages will be sent in the next round(s) even over
141
+ * unidirectional and stateless transports.
142
+ */
143
+ export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages, maxSize) => {
144
+ const buffer = createProtocolMessageBuffer(owner.id, {
145
+ totalMaxSize: maxSize ?? maxProtocolMessageSize,
146
+ writeKey: owner.writeKey,
147
+ });
148
+ let notAllMessagesSent = false;
149
+ for (const message of messages) {
150
+ const change = encodeAndEncryptDbChange(deps)(message.change, owner.encryptionKey);
151
+ const encryptedCrdtMessage = { timestamp: message.timestamp, change };
152
+ if (buffer.canAddMessage(encryptedCrdtMessage)) {
153
+ buffer.addMessage(encryptedCrdtMessage);
154
+ }
155
+ else {
156
+ notAllMessagesSent = true;
157
+ break;
158
+ }
159
+ }
160
+ if (notAllMessagesSent) {
161
+ /**
162
+ * DEV: If not all messages fit due to size limits, we trigger a sync
163
+ * continuation by appending a Range with a random fingerprint. This
164
+ * ensures the receiver always responds with ranges, prompting another
165
+ * sync round.
166
+ *
167
+ * The ideal approach would be to send three ranges (skip, fingerprint,
168
+ * skip) where the fingerprint of unsent messages would act as narrow sync
169
+ * probe. I think we can send {@link zeroFingerprint} which can be
170
+ * interpreted as an indication that the other side should reply with
171
+ * {@link TimestampsRange}, so no need to restart syncing.
172
+ *
173
+ * For now, using a random fingerprint avoids extra complexity and is good
174
+ * enough for this case.
175
+ */
176
+ const randomFingerprint = deps.createRandomBytes(fingerprintSize);
177
+ // There is always a space for Fingerprint with InfiniteUpperBound.
178
+ buffer.addRange({
179
+ type: RangeType.Fingerprint,
180
+ upperBound: InfiniteUpperBound,
181
+ fingerprint: randomFingerprint,
182
+ });
183
+ }
184
+ return buffer.unwrap();
185
+ };
186
+ /** Creates a {@link ProtocolMessage} for sync. */
187
+ export const createProtocolMessageForSync = (deps) => (ownerId) => {
188
+ const buffer = createProtocolMessageBuffer(ownerId);
189
+ const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
190
+ const size = deps.storage.getSize(binaryOwnerId);
191
+ // Errors are handled by the storage.
192
+ if (size == null)
193
+ return null;
194
+ splitRange(deps)(binaryOwnerId, 0, size, InfiniteUpperBound, buffer);
195
+ return buffer.unwrap();
196
+ };
197
+ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
198
+ const { errorCode, writeKey, totalMaxSize = maxProtocolMessageSize, rangesMaxSize = maxProtocolMessageRangesSize, version = protocolVersion, } = options;
199
+ const buffers = {
200
+ header: createBuffer(),
201
+ messages: {
202
+ timestamps: createTimestampsBuffer(),
203
+ dbChanges: createBuffer(),
204
+ },
205
+ ranges: {
206
+ timestamps: createTimestampsBuffer(),
207
+ types: createBuffer(),
208
+ payloads: createBuffer(),
209
+ },
210
+ };
211
+ encodeNonNegativeInt(buffers.header, version);
212
+ buffers.header.extend(ownerIdToBinaryOwnerId(ownerId));
213
+ if (errorCode != null)
214
+ buffers.header.extend([errorCode]);
215
+ let isLastRangeInfinite = false;
216
+ const isWithinSizeLimits = () => getSize() <= totalMaxSize;
217
+ const getSize = () => (getHeaderAndMessagesSize() + getRangesSize());
218
+ const getHeaderAndMessagesSize = () => buffers.header.getLength() +
219
+ buffers.messages.timestamps.getLength() +
220
+ buffers.messages.dbChanges.getLength() +
221
+ (buffers.messages.timestamps.getCount() > 0 && writeKey
222
+ ? writeKeyLength
223
+ : 0);
224
+ const getRangesSize = () => buffers.ranges.timestamps.getCount() > 0
225
+ ? buffers.ranges.timestamps.getLength() +
226
+ buffers.ranges.types.getLength() +
227
+ buffers.ranges.payloads.getLength() +
228
+ safeMargins.remainingRange
229
+ : 0;
230
+ /**
231
+ * We calculated worst-case sizes as closely as possible and added a small
232
+ * safety margin, since computing exact worst cases is difficult due to
233
+ * variable-length, run-length, and delta encoding.
234
+ *
235
+ * Runtime assertions (`assert`) are used to guarantee that size limits are
236
+ * never exceeded. If a limit is exceeded, the assertion will fail at the
237
+ * precise location, making it easy to identify and fix the issue.
238
+ *
239
+ * While it would be possible to avoid the safety margin by snapshotting
240
+ * buffer states and rolling back changes, this would likely impact
241
+ * performance. If someone has time and wants to experiment with this
242
+ * approach, contributions are welcome.
243
+ */
244
+ const safeMargins = {
245
+ remainingRange: fingerprintSize + 10, // bytes: range type + possible increased count varint
246
+ timestamp: 30, // bytes: max millis + max count + NodeId
247
+ dbChangeLength: 8, // bytes: maximum encoded DbChange length varint
248
+ splitRange: 800, // bytes: worst case is around 650 bytes
249
+ timestampsRange: 50, // bytes: range type + its upperBound + possible increased count varint
250
+ };
251
+ const addMessageSafeMargin = safeMargins.timestamp +
252
+ safeMargins.dbChangeLength +
253
+ safeMargins.remainingRange;
254
+ return {
255
+ canAddMessage: (message) => getSize() + addMessageSafeMargin + message.change.length <= totalMaxSize,
256
+ addMessage: (message) => {
257
+ buffers.messages.timestamps.add(message.timestamp);
258
+ encodeLength(buffers.messages.dbChanges, message.change);
259
+ buffers.messages.dbChanges.extend(message.change);
260
+ assert(isWithinSizeLimits(), "the message is too big");
261
+ },
262
+ canSplitRange: () => {
263
+ return getRangesSize() + safeMargins.splitRange <= rangesMaxSize;
264
+ },
265
+ canAddTimestampsRangeAndMessage: (timestamps, message) => {
266
+ const rangesNewSize = getRangesSize() + timestamps.getLength() + safeMargins.timestampsRange;
267
+ return (rangesNewSize <= rangesMaxSize &&
268
+ (message
269
+ ? getHeaderAndMessagesSize() +
270
+ rangesNewSize +
271
+ addMessageSafeMargin +
272
+ message.change.length <=
273
+ totalMaxSize
274
+ : true));
275
+ },
276
+ addRange: (range) => {
277
+ assert(!isLastRangeInfinite, "Cannot add a range after an InfiniteUpperBound range");
278
+ isLastRangeInfinite = range.upperBound === InfiniteUpperBound;
279
+ /**
280
+ * We don't have to encode InfiniteUpperBound timestamp since it's always
281
+ * the last because ranges cover the whole universe. For partial sync, we
282
+ * use SkipRange.
283
+ */
284
+ if (range.upperBound !== InfiniteUpperBound)
285
+ buffers.ranges.timestamps.add(binaryTimestampToTimestamp(range.upperBound));
286
+ else {
287
+ buffers.ranges.timestamps.addInfinite();
288
+ }
289
+ encodeNonNegativeInt(buffers.ranges.types, range.type);
290
+ switch (range.type) {
291
+ case RangeType.Skip:
292
+ break;
293
+ case RangeType.Fingerprint:
294
+ buffers.ranges.payloads.extend(range.fingerprint);
295
+ break;
296
+ case RangeType.Timestamps: {
297
+ range.timestamps.append(buffers.ranges.payloads);
298
+ break;
299
+ }
300
+ }
301
+ assert(isWithinSizeLimits(), `the range ${range.type} is too big`);
302
+ },
303
+ unwrap: () => {
304
+ if (buffers.ranges.timestamps.getCount() > 0) {
305
+ assert(isLastRangeInfinite, "The last range's upperBound must be InfiniteUpperBound");
306
+ }
307
+ buffers.messages.timestamps.append(buffers.header);
308
+ buffers.header.extend(buffers.messages.dbChanges.unwrap());
309
+ if (buffers.messages.timestamps.getCount() > 0 && writeKey)
310
+ buffers.header.extend(writeKey);
311
+ if (buffers.ranges.timestamps.getCount() > 0) {
312
+ buffers.ranges.timestamps.append(buffers.header);
313
+ buffers.header.extend(buffers.ranges.types.unwrap());
314
+ buffers.header.extend(buffers.ranges.payloads.unwrap());
315
+ }
316
+ return buffers.header.unwrap();
317
+ },
318
+ getSize,
319
+ };
320
+ };
321
+ export const createTimestampsBuffer = () => {
322
+ let count = 0;
323
+ const countBuffer = createBuffer();
324
+ const syncCount = () => {
325
+ countBuffer.reset();
326
+ encodeNonNegativeInt(countBuffer, count);
327
+ };
328
+ syncCount();
329
+ const millisBuffer = createBuffer();
330
+ let previousMillis = 0;
331
+ const counterEncoder = createRunLengthEncoder((buffer, value) => {
332
+ encodeNonNegativeInt(buffer, value);
333
+ });
334
+ const nodeIdEncoder = createRunLengthEncoder((buffer, value) => {
335
+ encodeNodeId(buffer, value);
336
+ });
337
+ return {
338
+ add: (timestamp) => {
339
+ const delta = timestamp.millis - previousMillis;
340
+ assert(NonNegativeInt.is(delta), "The delta must be NonNegativeInt");
341
+ count++;
342
+ syncCount();
343
+ previousMillis = timestamp.millis;
344
+ encodeNonNegativeInt(millisBuffer, delta);
345
+ counterEncoder.add(timestamp.counter);
346
+ nodeIdEncoder.add(timestamp.nodeId);
347
+ },
348
+ addInfinite: () => {
349
+ count++;
350
+ syncCount();
351
+ },
352
+ getCount: () => count,
353
+ getLength: () => countBuffer.getLength() +
354
+ millisBuffer.getLength() +
355
+ counterEncoder.getLength() +
356
+ nodeIdEncoder.getLength(),
357
+ append: (buffer) => {
358
+ buffer.extend(countBuffer.unwrap());
359
+ buffer.extend(millisBuffer.unwrap());
360
+ buffer.extend(counterEncoder.unwrap());
361
+ buffer.extend(nodeIdEncoder.unwrap());
362
+ },
363
+ };
364
+ };
365
+ const createRunLengthEncoder = (encodeValue) => {
366
+ const buffer = createBuffer();
367
+ let previousLength = 0;
368
+ let previousValue = null;
369
+ let runLength = 0;
370
+ return {
371
+ add: (value) => {
372
+ if (value === previousValue) {
373
+ runLength++;
374
+ buffer.truncate(previousLength);
375
+ }
376
+ else {
377
+ previousValue = value;
378
+ runLength = 1;
379
+ }
380
+ previousLength = buffer.getLength();
381
+ encodeValue(buffer, value);
382
+ encodeNonNegativeInt(buffer, runLength);
383
+ },
384
+ getLength: () => buffer.getLength(),
385
+ unwrap: () => buffer.unwrap(),
386
+ };
387
+ };
388
+ export const applyProtocolMessageAsClient = (deps) => (inputMessage, { getWriteKey, version = protocolVersion, totalMaxSize, rangesMaxSize, } = {}) => tryDecodeProtocolData(inputMessage, (input) => {
389
+ const requestedVersion = decodeNonNegativeInt(input);
390
+ if (requestedVersion !== version) {
391
+ return err({
392
+ type: "ProtocolUnsupportedVersionError",
393
+ unsupportedVersion: requestedVersion,
394
+ isInitiator: version < requestedVersion,
395
+ });
396
+ }
397
+ const ownerId = decodeOwnerId(input);
398
+ const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
399
+ const errorCode = input.shift();
400
+ if (errorCode !== ProtocolErrorCode.NoError) {
401
+ switch (errorCode) {
402
+ case ProtocolErrorCode.WriteKeyError:
403
+ return err({
404
+ type: "ProtocolWriteKeyError",
405
+ });
406
+ case ProtocolErrorCode.WriteError:
407
+ return err({
408
+ type: "ProtocolWriteError",
409
+ });
410
+ case ProtocolErrorCode.SyncError:
411
+ return err({
412
+ type: "ProtocolSyncError",
413
+ });
414
+ default:
415
+ throw new ProtocolDecodeError(`Invalid ProtocolErrorCode: ${errorCode}`);
416
+ }
417
+ }
418
+ const messages = decodeMessages(input);
419
+ if (isNonEmptyReadonlyArray(messages) &&
420
+ !deps.storage.writeMessages(binaryOwnerId, messages)) {
421
+ return ok(null);
422
+ }
423
+ if (!getWriteKey)
424
+ return ok(null);
425
+ const writeKey = getWriteKey(ownerId);
426
+ if (writeKey == null)
427
+ return ok(null);
428
+ const output = createProtocolMessageBuffer(ownerId, {
429
+ writeKey,
430
+ totalMaxSize,
431
+ rangesMaxSize,
432
+ });
433
+ return sync(deps)("initiator", input, output, binaryOwnerId);
434
+ });
435
+ export const applyProtocolMessageAsRelay = (deps) => (inputMessage, { subscribe, broadcast, totalMaxSize, rangesMaxSize, } = {},
436
+ /** For testing purposes only; should not be used in production. */
437
+ version = protocolVersion) => tryDecodeProtocolData(inputMessage, (input) => {
438
+ const requestedVersion = decodeNonNegativeInt(input);
439
+ if (requestedVersion !== version) {
440
+ // Non-initiator responds with its version.
441
+ const output = createBuffer();
442
+ encodeNonNegativeInt(output, version);
443
+ return ok(output.unwrap());
444
+ }
445
+ const ownerId = decodeOwnerId(input);
446
+ const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
447
+ subscribe?.(ownerId);
448
+ const messages = decodeMessages(input);
449
+ if (isNonEmptyReadonlyArray(messages)) {
450
+ const messagesEnd = inputMessage.length - input.getLength();
451
+ const writeKey = input.shiftN(writeKeyLength);
452
+ const writeKeyIsValid = deps.storage.validateWriteKey(binaryOwnerId, writeKey);
453
+ if (!writeKeyIsValid)
454
+ return ok(createProtocolMessageBuffer(ownerId, {
455
+ errorCode: ProtocolErrorCode.WriteKeyError,
456
+ }).unwrap());
457
+ if (broadcast) {
458
+ // Instead of encoding a new protocol message, we reuse the inputMessage.
459
+ const broadcastMessage = concatBytes(inputMessage.slice(0, 17), new Uint8Array([ProtocolErrorCode.NoError]), inputMessage.slice(17, messagesEnd));
460
+ broadcast(ownerId, broadcastMessage);
461
+ }
462
+ if (!deps.storage.writeMessages(binaryOwnerId, messages))
463
+ return ok(createProtocolMessageBuffer(ownerId, {
464
+ errorCode: ProtocolErrorCode.WriteError,
465
+ }).unwrap());
466
+ }
467
+ const output = createProtocolMessageBuffer(ownerId, {
468
+ errorCode: ProtocolErrorCode.NoError,
469
+ totalMaxSize,
470
+ rangesMaxSize,
471
+ });
472
+ return sync(deps)("non-initiator", input, output, binaryOwnerId);
473
+ });
474
+ /**
475
+ * Wraps Evolu Protocol decoding functions, which use exceptions instead of
476
+ * {@link Result} to provide stack traces for debugging and reduce allocation
477
+ * overhead in success cases.
478
+ */
479
+ const tryDecodeProtocolData = (data, callback) => {
480
+ try {
481
+ return callback(createBuffer(data));
482
+ }
483
+ catch (error) {
484
+ if (error instanceof ProtocolDecodeError || error instanceof BufferError)
485
+ return err({
486
+ type: "ProtocolInvalidDataError",
487
+ data,
488
+ error,
489
+ });
490
+ throw error;
491
+ }
492
+ };
493
+ /**
494
+ * Error thrown for internal protocol validation failures, such as invalid data
495
+ * or type errors.
496
+ */
497
+ class ProtocolDecodeError extends Error {
498
+ constructor(message) {
499
+ super(message);
500
+ this.name = this.constructor.name;
501
+ Error.captureStackTrace(this, this.constructor);
502
+ }
503
+ }
504
+ const decodeMessages = (buffer) => {
505
+ const timestamps = decodeTimestamps(buffer);
506
+ const messages = [];
507
+ for (const timestamp of timestamps) {
508
+ const changeLength = decodeLength(buffer);
509
+ const change = buffer.shiftN(changeLength);
510
+ messages.push({ timestamp, change });
511
+ }
512
+ return messages;
513
+ };
514
+ const sync = (deps) => (role, input, output, ownerId) => {
515
+ const ranges = decodeRanges(input);
516
+ if (!isNonEmptyReadonlyArray(ranges)) {
517
+ // Nothing to sync.
518
+ return ok(null);
519
+ }
520
+ const binaryOwnerId = binaryOwnerIdToOwnerId(ownerId);
521
+ const outputInitialSize = output.getSize();
522
+ const syncFail = () => {
523
+ // Only the relay (non-initiator) reports sync errors, not the client (initiator).
524
+ if (role === "initiator") {
525
+ return ok(null);
526
+ }
527
+ const message = createProtocolMessageBuffer(binaryOwnerId, {
528
+ errorCode: ProtocolErrorCode.SyncError,
529
+ });
530
+ return ok(message.unwrap());
531
+ };
532
+ const storageSize = deps.storage.getSize(ownerId);
533
+ if (storageSize == null)
534
+ return syncFail();
535
+ let prevUpperBound = null;
536
+ let prevIndex = 0;
537
+ let skip = false;
538
+ let nonSkipRangeAdded = false;
539
+ const skipRange = (range) => {
540
+ // The last range, if any non skip was added, must have InfiniteUpperBound.
541
+ if (nonSkipRangeAdded && range.upperBound === InfiniteUpperBound) {
542
+ output.addRange({
543
+ type: RangeType.Skip,
544
+ upperBound: InfiniteUpperBound,
545
+ });
546
+ }
547
+ else {
548
+ skip = true;
549
+ }
550
+ };
551
+ const coalesceSkipsBeforeAdd = () => {
552
+ // Set to true because we are going to add a non skip range.
553
+ nonSkipRangeAdded = true;
554
+ if (skip) {
555
+ skip = false;
556
+ assert(prevUpperBound != null, "prevUpperBound is null");
557
+ // There is always a space for a skip range before adding.
558
+ output.addRange({
559
+ type: RangeType.Skip,
560
+ upperBound: prevUpperBound,
561
+ });
562
+ }
563
+ };
564
+ // When we don't have a space...
565
+ const addFingerprintForRemainingRange = (begin) => {
566
+ const fingerprint = deps.storage.fingerprint(ownerId, begin, storageSize);
567
+ if (!fingerprint)
568
+ return false;
569
+ // There is always a space for a ramaining range.
570
+ output.addRange({
571
+ type: RangeType.Fingerprint,
572
+ upperBound: InfiniteUpperBound,
573
+ fingerprint,
574
+ });
575
+ return true;
576
+ };
577
+ for (const range of ranges) {
578
+ const currentUpperBound = range.upperBound;
579
+ const lower = prevIndex;
580
+ let upper = deps.storage.findLowerBound(ownerId, prevIndex, storageSize, currentUpperBound);
581
+ if (upper == null)
582
+ return syncFail();
583
+ switch (range.type) {
584
+ case RangeType.Skip: {
585
+ skipRange(range);
586
+ break;
587
+ }
588
+ case RangeType.Fingerprint: {
589
+ const ourFingerprint = deps.storage.fingerprint(ownerId, lower, upper);
590
+ if (ourFingerprint == null)
591
+ return syncFail();
592
+ if (eqArrayNumber(range.fingerprint, ourFingerprint)) {
593
+ skipRange(range);
594
+ }
595
+ else {
596
+ if (output.canSplitRange()) {
597
+ coalesceSkipsBeforeAdd();
598
+ splitRange(deps)(ownerId, lower, upper, currentUpperBound, output);
599
+ }
600
+ else {
601
+ if (!addFingerprintForRemainingRange(upper))
602
+ return syncFail();
603
+ return ok(output.unwrap());
604
+ }
605
+ }
606
+ break;
607
+ }
608
+ case RangeType.Timestamps: {
609
+ let endBound = currentUpperBound;
610
+ const timestampsWeNeed = new Map(range.timestamps.map((t) => [t.join(), true]));
611
+ const ourTimestamps = createTimestampsBuffer();
612
+ let storageError = false;
613
+ let exceeded = false;
614
+ deps.storage.iterate(ownerId, lower, upper, (timestamp, index) => {
615
+ const timestampString = timestamp.join();
616
+ const timestampBinary = binaryTimestampToTimestamp(timestamp);
617
+ let message = null;
618
+ if (timestampsWeNeed.has(timestampString)) {
619
+ timestampsWeNeed.delete(timestampString);
620
+ }
621
+ else {
622
+ const dbChange = deps.storage.readDbChange(ownerId, timestamp);
623
+ if (dbChange == null) {
624
+ storageError = true;
625
+ return false;
626
+ }
627
+ message = {
628
+ timestamp: timestampBinary,
629
+ change: dbChange,
630
+ };
631
+ }
632
+ if (!output.canAddTimestampsRangeAndMessage(ourTimestamps, message)) {
633
+ exceeded = true;
634
+ endBound = timestamp;
635
+ upper = index;
636
+ return false;
637
+ }
638
+ ourTimestamps.add(timestampBinary);
639
+ if (message)
640
+ output.addMessage(message);
641
+ return true;
642
+ });
643
+ if (storageError) {
644
+ return syncFail();
645
+ }
646
+ const addRange = () => {
647
+ coalesceSkipsBeforeAdd();
648
+ output.addRange({
649
+ type: RangeType.Timestamps,
650
+ upperBound: endBound,
651
+ timestamps: ourTimestamps,
652
+ });
653
+ };
654
+ if (exceeded) {
655
+ addRange();
656
+ if (!addFingerprintForRemainingRange(upper)) {
657
+ return syncFail();
658
+ }
659
+ return ok(output.unwrap());
660
+ }
661
+ // If we need something, we have to respond with our timestamps.
662
+ if (timestampsWeNeed.size > 0) {
663
+ addRange();
664
+ }
665
+ else {
666
+ skipRange(range);
667
+ }
668
+ break;
669
+ }
670
+ }
671
+ prevIndex = upper;
672
+ prevUpperBound = currentUpperBound;
673
+ }
674
+ // If all ranges were skipped, there are no changes and sync is complete.
675
+ const hasChange = output.getSize() > outputInitialSize;
676
+ return ok(hasChange ? output.unwrap() : null);
677
+ };
678
+ const splitRange = (deps) => (ownerId, lower, upper, upperBound, buffer) => {
679
+ const itemCount = (upper - lower);
680
+ const buckets = computeBalancedBuckets(itemCount);
681
+ if (!buckets.ok) {
682
+ const range = {
683
+ type: RangeType.Timestamps,
684
+ upperBound,
685
+ timestamps: createTimestampsBuffer(),
686
+ };
687
+ deps.storage.iterate(ownerId, 0, itemCount, (timestamp) => {
688
+ range.timestamps.add(binaryTimestampToTimestamp(timestamp));
689
+ return true;
690
+ });
691
+ buffer.addRange(range);
692
+ return;
693
+ }
694
+ // Check Storage.ts `fingerprint` and `fingerprintRanges` docs.
695
+ const fingerprintRangesBuckets = lower === 0
696
+ ? buckets.value
697
+ : [lower, ...buckets.value.map((b) => (b + lower))];
698
+ const fingerprintRanges = deps.storage.fingerprintRanges(ownerId, fingerprintRangesBuckets, upperBound);
699
+ // Errors are handled by the storage.
700
+ if (fingerprintRanges == null)
701
+ return;
702
+ const rangesToUse = lower > 0 ? fingerprintRanges.slice(1) : fingerprintRanges;
703
+ for (const range of rangesToUse) {
704
+ buffer.addRange(range);
705
+ }
706
+ };
707
+ const decodeRanges = (buffer) => {
708
+ if (buffer.getLength() === 0)
709
+ return [];
710
+ const rangesCount = decodeNonNegativeInt(buffer);
711
+ if (rangesCount === 0)
712
+ return [];
713
+ const timestampsCount = (rangesCount - 1);
714
+ const timestamps = decodeTimestamps(buffer, timestampsCount);
715
+ const rangeTypes = [];
716
+ for (let i = 0; i < rangesCount; i++) {
717
+ const rangeType = decodeNonNegativeInt(buffer);
718
+ switch (rangeType) {
719
+ case RangeType.Fingerprint:
720
+ case RangeType.Skip:
721
+ case RangeType.Timestamps:
722
+ rangeTypes.push(rangeType);
723
+ break;
724
+ default:
725
+ throw new ProtocolDecodeError(`Invalid RangeType: ${rangeType}`);
726
+ }
727
+ }
728
+ const ranges = [];
729
+ for (let i = 0; i < rangesCount; i++) {
730
+ const upperBound = i < timestampsCount
731
+ ? timestampToBinaryTimestamp(timestamps[i])
732
+ : InfiniteUpperBound;
733
+ const rangeType = rangeTypes[i];
734
+ switch (rangeType) {
735
+ case RangeType.Skip:
736
+ ranges.push({ type: RangeType.Skip, upperBound });
737
+ break;
738
+ case RangeType.Fingerprint: {
739
+ const fingerprint = buffer.shiftN(fingerprintSize);
740
+ ranges.push({
741
+ type: RangeType.Fingerprint,
742
+ upperBound,
743
+ fingerprint,
744
+ });
745
+ break;
746
+ }
747
+ case RangeType.Timestamps: {
748
+ const timestamps = decodeTimestamps(buffer).map(timestampToBinaryTimestamp);
749
+ ranges.push({
750
+ type: RangeType.Timestamps,
751
+ upperBound,
752
+ timestamps,
753
+ });
754
+ break;
755
+ }
756
+ }
757
+ }
758
+ return ranges;
759
+ };
760
+ /** Decodes an array of sorted timestamps with delta-encoded millis. */
761
+ const decodeTimestamps = (buffer, length) => {
762
+ length ??= decodeNonNegativeInt(buffer);
763
+ let previousMillis = 0;
764
+ const millises = [];
765
+ for (let i = 0; i < length; i++) {
766
+ const deltaMillis = decodeNonNegativeInt(buffer);
767
+ const millis = Millis.from(previousMillis + deltaMillis);
768
+ if (!millis.ok)
769
+ throw new Error(millis.error.type);
770
+ millises.push(millis.value);
771
+ previousMillis = millis.value;
772
+ }
773
+ const counters = [];
774
+ let counterIndex = 0;
775
+ while (counterIndex < length) {
776
+ const counter = Counter.from(decodeNonNegativeInt(buffer));
777
+ if (!counter.ok)
778
+ throw new Error(counter.error.type);
779
+ const runLength = decodeNonNegativeInt(buffer);
780
+ for (let i = 0; i < runLength; i++) {
781
+ counters.push(counter.value);
782
+ counterIndex++;
783
+ }
784
+ }
785
+ const nodeIds = [];
786
+ let nodeIdIndex = 0;
787
+ while (nodeIdIndex < length) {
788
+ const nodeId = decodeNodeId(buffer);
789
+ const runLength = decodeNonNegativeInt(buffer);
790
+ for (let i = 0; i < runLength; i++) {
791
+ nodeIds.push(nodeId);
792
+ nodeIdIndex++;
793
+ }
794
+ }
795
+ const timestamps = [];
796
+ for (let i = 0; i < length; i++) {
797
+ timestamps.push({
798
+ millis: millises[i],
799
+ counter: counters[i],
800
+ nodeId: nodeIds[i],
801
+ });
802
+ }
803
+ return timestamps;
804
+ };
805
+ export const binaryIdLength = 16;
806
+ export const idToBinaryId = (id) => base64Url256ToBytes(id);
807
+ export const binaryIdToId = (binaryId) => decodeId(createBuffer(binaryId));
808
+ export const ownerIdToBinaryOwnerId = (ownerId) => base64Url256ToBytes(ownerId);
809
+ export const binaryOwnerIdToOwnerId = (binaryOwnerId) => decodeOwnerId(createBuffer(binaryOwnerId));
810
+ /**
811
+ * Base64Url string with maximum length of 256 characters. Encoding strings as
812
+ * Base64UrlString saves up to 25% in size compared to regular strings.
813
+ */
814
+ export const Base64Url256 = maxLength(256)(Base64Url);
815
+ /**
816
+ * Alphabet used for Base64Url encoding. This is copied from the `nanoid`
817
+ * library to avoid dependency on a specific version of `nanoid`.
818
+ */
819
+ const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
820
+ /**
821
+ * Converts a Base64Url string to a Uint8Array for binary storage. This encoding
822
+ * is more space-efficient than UTF-8 for Base64Url strings.
823
+ */
824
+ export const base64Url256ToBytes = (string) => {
825
+ const totalBits = string.length * 6; // 6 bits per character
826
+ const byteLength = Math.ceil(totalBits / 8);
827
+ const value = new globalThis.Uint8Array(byteLength);
828
+ let bitBuffer = 0;
829
+ let bitsInBuffer = 0;
830
+ let byteIndex = 0;
831
+ for (const char of string) {
832
+ const charValue = urlAlphabet.indexOf(char);
833
+ bitBuffer = (bitBuffer << 6) | charValue;
834
+ bitsInBuffer += 6;
835
+ while (bitsInBuffer >= 8) {
836
+ bitsInBuffer -= 8;
837
+ value[byteIndex++] = (bitBuffer >> bitsInBuffer) & 0xff;
838
+ }
839
+ }
840
+ if (bitsInBuffer > 0 && byteIndex < byteLength) {
841
+ value[byteIndex] = (bitBuffer << (8 - bitsInBuffer)) & 0xff;
842
+ }
843
+ return value;
844
+ };
845
+ export const decodeBase64Url256 = (buffer, stringLength) => {
846
+ const bytes = buffer.shiftN(Math.ceil((stringLength * 6) / 8));
847
+ let bitBuffer = 0;
848
+ let bitsInBuffer = 0;
849
+ let string = "";
850
+ for (const byte of bytes) {
851
+ bitBuffer = (bitBuffer << 8) | byte;
852
+ bitsInBuffer += 8;
853
+ while (bitsInBuffer >= 6) {
854
+ bitsInBuffer -= 6;
855
+ if (string.length < stringLength) {
856
+ const charValue = (bitBuffer >> bitsInBuffer) & 0x3f;
857
+ if (charValue < 0 || charValue >= urlAlphabet.length) {
858
+ throw new ProtocolDecodeError("invalid charValue");
859
+ }
860
+ string += urlAlphabet[charValue];
861
+ }
862
+ }
863
+ }
864
+ const result = Base64Url256.from(string);
865
+ if (!result.ok)
866
+ throw new ProtocolDecodeError(result.error.type);
867
+ return result.value;
868
+ };
869
+ const decodeId = (buffer) => decodeBase64Url256(buffer, idTypeValueLength);
870
+ /** Not all 16 bytes are valid {@link OwnerId}. */
871
+ const decodeOwnerId = (buffer) => decodeId(buffer);
872
+ /**
873
+ * Evolu uses MessagePack to handle all number variants except for
874
+ * NonNegativeInt. For NonNegativeInt, Evolu provides more efficient encoding.
875
+ */
876
+ export const encodeNumber = (buffer, number) => {
877
+ buffer.extend(pack(number));
878
+ };
879
+ export const decodeNumber = (buffer) => {
880
+ let number;
881
+ let end;
882
+ unpackMultiple(buffer.unwrap(), (n, _, e) => {
883
+ number = n;
884
+ end = e;
885
+ return false;
886
+ });
887
+ const endResult = NonNegativeInt.fromUnknown(end);
888
+ if (!endResult.ok)
889
+ throw new ProtocolDecodeError(endResult.error.type);
890
+ const numberResult = Number.fromUnknown(number);
891
+ if (!numberResult.ok)
892
+ throw new ProtocolDecodeError(numberResult.error.type);
893
+ buffer.shiftN(endResult.value);
894
+ return numberResult.value;
895
+ };
896
+ export const binaryTimestampToFingerprint = (timestamp) => {
897
+ const hash = sha256(timestamp).slice(0, fingerprintSize);
898
+ return hash;
899
+ };
900
+ /**
901
+ * Encodes and encrypts a {@link DbChange} using the provided owner's encryption
902
+ * key. Returns an encrypted binary representation as {@link EncryptedDbChange}.
903
+ */
904
+ export const encodeAndEncryptDbChange = (deps) => (change, key) => {
905
+ const buffer = createBuffer();
906
+ encodeBase64Url256(buffer, change.table);
907
+ buffer.extend(idToBinaryId(change.id));
908
+ const entries = objectToEntries(change.values).map(([column, value]) => {
909
+ return [column, value];
910
+ });
911
+ encodeLength(buffer, entries);
912
+ for (const [column, value] of entries) {
913
+ encodeBase64Url256(buffer, column);
914
+ encodeSqliteValue(buffer, value);
915
+ }
916
+ const paddingLength = padmePaddingLength(buffer.getLength());
917
+ // Add zero bytes as PADMÉ padding - these will be ignored during decoding.
918
+ buffer.extend(new Uint8Array(paddingLength));
919
+ const { nonce, ciphertext } = deps.symmetricCrypto.encrypt(buffer.unwrap(), key);
920
+ buffer.reset();
921
+ buffer.extend(nonce);
922
+ encodeLength(buffer, ciphertext);
923
+ buffer.extend(ciphertext);
924
+ return buffer.unwrap();
925
+ };
926
+ /**
927
+ * Decrypts and decodes an {@link EncryptedDbChange} using the provided owner's
928
+ * encryption key.
929
+ */
930
+ export const decryptAndDecodeDbChange = (deps) => (change, key) => tryDecodeProtocolData(change, (buffer) => {
931
+ const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
932
+ const ciphertextLength = decodeLength(buffer);
933
+ const ciphertext = buffer.shiftN(ciphertextLength);
934
+ const plaintextBytes = deps.symmetricCrypto.decrypt(ciphertext, key, nonce);
935
+ if (!plaintextBytes.ok)
936
+ return plaintextBytes;
937
+ buffer.reset();
938
+ buffer.extend(plaintextBytes.value);
939
+ const table = decodeBase64Url256WithLength(buffer);
940
+ const id = decodeId(buffer);
941
+ const length = decodeLength(buffer);
942
+ const values = Object.create(null);
943
+ for (let i = 0; i < length; i++) {
944
+ const column = decodeBase64Url256WithLength(buffer);
945
+ const value = decodeSqliteValue(buffer);
946
+ values[column] = value;
947
+ }
948
+ const dbChange = { table, id, values };
949
+ return ok(dbChange);
950
+ });
951
+ /**
952
+ * Encodes a non-negative integer into a variable-length integer format. It's
953
+ * more efficient than encoding via {@link encodeNumber}.
954
+ *
955
+ * https://en.wikipedia.org/wiki/Variable-length_quantity
956
+ */
957
+ export const encodeNonNegativeInt = (buffer, int) => {
958
+ if (int === 0) {
959
+ buffer.extend([0]);
960
+ return;
961
+ }
962
+ let remaining = BigInt(int);
963
+ const bytes = [];
964
+ while (remaining !== 0n) {
965
+ const byte = globalThis.Number(remaining & 127n);
966
+ bytes.push(byte);
967
+ remaining >>= 7n;
968
+ }
969
+ for (let i = 0; i < bytes.length - 1; i++) {
970
+ bytes[i] |= 128;
971
+ }
972
+ buffer.extend(bytes);
973
+ };
974
+ /**
975
+ * Decodes a non-negative integer from a variable-length integer format.
976
+ *
977
+ * https://en.wikipedia.org/wiki/Variable-length_quantity
978
+ */
979
+ export const decodeNonNegativeInt = (buffer) => {
980
+ let result = 0n;
981
+ let shift = 0n;
982
+ let byte;
983
+ // 8 is the smallest required count
984
+ for (let byteCount = 0; byteCount < 8; byteCount++) {
985
+ byte = buffer.shift();
986
+ result |= BigInt(byte & 127) << shift;
987
+ if ((byte & 128) === 0)
988
+ break;
989
+ shift += 7n;
990
+ }
991
+ const int = NonNegativeInt.from(globalThis.Number(result));
992
+ if (!int.ok)
993
+ throw new ProtocolDecodeError(int.error.type);
994
+ return int.value;
995
+ };
996
+ export const encodeLength = (buffer, value) => {
997
+ encodeNonNegativeInt(buffer, value.length);
998
+ };
999
+ export const decodeLength = decodeNonNegativeInt;
1000
+ export const encodeString = (buffer, value) => {
1001
+ const bytes = utf8ToBytes(value);
1002
+ encodeLength(buffer, bytes);
1003
+ buffer.extend(bytes);
1004
+ };
1005
+ export const decodeString = (buffer) => {
1006
+ const length = decodeLength(buffer);
1007
+ const bytes = buffer.shiftN(length);
1008
+ return bytesToUtf8(bytes);
1009
+ };
1010
+ export const encodeNodeId = (buffer, nodeId) => {
1011
+ buffer.extend(hexToBytes(nodeId));
1012
+ };
1013
+ export const decodeNodeId = (buffer) => {
1014
+ const bytes = buffer.shiftN(8);
1015
+ return bytesToHex(bytes);
1016
+ };
1017
+ export const encodeBase64Url256 = (buffer, string) => {
1018
+ encodeLength(buffer, string);
1019
+ buffer.extend(base64Url256ToBytes(string));
1020
+ };
1021
+ export const decodeBase64Url256WithLength = (buffer) => {
1022
+ const length = decodeLength(buffer);
1023
+ return decodeBase64Url256(buffer, length);
1024
+ };
1025
+ // Small ints are encoded into ProtocolValueType, saving one byte per int.
1026
+ const isSmallInt = (value) => value >= 0 && value < 20;
1027
+ export const ProtocolValueType = {
1028
+ // 0-19 small ints
1029
+ // SQLite types
1030
+ String: 20,
1031
+ Number: 21,
1032
+ Null: 22,
1033
+ Binary: 23,
1034
+ // We can add more types for other DBs or anything else later.
1035
+ // Optimized types
1036
+ Id: 30,
1037
+ Base64Url256: 31,
1038
+ NonNegativeInt: 32,
1039
+ Json: 33,
1040
+ // new Date().toISOString() - 24 bytes
1041
+ // encoded with fixed length - 8 bytes
1042
+ // encode as NonNegativeInt - 6 bytes (additional 25% reduction)
1043
+ DateIsoWithNonNegativeTime: 34,
1044
+ DateIsoWithNegativeTime: 35, // 9 bytes
1045
+ // TODO: Operations (from 40)
1046
+ // Increment, Decrement, Patch, whatever.
1047
+ };
1048
+ export const encodeSqliteValue = (buffer, value) => {
1049
+ if (value === null) {
1050
+ encodeNonNegativeInt(buffer, ProtocolValueType.Null);
1051
+ return;
1052
+ }
1053
+ switch (typeof value) {
1054
+ case "string": {
1055
+ const dateIsoString = DateIsoString.from(value);
1056
+ if (dateIsoString.ok) {
1057
+ const time = new Date(dateIsoString.value).getTime();
1058
+ if (NonNegativeInt.is(time)) {
1059
+ encodeNonNegativeInt(buffer, ProtocolValueType.DateIsoWithNonNegativeTime);
1060
+ encodeNonNegativeInt(buffer, time);
1061
+ }
1062
+ else {
1063
+ encodeNonNegativeInt(buffer, ProtocolValueType.DateIsoWithNegativeTime);
1064
+ encodeNumber(buffer, time);
1065
+ }
1066
+ return;
1067
+ }
1068
+ const base64Url256 = Base64Url256.from(value);
1069
+ if (base64Url256.ok) {
1070
+ if (base64Url256.value.length === idTypeValueLength) {
1071
+ encodeNonNegativeInt(buffer, ProtocolValueType.Id);
1072
+ buffer.extend(base64Url256ToBytes(base64Url256.value));
1073
+ return;
1074
+ }
1075
+ encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url256);
1076
+ encodeBase64Url256(buffer, base64Url256.value);
1077
+ return;
1078
+ }
1079
+ const jsonValue = JsonValueFromString.fromParent(value);
1080
+ if (jsonValue.ok) {
1081
+ const jsonBytes = pack(jsonValue.value);
1082
+ encodeNonNegativeInt(buffer, ProtocolValueType.Json);
1083
+ encodeLength(buffer, jsonBytes);
1084
+ buffer.extend(jsonBytes);
1085
+ return;
1086
+ }
1087
+ encodeNonNegativeInt(buffer, ProtocolValueType.String);
1088
+ encodeString(buffer, value);
1089
+ return;
1090
+ }
1091
+ case "number": {
1092
+ if (NonNegativeInt.is(value)) {
1093
+ if (isSmallInt(value)) {
1094
+ encodeNonNegativeInt(buffer, value);
1095
+ return;
1096
+ }
1097
+ encodeNonNegativeInt(buffer, ProtocolValueType.NonNegativeInt);
1098
+ encodeNonNegativeInt(buffer, value);
1099
+ return;
1100
+ }
1101
+ encodeNonNegativeInt(buffer, ProtocolValueType.Number);
1102
+ encodeNumber(buffer, value);
1103
+ return;
1104
+ }
1105
+ }
1106
+ encodeNonNegativeInt(buffer, ProtocolValueType.Binary);
1107
+ encodeLength(buffer, value);
1108
+ buffer.extend(value);
1109
+ };
1110
+ export const decodeSqliteValue = (buffer) => {
1111
+ const type = decodeNonNegativeInt(buffer);
1112
+ if (isSmallInt(type)) {
1113
+ return type;
1114
+ }
1115
+ switch (type) {
1116
+ case ProtocolValueType.String:
1117
+ return decodeString(buffer);
1118
+ case ProtocolValueType.Number:
1119
+ return decodeNumber(buffer);
1120
+ case ProtocolValueType.Null:
1121
+ return null;
1122
+ case ProtocolValueType.Binary: {
1123
+ const length = decodeLength(buffer);
1124
+ return buffer.shiftN(length);
1125
+ }
1126
+ case ProtocolValueType.Id: {
1127
+ return decodeId(buffer);
1128
+ }
1129
+ case ProtocolValueType.Base64Url256:
1130
+ return decodeBase64Url256WithLength(buffer);
1131
+ case ProtocolValueType.NonNegativeInt:
1132
+ return decodeNonNegativeInt(buffer);
1133
+ case ProtocolValueType.Json: {
1134
+ const length = decodeLength(buffer);
1135
+ const bytes = buffer.shiftN(length);
1136
+ return JSON.stringify(unpack(bytes));
1137
+ }
1138
+ case ProtocolValueType.DateIsoWithNonNegativeTime:
1139
+ case ProtocolValueType.DateIsoWithNegativeTime: {
1140
+ const time = type === ProtocolValueType.DateIsoWithNonNegativeTime
1141
+ ? decodeNonNegativeInt(buffer)
1142
+ : decodeNumber(buffer);
1143
+ const dateIsoString = DateIsoString.fromParent(new Date(time).toISOString());
1144
+ if (!dateIsoString.ok)
1145
+ throw new ProtocolDecodeError(dateIsoString.error.type);
1146
+ return dateIsoString.value;
1147
+ }
1148
+ default:
1149
+ throw new ProtocolDecodeError("invalid ProtocolValueType");
1150
+ }
1151
+ };