@evolu/common 6.0.1-preview.2 → 6.0.1-preview.21

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 (154) hide show
  1. package/dist/src/Assert.d.ts +6 -3
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +7 -4
  4. package/dist/src/Brand.d.ts +75 -0
  5. package/dist/src/Brand.d.ts.map +1 -0
  6. package/dist/src/Brand.js +1 -0
  7. package/dist/src/Buffer.d.ts +1 -1
  8. package/dist/src/Buffer.d.ts.map +1 -1
  9. package/dist/src/Buffer.js +1 -1
  10. package/dist/src/CallbackRegistry.d.ts +53 -0
  11. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  12. package/dist/src/CallbackRegistry.js +25 -0
  13. package/dist/src/Console.d.ts +31 -6
  14. package/dist/src/Console.d.ts.map +1 -1
  15. package/dist/src/Console.js +72 -9
  16. package/dist/src/Crypto.d.ts +61 -34
  17. package/dist/src/Crypto.d.ts.map +1 -1
  18. package/dist/src/Crypto.js +29 -42
  19. package/dist/src/Evolu/Db.d.ts +159 -66
  20. package/dist/src/Evolu/Db.d.ts.map +1 -1
  21. package/dist/src/Evolu/Db.js +300 -705
  22. package/dist/src/Evolu/Diff.d.ts +3 -3
  23. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  24. package/dist/src/Evolu/Diff.js +7 -5
  25. package/dist/src/Evolu/Evolu.d.ts +200 -115
  26. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  27. package/dist/src/Evolu/Evolu.js +287 -157
  28. package/dist/src/Evolu/Internal.d.ts +0 -2
  29. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  30. package/dist/src/Evolu/Internal.js +0 -2
  31. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  32. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  33. package/dist/src/Evolu/LocalAuth.js +171 -0
  34. package/dist/src/Evolu/Owner.d.ts +209 -124
  35. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  36. package/dist/src/Evolu/Owner.js +118 -104
  37. package/dist/src/Evolu/Platform.d.ts +9 -7
  38. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  39. package/dist/src/Evolu/Protocol.d.ts +256 -211
  40. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  41. package/dist/src/Evolu/Protocol.js +558 -359
  42. package/dist/src/Evolu/Public.d.ts +6 -8
  43. package/dist/src/Evolu/Public.d.ts.map +1 -1
  44. package/dist/src/Evolu/Public.js +2 -3
  45. package/dist/src/Evolu/PublicKysely.js +3 -3
  46. package/dist/src/Evolu/Query.d.ts +2 -1
  47. package/dist/src/Evolu/Query.d.ts.map +1 -1
  48. package/dist/src/Evolu/Relay.d.ts +72 -4
  49. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  50. package/dist/src/Evolu/Relay.js +124 -13
  51. package/dist/src/Evolu/Schema.d.ts +129 -73
  52. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  53. package/dist/src/Evolu/Schema.js +169 -89
  54. package/dist/src/Evolu/Storage.d.ts +159 -14
  55. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  56. package/dist/src/Evolu/Storage.js +42 -32
  57. package/dist/src/Evolu/Sync.d.ts +68 -13
  58. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  59. package/dist/src/Evolu/Sync.js +414 -20
  60. package/dist/src/Evolu/Timestamp.d.ts +35 -27
  61. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  62. package/dist/src/Evolu/Timestamp.js +27 -18
  63. package/dist/src/ManyToManyMap.d.ts +74 -10
  64. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  65. package/dist/src/ManyToManyMap.js +41 -6
  66. package/dist/src/Number.d.ts +2 -1
  67. package/dist/src/Number.d.ts.map +1 -1
  68. package/dist/src/Random.d.ts +3 -2
  69. package/dist/src/Random.d.ts.map +1 -1
  70. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  71. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  72. package/dist/src/RefCountedResourceManager.js +197 -0
  73. package/dist/src/Result.d.ts +176 -51
  74. package/dist/src/Result.d.ts.map +1 -1
  75. package/dist/src/Result.js +30 -241
  76. package/dist/src/Sqlite.d.ts +21 -4
  77. package/dist/src/Sqlite.d.ts.map +1 -1
  78. package/dist/src/Sqlite.js +50 -8
  79. package/dist/src/Task.d.ts +511 -0
  80. package/dist/src/Task.d.ts.map +1 -0
  81. package/dist/src/Task.js +410 -0
  82. package/dist/src/Time.d.ts +59 -0
  83. package/dist/src/Time.d.ts.map +1 -1
  84. package/dist/src/Time.js +87 -4
  85. package/dist/src/Type.d.ts +474 -327
  86. package/dist/src/Type.d.ts.map +1 -1
  87. package/dist/src/Type.js +502 -436
  88. package/dist/src/Types.d.ts +0 -74
  89. package/dist/src/Types.d.ts.map +1 -1
  90. package/dist/src/WebSocket.d.ts +5 -2
  91. package/dist/src/WebSocket.d.ts.map +1 -1
  92. package/dist/src/WebSocket.js +12 -13
  93. package/dist/src/Worker.d.ts +39 -11
  94. package/dist/src/Worker.d.ts.map +1 -1
  95. package/dist/src/Worker.js +22 -4
  96. package/dist/src/index.d.ts +3 -3
  97. package/dist/src/index.d.ts.map +1 -1
  98. package/dist/src/index.js +3 -3
  99. package/package.json +14 -13
  100. package/src/Assert.ts +8 -7
  101. package/src/Brand.ts +75 -0
  102. package/src/Buffer.ts +1 -1
  103. package/src/CallbackRegistry.ts +84 -0
  104. package/src/Console.ts +91 -11
  105. package/src/Crypto.ts +94 -79
  106. package/src/Evolu/Db.ts +528 -1027
  107. package/src/Evolu/Diff.ts +7 -5
  108. package/src/Evolu/Evolu.ts +682 -329
  109. package/src/Evolu/Internal.ts +0 -2
  110. package/src/Evolu/LocalAuth.ts +422 -0
  111. package/src/Evolu/Owner.ts +282 -227
  112. package/src/Evolu/Platform.ts +9 -9
  113. package/src/Evolu/Protocol.ts +788 -640
  114. package/src/Evolu/Public.ts +7 -14
  115. package/src/Evolu/PublicKysely.ts +3 -3
  116. package/src/Evolu/Query.ts +2 -1
  117. package/src/Evolu/Relay.ts +238 -18
  118. package/src/Evolu/Schema.ts +391 -191
  119. package/src/Evolu/Storage.ts +275 -55
  120. package/src/Evolu/Sync.ts +708 -37
  121. package/src/Evolu/Timestamp.ts +38 -35
  122. package/src/ManyToManyMap.ts +127 -24
  123. package/src/Number.ts +2 -6
  124. package/src/Random.ts +3 -2
  125. package/src/RefCountedResourceManager.ts +368 -0
  126. package/src/Result.ts +183 -53
  127. package/src/Sqlite.ts +59 -23
  128. package/src/Task.ts +779 -0
  129. package/src/Time.ts +168 -4
  130. package/src/Type.ts +708 -660
  131. package/src/Types.ts +0 -76
  132. package/src/WebSocket.ts +23 -17
  133. package/src/Worker.ts +72 -23
  134. package/src/index.ts +3 -3
  135. package/dist/src/Callbacks.d.ts +0 -20
  136. package/dist/src/Callbacks.d.ts.map +0 -1
  137. package/dist/src/Callbacks.js +0 -18
  138. package/dist/src/Evolu/Config.d.ts +0 -69
  139. package/dist/src/Evolu/Config.d.ts.map +0 -1
  140. package/dist/src/Evolu/Config.js +0 -9
  141. package/dist/src/Evolu/Kysely.d.ts +0 -6
  142. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  143. package/dist/src/Evolu/Kysely.js +0 -21
  144. package/dist/src/NanoId.d.ts +0 -27
  145. package/dist/src/NanoId.d.ts.map +0 -1
  146. package/dist/src/NanoId.js +0 -6
  147. package/dist/src/Promise.d.ts +0 -180
  148. package/dist/src/Promise.d.ts.map +0 -1
  149. package/dist/src/Promise.js +0 -176
  150. package/src/Callbacks.ts +0 -43
  151. package/src/Evolu/Config.ts +0 -83
  152. package/src/Evolu/Kysely.ts +0 -38
  153. package/src/NanoId.ts +0 -39
  154. package/src/Promise.ts +0 -295
@@ -3,33 +3,54 @@
3
3
  *
4
4
  * Evolu Protocol is a local-first, end-to-end encrypted binary synchronization
5
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.
6
+ * between a client and a relay. In the future, direct peer-to-peer (P2P) sync
7
+ * between clients will be possible without a relay.
8
+ *
9
+ * Relays don't need to sync with each other—clients using those relays will
10
+ * sync them eventually. If a relay is offline (e.g., for maintenance), it will
11
+ * sync automatically later via client sync logic. For relay backup using
12
+ * SQLite, see https://sqlite.org/rsync.html (uses a similar algorithm to Evolu
13
+ * RBSR).
8
14
  *
9
15
  * Evolu Protocol is designed for SQLite but can be extended to any database. It
10
16
  * implements [Range-Based Set
11
17
  * Reconciliation](https://arxiv.org/abs/2212.13567). To learn how RBSR works,
12
18
  * check [Negentropy](https://logperiodic.com/rbsr.html). Evolu Protocol is
13
19
  * similar to Negentropy but uses different encoding and also provides data
14
- * transfer and ownership.
20
+ * transfer, ownership, real-time broadcasting, request-response semantics, and
21
+ * error handling.
22
+ *
23
+ * ### Message structure
15
24
  *
16
- * ### Message Structure
25
+ * | Field | Notes |
26
+ * | :----------------------------- | :------------------------ |
27
+ * | **Header** | |
28
+ * | - {@link protocolVersion} | |
29
+ * | - {@link OwnerId} | {@link Owner} |
30
+ * | - messageType | {@link MessageType} |
31
+ * | **Request (messageType=0)** | |
32
+ * | - hasWriteKey | 0 = no, 1 = yes |
33
+ * | - {@link OwnerWriteKey} | If hasWriteKey = 1 |
34
+ * | - subscriptionFlag | {@link SubscriptionFlags} |
35
+ * | **Response (messageType=1)** | |
36
+ * | - {@link ProtocolErrorCode} | |
37
+ * | **Broadcast (messageType=2)** | |
38
+ * | - (no additional fields) | |
39
+ * | **Messages** | |
40
+ * | - {@link NonNegativeInt} | A number of messages. |
41
+ * | - {@link EncryptedCrdtMessage} | |
42
+ * | **Ranges** | |
43
+ * | - {@link NonNegativeInt} | Number of ranges. |
44
+ * | - {@link Range} | |
17
45
  *
18
- * | Field | Notes |
19
- * | :----------------------------- | :------------------------- |
20
- * | **Header** | |
21
- * | - {@link protocolVersion} | |
22
- * | - {@link OwnerId} | |
23
- * | - {@link ProtocolErrorCode} | In non-initiator response. |
24
- * | **Messages** | |
25
- * | - {@link NonNegativeInt} | A number of messages. |
26
- * | - {@link EncryptedCrdtMessage} | |
27
- * | - {@link WriteKey} | In initiator request. |
28
- * | **Ranges** | |
29
- * | - {@link NonNegativeInt} | Number of ranges. |
30
- * | - {@link Range} | |
46
+ * ### WriteKey validation
31
47
  *
32
- * Every protocol message belongs to an {@link Owner}.
48
+ * The initiator sends a hasWriteKey flag and optionally a WriteKey. The
49
+ * WriteKey is required when sending messages as a secure token proving the
50
+ * initiator can write changes. It's ok to not send a WriteKey if the initiator
51
+ * is only syncing (read-only) and not sending messages. The non-initiator
52
+ * validates the WriteKey immediately after parsing the initiator header, before
53
+ * processing any messages or ranges.
33
54
  *
34
55
  * ### Synchronization
35
56
  *
@@ -43,38 +64,48 @@
43
64
  * if further sync is needed or possible, continuing until both sides are
44
65
  * synchronized.
45
66
  *
67
+ * The **non-initiator always responds** to provide sync completion feedback,
68
+ * even with empty messages containing only the header and no error. This allows
69
+ * the initiator to detect when synchronization is complete.
70
+ *
46
71
  * Both **Messages** and **Ranges** are optional, allowing each side to send,
47
72
  * sync, or only subscribe data as needed.
48
73
  *
49
- * When the initiator sends data, the {@link WriteKey} is required in Messages as
50
- * a secure token proving the initiator can write changes. The non-initiator
51
- * responds without a {@link WriteKey}, since the initiator’s request already
52
- * signals it wants data. If the non-initiator detects an issue, it sends an
53
- * error code via the `Error` field in the header back to the initiator. In
54
- * relay-to-relay or P2P sync, both sides may require the {@link WriteKey}
55
- * depending on who is the initiator.
74
+ * When the initiator sends data, the {@link OwnerWriteKey} is required as a
75
+ * secure token proving the initiator can write changes. The non-initiator
76
+ * responds without a {@link OwnerWriteKey}, since the initiator’s request
77
+ * already signals it wants data. If the non-initiator detects an issue, it
78
+ * sends an error code via the `Error` field in the header back to the
79
+ * initiator. In relay-to-relay or P2P sync, both sides may require the
80
+ * {@link OwnerWriteKey} depending on who is the initiator.
56
81
  *
57
- * ### Protocol Errors
82
+ * ### Protocol errors
58
83
  *
59
84
  * The protocol uses error codes in the header to signal issues:
60
85
  *
61
86
  * - {@link ProtocolWriteKeyError}: The provided WriteKey is invalid or missing.
62
- * - {@link ProtocolWriteError}: A write operation failed (e.g., due to storage
63
- * limits or billing).
64
- * - {@link ProtocolSyncError}: A generic or unexpected synchronization failure
87
+ * - {@link ProtocolWriteError}: A serious relay-side write failure occurred.
88
+ * - {@link ProtocolSyncError}: A serious relay-side synchronization failure
65
89
  * occurred.
90
+ * - {@link ProtocolQuotaExceededError}: Storage or billing quota exceeded.
66
91
  * - {@link ProtocolUnsupportedVersionError}: Protocol version mismatch.
67
92
  * - {@link ProtocolInvalidDataError}: The message is malformed or corrupted.
68
93
  *
69
- * All protocol errors except `ProtocolInvalidDataError` include the `ownerId`
94
+ * All protocol errors except `ProtocolInvalidDataError` include the `OwnerId`
70
95
  * to allow clients to associate errors with the correct owner.
71
96
  *
72
- * ### Message Size Limit
97
+ * ### Message size limit
73
98
  *
74
99
  * The protocol enforces a strict maximum size for all messages, defined by
75
- * {@link maxProtocolMessageSize}. This ensures every {@link ProtocolMessage} is
76
- * less than or equal to this limit, eliminating the need for applications to
77
- * fragment and reconstruct messages during transmission.
100
+ * {@link ProtocolMessageMaxSize}. This ensures every {@link ProtocolMessage} is
101
+ * less than or equal to this limit, enabling stateless transports, simplified
102
+ * relay implementation, and predictable memory usage. When all messages don't
103
+ * fit within the limit, the protocol automatically continues synchronization in
104
+ * subsequent rounds using range-based reconciliation.
105
+ *
106
+ * Database mutations are limited to 640KB, which is smaller than the protocol
107
+ * message limit to ensure efficient sync with
108
+ * {@link defaultProtocolMessageRangesMaxSize}.
78
109
  *
79
110
  * ### Why Binary?
80
111
  *
@@ -82,13 +113,12 @@
82
113
  *
83
114
  * - Encrypted data doesn’t compress well, unlike plain JSON.
84
115
  * - Message size must be controlled during creation.
85
- * - Sequential byte reading is faster than parsing and can avoid conversions.
116
+ * - Sequential byte reading is faster than parsing and avoids conversions.
86
117
  *
87
118
  * It uses structure-aware encoding, significantly outperforming generic binary
88
119
  * serialization formats with the following optimizations:
89
120
  *
90
121
  * - **NonNegativeInt:** Up to 33% smaller than MessagePack.
91
- * - **Base64Url Strings:** Up to 25% size reduction.
92
122
  * - **DateIso:** Up to 75% smaller.
93
123
  * - **Timestamp Encoding:** Delta encoding for milliseconds and run-length
94
124
  * encoding (RLE) for counters and NodeIds.
@@ -118,27 +148,100 @@
118
148
  * Version negotiation is per-owner, allowing Evolu Protocol to evolve safely
119
149
  * over time and provide clear feedback about version mismatches.
120
150
  *
151
+ * ### Credible exit
152
+ *
153
+ * The protocol specification is intentionally non-configurable to ensure
154
+ * universal compatibility. This design allows applications (users) to switch
155
+ * between any compliant relay without negotiation or compatibility checks
156
+ * beyond version matching. Relays are generic infrastructure that any
157
+ * application can use interchangeably making exit from any single provider
158
+ * technically feasible and economically viable.
159
+ *
121
160
  * @module
122
161
  */
123
- import { sha256 } from "@noble/hashes/sha2";
124
- import { pack, unpack, unpackMultiple } from "msgpackr";
162
+ import { Packr } from "msgpackr";
125
163
  import { isNonEmptyReadonlyArray } from "../Array.js";
126
164
  import { assert } from "../Assert.js";
127
- import { BufferError, bytesToHex, bytesToUtf8, concatBytes, createBuffer, hexToBytes, utf8ToBytes, } from "../Buffer.js";
165
+ import { bytesToHex, bytesToUtf8, createBuffer, hexToBytes, utf8ToBytes, } from "../Buffer.js";
128
166
  import { padmePaddingLength, } from "../Crypto.js";
129
167
  import { eqArrayNumber } from "../Eq.js";
130
168
  import { computeBalancedBuckets } from "../Number.js";
131
169
  import { objectToEntries } from "../Object.js";
132
170
  import { err, ok } from "../Result.js";
133
- import { Base64Url, DateIsoString, idTypeValueLength, JsonValueFromString, maxLength, NonNegativeInt, Number, } from "../Type.js";
134
- import { writeKeyLength, } from "./Owner.js";
135
- import { binaryTimestampToTimestamp, Counter, Millis, timestampToBinaryTimestamp, } from "./Timestamp.js";
136
- /** Maximum size of the entire protocol message in bytes. */
137
- export const maxProtocolMessageSize = 1_000_000;
138
- /** Maximum size of the ranges in bytes. */
139
- export const maxProtocolMessageRangesSize = 30_000;
171
+ import { Base64Url, base64UrlToUint8Array, between, DateIso, Id, idBytesToId, idBytesTypeValueLength, idToIdBytes, Int, Json, jsonToJsonValue, NonNegativeInt, Number, uint8ArrayToBase64Url, } from "../Type.js";
172
+ import { ownerIdToOwnerIdBytes, ownerWriteKeyLength, } from "./Owner.js";
173
+ import { fingerprintSize, InfiniteUpperBound, RangeType, } from "./Storage.js";
174
+ import { Counter, eqTimestamp, Millis, timestampBytesLength, timestampBytesToTimestamp, timestampToTimestampBytes, } from "./Timestamp.js";
175
+ /**
176
+ * Evolu uses MessagePack for numbers and JSONs.
177
+ *
178
+ * - `variableMapSize: true` - More compact maps, ~5-10% slower encoding
179
+ * - `useRecords: false` - Standard MessagePack without extensions
180
+ */
181
+ const packr = new Packr({ variableMapSize: true, useRecords: false });
182
+ const minProtocolMessageMaxSize = 1_000_000;
183
+ const maxProtocolMessageMaxSize = 100_000_000;
184
+ /**
185
+ * Protocol message maximum size.
186
+ *
187
+ * Defines the upper limit for how large a single protocol message can be.
188
+ * Implementations must enforce a maximum size between 1MB and 100MB to ensure
189
+ * compatibility across all Evolu implementations (the maximum size of mutation
190
+ * change is hardcoded and enforced hence the maximum size can't be smaller).
191
+ *
192
+ * Larger maximum sizes can be configured by relays to reduce roundtrips. For
193
+ * example, a dedicated relay with ample resources could configure a 100MB
194
+ * maximum to minimize roundtrips for large syncs.
195
+ *
196
+ * Only relays can safely configure larger sizes, as clients will handle them.
197
+ * Increasing this value on the client side would break compatibility with
198
+ * relays that enforce smaller limits.
199
+ */
200
+ export const ProtocolMessageMaxSize = between(minProtocolMessageMaxSize, maxProtocolMessageMaxSize)(Int);
201
+ /**
202
+ * Default {@link ProtocolMessageMaxSize} (1MB).
203
+ *
204
+ * The standard size used across Evolu implementations. Relays with more
205
+ * resources can configure larger sizes to reduce roundtrips.
206
+ */
207
+ export const defaultProtocolMessageMaxSize = minProtocolMessageMaxSize;
208
+ /**
209
+ * Protocol message ranges maximum size.
210
+ *
211
+ * Defines the upper limit for how large the ranges section of a protocol
212
+ * message can be. Implementations must enforce a maximum size between 3KB and
213
+ * 100KB to ensure compatibility.
214
+ *
215
+ * The upper bound is set to ensure ranges fit within the default 1MB
216
+ * {@link defaultProtocolMessageMaxSize}, maintaining compatibility between all
217
+ * clients and relays.
218
+ */
219
+ export const ProtocolMessageRangesMaxSize = between(3_000, 100_000)(Int);
220
+ /**
221
+ * Default {@link ProtocolMessageRangesMaxSize} (30KB).
222
+ *
223
+ * The standard size used across Evolu implementations. Relays with more
224
+ * resources can configure larger sizes to reduce roundtrips.
225
+ */
226
+ export const defaultProtocolMessageRangesMaxSize = 30_000;
140
227
  /** Evolu Protocol version. */
141
228
  export const protocolVersion = 0;
229
+ export const MessageType = {
230
+ /** Request message from initiator (client) to non-initiator (relay). */
231
+ Request: 0,
232
+ /** Response message from non-initiator (relay) to initiator (client). */
233
+ Response: 1,
234
+ /** Broadcast message from non-initiator (relay) to subscribed clients. */
235
+ Broadcast: 2,
236
+ };
237
+ export const SubscriptionFlags = {
238
+ /** No subscription changes for this owner. */
239
+ None: 0,
240
+ /** Subscribe to updates for this owner. */
241
+ Subscribe: 1,
242
+ /** Unsubscribe from updates for this owner. */
243
+ Unsubscribe: 2,
244
+ };
142
245
  export const ProtocolErrorCode = {
143
246
  NoError: 0,
144
247
  /** A code for {@link ProtocolWriteKeyError}. */
@@ -147,31 +250,25 @@ export const ProtocolErrorCode = {
147
250
  WriteError: 2,
148
251
  /** A code for {@link ProtocolSyncError}. */
149
252
  SyncError: 3,
253
+ /** A code for {@link ProtocolQuotaExceededError}. */
254
+ QuotaExceededError: 4,
150
255
  };
151
- export const RangeType = {
152
- Fingerprint: 1,
153
- Skip: 0,
154
- Timestamps: 2,
155
- };
156
- export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
157
- export const fingerprintSize = 12;
158
- /** A fingerprint of an empty range. */
159
- export const zeroFingerprint = new Uint8Array(fingerprintSize);
160
256
  /**
161
257
  * Creates a {@link ProtocolMessage} from CRDT messages.
162
258
  *
163
- * If the message size would exceed {@link maxProtocolMessageSize}, the protocol
164
- * ensures all messages will be sent in the next round(s) even over
259
+ * If the message size would exceed {@link defaultProtocolMessageMaxSize}, the
260
+ * protocol ensures all messages will be sent in the next round(s) even over
165
261
  * unidirectional and stateless transports.
166
262
  */
167
263
  export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages, maxSize) => {
168
264
  const buffer = createProtocolMessageBuffer(owner.id, {
169
- totalMaxSize: maxSize ?? maxProtocolMessageSize,
265
+ messageType: MessageType.Request,
266
+ totalMaxSize: maxSize ?? defaultProtocolMessageMaxSize,
170
267
  writeKey: owner.writeKey,
171
268
  });
172
269
  let notAllMessagesSent = false;
173
270
  for (const message of messages) {
174
- const change = encodeAndEncryptDbChange(deps)(message.change, owner.encryptionKey);
271
+ const change = encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
175
272
  const encryptedCrdtMessage = { timestamp: message.timestamp, change };
176
273
  if (buffer.canAddMessage(encryptedCrdtMessage)) {
177
274
  buffer.addMessage(encryptedCrdtMessage);
@@ -190,14 +287,14 @@ export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages,
190
287
  *
191
288
  * The ideal approach would be to send three ranges (skip, fingerprint,
192
289
  * skip) where the fingerprint of unsent messages would act as narrow sync
193
- * probe. I think we can send {@link zeroFingerprint} which can be
194
- * interpreted as an indication that the other side should reply with
290
+ * probe. I think we can send `zeroFingerprint` which can be interpreted
291
+ * as an indication that the other side should reply with
195
292
  * {@link TimestampsRange}, so no need to restart syncing.
196
293
  *
197
294
  * For now, using a random fingerprint avoids extra complexity and is good
198
295
  * enough for this case.
199
296
  */
200
- const randomFingerprint = deps.createRandomBytes(fingerprintSize);
297
+ const randomFingerprint = deps.randomBytes.create(fingerprintSize);
201
298
  // There is always a space for Fingerprint with InfiniteUpperBound.
202
299
  buffer.addRange({
203
300
  type: RangeType.Fingerprint,
@@ -208,18 +305,25 @@ export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages,
208
305
  return buffer.unwrap();
209
306
  };
210
307
  /** Creates a {@link ProtocolMessage} for sync. */
211
- export const createProtocolMessageForSync = (deps) => (ownerId) => {
212
- const buffer = createProtocolMessageBuffer(ownerId);
213
- const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
214
- const size = deps.storage.getSize(binaryOwnerId);
308
+ export const createProtocolMessageForSync = (deps) => (ownerId, subscriptionFlag) => {
309
+ const buffer = createProtocolMessageBuffer(ownerId, {
310
+ messageType: MessageType.Request,
311
+ subscriptionFlag: subscriptionFlag ?? SubscriptionFlags.None,
312
+ });
313
+ const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
314
+ const size = deps.storage.getSize(ownerIdBytes);
215
315
  // Errors are handled by the storage.
216
316
  if (size == null)
217
317
  return null;
218
- splitRange(deps)(binaryOwnerId, 0, size, InfiniteUpperBound, buffer);
318
+ splitRange(deps)(ownerIdBytes, 0, size, InfiniteUpperBound, buffer);
219
319
  return buffer.unwrap();
220
320
  };
221
- export const createProtocolMessageBuffer = (ownerId, options = {}) => {
222
- const { errorCode, writeKey, totalMaxSize = maxProtocolMessageSize, rangesMaxSize = maxProtocolMessageRangesSize, version = protocolVersion, } = options;
321
+ export const createProtocolMessageForUnsubscribe = (ownerId) => createProtocolMessageBuffer(ownerId, {
322
+ messageType: MessageType.Request,
323
+ subscriptionFlag: SubscriptionFlags.Unsubscribe,
324
+ }).unwrap();
325
+ export const createProtocolMessageBuffer = (ownerId, options) => {
326
+ const { totalMaxSize = defaultProtocolMessageMaxSize, rangesMaxSize = defaultProtocolMessageRangesMaxSize, version = protocolVersion, } = options;
223
327
  const buffers = {
224
328
  header: createBuffer(),
225
329
  messages: {
@@ -233,18 +337,28 @@ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
233
337
  },
234
338
  };
235
339
  encodeNonNegativeInt(buffers.header, version);
236
- buffers.header.extend(ownerIdToBinaryOwnerId(ownerId));
237
- if (errorCode != null)
238
- buffers.header.extend([errorCode]);
340
+ buffers.header.extend(ownerIdToOwnerIdBytes(ownerId));
341
+ buffers.header.extend([options.messageType]);
342
+ if (options.messageType === MessageType.Request) {
343
+ if (!options.writeKey) {
344
+ buffers.header.extend([0]);
345
+ }
346
+ else {
347
+ buffers.header.extend([1]);
348
+ buffers.header.extend(options.writeKey);
349
+ }
350
+ const subscriptionFlag = options.subscriptionFlag ?? SubscriptionFlags.None;
351
+ buffers.header.extend([subscriptionFlag]);
352
+ }
353
+ else if (options.messageType === MessageType.Response) {
354
+ buffers.header.extend([options.errorCode]);
355
+ }
239
356
  let isLastRangeInfinite = false;
240
357
  const isWithinSizeLimits = () => getSize() <= totalMaxSize;
241
358
  const getSize = () => (getHeaderAndMessagesSize() + getRangesSize());
242
359
  const getHeaderAndMessagesSize = () => buffers.header.getLength() +
243
360
  buffers.messages.timestamps.getLength() +
244
- buffers.messages.dbChanges.getLength() +
245
- (buffers.messages.timestamps.getCount() > 0 && writeKey
246
- ? writeKeyLength
247
- : 0);
361
+ buffers.messages.dbChanges.getLength();
248
362
  const getRangesSize = () => buffers.ranges.timestamps.getCount() > 0
249
363
  ? buffers.ranges.timestamps.getLength() +
250
364
  buffers.ranges.types.getLength() +
@@ -298,6 +412,7 @@ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
298
412
  : true));
299
413
  },
300
414
  addRange: (range) => {
415
+ assert(options.messageType !== MessageType.Broadcast, "Cannot add a range into broadcast message");
301
416
  assert(!isLastRangeInfinite, "Cannot add a range after an InfiniteUpperBound range");
302
417
  isLastRangeInfinite = range.upperBound === InfiniteUpperBound;
303
418
  /**
@@ -306,7 +421,7 @@ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
306
421
  * use SkipRange.
307
422
  */
308
423
  if (range.upperBound !== InfiniteUpperBound)
309
- buffers.ranges.timestamps.add(binaryTimestampToTimestamp(range.upperBound));
424
+ buffers.ranges.timestamps.add(timestampBytesToTimestamp(range.upperBound));
310
425
  else {
311
426
  buffers.ranges.timestamps.addInfinite();
312
427
  }
@@ -330,8 +445,6 @@ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
330
445
  }
331
446
  buffers.messages.timestamps.append(buffers.header);
332
447
  buffers.header.extend(buffers.messages.dbChanges.unwrap());
333
- if (buffers.messages.timestamps.getCount() > 0 && writeKey)
334
- buffers.header.extend(writeKey);
335
448
  if (buffers.ranges.timestamps.getCount() > 0) {
336
449
  buffers.ranges.timestamps.append(buffers.header);
337
450
  buffers.header.extend(buffers.ranges.types.unwrap());
@@ -409,118 +522,224 @@ const createRunLengthEncoder = (encodeValue) => {
409
522
  unwrap: () => buffer.unwrap(),
410
523
  };
411
524
  };
412
- export const applyProtocolMessageAsClient = (deps) => (inputMessage, { getWriteKey, version = protocolVersion, totalMaxSize, rangesMaxSize, } = {}) => tryDecodeProtocolData(inputMessage, (input) => {
413
- const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
414
- if (requestedVersion !== version) {
525
+ export const applyProtocolMessageAsClient = (deps) => async (inputMessage, options = {}) => {
526
+ // try-catch instead of Result for performance and stacktraces
527
+ try {
528
+ const input = createBuffer(inputMessage);
529
+ const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
530
+ const version = options.version ?? protocolVersion;
531
+ if (requestedVersion !== version) {
532
+ return err({
533
+ type: "ProtocolUnsupportedVersionError",
534
+ unsupportedVersion: requestedVersion,
535
+ isInitiator: version < requestedVersion,
536
+ ownerId,
537
+ });
538
+ }
539
+ const messageType = input.shift();
540
+ assert(messageType === MessageType.Response ||
541
+ messageType === MessageType.Broadcast, "Invalid MessageType");
542
+ if (messageType === MessageType.Response) {
543
+ const errorCode = input.shift();
544
+ if (errorCode !== ProtocolErrorCode.NoError) {
545
+ switch (errorCode) {
546
+ case ProtocolErrorCode.WriteKeyError:
547
+ return err({
548
+ type: "ProtocolWriteKeyError",
549
+ ownerId,
550
+ });
551
+ case ProtocolErrorCode.WriteError:
552
+ return err({
553
+ type: "ProtocolWriteError",
554
+ ownerId,
555
+ });
556
+ case ProtocolErrorCode.SyncError:
557
+ return err({
558
+ type: "ProtocolSyncError",
559
+ ownerId,
560
+ });
561
+ case ProtocolErrorCode.QuotaExceededError:
562
+ return err({
563
+ type: "ProtocolQuotaExceededError",
564
+ ownerId,
565
+ });
566
+ default:
567
+ throw new ProtocolDecodeError(`Invalid ProtocolErrorCode: ${errorCode}`);
568
+ }
569
+ }
570
+ }
571
+ const messages = decodeMessages(input);
572
+ const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
573
+ if (isNonEmptyReadonlyArray(messages) &&
574
+ !(await deps.storage.writeMessages(ownerIdBytes, messages))) {
575
+ return ok({ type: "no-response" });
576
+ }
577
+ // Now: No writeKey, no sync.
578
+ // TODO: Allow to sync SharedReadonlyOwner
579
+ // Without local changes, writeKey will not be required.
580
+ // With local changes, writeKey will be required and if not provided,
581
+ // the sync will stop.
582
+ const writeKey = options.getWriteKey?.(ownerId);
583
+ if (writeKey == null) {
584
+ return ok({ type: "no-response" });
585
+ }
586
+ if (messageType === MessageType.Broadcast) {
587
+ return ok({ type: "broadcast" });
588
+ }
589
+ const ranges = decodeRanges(input);
590
+ if (!isNonEmptyReadonlyArray(ranges)) {
591
+ return ok({ type: "no-response" });
592
+ }
593
+ const output = createProtocolMessageBuffer(ownerId, {
594
+ messageType: MessageType.Request,
595
+ writeKey,
596
+ rangesMaxSize: options.rangesMaxSize,
597
+ });
598
+ const syncResult = sync(deps)(ranges, output, ownerIdBytes);
599
+ // Client sync error (handled via Storage) or no changes.
600
+ if (!syncResult.ok || !syncResult.value) {
601
+ return ok({ type: "no-response" });
602
+ }
603
+ return ok({ type: "response", message: output.unwrap() });
604
+ }
605
+ catch (error) {
415
606
  return err({
416
- type: "ProtocolUnsupportedVersionError",
417
- unsupportedVersion: requestedVersion,
418
- isInitiator: version < requestedVersion,
419
- ownerId,
607
+ type: "ProtocolInvalidDataError",
608
+ data: inputMessage,
609
+ error,
420
610
  });
421
611
  }
422
- const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
423
- const errorCode = input.shift();
424
- if (errorCode !== ProtocolErrorCode.NoError) {
425
- switch (errorCode) {
426
- case ProtocolErrorCode.WriteKeyError:
427
- return err({
428
- type: "ProtocolWriteKeyError",
429
- ownerId,
612
+ };
613
+ export const applyProtocolMessageAsRelay = (deps) => async (inputMessage, options = {},
614
+ /** For testing purposes only; should not be used in production. */
615
+ version = protocolVersion) => {
616
+ // try-catch instead of Result for performance and stacktraces
617
+ try {
618
+ const input = createBuffer(inputMessage);
619
+ const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
620
+ const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
621
+ if (requestedVersion !== version) {
622
+ // Non-initiator responds with its version and ownerId.
623
+ const output = createBuffer();
624
+ encodeNonNegativeInt(output, version);
625
+ output.extend(ownerIdBytes);
626
+ return ok({
627
+ type: "response",
628
+ message: output.unwrap(),
629
+ });
630
+ }
631
+ const messageType = input.shift();
632
+ assert(messageType === MessageType.Request, "Invalid MessageType");
633
+ const hasWriteKey = input.shift();
634
+ let writeKey;
635
+ if (hasWriteKey === 1) {
636
+ writeKey = input.shiftN(ownerWriteKeyLength);
637
+ }
638
+ const subscriptionFlag = input.shift();
639
+ switch (subscriptionFlag) {
640
+ case SubscriptionFlags.Subscribe:
641
+ options.subscribe?.(ownerId);
642
+ break;
643
+ case SubscriptionFlags.Unsubscribe:
644
+ options.unsubscribe?.(ownerId);
645
+ break;
646
+ case SubscriptionFlags.None:
647
+ break;
648
+ }
649
+ if (writeKey) {
650
+ const isValid = deps.storage.validateWriteKey(ownerIdBytes, writeKey);
651
+ if (!isValid) {
652
+ return ok({
653
+ type: "response",
654
+ message: createProtocolMessageBuffer(ownerId, {
655
+ messageType: MessageType.Response,
656
+ errorCode: ProtocolErrorCode.WriteKeyError,
657
+ }).unwrap(),
430
658
  });
431
- case ProtocolErrorCode.WriteError:
432
- return err({
433
- type: "ProtocolWriteError",
434
- ownerId,
659
+ }
660
+ }
661
+ const messages = decodeMessages(input);
662
+ if (isNonEmptyReadonlyArray(messages)) {
663
+ if (!writeKey) {
664
+ return ok({
665
+ type: "response",
666
+ message: createProtocolMessageBuffer(ownerId, {
667
+ messageType: MessageType.Response,
668
+ errorCode: ProtocolErrorCode.WriteKeyError,
669
+ }).unwrap(),
435
670
  });
436
- case ProtocolErrorCode.SyncError:
437
- return err({
438
- type: "ProtocolSyncError",
439
- ownerId,
671
+ }
672
+ /**
673
+ * Broadcast messages to all subscribed devices. This ensures real-time
674
+ * synchronization between clients.
675
+ *
676
+ * When a relay's database is deleted or clients migrate to a new relay
677
+ * (without data migration), clients will sync their data to the relay,
678
+ * and the relay will broadcast those messages to other connected
679
+ * clients. Those clients may receive messages they already have, but
680
+ * this is safe because `applyMessages` is idempotent. As the relay
681
+ * becomes more synchronized with clients over time, fewer duplicate
682
+ * messages will be broadcasted.
683
+ */
684
+ if (options.broadcast) {
685
+ const broadcastBuffer = createProtocolMessageBuffer(ownerId, {
686
+ messageType: MessageType.Broadcast,
687
+ totalMaxSize: options.totalMaxSize,
688
+ rangesMaxSize: options.rangesMaxSize,
689
+ version,
440
690
  });
441
- default:
442
- throw new ProtocolDecodeError(`Invalid ProtocolErrorCode: ${errorCode}`);
691
+ for (const message of messages) {
692
+ broadcastBuffer.addMessage(message);
693
+ }
694
+ options.broadcast(ownerId, broadcastBuffer.unwrap());
695
+ }
696
+ if (!(await deps.storage.writeMessages(ownerIdBytes, messages))) {
697
+ return ok({
698
+ type: "response",
699
+ message: createProtocolMessageBuffer(ownerId, {
700
+ messageType: MessageType.Response,
701
+ errorCode: ProtocolErrorCode.WriteError,
702
+ }).unwrap(),
703
+ });
704
+ }
443
705
  }
444
- }
445
- const messages = decodeMessages(input);
446
- if (isNonEmptyReadonlyArray(messages) &&
447
- !deps.storage.writeMessages(binaryOwnerId, messages)) {
448
- return ok(null);
449
- }
450
- if (!getWriteKey)
451
- return ok(null);
452
- const writeKey = getWriteKey(ownerId);
453
- if (writeKey == null)
454
- return ok(null);
455
- const output = createProtocolMessageBuffer(ownerId, {
456
- writeKey,
457
- totalMaxSize,
458
- rangesMaxSize,
459
- });
460
- return sync(deps)("initiator", input, output, binaryOwnerId);
461
- });
462
- export const applyProtocolMessageAsRelay = (deps) => (inputMessage, { subscribe, broadcast, totalMaxSize, rangesMaxSize, } = {},
463
- /** For testing purposes only; should not be used in production. */
464
- version = protocolVersion) => tryDecodeProtocolData(inputMessage, (input) => {
465
- const requestedVersion = decodeNonNegativeInt(input);
466
- const ownerId = decodeOwnerId(input);
467
- const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
468
- if (requestedVersion !== version) {
469
- // Non-initiator responds with its version and ownerId.
470
- const output = createBuffer();
471
- encodeNonNegativeInt(output, version);
472
- output.extend(binaryOwnerId);
473
- return ok(output.unwrap());
474
- }
475
- subscribe?.(ownerId);
476
- const messages = decodeMessages(input);
477
- if (isNonEmptyReadonlyArray(messages)) {
478
- const messagesEnd = inputMessage.length - input.getLength();
479
- const writeKey = input.shiftN(writeKeyLength);
480
- const writeKeyIsValid = deps.storage.validateWriteKey(binaryOwnerId, writeKey);
481
- if (!writeKeyIsValid)
482
- return ok(createProtocolMessageBuffer(ownerId, {
483
- errorCode: ProtocolErrorCode.WriteKeyError,
484
- }).unwrap());
485
- if (broadcast) {
486
- // Instead of encoding a new protocol message, we reuse the inputMessage.
487
- const broadcastMessage = concatBytes(inputMessage.slice(0, 17), new Uint8Array([ProtocolErrorCode.NoError]), inputMessage.slice(17, messagesEnd));
488
- broadcast(ownerId, broadcastMessage);
706
+ const ranges = decodeRanges(input);
707
+ const output = createProtocolMessageBuffer(ownerId, {
708
+ messageType: MessageType.Response,
709
+ errorCode: ProtocolErrorCode.NoError,
710
+ totalMaxSize: options.totalMaxSize,
711
+ rangesMaxSize: options.rangesMaxSize,
712
+ });
713
+ // Non-initiators always respond to provide sync completion feedback,
714
+ // even when there's nothing to sync.
715
+ if (!isNonEmptyReadonlyArray(ranges)) {
716
+ return ok({ type: "response", message: output.unwrap() });
489
717
  }
490
- if (!deps.storage.writeMessages(binaryOwnerId, messages))
491
- return ok(createProtocolMessageBuffer(ownerId, {
492
- errorCode: ProtocolErrorCode.WriteError,
493
- }).unwrap());
494
- }
495
- const output = createProtocolMessageBuffer(ownerId, {
496
- errorCode: ProtocolErrorCode.NoError,
497
- totalMaxSize,
498
- rangesMaxSize,
499
- });
500
- return sync(deps)("non-initiator", input, output, binaryOwnerId);
501
- });
502
- /**
503
- * Wraps Evolu Protocol decoding functions, which use exceptions instead of
504
- * {@link Result} to provide stack traces for debugging and reduce allocation
505
- * overhead in success cases.
506
- */
507
- const tryDecodeProtocolData = (data, callback) => {
508
- try {
509
- return callback(createBuffer(data));
718
+ const syncResult = sync(deps)(ranges, output, ownerIdBytes);
719
+ const message = syncResult.ok
720
+ ? output.unwrap()
721
+ : createProtocolMessageBuffer(ownerId, {
722
+ messageType: MessageType.Response,
723
+ errorCode: syncResult.error,
724
+ }).unwrap();
725
+ // Non-initiators always respond to provide sync completion feedback,
726
+ return ok({ type: "response", message });
510
727
  }
511
728
  catch (error) {
512
- if (error instanceof ProtocolDecodeError || error instanceof BufferError)
513
- return err({
514
- type: "ProtocolInvalidDataError",
515
- data,
516
- error,
517
- });
518
- throw error;
729
+ return err({
730
+ type: "ProtocolInvalidDataError",
731
+ data: inputMessage,
732
+ error,
733
+ });
519
734
  }
520
735
  };
521
736
  const decodeVersionAndOwner = (input) => {
737
+ // This structure must never change across protocol versions. The version
738
+ // and owner ID must always be the first two fields in every protocol message
739
+ // to enable version negotiation and owner identification before any other
740
+ // processing occurs.
522
741
  const version = decodeNonNegativeInt(input);
523
- const ownerId = decodeOwnerId(input);
742
+ const ownerId = decodeId(input);
524
743
  return [version, ownerId];
525
744
  };
526
745
  /**
@@ -544,27 +763,11 @@ const decodeMessages = (buffer) => {
544
763
  }
545
764
  return messages;
546
765
  };
547
- const sync = (deps) => (role, input, output, ownerId) => {
548
- const ranges = decodeRanges(input);
549
- if (!isNonEmptyReadonlyArray(ranges)) {
550
- // Nothing to sync.
551
- return ok(null);
552
- }
553
- const binaryOwnerId = binaryOwnerIdToOwnerId(ownerId);
766
+ const sync = (deps) => (ranges, output, ownerIdBytes) => {
554
767
  const outputInitialSize = output.getSize();
555
- const syncFail = () => {
556
- // Only the relay (non-initiator) reports sync errors, not the client (initiator).
557
- if (role === "initiator") {
558
- return ok(null);
559
- }
560
- const message = createProtocolMessageBuffer(binaryOwnerId, {
561
- errorCode: ProtocolErrorCode.SyncError,
562
- });
563
- return ok(message.unwrap());
564
- };
565
- const storageSize = deps.storage.getSize(ownerId);
768
+ const storageSize = deps.storage.getSize(ownerIdBytes);
566
769
  if (storageSize == null)
567
- return syncFail();
770
+ return err(ProtocolErrorCode.SyncError);
568
771
  let prevUpperBound = null;
569
772
  let prevIndex = 0;
570
773
  let skip = false;
@@ -596,7 +799,7 @@ const sync = (deps) => (role, input, output, ownerId) => {
596
799
  };
597
800
  // When we don't have a space...
598
801
  const addFingerprintForRemainingRange = (begin) => {
599
- const fingerprint = deps.storage.fingerprint(ownerId, begin, storageSize);
802
+ const fingerprint = deps.storage.fingerprint(ownerIdBytes, begin, storageSize);
600
803
  if (!fingerprint)
601
804
  return false;
602
805
  // There is always a space for a ramaining range.
@@ -610,30 +813,30 @@ const sync = (deps) => (role, input, output, ownerId) => {
610
813
  for (const range of ranges) {
611
814
  const currentUpperBound = range.upperBound;
612
815
  const lower = prevIndex;
613
- let upper = deps.storage.findLowerBound(ownerId, prevIndex, storageSize, currentUpperBound);
816
+ let upper = deps.storage.findLowerBound(ownerIdBytes, prevIndex, storageSize, currentUpperBound);
614
817
  if (upper == null)
615
- return syncFail();
818
+ return err(ProtocolErrorCode.SyncError);
616
819
  switch (range.type) {
617
820
  case RangeType.Skip: {
618
821
  skipRange(range);
619
822
  break;
620
823
  }
621
824
  case RangeType.Fingerprint: {
622
- const ourFingerprint = deps.storage.fingerprint(ownerId, lower, upper);
825
+ const ourFingerprint = deps.storage.fingerprint(ownerIdBytes, lower, upper);
623
826
  if (ourFingerprint == null)
624
- return syncFail();
827
+ return err(ProtocolErrorCode.SyncError);
625
828
  if (eqArrayNumber(range.fingerprint, ourFingerprint)) {
626
829
  skipRange(range);
627
830
  }
628
831
  else {
629
832
  if (output.canSplitRange()) {
630
833
  coalesceSkipsBeforeAdd();
631
- splitRange(deps)(ownerId, lower, upper, currentUpperBound, output);
834
+ splitRange(deps)(ownerIdBytes, lower, upper, currentUpperBound, output);
632
835
  }
633
836
  else {
634
- if (!addFingerprintForRemainingRange(upper))
635
- return syncFail();
636
- return ok(output.unwrap());
837
+ return addFingerprintForRemainingRange(upper)
838
+ ? ok(true)
839
+ : err(ProtocolErrorCode.SyncError);
637
840
  }
638
841
  }
639
842
  break;
@@ -642,19 +845,19 @@ const sync = (deps) => (role, input, output, ownerId) => {
642
845
  let endBound = currentUpperBound;
643
846
  const timestampsWeNeed = new Map(range.timestamps.map((t) => [t.join(), true]));
644
847
  const ourTimestamps = createTimestampsBuffer();
645
- let storageError = false;
848
+ let cantReadDbChange = false;
646
849
  let exceeded = false;
647
- deps.storage.iterate(ownerId, lower, upper, (timestamp, index) => {
850
+ deps.storage.iterate(ownerIdBytes, lower, upper, (timestamp, index) => {
648
851
  const timestampString = timestamp.join();
649
- const timestampBinary = binaryTimestampToTimestamp(timestamp);
852
+ const timestampBinary = timestampBytesToTimestamp(timestamp);
650
853
  let message = null;
651
854
  if (timestampsWeNeed.has(timestampString)) {
652
855
  timestampsWeNeed.delete(timestampString);
653
856
  }
654
857
  else {
655
- const dbChange = deps.storage.readDbChange(ownerId, timestamp);
858
+ const dbChange = deps.storage.readDbChange(ownerIdBytes, timestamp);
656
859
  if (dbChange == null) {
657
- storageError = true;
860
+ cantReadDbChange = true;
658
861
  return false;
659
862
  }
660
863
  message = {
@@ -673,8 +876,8 @@ const sync = (deps) => (role, input, output, ownerId) => {
673
876
  output.addMessage(message);
674
877
  return true;
675
878
  });
676
- if (storageError) {
677
- return syncFail();
879
+ if (cantReadDbChange) {
880
+ return err(ProtocolErrorCode.SyncError);
678
881
  }
679
882
  const addRange = () => {
680
883
  coalesceSkipsBeforeAdd();
@@ -687,9 +890,9 @@ const sync = (deps) => (role, input, output, ownerId) => {
687
890
  if (exceeded) {
688
891
  addRange();
689
892
  if (!addFingerprintForRemainingRange(upper)) {
690
- return syncFail();
893
+ return err(ProtocolErrorCode.SyncError);
691
894
  }
692
- return ok(output.unwrap());
895
+ return ok(true);
693
896
  }
694
897
  // If we need something, we have to respond with our timestamps.
695
898
  if (timestampsWeNeed.size > 0) {
@@ -706,7 +909,7 @@ const sync = (deps) => (role, input, output, ownerId) => {
706
909
  }
707
910
  // If all ranges were skipped, there are no changes and sync is complete.
708
911
  const hasChange = output.getSize() > outputInitialSize;
709
- return ok(hasChange ? output.unwrap() : null);
912
+ return ok(hasChange);
710
913
  };
711
914
  const splitRange = (deps) => (ownerId, lower, upper, upperBound, buffer) => {
712
915
  const itemCount = (upper - lower);
@@ -718,7 +921,7 @@ const splitRange = (deps) => (ownerId, lower, upper, upperBound, buffer) => {
718
921
  timestamps: createTimestampsBuffer(),
719
922
  };
720
923
  deps.storage.iterate(ownerId, 0, itemCount, (timestamp) => {
721
- range.timestamps.add(binaryTimestampToTimestamp(timestamp));
924
+ range.timestamps.add(timestampBytesToTimestamp(timestamp));
722
925
  return true;
723
926
  });
724
927
  buffer.addRange(range);
@@ -761,7 +964,7 @@ const decodeRanges = (buffer) => {
761
964
  const ranges = [];
762
965
  for (let i = 0; i < rangesCount; i++) {
763
966
  const upperBound = i < timestampsCount
764
- ? timestampToBinaryTimestamp(timestamps[i])
967
+ ? timestampToTimestampBytes(timestamps[i])
765
968
  : InfiniteUpperBound;
766
969
  const rangeType = rangeTypes[i];
767
970
  switch (rangeType) {
@@ -778,7 +981,7 @@ const decodeRanges = (buffer) => {
778
981
  break;
779
982
  }
780
983
  case RangeType.Timestamps: {
781
- const timestamps = decodeTimestamps(buffer).map(timestampToBinaryTimestamp);
984
+ const timestamps = decodeTimestamps(buffer).map(timestampToTimestampBytes);
782
985
  ranges.push({
783
986
  type: RangeType.Timestamps,
784
987
  upperBound,
@@ -799,7 +1002,7 @@ const decodeTimestamps = (buffer, length) => {
799
1002
  const deltaMillis = decodeNonNegativeInt(buffer);
800
1003
  const millis = Millis.from(previousMillis + deltaMillis);
801
1004
  if (!millis.ok)
802
- throw new Error(millis.error.type);
1005
+ throw new ProtocolDecodeError(millis.error.type);
803
1006
  millises.push(millis.value);
804
1007
  previousMillis = millis.value;
805
1008
  }
@@ -808,7 +1011,7 @@ const decodeTimestamps = (buffer, length) => {
808
1011
  while (counterIndex < length) {
809
1012
  const counter = Counter.from(decodeNonNegativeInt(buffer));
810
1013
  if (!counter.ok)
811
- throw new Error(counter.error.type);
1014
+ throw new ProtocolDecodeError(counter.error.type);
812
1015
  const runLength = decodeNonNegativeInt(buffer);
813
1016
  for (let i = 0; i < runLength; i++) {
814
1017
  counters.push(counter.value);
@@ -835,84 +1038,21 @@ const decodeTimestamps = (buffer, length) => {
835
1038
  }
836
1039
  return timestamps;
837
1040
  };
838
- export const binaryIdLength = 16;
839
- export const idToBinaryId = (id) => base64Url256ToBytes(id);
840
- export const binaryIdToId = (binaryId) => decodeId(createBuffer(binaryId));
841
- export const ownerIdToBinaryOwnerId = (ownerId) => base64Url256ToBytes(ownerId);
842
- export const binaryOwnerIdToOwnerId = (binaryOwnerId) => decodeOwnerId(createBuffer(binaryOwnerId));
843
- /**
844
- * Base64Url string with maximum length of 256 characters. Encoding strings as
845
- * Base64UrlString saves up to 25% in size compared to regular strings.
846
- */
847
- export const Base64Url256 = maxLength(256)(Base64Url);
848
- /**
849
- * Alphabet used for Base64Url encoding. This is copied from the `nanoid`
850
- * library to avoid dependency on a specific version of `nanoid`.
851
- */
852
- const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
853
- /**
854
- * Converts a Base64Url string to a Uint8Array for binary storage. This encoding
855
- * is more space-efficient than UTF-8 for Base64Url strings.
856
- */
857
- export const base64Url256ToBytes = (string) => {
858
- const totalBits = string.length * 6; // 6 bits per character
859
- const byteLength = Math.ceil(totalBits / 8);
860
- const value = new globalThis.Uint8Array(byteLength);
861
- let bitBuffer = 0;
862
- let bitsInBuffer = 0;
863
- let byteIndex = 0;
864
- for (const char of string) {
865
- const charValue = urlAlphabet.indexOf(char);
866
- bitBuffer = (bitBuffer << 6) | charValue;
867
- bitsInBuffer += 6;
868
- while (bitsInBuffer >= 8) {
869
- bitsInBuffer -= 8;
870
- value[byteIndex++] = (bitBuffer >> bitsInBuffer) & 0xff;
871
- }
872
- }
873
- if (bitsInBuffer > 0 && byteIndex < byteLength) {
874
- value[byteIndex] = (bitBuffer << (8 - bitsInBuffer)) & 0xff;
875
- }
876
- return value;
877
- };
878
- export const decodeBase64Url256 = (buffer, stringLength) => {
879
- const bytes = buffer.shiftN(Math.ceil((stringLength * 6) / 8));
880
- let bitBuffer = 0;
881
- let bitsInBuffer = 0;
882
- let string = "";
883
- for (const byte of bytes) {
884
- bitBuffer = (bitBuffer << 8) | byte;
885
- bitsInBuffer += 8;
886
- while (bitsInBuffer >= 6) {
887
- bitsInBuffer -= 6;
888
- if (string.length < stringLength) {
889
- const charValue = (bitBuffer >> bitsInBuffer) & 0x3f;
890
- if (charValue < 0 || charValue >= urlAlphabet.length) {
891
- throw new ProtocolDecodeError("invalid charValue");
892
- }
893
- string += urlAlphabet[charValue];
894
- }
895
- }
896
- }
897
- const result = Base64Url256.from(string);
898
- if (!result.ok)
899
- throw new ProtocolDecodeError(result.error.type);
900
- return result.value;
1041
+ const decodeId = (buffer) => {
1042
+ const bytes = buffer.shiftN(idBytesTypeValueLength);
1043
+ return idBytesToId(bytes);
901
1044
  };
902
- const decodeId = (buffer) => decodeBase64Url256(buffer, idTypeValueLength);
903
- /** Not all 16 bytes are valid {@link OwnerId}. */
904
- const decodeOwnerId = (buffer) => decodeId(buffer);
905
1045
  /**
906
1046
  * Evolu uses MessagePack to handle all number variants except for
907
1047
  * NonNegativeInt. For NonNegativeInt, Evolu provides more efficient encoding.
908
1048
  */
909
1049
  export const encodeNumber = (buffer, number) => {
910
- buffer.extend(pack(number));
1050
+ buffer.extend(packr.pack(number));
911
1051
  };
912
1052
  export const decodeNumber = (buffer) => {
913
1053
  let number;
914
1054
  let end;
915
- unpackMultiple(buffer.unwrap(), (n, _, e) => {
1055
+ packr.unpackMultiple(buffer.unwrap(), (n, _, e) => {
916
1056
  number = n;
917
1057
  end = e;
918
1058
  return false;
@@ -926,24 +1066,30 @@ export const decodeNumber = (buffer) => {
926
1066
  buffer.shiftN(endResult.value);
927
1067
  return numberResult.value;
928
1068
  };
929
- export const binaryTimestampToFingerprint = (timestamp) => {
930
- const hash = sha256(timestamp).slice(0, fingerprintSize);
931
- return hash;
932
- };
933
1069
  /**
934
1070
  * Encodes and encrypts a {@link DbChange} using the provided owner's encryption
935
1071
  * key. Returns an encrypted binary representation as {@link EncryptedDbChange}.
1072
+ *
1073
+ * The format includes the protocol version for backward compatibility and the
1074
+ * timestamp for tamper-proof verification that the timestamp matches the change
1075
+ * data.
936
1076
  */
937
- export const encodeAndEncryptDbChange = (deps) => (change, key) => {
1077
+ export const encodeAndEncryptDbChange = (deps) => (message, key) => {
1078
+ const change = message.change;
938
1079
  const buffer = createBuffer();
939
- encodeBase64Url256(buffer, change.table);
940
- buffer.extend(idToBinaryId(change.id));
1080
+ // Encode protocol version first for backward compatibility
1081
+ encodeNonNegativeInt(buffer, protocolVersion);
1082
+ // Encode the timestamp (after version) for tamper verification
1083
+ const timestampBytes = timestampToTimestampBytes(message.timestamp);
1084
+ buffer.extend(timestampBytes);
1085
+ encodeString(buffer, change.table);
1086
+ buffer.extend(idToIdBytes(change.id));
941
1087
  const entries = objectToEntries(change.values).map(([column, value]) => {
942
1088
  return [column, value];
943
1089
  });
944
1090
  encodeLength(buffer, entries);
945
1091
  for (const [column, value] of entries) {
946
- encodeBase64Url256(buffer, column);
1092
+ encodeString(buffer, column);
947
1093
  encodeSqliteValue(buffer, value);
948
1094
  }
949
1095
  const paddingLength = padmePaddingLength(buffer.getLength());
@@ -957,30 +1103,55 @@ export const encodeAndEncryptDbChange = (deps) => (change, key) => {
957
1103
  return buffer.unwrap();
958
1104
  };
959
1105
  /**
960
- * Decrypts and decodes an {@link EncryptedDbChange} using the provided owner's
961
- * encryption key.
1106
+ * Decrypts and decodes an {@link EncryptedCrdtMessage} using the provided
1107
+ * owner's encryption key. Verifies that the embedded timestamp matches the
1108
+ * expected timestamp to ensure message integrity.
962
1109
  */
963
- export const decryptAndDecodeDbChange = (deps) => (change, key) => tryDecodeProtocolData(change, (buffer) => {
964
- const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
965
- const ciphertextLength = decodeLength(buffer);
966
- const ciphertext = buffer.shiftN(ciphertextLength);
967
- const plaintextBytes = deps.symmetricCrypto.decrypt(ciphertext, key, nonce);
968
- if (!plaintextBytes.ok)
969
- return plaintextBytes;
970
- buffer.reset();
971
- buffer.extend(plaintextBytes.value);
972
- const table = decodeBase64Url256WithLength(buffer);
973
- const id = decodeId(buffer);
974
- const length = decodeLength(buffer);
975
- const values = Object.create(null);
976
- for (let i = 0; i < length; i++) {
977
- const column = decodeBase64Url256WithLength(buffer);
978
- const value = decodeSqliteValue(buffer);
979
- values[column] = value;
1110
+ export const decryptAndDecodeDbChange = (deps) => (message, key) => {
1111
+ // try-catch instead of Result for performance and stacktraces
1112
+ try {
1113
+ const buffer = createBuffer(message.change);
1114
+ const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
1115
+ const ciphertextLength = decodeLength(buffer);
1116
+ const ciphertext = buffer.shiftN(ciphertextLength);
1117
+ const plaintextBytes = deps.symmetricCrypto.decrypt(ciphertext, key, nonce);
1118
+ if (!plaintextBytes.ok)
1119
+ return plaintextBytes;
1120
+ buffer.reset();
1121
+ buffer.extend(plaintextBytes.value);
1122
+ // Decode version (for future compatibility, no validation needed for now)
1123
+ decodeNonNegativeInt(buffer);
1124
+ // Decode and verify the embedded timestamp
1125
+ const embeddedTimestampBytes = buffer.shiftN(timestampBytesLength);
1126
+ const embeddedTimestamp = timestampBytesToTimestamp(embeddedTimestampBytes);
1127
+ // Verify timestamp integrity
1128
+ if (!eqTimestamp(embeddedTimestamp, message.timestamp)) {
1129
+ return err({
1130
+ type: "ProtocolTimestampMismatchError",
1131
+ expected: message.timestamp,
1132
+ embedded: embeddedTimestamp,
1133
+ });
1134
+ }
1135
+ const table = decodeString(buffer);
1136
+ const id = decodeId(buffer);
1137
+ const length = decodeLength(buffer);
1138
+ const values = Object.create(null);
1139
+ for (let i = 0; i < length; i++) {
1140
+ const column = decodeString(buffer);
1141
+ const value = decodeSqliteValue(buffer);
1142
+ values[column] = value;
1143
+ }
1144
+ const dbChange = { table, id, values };
1145
+ return ok(dbChange);
980
1146
  }
981
- const dbChange = { table, id, values };
982
- return ok(dbChange);
983
- });
1147
+ catch (error) {
1148
+ return err({
1149
+ type: "ProtocolInvalidDataError",
1150
+ data: message.change,
1151
+ error,
1152
+ });
1153
+ }
1154
+ };
984
1155
  /**
985
1156
  * Encodes a non-negative integer into a variable-length integer format. It's
986
1157
  * more efficient than encoding via {@link encodeNumber}.
@@ -1047,14 +1218,6 @@ export const decodeNodeId = (buffer) => {
1047
1218
  const bytes = buffer.shiftN(8);
1048
1219
  return bytesToHex(bytes);
1049
1220
  };
1050
- export const encodeBase64Url256 = (buffer, string) => {
1051
- encodeLength(buffer, string);
1052
- buffer.extend(base64Url256ToBytes(string));
1053
- };
1054
- export const decodeBase64Url256WithLength = (buffer) => {
1055
- const length = decodeLength(buffer);
1056
- return decodeBase64Url256(buffer, length);
1057
- };
1058
1221
  // Small ints are encoded into ProtocolValueType, saving one byte per int.
1059
1222
  const isSmallInt = (value) => value >= 0 && value < 20;
1060
1223
  export const ProtocolValueType = {
@@ -1063,18 +1226,20 @@ export const ProtocolValueType = {
1063
1226
  String: 20,
1064
1227
  Number: 21,
1065
1228
  Null: 22,
1066
- Binary: 23,
1229
+ Bytes: 23,
1067
1230
  // We can add more types for other DBs or anything else later.
1068
1231
  // Optimized types
1069
- Id: 30,
1070
- Base64Url256: 31,
1071
- NonNegativeInt: 32,
1072
- Json: 33,
1232
+ NonNegativeInt: 30,
1233
+ // String optimizations
1234
+ EmptyString: 31, // 1 byte vs 2 bytes (50% reduction)
1235
+ Base64Url: 32,
1236
+ Id: 33,
1237
+ Json: 34,
1073
1238
  // new Date().toISOString() - 24 bytes
1074
1239
  // encoded with fixed length - 8 bytes
1075
1240
  // encode as NonNegativeInt - 6 bytes (additional 25% reduction)
1076
- DateIsoWithNonNegativeTime: 34,
1077
- DateIsoWithNegativeTime: 35, // 9 bytes
1241
+ DateIsoWithNonNegativeTime: 35,
1242
+ DateIsoWithNegativeTime: 36, // 9 bytes
1078
1243
  // TODO: Operations (from 40)
1079
1244
  // Increment, Decrement, Patch, whatever.
1080
1245
  };
@@ -1085,9 +1250,13 @@ export const encodeSqliteValue = (buffer, value) => {
1085
1250
  }
1086
1251
  switch (typeof value) {
1087
1252
  case "string": {
1088
- const dateIsoString = DateIsoString.from(value);
1089
- if (dateIsoString.ok) {
1090
- const time = new Date(dateIsoString.value).getTime();
1253
+ if (value === "") {
1254
+ encodeNonNegativeInt(buffer, ProtocolValueType.EmptyString);
1255
+ return;
1256
+ }
1257
+ const dateIso = DateIso.fromParent(value);
1258
+ if (dateIso.ok) {
1259
+ const time = new Date(dateIso.value).getTime();
1091
1260
  if (NonNegativeInt.is(time)) {
1092
1261
  encodeNonNegativeInt(buffer, ProtocolValueType.DateIsoWithNonNegativeTime);
1093
1262
  encodeNonNegativeInt(buffer, time);
@@ -1098,25 +1267,31 @@ export const encodeSqliteValue = (buffer, value) => {
1098
1267
  }
1099
1268
  return;
1100
1269
  }
1101
- const base64Url256 = Base64Url256.from(value);
1102
- if (base64Url256.ok) {
1103
- if (base64Url256.value.length === idTypeValueLength) {
1104
- encodeNonNegativeInt(buffer, ProtocolValueType.Id);
1105
- buffer.extend(base64Url256ToBytes(base64Url256.value));
1106
- return;
1107
- }
1108
- encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url256);
1109
- encodeBase64Url256(buffer, base64Url256.value);
1270
+ const id = Id.fromParent(value);
1271
+ if (id.ok) {
1272
+ encodeNonNegativeInt(buffer, ProtocolValueType.Id);
1273
+ buffer.extend(idToIdBytes(id.value));
1110
1274
  return;
1111
1275
  }
1112
- const jsonValue = JsonValueFromString.fromParent(value);
1113
- if (jsonValue.ok) {
1114
- const jsonBytes = pack(jsonValue.value);
1276
+ const json = Json.fromParent(value);
1277
+ // Only encode as Json if it survives JSON.parse/JSON.stringify round-trip.
1278
+ // Some valid JSON strings like "-0E0" get normalized to "0" during parsing,
1279
+ // which would cause data corruption if we don't verify round-trip safety.
1280
+ if (json.ok && JSON.stringify(jsonToJsonValue(json.value)) === value) {
1281
+ const jsonBytes = packr.pack(jsonToJsonValue(json.value));
1115
1282
  encodeNonNegativeInt(buffer, ProtocolValueType.Json);
1116
1283
  encodeLength(buffer, jsonBytes);
1117
1284
  buffer.extend(jsonBytes);
1118
1285
  return;
1119
1286
  }
1287
+ const base64Url = Base64Url.fromParent(value);
1288
+ if (base64Url.ok) {
1289
+ encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url);
1290
+ const bytes = base64UrlToUint8Array(base64Url.value);
1291
+ encodeLength(buffer, bytes);
1292
+ buffer.extend(bytes);
1293
+ return;
1294
+ }
1120
1295
  encodeNonNegativeInt(buffer, ProtocolValueType.String);
1121
1296
  encodeString(buffer, value);
1122
1297
  return;
@@ -1136,7 +1311,7 @@ export const encodeSqliteValue = (buffer, value) => {
1136
1311
  return;
1137
1312
  }
1138
1313
  }
1139
- encodeNonNegativeInt(buffer, ProtocolValueType.Binary);
1314
+ encodeNonNegativeInt(buffer, ProtocolValueType.Bytes);
1140
1315
  encodeLength(buffer, value);
1141
1316
  buffer.extend(value);
1142
1317
  };
@@ -1152,33 +1327,57 @@ export const decodeSqliteValue = (buffer) => {
1152
1327
  return decodeNumber(buffer);
1153
1328
  case ProtocolValueType.Null:
1154
1329
  return null;
1155
- case ProtocolValueType.Binary: {
1330
+ case ProtocolValueType.Bytes: {
1156
1331
  const length = decodeLength(buffer);
1157
1332
  return buffer.shiftN(length);
1158
1333
  }
1159
- case ProtocolValueType.Id: {
1334
+ case ProtocolValueType.Id:
1160
1335
  return decodeId(buffer);
1161
- }
1162
- case ProtocolValueType.Base64Url256:
1163
- return decodeBase64Url256WithLength(buffer);
1164
1336
  case ProtocolValueType.NonNegativeInt:
1165
1337
  return decodeNonNegativeInt(buffer);
1166
1338
  case ProtocolValueType.Json: {
1167
1339
  const length = decodeLength(buffer);
1168
1340
  const bytes = buffer.shiftN(length);
1169
- return JSON.stringify(unpack(bytes));
1341
+ return JSON.stringify(packr.unpack(bytes));
1170
1342
  }
1171
1343
  case ProtocolValueType.DateIsoWithNonNegativeTime:
1172
1344
  case ProtocolValueType.DateIsoWithNegativeTime: {
1173
1345
  const time = type === ProtocolValueType.DateIsoWithNonNegativeTime
1174
1346
  ? decodeNonNegativeInt(buffer)
1175
1347
  : decodeNumber(buffer);
1176
- const dateIsoString = DateIsoString.fromParent(new Date(time).toISOString());
1177
- if (!dateIsoString.ok)
1178
- throw new ProtocolDecodeError(dateIsoString.error.type);
1179
- return dateIsoString.value;
1348
+ const dateIso = DateIso.fromParent(new Date(time).toISOString());
1349
+ if (!dateIso.ok)
1350
+ throw new ProtocolDecodeError(dateIso.error.type);
1351
+ return dateIso.value;
1352
+ }
1353
+ case ProtocolValueType.EmptyString:
1354
+ return "";
1355
+ case ProtocolValueType.Base64Url: {
1356
+ const length = decodeLength(buffer);
1357
+ const bytes = buffer.shiftN(length);
1358
+ return uint8ArrayToBase64Url(bytes);
1180
1359
  }
1181
1360
  default:
1182
1361
  throw new ProtocolDecodeError("invalid ProtocolValueType");
1183
1362
  }
1184
1363
  };
1364
+ /**
1365
+ * Decodes a ProtocolMessage into a readable JSON object for debugging.
1366
+ *
1367
+ * Note: This is a stub for future implementation. It should use:
1368
+ *
1369
+ * - DecodeVersionAndOwner
1370
+ * - DecodeError or decodeWriteKeys (depending on context)
1371
+ * - DecodeMessages
1372
+ * - DecodeRanges
1373
+ *
1374
+ * If you want to help, please contribute to this function.
1375
+ */
1376
+ export const decodeProtocolMessageToJson = (_protocolMessage, _isInitiator) => {
1377
+ // TODO: Implement using
1378
+ // - decodeVersionAndOwner
1379
+ // -- decodeError or decodeWriteKeys (should be refactored out),
1380
+ // -- decodeMessages, and decodeRanges.
1381
+ // This is a stub for PRs and community contributions.
1382
+ throw new Error("decodeProtocolMessageToJson is not implemented yet.");
1383
+ };