@evolu/common 6.0.1-preview.9 → 7.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.
- package/README.md +2 -2
- package/dist/src/Array.d.ts +256 -12
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +215 -9
- package/dist/src/Assert.d.ts +0 -13
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +0 -15
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +8 -7
- package/dist/src/Cache.d.ts +44 -0
- package/dist/src/Cache.d.ts.map +1 -0
- package/dist/src/Cache.js +52 -0
- package/dist/src/Callbacks.d.ts +45 -12
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +14 -7
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +56 -42
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +40 -53
- package/dist/src/Evolu/Db.d.ts +162 -74
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +284 -702
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +213 -133
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +189 -180
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +150 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +174 -0
- package/dist/src/Evolu/Owner.d.ts +273 -120
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +130 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +268 -240
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +595 -447
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
- package/dist/src/Evolu/PublicKysely.js +3 -4
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Query.js +1 -1
- package/dist/src/Evolu/Relay.d.ts +91 -8
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +214 -88
- package/dist/src/Evolu/Schema.d.ts +125 -47
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +175 -31
- package/dist/src/Evolu/Storage.d.ts +249 -27
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +198 -92
- package/dist/src/Evolu/Sync.d.ts +68 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +469 -20
- package/dist/src/Evolu/Timestamp.d.ts +83 -30
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +79 -34
- package/dist/src/Identicon.d.ts +35 -0
- package/dist/src/Identicon.d.ts.map +1 -0
- package/dist/src/Identicon.js +143 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Object.d.ts +10 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +9 -3
- package/dist/src/Platform.d.ts +20 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +22 -0
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Relation.d.ts +79 -0
- package/dist/src/Relation.d.ts.map +1 -0
- package/dist/src/Relation.js +127 -0
- package/dist/src/Resources.d.ts +118 -0
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/Resources.js +197 -0
- package/dist/src/Result.d.ts +184 -52
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Sqlite.d.ts +89 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +136 -9
- package/dist/src/Task.d.ts +586 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +469 -0
- package/dist/src/Time.d.ts +66 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +99 -5
- package/dist/src/Type.d.ts +676 -343
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +718 -467
- package/dist/src/Types.d.ts +1 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -18
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -3
- package/package.json +14 -13
- package/src/Array.ts +301 -19
- package/src/Assert.ts +0 -21
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +85 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -90
- package/src/Evolu/Db.ts +519 -1026
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +476 -360
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +463 -0
- package/src/Evolu/Owner.ts +355 -228
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +846 -742
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +4 -5
- package/src/Evolu/Query.ts +3 -2
- package/src/Evolu/Relay.ts +406 -103
- package/src/Evolu/Schema.ts +323 -91
- package/src/Evolu/Storage.ts +559 -137
- package/src/Evolu/Sync.ts +819 -36
- package/src/Evolu/Timestamp.ts +90 -58
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/Number.ts +6 -10
- package/src/Object.ts +13 -5
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Relation.ts +234 -0
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +152 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1135 -730
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +8 -3
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/ManyToManyMap.d.ts +0 -26
- package/dist/src/ManyToManyMap.d.ts.map +0 -1
- package/dist/src/ManyToManyMap.js +0 -92
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/ManyToManyMap.ts +0 -140
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Protocol.ts
CHANGED
|
@@ -3,29 +3,39 @@
|
|
|
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
|
|
7
|
-
*
|
|
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
|
|
20
|
+
* transfer, ownership, real-time broadcasting, request-response semantics, and
|
|
21
|
+
* error handling.
|
|
15
22
|
*
|
|
16
|
-
* ### Message
|
|
23
|
+
* ### Message structure
|
|
17
24
|
*
|
|
18
25
|
* | Field | Notes |
|
|
19
26
|
* | :----------------------------- | :------------------------ |
|
|
20
27
|
* | **Header** | |
|
|
21
28
|
* | - {@link protocolVersion} | |
|
|
22
29
|
* | - {@link OwnerId} | {@link Owner} |
|
|
23
|
-
* |
|
|
24
|
-
* |
|
|
25
|
-
* | -
|
|
26
|
-
* | - {@link
|
|
27
|
-
* |
|
|
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)** | |
|
|
28
36
|
* | - {@link ProtocolErrorCode} | |
|
|
37
|
+
* | **Broadcast (messageType=2)** | |
|
|
38
|
+
* | - (no additional fields) | |
|
|
29
39
|
* | **Messages** | |
|
|
30
40
|
* | - {@link NonNegativeInt} | A number of messages. |
|
|
31
41
|
* | - {@link EncryptedCrdtMessage} | |
|
|
@@ -33,21 +43,14 @@
|
|
|
33
43
|
* | - {@link NonNegativeInt} | Number of ranges. |
|
|
34
44
|
* | - {@link Range} | |
|
|
35
45
|
*
|
|
36
|
-
* ### WriteKey
|
|
37
|
-
*
|
|
38
|
-
* The initiator sends WriteKeyMode and optionally one or two WriteKeys. One key
|
|
39
|
-
* for write operations and two for key rotation (current and new). Note that
|
|
40
|
-
* it's ok to not send any key if initiator is going to be synced with readonly
|
|
41
|
-
* owner. The non-initiator validates them immediately after parsing the
|
|
42
|
-
* initiator header, before processing any messages or ranges.
|
|
43
|
-
*
|
|
44
|
-
* ### WriteKey Rotation
|
|
46
|
+
* ### WriteKey validation
|
|
45
47
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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.
|
|
51
54
|
*
|
|
52
55
|
* ### Synchronization
|
|
53
56
|
*
|
|
@@ -61,38 +64,48 @@
|
|
|
61
64
|
* if further sync is needed or possible, continuing until both sides are
|
|
62
65
|
* synchronized.
|
|
63
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
|
+
*
|
|
64
71
|
* Both **Messages** and **Ranges** are optional, allowing each side to send,
|
|
65
72
|
* sync, or only subscribe data as needed.
|
|
66
73
|
*
|
|
67
|
-
* When the initiator sends data, the {@link
|
|
68
|
-
*
|
|
69
|
-
* responds without a {@link
|
|
70
|
-
* signals it wants data. If the non-initiator detects an issue, it
|
|
71
|
-
* error code via the `Error` field in the header back to the
|
|
72
|
-
* relay-to-relay or P2P sync, both sides may require the
|
|
73
|
-
* 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.
|
|
74
81
|
*
|
|
75
|
-
* ### Protocol
|
|
82
|
+
* ### Protocol errors
|
|
76
83
|
*
|
|
77
84
|
* The protocol uses error codes in the header to signal issues:
|
|
78
85
|
*
|
|
79
86
|
* - {@link ProtocolWriteKeyError}: The provided WriteKey is invalid or missing.
|
|
80
|
-
* - {@link ProtocolWriteError}: A write
|
|
81
|
-
*
|
|
82
|
-
* - {@link ProtocolSyncError}: A
|
|
87
|
+
* - {@link ProtocolWriteError}: A serious relay-side write failure occurred.
|
|
88
|
+
* - {@link ProtocolQuotaError}: Storage or billing quota exceeded.
|
|
89
|
+
* - {@link ProtocolSyncError}: A serious relay-side synchronization failure
|
|
83
90
|
* occurred.
|
|
84
|
-
* - {@link
|
|
91
|
+
* - {@link ProtocolVersionError}: Protocol version mismatch.
|
|
85
92
|
* - {@link ProtocolInvalidDataError}: The message is malformed or corrupted.
|
|
86
93
|
*
|
|
87
|
-
* All protocol errors except `ProtocolInvalidDataError` include the `
|
|
94
|
+
* All protocol errors except `ProtocolInvalidDataError` include the `OwnerId`
|
|
88
95
|
* to allow clients to associate errors with the correct owner.
|
|
89
96
|
*
|
|
90
|
-
* ### Message
|
|
97
|
+
* ### Message size limit
|
|
91
98
|
*
|
|
92
99
|
* The protocol enforces a strict maximum size for all messages, defined by
|
|
93
|
-
* {@link
|
|
94
|
-
* less than or equal to this limit,
|
|
95
|
-
*
|
|
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}.
|
|
96
109
|
*
|
|
97
110
|
* ### Why Binary?
|
|
98
111
|
*
|
|
@@ -100,13 +113,12 @@
|
|
|
100
113
|
*
|
|
101
114
|
* - Encrypted data doesn’t compress well, unlike plain JSON.
|
|
102
115
|
* - Message size must be controlled during creation.
|
|
103
|
-
* - Sequential byte reading is faster than parsing and
|
|
116
|
+
* - Sequential byte reading is faster than parsing and avoids conversions.
|
|
104
117
|
*
|
|
105
118
|
* It uses structure-aware encoding, significantly outperforming generic binary
|
|
106
119
|
* serialization formats with the following optimizations:
|
|
107
120
|
*
|
|
108
121
|
* - **NonNegativeInt:** Up to 33% smaller than MessagePack.
|
|
109
|
-
* - **Base64Url Strings:** Up to 25% size reduction.
|
|
110
122
|
* - **DateIso:** Up to 75% smaller.
|
|
111
123
|
* - **Timestamp Encoding:** Delta encoding for milliseconds and run-length
|
|
112
124
|
* encoding (RLE) for counters and NodeIds.
|
|
@@ -136,13 +148,39 @@
|
|
|
136
148
|
* Version negotiation is per-owner, allowing Evolu Protocol to evolve safely
|
|
137
149
|
* over time and provide clear feedback about version mismatches.
|
|
138
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
|
+
*
|
|
139
160
|
* @module
|
|
140
161
|
*/
|
|
141
162
|
|
|
142
|
-
|
|
143
|
-
|
|
163
|
+
/**
|
|
164
|
+
* TODO:
|
|
165
|
+
*
|
|
166
|
+
* - The client-relay naming convention in functions like
|
|
167
|
+
* `applyProtocolMessageAsClient` and `applyProtocolMessageAsRelay` is not
|
|
168
|
+
* ideal. In the future, clients will be able to sync directly with each other
|
|
169
|
+
* (P2P), making the current naming misleading. Consider using
|
|
170
|
+
* initiator/non-initiator terminology instead, and consolidate into a single
|
|
171
|
+
* `applyProtocolMessage` function with conditional arguments to reduce code
|
|
172
|
+
* duplication.
|
|
173
|
+
* - Replace try-catch with Result + new Error (to preserve stacktraces). Measure
|
|
174
|
+
* Result overhead, it should be super small.
|
|
175
|
+
* - Allow clients to broadcast messages that are not persisted by relays. This
|
|
176
|
+
* would enable real-time ephemeral data (like cursor positions, typing
|
|
177
|
+
* indicators) to be forwarded by relays without storage overhead.
|
|
178
|
+
*/
|
|
179
|
+
|
|
180
|
+
import { Packr } from "msgpackr";
|
|
144
181
|
import { isNonEmptyReadonlyArray, NonEmptyReadonlyArray } from "../Array.js";
|
|
145
182
|
import { assert } from "../Assert.js";
|
|
183
|
+
import { Brand } from "../Brand.js";
|
|
146
184
|
import {
|
|
147
185
|
Buffer,
|
|
148
186
|
bytesToHex,
|
|
@@ -152,9 +190,9 @@ import {
|
|
|
152
190
|
utf8ToBytes,
|
|
153
191
|
} from "../Buffer.js";
|
|
154
192
|
import {
|
|
155
|
-
|
|
193
|
+
createPadmePadding,
|
|
156
194
|
EncryptionKey,
|
|
157
|
-
|
|
195
|
+
RandomBytesDep,
|
|
158
196
|
SymmetricCryptoDecryptError,
|
|
159
197
|
SymmetricCryptoDep,
|
|
160
198
|
} from "../Crypto.js";
|
|
@@ -165,253 +203,188 @@ import { err, ok, Result } from "../Result.js";
|
|
|
165
203
|
import { SqliteValue } from "../Sqlite.js";
|
|
166
204
|
import {
|
|
167
205
|
Base64Url,
|
|
168
|
-
|
|
206
|
+
base64UrlToUint8Array,
|
|
207
|
+
between,
|
|
208
|
+
DateIso,
|
|
169
209
|
Id,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
210
|
+
IdBytes,
|
|
211
|
+
idBytesToId,
|
|
212
|
+
idBytesTypeValueLength,
|
|
213
|
+
idToIdBytes,
|
|
214
|
+
Int,
|
|
215
|
+
Json,
|
|
216
|
+
jsonToJsonValue,
|
|
174
217
|
NonNegativeInt,
|
|
175
218
|
Number,
|
|
176
|
-
object,
|
|
177
219
|
PositiveInt,
|
|
178
|
-
|
|
220
|
+
uint8ArrayToBase64Url,
|
|
179
221
|
} from "../Type.js";
|
|
180
|
-
import {
|
|
222
|
+
import { Predicate } from "../Types.js";
|
|
181
223
|
import {
|
|
224
|
+
BaseOwnerError,
|
|
182
225
|
Owner,
|
|
183
226
|
OwnerId,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
227
|
+
OwnerIdBytes,
|
|
228
|
+
ownerIdToOwnerIdBytes,
|
|
229
|
+
OwnerWriteKey,
|
|
230
|
+
ownerWriteKeyLength,
|
|
187
231
|
} from "./Owner.js";
|
|
188
232
|
import {
|
|
189
|
-
|
|
190
|
-
|
|
233
|
+
BaseRange,
|
|
234
|
+
CrdtMessage,
|
|
235
|
+
DbChange,
|
|
236
|
+
EncryptedCrdtMessage,
|
|
237
|
+
EncryptedDbChange,
|
|
238
|
+
Fingerprint,
|
|
239
|
+
FingerprintRange,
|
|
240
|
+
fingerprintSize,
|
|
241
|
+
InfiniteUpperBound,
|
|
242
|
+
Range,
|
|
243
|
+
RangeType,
|
|
244
|
+
RangeUpperBound,
|
|
245
|
+
SkipRange,
|
|
246
|
+
StorageDep,
|
|
247
|
+
TimestampsRange,
|
|
248
|
+
} from "./Storage.js";
|
|
249
|
+
import {
|
|
191
250
|
Counter,
|
|
251
|
+
eqTimestamp,
|
|
192
252
|
Millis,
|
|
193
253
|
NodeId,
|
|
194
254
|
Timestamp,
|
|
195
|
-
|
|
255
|
+
TimestampBytes,
|
|
256
|
+
timestampBytesLength,
|
|
257
|
+
timestampBytesToTimestamp,
|
|
258
|
+
timestampToTimestampBytes,
|
|
196
259
|
} from "./Timestamp.js";
|
|
197
260
|
|
|
198
|
-
/** Maximum size of the entire protocol message in bytes. */
|
|
199
|
-
export const maxProtocolMessageSize = 1_000_000 as PositiveInt;
|
|
200
|
-
|
|
201
|
-
/** Maximum size of the ranges in bytes. */
|
|
202
|
-
export const maxProtocolMessageRangesSize = 30_000 as PositiveInt;
|
|
203
|
-
|
|
204
|
-
/** Evolu Protocol Message. */
|
|
205
|
-
export type ProtocolMessage = Uint8Array & Brand<"ProtocolMessage">;
|
|
206
|
-
|
|
207
|
-
/** Evolu Protocol version. */
|
|
208
|
-
export const protocolVersion = 0 as NonNegativeInt;
|
|
209
|
-
|
|
210
|
-
export const ProtocolErrorCode = {
|
|
211
|
-
NoError: 0,
|
|
212
|
-
/** A code for {@link ProtocolWriteKeyError}. */
|
|
213
|
-
WriteKeyError: 1,
|
|
214
|
-
/** A code for {@link ProtocolWriteError}. */
|
|
215
|
-
WriteError: 2,
|
|
216
|
-
/** A code for {@link ProtocolSyncError}. */
|
|
217
|
-
SyncError: 3,
|
|
218
|
-
} as const;
|
|
219
|
-
|
|
220
|
-
type ProtocolErrorCode =
|
|
221
|
-
(typeof ProtocolErrorCode)[keyof typeof ProtocolErrorCode];
|
|
222
|
-
|
|
223
|
-
export const WriteKeyMode = {
|
|
224
|
-
None: 0,
|
|
225
|
-
Single: 1,
|
|
226
|
-
Rotation: 2,
|
|
227
|
-
} as const;
|
|
228
|
-
|
|
229
|
-
type WriteKeyMode = (typeof WriteKeyMode)[keyof typeof WriteKeyMode];
|
|
230
|
-
|
|
231
261
|
/**
|
|
232
|
-
* Evolu
|
|
262
|
+
* Evolu uses MessagePack for numbers and JSONs.
|
|
233
263
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* handle their own errors; return values only indicates overall success or
|
|
237
|
-
* failure.
|
|
264
|
+
* - `variableMapSize: true` - More compact maps, ~5-10% slower encoding
|
|
265
|
+
* - `useRecords: false` - Standard MessagePack without extensions
|
|
238
266
|
*/
|
|
239
|
-
|
|
240
|
-
readonly getSize: (ownerId: BinaryOwnerId) => NonNegativeInt | null;
|
|
241
|
-
|
|
242
|
-
readonly fingerprint: (
|
|
243
|
-
ownerId: BinaryOwnerId,
|
|
244
|
-
begin: NonNegativeInt,
|
|
245
|
-
end: NonNegativeInt,
|
|
246
|
-
) => Fingerprint | null;
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Computes fingerprints with their upper bounds in one call.
|
|
250
|
-
*
|
|
251
|
-
* This function can be replaced with many fingerprint/findLowerBound calls,
|
|
252
|
-
* but implementations can leverage it for batching and more efficient
|
|
253
|
-
* fingerprint computation.
|
|
254
|
-
*/
|
|
255
|
-
readonly fingerprintRanges: (
|
|
256
|
-
ownerId: BinaryOwnerId,
|
|
257
|
-
buckets: ReadonlyArray<NonNegativeInt>,
|
|
258
|
-
upperBound?: RangeUpperBound,
|
|
259
|
-
) => ReadonlyArray<FingerprintRange> | null;
|
|
260
|
-
|
|
261
|
-
readonly findLowerBound: (
|
|
262
|
-
ownerId: BinaryOwnerId,
|
|
263
|
-
begin: NonNegativeInt,
|
|
264
|
-
end: NonNegativeInt,
|
|
265
|
-
upperBound: RangeUpperBound,
|
|
266
|
-
) => NonNegativeInt | null;
|
|
267
|
+
const packr = new Packr({ variableMapSize: true, useRecords: false });
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
begin: NonNegativeInt,
|
|
271
|
-
end: NonNegativeInt,
|
|
272
|
-
callback: (timestamp: BinaryTimestamp, index: NonNegativeInt) => boolean,
|
|
273
|
-
) => void;
|
|
274
|
-
|
|
275
|
-
/** Validates the {@link WriteKey} for the given {@link Owner}. */
|
|
276
|
-
readonly validateWriteKey: (
|
|
277
|
-
ownerId: BinaryOwnerId,
|
|
278
|
-
writeKey: WriteKey,
|
|
279
|
-
) => boolean;
|
|
280
|
-
|
|
281
|
-
/** Sets the {@link WriteKey} for the given {@link Owner}. */
|
|
282
|
-
readonly setWriteKey: (ownerId: BinaryOwnerId, writeKey: WriteKey) => boolean;
|
|
283
|
-
|
|
284
|
-
/** Write encrypted {@link CrdtMessage}s to storage. */
|
|
285
|
-
readonly writeMessages: (
|
|
286
|
-
ownerId: BinaryOwnerId,
|
|
287
|
-
messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
|
|
288
|
-
) => boolean;
|
|
289
|
-
|
|
290
|
-
/** Read encrypted {@link DbChange}s from storage. */
|
|
291
|
-
readonly readDbChange: (
|
|
292
|
-
ownerId: BinaryOwnerId,
|
|
293
|
-
timestamp: BinaryTimestamp,
|
|
294
|
-
) => EncryptedDbChange | null;
|
|
295
|
-
|
|
296
|
-
/** Delete all data for the given {@link Owner}. */
|
|
297
|
-
readonly deleteOwner: (ownerId: BinaryOwnerId) => boolean;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export interface StorageDep {
|
|
301
|
-
readonly storage: Storage;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/** An encrypted {@link CrdtMessage}. */
|
|
305
|
-
export interface EncryptedCrdtMessage {
|
|
306
|
-
readonly timestamp: Timestamp;
|
|
307
|
-
readonly change: EncryptedDbChange;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** Encrypted DbChange */
|
|
311
|
-
export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
|
|
269
|
+
const minProtocolMessageMaxSize = 1_000_000;
|
|
270
|
+
const maxProtocolMessageMaxSize = 100_000_000;
|
|
312
271
|
|
|
313
272
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
273
|
+
* Protocol message maximum size.
|
|
274
|
+
*
|
|
275
|
+
* Defines the upper limit for how large a single protocol message can be.
|
|
276
|
+
* Implementations must enforce a maximum size between 1MB and 100MB to ensure
|
|
277
|
+
* compatibility across all Evolu implementations (the maximum size of mutation
|
|
278
|
+
* change is hardcoded and enforced hence the maximum size can't be smaller).
|
|
279
|
+
*
|
|
280
|
+
* Larger maximum sizes can be configured by relays to reduce roundtrips. For
|
|
281
|
+
* example, a dedicated relay with ample resources could configure a 100MB
|
|
282
|
+
* maximum to minimize roundtrips for large syncs.
|
|
283
|
+
*
|
|
284
|
+
* Only relays can safely configure larger sizes, as clients will handle them.
|
|
285
|
+
* Increasing this value on the client side would break compatibility with
|
|
286
|
+
* relays that enforce smaller limits.
|
|
316
287
|
*/
|
|
317
|
-
export
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
288
|
+
export const ProtocolMessageMaxSize = between(
|
|
289
|
+
minProtocolMessageMaxSize,
|
|
290
|
+
maxProtocolMessageMaxSize,
|
|
291
|
+
)(Int);
|
|
321
292
|
|
|
322
|
-
|
|
323
|
-
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
324
|
-
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
325
|
-
*/
|
|
326
|
-
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
327
|
-
export type Base64Url256 = typeof Base64Url256.Type;
|
|
293
|
+
export type ProtocolMessageMaxSize = typeof ProtocolMessageMaxSize.Type;
|
|
328
294
|
|
|
329
295
|
/**
|
|
330
|
-
*
|
|
331
|
-
*
|
|
296
|
+
* Default {@link ProtocolMessageMaxSize} (1MB).
|
|
297
|
+
*
|
|
298
|
+
* The standard size used across Evolu implementations. Relays with more
|
|
299
|
+
* resources can configure larger sizes to reduce roundtrips.
|
|
332
300
|
*/
|
|
333
|
-
export const
|
|
334
|
-
|
|
335
|
-
id: Id,
|
|
336
|
-
values: record(Base64Url256, SqliteValue),
|
|
337
|
-
});
|
|
338
|
-
export type DbChange = typeof DbChange.Type;
|
|
339
|
-
|
|
340
|
-
export const RangeType = {
|
|
341
|
-
Fingerprint: 1,
|
|
342
|
-
Skip: 0,
|
|
343
|
-
Timestamps: 2,
|
|
344
|
-
} as const;
|
|
345
|
-
|
|
346
|
-
export type RangeType = (typeof RangeType)[keyof typeof RangeType];
|
|
347
|
-
|
|
348
|
-
export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
|
|
349
|
-
export type InfiniteUpperBound = typeof InfiniteUpperBound;
|
|
301
|
+
export const defaultProtocolMessageMaxSize =
|
|
302
|
+
minProtocolMessageMaxSize as ProtocolMessageMaxSize;
|
|
350
303
|
|
|
351
304
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
305
|
+
* Protocol message ranges maximum size.
|
|
306
|
+
*
|
|
307
|
+
* Defines the upper limit for how large the ranges section of a protocol
|
|
308
|
+
* message can be. Implementations must enforce a maximum size between 3KB and
|
|
309
|
+
* 100KB to ensure compatibility.
|
|
310
|
+
*
|
|
311
|
+
* The upper bound is set to ensure ranges fit within the default 1MB
|
|
312
|
+
* {@link defaultProtocolMessageMaxSize}, maintaining compatibility between all
|
|
313
|
+
* clients and relays.
|
|
354
314
|
*/
|
|
355
|
-
export
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
readonly upperBound: RangeUpperBound;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
export interface SkipRange extends BaseRange {
|
|
362
|
-
readonly type: typeof RangeType.Skip;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export interface FingerprintRange extends BaseRange {
|
|
366
|
-
readonly type: typeof RangeType.Fingerprint;
|
|
367
|
-
readonly fingerprint: Fingerprint;
|
|
368
|
-
}
|
|
315
|
+
export const ProtocolMessageRangesMaxSize = between(3_000, 100_000)(Int);
|
|
316
|
+
export type ProtocolMessageRangesMaxSize =
|
|
317
|
+
typeof ProtocolMessageRangesMaxSize.Type;
|
|
369
318
|
|
|
370
319
|
/**
|
|
371
|
-
*
|
|
372
|
-
* {@link BinaryTimestamp}s.
|
|
320
|
+
* Default {@link ProtocolMessageRangesMaxSize} (30KB).
|
|
373
321
|
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
322
|
+
* The standard size used across Evolu implementations. Relays with more
|
|
323
|
+
* resources can configure larger sizes to reduce roundtrips.
|
|
376
324
|
*/
|
|
377
|
-
export
|
|
325
|
+
export const defaultProtocolMessageRangesMaxSize =
|
|
326
|
+
30_000 as ProtocolMessageRangesMaxSize;
|
|
378
327
|
|
|
379
|
-
|
|
328
|
+
/** Evolu Protocol Message. */
|
|
329
|
+
export type ProtocolMessage = Uint8Array & Brand<"ProtocolMessage">;
|
|
380
330
|
|
|
381
|
-
/**
|
|
382
|
-
export const
|
|
331
|
+
/** Evolu Protocol version. */
|
|
332
|
+
export const protocolVersion = NonNegativeInt.orThrow(1);
|
|
333
|
+
|
|
334
|
+
export const MessageType = {
|
|
335
|
+
/** Request message from initiator (client) to non-initiator (relay). */
|
|
336
|
+
Request: 0,
|
|
337
|
+
/** Response message from non-initiator (relay) to initiator (client). */
|
|
338
|
+
Response: 1,
|
|
339
|
+
/** Broadcast message from non-initiator (relay) to subscribed clients. */
|
|
340
|
+
Broadcast: 2,
|
|
341
|
+
} as const;
|
|
383
342
|
|
|
384
|
-
export
|
|
385
|
-
readonly type: typeof RangeType.Timestamps;
|
|
386
|
-
readonly timestamps: ReadonlyArray<BinaryTimestamp>;
|
|
387
|
-
}
|
|
343
|
+
export type MessageType = (typeof MessageType)[keyof typeof MessageType];
|
|
388
344
|
|
|
389
|
-
export
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
345
|
+
export const SubscriptionFlags = {
|
|
346
|
+
/** No subscription changes for this owner. */
|
|
347
|
+
None: 0,
|
|
348
|
+
/** Subscribe to updates for this owner. */
|
|
349
|
+
Subscribe: 1,
|
|
350
|
+
/** Unsubscribe from updates for this owner. */
|
|
351
|
+
Unsubscribe: 2,
|
|
352
|
+
} as const;
|
|
353
|
+
|
|
354
|
+
export type SubscriptionFlag =
|
|
355
|
+
(typeof SubscriptionFlags)[keyof typeof SubscriptionFlags];
|
|
356
|
+
|
|
357
|
+
export const ProtocolErrorCode = {
|
|
358
|
+
NoError: 0,
|
|
359
|
+
/** A code for {@link ProtocolWriteKeyError}. */
|
|
360
|
+
WriteKeyError: 1,
|
|
361
|
+
/** A code for {@link ProtocolWriteError}. */
|
|
362
|
+
WriteError: 2,
|
|
363
|
+
/** A code for {@link ProtocolQuotaError}. */
|
|
364
|
+
QuotaError: 3,
|
|
365
|
+
/** A code for {@link ProtocolSyncError}. */
|
|
366
|
+
SyncError: 4,
|
|
367
|
+
} as const;
|
|
393
368
|
|
|
394
|
-
|
|
369
|
+
type ProtocolErrorCode =
|
|
370
|
+
(typeof ProtocolErrorCode)[keyof typeof ProtocolErrorCode];
|
|
395
371
|
|
|
396
372
|
export type ProtocolError =
|
|
397
|
-
|
|
|
373
|
+
| ProtocolVersionError
|
|
398
374
|
| ProtocolInvalidDataError
|
|
399
375
|
| ProtocolWriteKeyError
|
|
400
376
|
| ProtocolWriteError
|
|
401
|
-
| ProtocolSyncError
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
export interface ProtocolErrorBase {
|
|
405
|
-
readonly ownerId: OwnerId;
|
|
406
|
-
}
|
|
377
|
+
| ProtocolSyncError
|
|
378
|
+
| ProtocolQuotaError
|
|
379
|
+
| ProtocolTimestampMismatchError;
|
|
407
380
|
|
|
408
381
|
/**
|
|
409
382
|
* Represents a version mismatch in the Evolu Protocol. Occurs when the
|
|
410
383
|
* initiator and non-initiator are using incompatible protocol versions.
|
|
411
384
|
*/
|
|
412
|
-
export interface
|
|
413
|
-
readonly type: "
|
|
414
|
-
readonly
|
|
385
|
+
export interface ProtocolVersionError extends BaseOwnerError {
|
|
386
|
+
readonly type: "ProtocolVersionError";
|
|
387
|
+
readonly version: NonNegativeInt;
|
|
415
388
|
/** Indicates which side is obsolete and should update. */
|
|
416
389
|
readonly isInitiator: boolean;
|
|
417
390
|
}
|
|
@@ -423,44 +396,72 @@ export interface ProtocolInvalidDataError {
|
|
|
423
396
|
readonly error: unknown;
|
|
424
397
|
}
|
|
425
398
|
|
|
426
|
-
/** Error when a {@link
|
|
427
|
-
export interface ProtocolWriteKeyError extends
|
|
399
|
+
/** Error when a {@link OwnerWriteKey} is invalid, missing, or fails validation. */
|
|
400
|
+
export interface ProtocolWriteKeyError extends BaseOwnerError {
|
|
428
401
|
readonly type: "ProtocolWriteKeyError";
|
|
429
402
|
}
|
|
430
403
|
|
|
431
404
|
/**
|
|
432
|
-
* Error
|
|
433
|
-
*
|
|
405
|
+
* Error indicating a serious relay-side write failure. Clients should log this
|
|
406
|
+
* error and show a generic sync error to the user.
|
|
434
407
|
*/
|
|
435
|
-
export interface ProtocolWriteError extends
|
|
408
|
+
export interface ProtocolWriteError extends BaseOwnerError {
|
|
436
409
|
readonly type: "ProtocolWriteError";
|
|
437
410
|
}
|
|
438
411
|
|
|
439
412
|
/**
|
|
440
|
-
* Error
|
|
441
|
-
*
|
|
413
|
+
* Error when storage or billing quota is exceeded.
|
|
414
|
+
*
|
|
415
|
+
* When relay rejects writes due to quota, the affected device stops syncing
|
|
416
|
+
* because RBSR requires both sides to converge—if the relay won't accept the
|
|
417
|
+
* client's data, they can never reach the same state. Only the device with
|
|
418
|
+
* excess local data is affected. Other devices that haven't exceeded quota can
|
|
419
|
+
* still sync normally.
|
|
420
|
+
*
|
|
421
|
+
* Clients should prompt the user to contact the relay provider or upgrade their
|
|
422
|
+
* plan. Quota monitoring and management is the relay provider's
|
|
423
|
+
* responsibility.
|
|
442
424
|
*/
|
|
443
|
-
export interface
|
|
425
|
+
export interface ProtocolQuotaError extends BaseOwnerError {
|
|
426
|
+
readonly type: "ProtocolQuotaError";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Error indicating a serious relay-side synchronization failure. Clients should
|
|
431
|
+
* log this error and show a generic sync error to the user.
|
|
432
|
+
*/
|
|
433
|
+
export interface ProtocolSyncError extends BaseOwnerError {
|
|
444
434
|
readonly type: "ProtocolSyncError";
|
|
445
435
|
}
|
|
446
436
|
|
|
437
|
+
/**
|
|
438
|
+
* Error when embedded timestamp doesn't match expected timestamp in
|
|
439
|
+
* EncryptedDbChange. Indicates potential tampering or corruption of CRDT
|
|
440
|
+
* messages.
|
|
441
|
+
*/
|
|
442
|
+
export interface ProtocolTimestampMismatchError {
|
|
443
|
+
readonly type: "ProtocolTimestampMismatchError";
|
|
444
|
+
readonly expected: Timestamp;
|
|
445
|
+
readonly timestamp: Timestamp;
|
|
446
|
+
}
|
|
447
|
+
|
|
447
448
|
/**
|
|
448
449
|
* Creates a {@link ProtocolMessage} from CRDT messages.
|
|
449
450
|
*
|
|
450
|
-
* If the message size would exceed {@link
|
|
451
|
-
* ensures all messages will be sent in the next round(s) even over
|
|
451
|
+
* If the message size would exceed {@link defaultProtocolMessageMaxSize}, the
|
|
452
|
+
* protocol ensures all messages will be sent in the next round(s) even over
|
|
452
453
|
* unidirectional and stateless transports.
|
|
453
454
|
*/
|
|
454
455
|
export const createProtocolMessageFromCrdtMessages =
|
|
455
|
-
(deps:
|
|
456
|
+
(deps: RandomBytesDep & SymmetricCryptoDep) =>
|
|
456
457
|
(
|
|
457
|
-
owner:
|
|
458
|
+
owner: Owner,
|
|
458
459
|
messages: NonEmptyReadonlyArray<CrdtMessage>,
|
|
459
|
-
maxSize?:
|
|
460
|
+
maxSize?: ProtocolMessageMaxSize,
|
|
460
461
|
): ProtocolMessage => {
|
|
461
462
|
const buffer = createProtocolMessageBuffer(owner.id, {
|
|
462
|
-
|
|
463
|
-
totalMaxSize: maxSize ??
|
|
463
|
+
messageType: MessageType.Request,
|
|
464
|
+
totalMaxSize: maxSize ?? defaultProtocolMessageMaxSize,
|
|
464
465
|
writeKey: owner.writeKey,
|
|
465
466
|
});
|
|
466
467
|
|
|
@@ -468,7 +469,7 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
468
469
|
|
|
469
470
|
for (const message of messages) {
|
|
470
471
|
const change = encodeAndEncryptDbChange(deps)(
|
|
471
|
-
message
|
|
472
|
+
message,
|
|
472
473
|
owner.encryptionKey,
|
|
473
474
|
);
|
|
474
475
|
const encryptedCrdtMessage = { timestamp: message.timestamp, change };
|
|
@@ -489,14 +490,14 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
489
490
|
*
|
|
490
491
|
* The ideal approach would be to send three ranges (skip, fingerprint,
|
|
491
492
|
* skip) where the fingerprint of unsent messages would act as narrow sync
|
|
492
|
-
* probe. I think we can send
|
|
493
|
-
*
|
|
493
|
+
* probe. I think we can send `zeroFingerprint` which can be interpreted
|
|
494
|
+
* as an indication that the other side should reply with
|
|
494
495
|
* {@link TimestampsRange}, so no need to restart syncing.
|
|
495
496
|
*
|
|
496
497
|
* For now, using a random fingerprint avoids extra complexity and is good
|
|
497
498
|
* enough for this case.
|
|
498
499
|
*/
|
|
499
|
-
const randomFingerprint = deps.
|
|
500
|
+
const randomFingerprint = deps.randomBytes.create(
|
|
500
501
|
fingerprintSize,
|
|
501
502
|
) as unknown as Fingerprint;
|
|
502
503
|
|
|
@@ -514,17 +515,23 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
514
515
|
/** Creates a {@link ProtocolMessage} for sync. */
|
|
515
516
|
export const createProtocolMessageForSync =
|
|
516
517
|
(deps: StorageDep) =>
|
|
517
|
-
(
|
|
518
|
-
|
|
519
|
-
|
|
518
|
+
(
|
|
519
|
+
ownerId: OwnerId,
|
|
520
|
+
subscriptionFlag?: SubscriptionFlag,
|
|
521
|
+
): ProtocolMessage | null => {
|
|
522
|
+
const buffer = createProtocolMessageBuffer(ownerId, {
|
|
523
|
+
messageType: MessageType.Request,
|
|
524
|
+
subscriptionFlag: subscriptionFlag ?? SubscriptionFlags.None,
|
|
525
|
+
});
|
|
526
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
520
527
|
|
|
521
|
-
const size = deps.storage.getSize(
|
|
528
|
+
const size = deps.storage.getSize(ownerIdBytes);
|
|
522
529
|
// Errors are handled by the storage.
|
|
523
530
|
if (size == null) return null;
|
|
524
531
|
|
|
525
532
|
splitRange(deps)(
|
|
526
|
-
|
|
527
|
-
0
|
|
533
|
+
ownerIdBytes,
|
|
534
|
+
NonNegativeInt.orThrow(0),
|
|
528
535
|
size,
|
|
529
536
|
InfiniteUpperBound,
|
|
530
537
|
buffer,
|
|
@@ -533,18 +540,13 @@ export const createProtocolMessageForSync =
|
|
|
533
540
|
return buffer.unwrap();
|
|
534
541
|
};
|
|
535
542
|
|
|
536
|
-
|
|
537
|
-
export const createProtocolMessageForWriteKeyRotation = (
|
|
543
|
+
export const createProtocolMessageForUnsubscribe = (
|
|
538
544
|
ownerId: OwnerId,
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
writeKey: [currentWriteKey, newWriteKey],
|
|
545
|
-
});
|
|
546
|
-
return buffer.unwrap();
|
|
547
|
-
};
|
|
545
|
+
): ProtocolMessage =>
|
|
546
|
+
createProtocolMessageBuffer(ownerId, {
|
|
547
|
+
messageType: MessageType.Request,
|
|
548
|
+
subscriptionFlag: SubscriptionFlags.Unsubscribe,
|
|
549
|
+
}).unwrap();
|
|
548
550
|
|
|
549
551
|
/**
|
|
550
552
|
* Mutable builder for constructing {@link ProtocolMessage} respecting size
|
|
@@ -573,24 +575,27 @@ export interface ProtocolMessageBuffer {
|
|
|
573
575
|
export const createProtocolMessageBuffer = (
|
|
574
576
|
ownerId: OwnerId,
|
|
575
577
|
options: {
|
|
576
|
-
readonly totalMaxSize?:
|
|
577
|
-
readonly rangesMaxSize?:
|
|
578
|
+
readonly totalMaxSize?: ProtocolMessageMaxSize | undefined;
|
|
579
|
+
readonly rangesMaxSize?: ProtocolMessageRangesMaxSize | undefined;
|
|
578
580
|
readonly version?: NonNegativeInt;
|
|
579
581
|
} & (
|
|
580
582
|
| {
|
|
581
|
-
readonly
|
|
582
|
-
|
|
583
|
-
readonly
|
|
583
|
+
readonly messageType: typeof MessageType.Request;
|
|
584
|
+
readonly writeKey?: OwnerWriteKey;
|
|
585
|
+
readonly subscriptionFlag?: SubscriptionFlag;
|
|
584
586
|
}
|
|
585
587
|
| {
|
|
586
|
-
readonly
|
|
588
|
+
readonly messageType: typeof MessageType.Response;
|
|
587
589
|
readonly errorCode: ProtocolErrorCode;
|
|
588
590
|
}
|
|
591
|
+
| {
|
|
592
|
+
readonly messageType: typeof MessageType.Broadcast;
|
|
593
|
+
}
|
|
589
594
|
),
|
|
590
595
|
): ProtocolMessageBuffer => {
|
|
591
596
|
const {
|
|
592
|
-
totalMaxSize =
|
|
593
|
-
rangesMaxSize =
|
|
597
|
+
totalMaxSize = defaultProtocolMessageMaxSize,
|
|
598
|
+
rangesMaxSize = defaultProtocolMessageRangesMaxSize,
|
|
594
599
|
version = protocolVersion,
|
|
595
600
|
} = options;
|
|
596
601
|
|
|
@@ -608,20 +613,19 @@ export const createProtocolMessageBuffer = (
|
|
|
608
613
|
};
|
|
609
614
|
|
|
610
615
|
encodeNonNegativeInt(buffers.header, version);
|
|
611
|
-
buffers.header.extend(
|
|
616
|
+
buffers.header.extend(ownerIdToOwnerIdBytes(ownerId));
|
|
617
|
+
buffers.header.extend([options.messageType]);
|
|
612
618
|
|
|
613
|
-
if (options.
|
|
619
|
+
if (options.messageType === MessageType.Request) {
|
|
614
620
|
if (!options.writeKey) {
|
|
615
|
-
buffers.header.extend([
|
|
616
|
-
} else if (!Array.isArray(options.writeKey)) {
|
|
617
|
-
buffers.header.extend([WriteKeyMode.Single]);
|
|
618
|
-
buffers.header.extend(options.writeKey as WriteKey);
|
|
621
|
+
buffers.header.extend([0]);
|
|
619
622
|
} else {
|
|
620
|
-
buffers.header.extend([
|
|
621
|
-
buffers.header.extend(options.writeKey
|
|
622
|
-
buffers.header.extend(options.writeKey[1] as WriteKey); // new
|
|
623
|
+
buffers.header.extend([1]);
|
|
624
|
+
buffers.header.extend(options.writeKey);
|
|
623
625
|
}
|
|
624
|
-
|
|
626
|
+
const subscriptionFlag = options.subscriptionFlag ?? SubscriptionFlags.None;
|
|
627
|
+
buffers.header.extend([subscriptionFlag]);
|
|
628
|
+
} else if (options.messageType === MessageType.Response) {
|
|
625
629
|
buffers.header.extend([options.errorCode]);
|
|
626
630
|
}
|
|
627
631
|
|
|
@@ -630,7 +634,7 @@ export const createProtocolMessageBuffer = (
|
|
|
630
634
|
const isWithinSizeLimits = () => getSize() <= totalMaxSize;
|
|
631
635
|
|
|
632
636
|
const getSize = () =>
|
|
633
|
-
(getHeaderAndMessagesSize() + getRangesSize())
|
|
637
|
+
PositiveInt.orThrow(getHeaderAndMessagesSize() + getRangesSize());
|
|
634
638
|
|
|
635
639
|
const getHeaderAndMessagesSize = () =>
|
|
636
640
|
buffers.header.getLength() +
|
|
@@ -704,10 +708,15 @@ export const createProtocolMessageBuffer = (
|
|
|
704
708
|
},
|
|
705
709
|
|
|
706
710
|
addRange: (range) => {
|
|
711
|
+
assert(
|
|
712
|
+
options.messageType !== MessageType.Broadcast,
|
|
713
|
+
"Cannot add a range into broadcast message",
|
|
714
|
+
);
|
|
707
715
|
assert(
|
|
708
716
|
!isLastRangeInfinite,
|
|
709
717
|
"Cannot add a range after an InfiniteUpperBound range",
|
|
710
718
|
);
|
|
719
|
+
|
|
711
720
|
isLastRangeInfinite = range.upperBound === InfiniteUpperBound;
|
|
712
721
|
|
|
713
722
|
/**
|
|
@@ -717,13 +726,16 @@ export const createProtocolMessageBuffer = (
|
|
|
717
726
|
*/
|
|
718
727
|
if (range.upperBound !== InfiniteUpperBound)
|
|
719
728
|
buffers.ranges.timestamps.add(
|
|
720
|
-
|
|
729
|
+
timestampBytesToTimestamp(range.upperBound),
|
|
721
730
|
);
|
|
722
731
|
else {
|
|
723
732
|
buffers.ranges.timestamps.addInfinite();
|
|
724
733
|
}
|
|
725
734
|
|
|
726
|
-
encodeNonNegativeInt(
|
|
735
|
+
encodeNonNegativeInt(
|
|
736
|
+
buffers.ranges.types,
|
|
737
|
+
NonNegativeInt.orThrow(range.type),
|
|
738
|
+
);
|
|
727
739
|
|
|
728
740
|
switch (range.type) {
|
|
729
741
|
case RangeType.Skip:
|
|
@@ -764,6 +776,11 @@ export const createProtocolMessageBuffer = (
|
|
|
764
776
|
};
|
|
765
777
|
};
|
|
766
778
|
|
|
779
|
+
export interface TimestampsRangeWithTimestampsBuffer extends BaseRange {
|
|
780
|
+
readonly type: typeof RangeType.Timestamps;
|
|
781
|
+
readonly timestamps: TimestampsBuffer;
|
|
782
|
+
}
|
|
783
|
+
|
|
767
784
|
export interface TimestampsBuffer {
|
|
768
785
|
readonly add: (timestamp: Timestamp) => void;
|
|
769
786
|
readonly addInfinite: () => void;
|
|
@@ -773,7 +790,7 @@ export interface TimestampsBuffer {
|
|
|
773
790
|
}
|
|
774
791
|
|
|
775
792
|
export const createTimestampsBuffer = (): TimestampsBuffer => {
|
|
776
|
-
let count = 0
|
|
793
|
+
let count = NonNegativeInt.orThrow(0);
|
|
777
794
|
const countBuffer = createBuffer();
|
|
778
795
|
|
|
779
796
|
const syncCount = () => {
|
|
@@ -840,9 +857,9 @@ const createRunLengthEncoder = <T>(
|
|
|
840
857
|
encodeValue: (buffer: Buffer, value: T) => void,
|
|
841
858
|
): RunLengthEncoder<T> => {
|
|
842
859
|
const buffer = createBuffer();
|
|
843
|
-
let previousLength = 0
|
|
860
|
+
let previousLength = NonNegativeInt.orThrow(0);
|
|
844
861
|
let previousValue = null as T | null;
|
|
845
|
-
let runLength = 0
|
|
862
|
+
let runLength = NonNegativeInt.orThrow(0);
|
|
846
863
|
|
|
847
864
|
return {
|
|
848
865
|
add: (value) => {
|
|
@@ -851,7 +868,7 @@ const createRunLengthEncoder = <T>(
|
|
|
851
868
|
buffer.truncate(previousLength);
|
|
852
869
|
} else {
|
|
853
870
|
previousValue = value;
|
|
854
|
-
runLength = 1
|
|
871
|
+
runLength = NonNegativeInt.orThrow(1);
|
|
855
872
|
}
|
|
856
873
|
previousLength = buffer.getLength();
|
|
857
874
|
encodeValue(buffer, value);
|
|
@@ -865,40 +882,61 @@ const createRunLengthEncoder = <T>(
|
|
|
865
882
|
};
|
|
866
883
|
|
|
867
884
|
export interface ApplyProtocolMessageAsClientOptions {
|
|
868
|
-
getWriteKey?: (ownerId: OwnerId) =>
|
|
885
|
+
getWriteKey?: (ownerId: OwnerId) => OwnerWriteKey | null;
|
|
869
886
|
|
|
870
|
-
|
|
871
|
-
version?: NonNegativeInt;
|
|
887
|
+
rangesMaxSize?: ProtocolMessageRangesMaxSize;
|
|
872
888
|
|
|
873
|
-
|
|
874
|
-
|
|
889
|
+
/** For tests only. */
|
|
890
|
+
version?: NonNegativeInt;
|
|
875
891
|
}
|
|
876
892
|
|
|
893
|
+
/**
|
|
894
|
+
* Result type for {@link applyProtocolMessageAsClient} that distinguishes
|
|
895
|
+
* between responses to client requests and broadcast messages.
|
|
896
|
+
*/
|
|
897
|
+
export type ApplyProtocolMessageAsClientResult =
|
|
898
|
+
| { readonly type: "response"; readonly message: ProtocolMessage }
|
|
899
|
+
| { readonly type: "no-response" }
|
|
900
|
+
| { readonly type: "broadcast" };
|
|
901
|
+
|
|
877
902
|
export const applyProtocolMessageAsClient =
|
|
878
903
|
(deps: StorageDep) =>
|
|
879
|
-
(
|
|
904
|
+
async (
|
|
880
905
|
inputMessage: Uint8Array,
|
|
881
|
-
{
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
isInitiator: version < requestedVersion,
|
|
898
|
-
ownerId,
|
|
899
|
-
});
|
|
900
|
-
}
|
|
906
|
+
options: ApplyProtocolMessageAsClientOptions = {},
|
|
907
|
+
): Promise<
|
|
908
|
+
Result<
|
|
909
|
+
ApplyProtocolMessageAsClientResult,
|
|
910
|
+
| ProtocolInvalidDataError
|
|
911
|
+
| ProtocolSyncError
|
|
912
|
+
| ProtocolVersionError
|
|
913
|
+
| ProtocolWriteError
|
|
914
|
+
| ProtocolWriteKeyError
|
|
915
|
+
| ProtocolQuotaError
|
|
916
|
+
>
|
|
917
|
+
> => {
|
|
918
|
+
try {
|
|
919
|
+
const input = createBuffer(inputMessage);
|
|
920
|
+
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
921
|
+
const version = options.version ?? protocolVersion;
|
|
901
922
|
|
|
923
|
+
if (requestedVersion !== version) {
|
|
924
|
+
return err<ProtocolVersionError>({
|
|
925
|
+
type: "ProtocolVersionError",
|
|
926
|
+
version: requestedVersion,
|
|
927
|
+
isInitiator: version < requestedVersion,
|
|
928
|
+
ownerId,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const messageType = input.shift() as MessageType;
|
|
933
|
+
assert(
|
|
934
|
+
messageType === MessageType.Response ||
|
|
935
|
+
messageType === MessageType.Broadcast,
|
|
936
|
+
"Invalid MessageType",
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
if (messageType === MessageType.Response) {
|
|
902
940
|
const errorCode = input.shift() as ProtocolErrorCode;
|
|
903
941
|
if (errorCode !== ProtocolErrorCode.NoError) {
|
|
904
942
|
switch (errorCode) {
|
|
@@ -912,6 +950,11 @@ export const applyProtocolMessageAsClient =
|
|
|
912
950
|
type: "ProtocolWriteError",
|
|
913
951
|
ownerId,
|
|
914
952
|
});
|
|
953
|
+
case ProtocolErrorCode.QuotaError:
|
|
954
|
+
return err<ProtocolQuotaError>({
|
|
955
|
+
type: "ProtocolQuotaError",
|
|
956
|
+
ownerId,
|
|
957
|
+
});
|
|
915
958
|
case ProtocolErrorCode.SyncError:
|
|
916
959
|
return err<ProtocolSyncError>({
|
|
917
960
|
type: "ProtocolSyncError",
|
|
@@ -923,180 +966,248 @@ export const applyProtocolMessageAsClient =
|
|
|
923
966
|
);
|
|
924
967
|
}
|
|
925
968
|
}
|
|
969
|
+
}
|
|
926
970
|
|
|
927
|
-
|
|
928
|
-
|
|
971
|
+
const messages = decodeMessages(input);
|
|
972
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
929
973
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
974
|
+
if (isNonEmptyReadonlyArray(messages)) {
|
|
975
|
+
const writeResult = await deps.storage.writeMessages(
|
|
976
|
+
ownerIdBytes,
|
|
977
|
+
messages,
|
|
978
|
+
);
|
|
979
|
+
// Errors are handled by the Storage. Here we just stop syncing.
|
|
980
|
+
if (!writeResult.ok) return ok({ type: "no-response" });
|
|
981
|
+
}
|
|
936
982
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
983
|
+
// Now: No writeKey, no sync.
|
|
984
|
+
// TODO: Allow to sync SharedReadonlyOwner
|
|
985
|
+
// Without local changes, writeKey will not be required.
|
|
986
|
+
// With local changes, writeKey will be required and if not provided,
|
|
987
|
+
// the sync will stop.
|
|
988
|
+
const writeKey = options.getWriteKey?.(ownerId);
|
|
989
|
+
if (writeKey == null) {
|
|
990
|
+
return ok({ type: "no-response" });
|
|
991
|
+
}
|
|
940
992
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
totalMaxSize,
|
|
945
|
-
rangesMaxSize,
|
|
946
|
-
});
|
|
993
|
+
if (messageType === MessageType.Broadcast) {
|
|
994
|
+
return ok({ type: "broadcast" });
|
|
995
|
+
}
|
|
947
996
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
997
|
+
const ranges = decodeRanges(input);
|
|
998
|
+
|
|
999
|
+
if (!isNonEmptyReadonlyArray(ranges)) {
|
|
1000
|
+
return ok({ type: "no-response" });
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const output = createProtocolMessageBuffer(ownerId, {
|
|
1004
|
+
messageType: MessageType.Request,
|
|
1005
|
+
writeKey,
|
|
1006
|
+
rangesMaxSize: options.rangesMaxSize,
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
const syncResult = sync(deps)(ranges, output, ownerIdBytes);
|
|
1010
|
+
|
|
1011
|
+
// Client sync error (handled via Storage) or no changes.
|
|
1012
|
+
if (!syncResult.ok || !syncResult.value) {
|
|
1013
|
+
return ok({ type: "no-response" });
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return ok({ type: "response", message: output.unwrap() });
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
return err<ProtocolInvalidDataError>({
|
|
1019
|
+
type: "ProtocolInvalidDataError",
|
|
1020
|
+
data: inputMessage,
|
|
1021
|
+
error,
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
951
1025
|
|
|
952
1026
|
export interface ApplyProtocolMessageAsRelayOptions {
|
|
953
1027
|
/** To subscribe an owner for broadcasting. */
|
|
954
1028
|
subscribe?: (ownerId: OwnerId) => void;
|
|
955
1029
|
|
|
1030
|
+
/** To unsubscribe an owner from broadcasting. */
|
|
1031
|
+
unsubscribe?: (ownerId: OwnerId) => void;
|
|
1032
|
+
|
|
956
1033
|
/** To broadcast a protocol message to all subscribers. */
|
|
957
1034
|
broadcast?: (ownerId: OwnerId, message: ProtocolMessage) => void;
|
|
958
1035
|
|
|
959
|
-
totalMaxSize?:
|
|
960
|
-
rangesMaxSize?:
|
|
1036
|
+
totalMaxSize?: ProtocolMessageMaxSize;
|
|
1037
|
+
rangesMaxSize?: ProtocolMessageRangesMaxSize;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Result type for {@link applyProtocolMessageAsRelay}.
|
|
1042
|
+
*
|
|
1043
|
+
* Unlike {@link ApplyProtocolMessageAsClientResult}, relays always respond with
|
|
1044
|
+
* a message to provide sync completion feedback. This ensures the initiator can
|
|
1045
|
+
* reliably detect when synchronization is complete, even when there's nothing
|
|
1046
|
+
* to sync. Clients may choose not to respond in certain cases (like when they
|
|
1047
|
+
* receive broadcast messages or when they lack a write key for syncing).
|
|
1048
|
+
*/
|
|
1049
|
+
export interface ApplyProtocolMessageAsRelayResult {
|
|
1050
|
+
readonly type: "response";
|
|
1051
|
+
readonly message: ProtocolMessage;
|
|
961
1052
|
}
|
|
962
1053
|
|
|
963
1054
|
export const applyProtocolMessageAsRelay =
|
|
964
1055
|
(deps: StorageDep) =>
|
|
965
|
-
(
|
|
1056
|
+
async (
|
|
966
1057
|
inputMessage: Uint8Array,
|
|
967
|
-
{
|
|
968
|
-
|
|
969
|
-
broadcast,
|
|
970
|
-
totalMaxSize,
|
|
971
|
-
rangesMaxSize,
|
|
972
|
-
}: ApplyProtocolMessageAsRelayOptions = {},
|
|
973
|
-
/** For testing purposes only; should not be used in production. */
|
|
1058
|
+
options: ApplyProtocolMessageAsRelayOptions = {},
|
|
1059
|
+
/** For tests only. */
|
|
974
1060
|
version = protocolVersion,
|
|
975
|
-
):
|
|
976
|
-
|
|
1061
|
+
): Promise<
|
|
1062
|
+
Result<ApplyProtocolMessageAsRelayResult, ProtocolInvalidDataError>
|
|
1063
|
+
> => {
|
|
1064
|
+
try {
|
|
1065
|
+
const input = createBuffer(inputMessage);
|
|
977
1066
|
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
978
|
-
const
|
|
1067
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
979
1068
|
|
|
980
1069
|
if (requestedVersion !== version) {
|
|
981
1070
|
// Non-initiator responds with its version and ownerId.
|
|
982
1071
|
const output = createBuffer();
|
|
983
1072
|
encodeNonNegativeInt(output, version);
|
|
984
|
-
output.extend(
|
|
985
|
-
return ok(
|
|
1073
|
+
output.extend(ownerIdBytes);
|
|
1074
|
+
return ok({
|
|
1075
|
+
type: "response",
|
|
1076
|
+
message: output.unwrap() as ProtocolMessage,
|
|
1077
|
+
});
|
|
986
1078
|
}
|
|
987
1079
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
let
|
|
993
|
-
|
|
994
|
-
if (
|
|
995
|
-
writeKey = input.shiftN(
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1080
|
+
const messageType = input.shift() as MessageType;
|
|
1081
|
+
assert(messageType === MessageType.Request, "Invalid MessageType");
|
|
1082
|
+
|
|
1083
|
+
const hasWriteKey = input.shift();
|
|
1084
|
+
let writeKey: OwnerWriteKey | undefined;
|
|
1085
|
+
|
|
1086
|
+
if (hasWriteKey === 1) {
|
|
1087
|
+
writeKey = input.shiftN(ownerWriteKeyLength) as OwnerWriteKey;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const subscriptionFlag = input.shift() as SubscriptionFlag;
|
|
1091
|
+
|
|
1092
|
+
switch (subscriptionFlag) {
|
|
1093
|
+
case SubscriptionFlags.Subscribe:
|
|
1094
|
+
options.subscribe?.(ownerId);
|
|
1095
|
+
break;
|
|
1096
|
+
case SubscriptionFlags.Unsubscribe:
|
|
1097
|
+
options.unsubscribe?.(ownerId);
|
|
1098
|
+
break;
|
|
1099
|
+
case SubscriptionFlags.None:
|
|
1100
|
+
break;
|
|
1007
1101
|
}
|
|
1008
1102
|
|
|
1009
1103
|
if (writeKey) {
|
|
1010
|
-
const isValid = deps.storage.validateWriteKey(
|
|
1104
|
+
const isValid = deps.storage.validateWriteKey(ownerIdBytes, writeKey);
|
|
1011
1105
|
if (!isValid) {
|
|
1012
|
-
return ok(
|
|
1013
|
-
|
|
1014
|
-
|
|
1106
|
+
return ok({
|
|
1107
|
+
type: "response",
|
|
1108
|
+
message: createProtocolMessageBuffer(ownerId, {
|
|
1109
|
+
messageType: MessageType.Response,
|
|
1015
1110
|
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
1016
1111
|
}).unwrap(),
|
|
1017
|
-
);
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
if (newWriteKey) {
|
|
1021
|
-
const rotationSuccess = deps.storage.setWriteKey(
|
|
1022
|
-
binaryOwnerId,
|
|
1023
|
-
newWriteKey,
|
|
1024
|
-
);
|
|
1025
|
-
if (!rotationSuccess) {
|
|
1026
|
-
return ok(
|
|
1027
|
-
createProtocolMessageBuffer(ownerId, {
|
|
1028
|
-
type: "non-initiator",
|
|
1029
|
-
errorCode: ProtocolErrorCode.WriteError,
|
|
1030
|
-
}).unwrap(),
|
|
1031
|
-
);
|
|
1032
|
-
}
|
|
1112
|
+
});
|
|
1033
1113
|
}
|
|
1034
1114
|
}
|
|
1035
1115
|
|
|
1036
1116
|
const messages = decodeMessages(input);
|
|
1037
1117
|
|
|
1038
1118
|
if (isNonEmptyReadonlyArray(messages)) {
|
|
1039
|
-
if (!writeKey)
|
|
1040
|
-
return ok(
|
|
1041
|
-
|
|
1042
|
-
|
|
1119
|
+
if (!writeKey) {
|
|
1120
|
+
return ok({
|
|
1121
|
+
type: "response",
|
|
1122
|
+
message: createProtocolMessageBuffer(ownerId, {
|
|
1123
|
+
messageType: MessageType.Response,
|
|
1043
1124
|
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
1044
1125
|
}).unwrap(),
|
|
1045
|
-
);
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const writeResult = await deps.storage.writeMessages(
|
|
1130
|
+
ownerIdBytes,
|
|
1131
|
+
messages,
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
if (!writeResult.ok) {
|
|
1135
|
+
const errorCode =
|
|
1136
|
+
writeResult.error.type === "StorageWriteError"
|
|
1137
|
+
? ProtocolErrorCode.WriteError
|
|
1138
|
+
: ProtocolErrorCode.QuotaError;
|
|
1139
|
+
const message = createProtocolMessageBuffer(ownerId, {
|
|
1140
|
+
messageType: MessageType.Response,
|
|
1141
|
+
errorCode,
|
|
1142
|
+
}).unwrap();
|
|
1143
|
+
return ok({ type: "response", message });
|
|
1144
|
+
}
|
|
1046
1145
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1146
|
+
/**
|
|
1147
|
+
* Broadcast messages to all subscribed owners for real-time
|
|
1148
|
+
* synchronization between clients.
|
|
1149
|
+
*
|
|
1150
|
+
* Messages are only broadcasted after successful write to ensure
|
|
1151
|
+
* devices that can still sync aren't affected by quota errors, and to
|
|
1152
|
+
* prevent using a half-working relay service (broadcasting without
|
|
1153
|
+
* persistence).
|
|
1154
|
+
*
|
|
1155
|
+
* When a relay's database is deleted or clients migrate to a new relay
|
|
1156
|
+
* (without data migration), clients will sync their data to the relay,
|
|
1157
|
+
* and the relay will broadcast those messages to other connected
|
|
1158
|
+
* clients. Those clients may receive messages they already have, but
|
|
1159
|
+
* this is safe because Evolu sync is idempotent. As the relay becomes
|
|
1160
|
+
* more synchronized with clients over time, fewer duplicate messages
|
|
1161
|
+
* will be broadcasted.
|
|
1162
|
+
*/
|
|
1163
|
+
if (options.broadcast) {
|
|
1049
1164
|
const broadcastBuffer = createProtocolMessageBuffer(ownerId, {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
rangesMaxSize,
|
|
1165
|
+
messageType: MessageType.Broadcast,
|
|
1166
|
+
totalMaxSize: options.totalMaxSize,
|
|
1167
|
+
rangesMaxSize: options.rangesMaxSize,
|
|
1054
1168
|
version,
|
|
1055
1169
|
});
|
|
1056
1170
|
for (const message of messages) {
|
|
1057
1171
|
broadcastBuffer.addMessage(message);
|
|
1058
1172
|
}
|
|
1059
|
-
broadcast(ownerId, broadcastBuffer.unwrap());
|
|
1173
|
+
options.broadcast(ownerId, broadcastBuffer.unwrap());
|
|
1060
1174
|
}
|
|
1061
|
-
|
|
1062
|
-
if (!deps.storage.writeMessages(binaryOwnerId, messages))
|
|
1063
|
-
return ok(
|
|
1064
|
-
createProtocolMessageBuffer(ownerId, {
|
|
1065
|
-
type: "non-initiator",
|
|
1066
|
-
errorCode: ProtocolErrorCode.WriteError,
|
|
1067
|
-
}).unwrap(),
|
|
1068
|
-
);
|
|
1069
1175
|
}
|
|
1070
1176
|
|
|
1177
|
+
const ranges = decodeRanges(input);
|
|
1178
|
+
|
|
1071
1179
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
1072
|
-
|
|
1180
|
+
messageType: MessageType.Response,
|
|
1073
1181
|
errorCode: ProtocolErrorCode.NoError,
|
|
1074
|
-
totalMaxSize,
|
|
1075
|
-
rangesMaxSize,
|
|
1182
|
+
totalMaxSize: options.totalMaxSize,
|
|
1183
|
+
rangesMaxSize: options.rangesMaxSize,
|
|
1076
1184
|
});
|
|
1077
1185
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1186
|
+
// Non-initiators always respond to provide sync completion feedback,
|
|
1187
|
+
// even when there's nothing to sync.
|
|
1188
|
+
if (!isNonEmptyReadonlyArray(ranges)) {
|
|
1189
|
+
return ok({ type: "response", message: output.unwrap() });
|
|
1190
|
+
}
|
|
1080
1191
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
};
|
|
1192
|
+
const syncResult = sync(deps)(ranges, output, ownerIdBytes);
|
|
1193
|
+
|
|
1194
|
+
const message = syncResult.ok
|
|
1195
|
+
? output.unwrap()
|
|
1196
|
+
: createProtocolMessageBuffer(ownerId, {
|
|
1197
|
+
messageType: MessageType.Response,
|
|
1198
|
+
errorCode: syncResult.error,
|
|
1199
|
+
}).unwrap();
|
|
1200
|
+
|
|
1201
|
+
// Non-initiators always respond to provide sync completion feedback,
|
|
1202
|
+
return ok({ type: "response", message });
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
return err<ProtocolInvalidDataError>({
|
|
1205
|
+
type: "ProtocolInvalidDataError",
|
|
1206
|
+
data: inputMessage,
|
|
1207
|
+
error,
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1100
1211
|
|
|
1101
1212
|
const decodeVersionAndOwner = (input: Buffer): [NonNegativeInt, OwnerId] => {
|
|
1102
1213
|
// This structure must never change across protocol versions. The version
|
|
@@ -1104,7 +1215,7 @@ const decodeVersionAndOwner = (input: Buffer): [NonNegativeInt, OwnerId] => {
|
|
|
1104
1215
|
// to enable version negotiation and owner identification before any other
|
|
1105
1216
|
// processing occurs.
|
|
1106
1217
|
const version = decodeNonNegativeInt(input);
|
|
1107
|
-
const ownerId =
|
|
1218
|
+
const ownerId = decodeId(input) as OwnerId;
|
|
1108
1219
|
return [version, ownerId];
|
|
1109
1220
|
};
|
|
1110
1221
|
|
|
@@ -1139,38 +1250,17 @@ const decodeMessages = (
|
|
|
1139
1250
|
const sync =
|
|
1140
1251
|
(deps: StorageDep) =>
|
|
1141
1252
|
(
|
|
1142
|
-
|
|
1143
|
-
input: Buffer,
|
|
1253
|
+
ranges: NonEmptyReadonlyArray<Range>,
|
|
1144
1254
|
output: ProtocolMessageBuffer,
|
|
1145
|
-
|
|
1146
|
-
): Result<
|
|
1147
|
-
const ranges = decodeRanges(input);
|
|
1148
|
-
|
|
1149
|
-
if (!isNonEmptyReadonlyArray(ranges)) {
|
|
1150
|
-
// Nothing to sync.
|
|
1151
|
-
return ok(null);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const binaryOwnerId = binaryOwnerIdToOwnerId(ownerId);
|
|
1255
|
+
ownerIdBytes: OwnerIdBytes,
|
|
1256
|
+
): Result<boolean, typeof ProtocolErrorCode.SyncError> => {
|
|
1155
1257
|
const outputInitialSize = output.getSize();
|
|
1156
1258
|
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
if (role === "initiator") {
|
|
1160
|
-
return ok(null);
|
|
1161
|
-
}
|
|
1162
|
-
const message = createProtocolMessageBuffer(binaryOwnerId, {
|
|
1163
|
-
type: "non-initiator",
|
|
1164
|
-
errorCode: ProtocolErrorCode.SyncError,
|
|
1165
|
-
});
|
|
1166
|
-
return ok(message.unwrap());
|
|
1167
|
-
};
|
|
1168
|
-
|
|
1169
|
-
const storageSize = deps.storage.getSize(ownerId);
|
|
1170
|
-
if (storageSize == null) return syncFail();
|
|
1259
|
+
const storageSize = deps.storage.getSize(ownerIdBytes);
|
|
1260
|
+
if (storageSize == null) return err(ProtocolErrorCode.SyncError);
|
|
1171
1261
|
|
|
1172
1262
|
let prevUpperBound: RangeUpperBound | null = null;
|
|
1173
|
-
let prevIndex = 0
|
|
1263
|
+
let prevIndex = NonNegativeInt.orThrow(0);
|
|
1174
1264
|
|
|
1175
1265
|
let skip = false;
|
|
1176
1266
|
let nonSkipRangeAdded = false;
|
|
@@ -1207,7 +1297,11 @@ const sync =
|
|
|
1207
1297
|
const addFingerprintForRemainingRange = (
|
|
1208
1298
|
begin: NonNegativeInt,
|
|
1209
1299
|
): boolean => {
|
|
1210
|
-
const fingerprint = deps.storage.fingerprint(
|
|
1300
|
+
const fingerprint = deps.storage.fingerprint(
|
|
1301
|
+
ownerIdBytes,
|
|
1302
|
+
begin,
|
|
1303
|
+
storageSize,
|
|
1304
|
+
);
|
|
1211
1305
|
if (!fingerprint) return false;
|
|
1212
1306
|
// There is always a space for a ramaining range.
|
|
1213
1307
|
output.addRange({
|
|
@@ -1223,12 +1317,12 @@ const sync =
|
|
|
1223
1317
|
|
|
1224
1318
|
const lower = prevIndex;
|
|
1225
1319
|
let upper = deps.storage.findLowerBound(
|
|
1226
|
-
|
|
1320
|
+
ownerIdBytes,
|
|
1227
1321
|
prevIndex,
|
|
1228
1322
|
storageSize,
|
|
1229
1323
|
currentUpperBound,
|
|
1230
1324
|
);
|
|
1231
|
-
if (upper == null) return
|
|
1325
|
+
if (upper == null) return err(ProtocolErrorCode.SyncError);
|
|
1232
1326
|
|
|
1233
1327
|
switch (range.type) {
|
|
1234
1328
|
case RangeType.Skip: {
|
|
@@ -1238,11 +1332,11 @@ const sync =
|
|
|
1238
1332
|
|
|
1239
1333
|
case RangeType.Fingerprint: {
|
|
1240
1334
|
const ourFingerprint = deps.storage.fingerprint(
|
|
1241
|
-
|
|
1335
|
+
ownerIdBytes,
|
|
1242
1336
|
lower,
|
|
1243
1337
|
upper,
|
|
1244
1338
|
);
|
|
1245
|
-
if (ourFingerprint == null) return
|
|
1339
|
+
if (ourFingerprint == null) return err(ProtocolErrorCode.SyncError);
|
|
1246
1340
|
|
|
1247
1341
|
if (eqArrayNumber(range.fingerprint, ourFingerprint)) {
|
|
1248
1342
|
skipRange(range);
|
|
@@ -1250,15 +1344,16 @@ const sync =
|
|
|
1250
1344
|
if (output.canSplitRange()) {
|
|
1251
1345
|
coalesceSkipsBeforeAdd();
|
|
1252
1346
|
splitRange(deps)(
|
|
1253
|
-
|
|
1347
|
+
ownerIdBytes,
|
|
1254
1348
|
lower,
|
|
1255
1349
|
upper,
|
|
1256
1350
|
currentUpperBound,
|
|
1257
1351
|
output,
|
|
1258
1352
|
);
|
|
1259
1353
|
} else {
|
|
1260
|
-
|
|
1261
|
-
|
|
1354
|
+
return addFingerprintForRemainingRange(upper)
|
|
1355
|
+
? ok(true)
|
|
1356
|
+
: err(ProtocolErrorCode.SyncError);
|
|
1262
1357
|
}
|
|
1263
1358
|
}
|
|
1264
1359
|
break;
|
|
@@ -1272,45 +1367,53 @@ const sync =
|
|
|
1272
1367
|
);
|
|
1273
1368
|
const ourTimestamps = createTimestampsBuffer();
|
|
1274
1369
|
|
|
1275
|
-
let
|
|
1370
|
+
let cantReadDbChange = false as boolean;
|
|
1276
1371
|
let exceeded = false as boolean;
|
|
1277
1372
|
|
|
1278
|
-
deps.storage.iterate(
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1373
|
+
deps.storage.iterate(
|
|
1374
|
+
ownerIdBytes,
|
|
1375
|
+
lower,
|
|
1376
|
+
upper,
|
|
1377
|
+
(timestamp, index) => {
|
|
1378
|
+
const timestampString = timestamp.join();
|
|
1379
|
+
const timestampBinary = timestampBytesToTimestamp(timestamp);
|
|
1380
|
+
|
|
1381
|
+
let message: EncryptedCrdtMessage | null = null;
|
|
1382
|
+
|
|
1383
|
+
if (timestampsWeNeed.has(timestampString)) {
|
|
1384
|
+
timestampsWeNeed.delete(timestampString);
|
|
1385
|
+
} else {
|
|
1386
|
+
const dbChange = deps.storage.readDbChange(
|
|
1387
|
+
ownerIdBytes,
|
|
1388
|
+
timestamp,
|
|
1389
|
+
);
|
|
1390
|
+
if (dbChange == null) {
|
|
1391
|
+
cantReadDbChange = true;
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
message = {
|
|
1395
|
+
timestamp: timestampBinary,
|
|
1396
|
+
change: dbChange,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1283
1399
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1400
|
+
if (
|
|
1401
|
+
!output.canAddTimestampsRangeAndMessage(ourTimestamps, message)
|
|
1402
|
+
) {
|
|
1403
|
+
exceeded = true;
|
|
1404
|
+
endBound = timestamp;
|
|
1405
|
+
upper = index;
|
|
1290
1406
|
return false;
|
|
1291
1407
|
}
|
|
1292
|
-
message = {
|
|
1293
|
-
timestamp: timestampBinary,
|
|
1294
|
-
change: dbChange,
|
|
1295
|
-
};
|
|
1296
|
-
}
|
|
1297
1408
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
upper = index;
|
|
1304
|
-
return false;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
ourTimestamps.add(timestampBinary);
|
|
1308
|
-
if (message) output.addMessage(message);
|
|
1309
|
-
return true;
|
|
1310
|
-
});
|
|
1409
|
+
ourTimestamps.add(timestampBinary);
|
|
1410
|
+
if (message) output.addMessage(message);
|
|
1411
|
+
return true;
|
|
1412
|
+
},
|
|
1413
|
+
);
|
|
1311
1414
|
|
|
1312
|
-
if (
|
|
1313
|
-
return
|
|
1415
|
+
if (cantReadDbChange) {
|
|
1416
|
+
return err(ProtocolErrorCode.SyncError);
|
|
1314
1417
|
}
|
|
1315
1418
|
|
|
1316
1419
|
const addRange = () => {
|
|
@@ -1325,9 +1428,9 @@ const sync =
|
|
|
1325
1428
|
if (exceeded) {
|
|
1326
1429
|
addRange();
|
|
1327
1430
|
if (!addFingerprintForRemainingRange(upper)) {
|
|
1328
|
-
return
|
|
1431
|
+
return err(ProtocolErrorCode.SyncError);
|
|
1329
1432
|
}
|
|
1330
|
-
return ok(
|
|
1433
|
+
return ok(true);
|
|
1331
1434
|
}
|
|
1332
1435
|
|
|
1333
1436
|
// If we need something, we have to respond with our timestamps.
|
|
@@ -1347,19 +1450,20 @@ const sync =
|
|
|
1347
1450
|
|
|
1348
1451
|
// If all ranges were skipped, there are no changes and sync is complete.
|
|
1349
1452
|
const hasChange = output.getSize() > outputInitialSize;
|
|
1350
|
-
|
|
1453
|
+
|
|
1454
|
+
return ok(hasChange);
|
|
1351
1455
|
};
|
|
1352
1456
|
|
|
1353
1457
|
const splitRange =
|
|
1354
1458
|
(deps: StorageDep) =>
|
|
1355
1459
|
(
|
|
1356
|
-
ownerId:
|
|
1460
|
+
ownerId: OwnerIdBytes,
|
|
1357
1461
|
lower: NonNegativeInt,
|
|
1358
1462
|
upper: NonNegativeInt,
|
|
1359
1463
|
upperBound: RangeUpperBound,
|
|
1360
1464
|
buffer: ProtocolMessageBuffer,
|
|
1361
1465
|
): void => {
|
|
1362
|
-
const itemCount = (upper - lower)
|
|
1466
|
+
const itemCount = NonNegativeInt.orThrow(upper - lower);
|
|
1363
1467
|
const buckets = computeBalancedBuckets(itemCount);
|
|
1364
1468
|
|
|
1365
1469
|
if (!buckets.ok) {
|
|
@@ -1371,10 +1475,10 @@ const splitRange =
|
|
|
1371
1475
|
|
|
1372
1476
|
deps.storage.iterate(
|
|
1373
1477
|
ownerId,
|
|
1374
|
-
0
|
|
1478
|
+
NonNegativeInt.orThrow(0),
|
|
1375
1479
|
itemCount,
|
|
1376
1480
|
(timestamp) => {
|
|
1377
|
-
range.timestamps.add(
|
|
1481
|
+
range.timestamps.add(timestampBytesToTimestamp(timestamp));
|
|
1378
1482
|
return true;
|
|
1379
1483
|
},
|
|
1380
1484
|
);
|
|
@@ -1387,7 +1491,10 @@ const splitRange =
|
|
|
1387
1491
|
const fingerprintRangesBuckets =
|
|
1388
1492
|
lower === 0
|
|
1389
1493
|
? buckets.value
|
|
1390
|
-
: [
|
|
1494
|
+
: [
|
|
1495
|
+
lower,
|
|
1496
|
+
...buckets.value.map((b) => NonNegativeInt.orThrow(b + lower)),
|
|
1497
|
+
];
|
|
1391
1498
|
|
|
1392
1499
|
const fingerprintRanges = deps.storage.fingerprintRanges(
|
|
1393
1500
|
ownerId,
|
|
@@ -1411,7 +1518,7 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
|
|
|
1411
1518
|
const rangesCount = decodeNonNegativeInt(buffer);
|
|
1412
1519
|
if (rangesCount === 0) return [];
|
|
1413
1520
|
|
|
1414
|
-
const timestampsCount = (rangesCount - 1)
|
|
1521
|
+
const timestampsCount = NonNegativeInt.orThrow(rangesCount - 1);
|
|
1415
1522
|
const timestamps = decodeTimestamps(buffer, timestampsCount);
|
|
1416
1523
|
const rangeTypes: Array<RangeType> = [];
|
|
1417
1524
|
|
|
@@ -1433,7 +1540,7 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
|
|
|
1433
1540
|
for (let i = 0; i < rangesCount; i++) {
|
|
1434
1541
|
const upperBound =
|
|
1435
1542
|
i < timestampsCount
|
|
1436
|
-
?
|
|
1543
|
+
? timestampToTimestampBytes(timestamps[i])
|
|
1437
1544
|
: InfiniteUpperBound;
|
|
1438
1545
|
|
|
1439
1546
|
const rangeType = rangeTypes[i];
|
|
@@ -1455,7 +1562,7 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
|
|
|
1455
1562
|
|
|
1456
1563
|
case RangeType.Timestamps: {
|
|
1457
1564
|
const timestamps = decodeTimestamps(buffer).map(
|
|
1458
|
-
|
|
1565
|
+
timestampToTimestampBytes,
|
|
1459
1566
|
);
|
|
1460
1567
|
ranges.push({
|
|
1461
1568
|
type: RangeType.Timestamps,
|
|
@@ -1483,7 +1590,7 @@ const decodeTimestamps = (
|
|
|
1483
1590
|
for (let i = 0; i < length; i++) {
|
|
1484
1591
|
const deltaMillis = decodeNonNegativeInt(buffer);
|
|
1485
1592
|
const millis = Millis.from(previousMillis + deltaMillis);
|
|
1486
|
-
if (!millis.ok) throw new
|
|
1593
|
+
if (!millis.ok) throw new ProtocolDecodeError(millis.error.type);
|
|
1487
1594
|
millises.push(millis.value);
|
|
1488
1595
|
previousMillis = millis.value;
|
|
1489
1596
|
}
|
|
@@ -1492,7 +1599,7 @@ const decodeTimestamps = (
|
|
|
1492
1599
|
let counterIndex = 0;
|
|
1493
1600
|
while (counterIndex < length) {
|
|
1494
1601
|
const counter = Counter.from(decodeNonNegativeInt(buffer));
|
|
1495
|
-
if (!counter.ok) throw new
|
|
1602
|
+
if (!counter.ok) throw new ProtocolDecodeError(counter.error.type);
|
|
1496
1603
|
const runLength = decodeNonNegativeInt(buffer);
|
|
1497
1604
|
for (let i = 0; i < runLength; i++) {
|
|
1498
1605
|
counters.push(counter.value);
|
|
@@ -1523,127 +1630,31 @@ const decodeTimestamps = (
|
|
|
1523
1630
|
return timestamps;
|
|
1524
1631
|
};
|
|
1525
1632
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
export const binaryIdLength = 16 as NonNegativeInt;
|
|
1530
|
-
|
|
1531
|
-
export const idToBinaryId = (id: Id): BinaryId =>
|
|
1532
|
-
base64Url256ToBytes(id) as BinaryId;
|
|
1533
|
-
|
|
1534
|
-
export const binaryIdToId = (binaryId: BinaryId): Id =>
|
|
1535
|
-
decodeId(createBuffer(binaryId));
|
|
1536
|
-
|
|
1537
|
-
/** Binary representation of {@link OwnerId}. */
|
|
1538
|
-
export type BinaryOwnerId = Uint8Array & Brand<"BinaryOwnerId">;
|
|
1539
|
-
|
|
1540
|
-
export const ownerIdToBinaryOwnerId = (ownerId: OwnerId): BinaryOwnerId =>
|
|
1541
|
-
base64Url256ToBytes(ownerId) as BinaryOwnerId;
|
|
1542
|
-
|
|
1543
|
-
export const binaryOwnerIdToOwnerId = (binaryOwnerId: BinaryOwnerId): OwnerId =>
|
|
1544
|
-
decodeOwnerId(createBuffer(binaryOwnerId));
|
|
1545
|
-
|
|
1546
|
-
/**
|
|
1547
|
-
* Union type for all variants of Base64Url strings with limited length. All
|
|
1548
|
-
* these types use Base64Url alphabet and are < 256 characters.
|
|
1549
|
-
*/
|
|
1550
|
-
export type Base64Url256Variant = Base64Url256 | Id | NanoId | OwnerId;
|
|
1551
|
-
|
|
1552
|
-
/**
|
|
1553
|
-
* Alphabet used for Base64Url encoding. This is copied from the `nanoid`
|
|
1554
|
-
* library to avoid dependency on a specific version of `nanoid`.
|
|
1555
|
-
*/
|
|
1556
|
-
const urlAlphabet =
|
|
1557
|
-
"useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
1558
|
-
|
|
1559
|
-
/**
|
|
1560
|
-
* Converts a Base64Url string to a Uint8Array for binary storage. This encoding
|
|
1561
|
-
* is more space-efficient than UTF-8 for Base64Url strings.
|
|
1562
|
-
*/
|
|
1563
|
-
export const base64Url256ToBytes = (
|
|
1564
|
-
string: Base64Url256Variant,
|
|
1565
|
-
): globalThis.Uint8Array => {
|
|
1566
|
-
const totalBits = string.length * 6; // 6 bits per character
|
|
1567
|
-
const byteLength = Math.ceil(totalBits / 8);
|
|
1568
|
-
const value = new globalThis.Uint8Array(byteLength);
|
|
1569
|
-
|
|
1570
|
-
let bitBuffer = 0;
|
|
1571
|
-
let bitsInBuffer = 0;
|
|
1572
|
-
let byteIndex = 0;
|
|
1573
|
-
|
|
1574
|
-
for (const char of string) {
|
|
1575
|
-
const charValue = urlAlphabet.indexOf(char);
|
|
1576
|
-
bitBuffer = (bitBuffer << 6) | charValue;
|
|
1577
|
-
bitsInBuffer += 6;
|
|
1578
|
-
while (bitsInBuffer >= 8) {
|
|
1579
|
-
bitsInBuffer -= 8;
|
|
1580
|
-
value[byteIndex++] = (bitBuffer >> bitsInBuffer) & 0xff;
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
if (bitsInBuffer > 0 && byteIndex < byteLength) {
|
|
1585
|
-
value[byteIndex] = (bitBuffer << (8 - bitsInBuffer)) & 0xff;
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
return value;
|
|
1589
|
-
};
|
|
1590
|
-
|
|
1591
|
-
export const decodeBase64Url256 = (
|
|
1592
|
-
buffer: Buffer,
|
|
1593
|
-
stringLength: number,
|
|
1594
|
-
): Base64Url256Variant => {
|
|
1595
|
-
const bytes = buffer.shiftN(
|
|
1596
|
-
Math.ceil((stringLength * 6) / 8) as NonNegativeInt,
|
|
1597
|
-
);
|
|
1598
|
-
|
|
1599
|
-
let bitBuffer = 0;
|
|
1600
|
-
let bitsInBuffer = 0;
|
|
1601
|
-
let string = "";
|
|
1602
|
-
|
|
1603
|
-
for (const byte of bytes) {
|
|
1604
|
-
bitBuffer = (bitBuffer << 8) | byte;
|
|
1605
|
-
bitsInBuffer += 8;
|
|
1606
|
-
while (bitsInBuffer >= 6) {
|
|
1607
|
-
bitsInBuffer -= 6;
|
|
1608
|
-
if (string.length < stringLength) {
|
|
1609
|
-
const charValue = (bitBuffer >> bitsInBuffer) & 0x3f;
|
|
1610
|
-
if (charValue < 0 || charValue >= urlAlphabet.length) {
|
|
1611
|
-
throw new ProtocolDecodeError("invalid charValue");
|
|
1612
|
-
}
|
|
1613
|
-
string += urlAlphabet[charValue];
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
const result = Base64Url256.from(string);
|
|
1619
|
-
if (!result.ok) throw new ProtocolDecodeError(result.error.type);
|
|
1620
|
-
|
|
1621
|
-
return result.value;
|
|
1633
|
+
const decodeId = (buffer: Buffer): Id => {
|
|
1634
|
+
const bytes = buffer.shiftN(idBytesTypeValueLength);
|
|
1635
|
+
return idBytesToId(bytes as IdBytes);
|
|
1622
1636
|
};
|
|
1623
1637
|
|
|
1624
|
-
const decodeId = (buffer: Buffer): Id =>
|
|
1625
|
-
decodeBase64Url256(buffer, idTypeValueLength) as Id;
|
|
1626
|
-
|
|
1627
|
-
/** Not all 16 bytes are valid {@link OwnerId}. */
|
|
1628
|
-
const decodeOwnerId = (buffer: Buffer): OwnerId => decodeId(buffer) as OwnerId;
|
|
1629
|
-
|
|
1630
1638
|
/**
|
|
1631
1639
|
* Evolu uses MessagePack to handle all number variants except for
|
|
1632
1640
|
* NonNegativeInt. For NonNegativeInt, Evolu provides more efficient encoding.
|
|
1633
1641
|
*/
|
|
1634
1642
|
export const encodeNumber = (buffer: Buffer, number: number): void => {
|
|
1635
|
-
buffer.extend(pack(number));
|
|
1643
|
+
buffer.extend(packr.pack(number));
|
|
1636
1644
|
};
|
|
1637
1645
|
|
|
1638
1646
|
export const decodeNumber = (buffer: Buffer): number => {
|
|
1639
1647
|
let number: unknown;
|
|
1640
1648
|
let end: unknown;
|
|
1641
1649
|
|
|
1642
|
-
unpackMultiple(
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1650
|
+
packr.unpackMultiple(
|
|
1651
|
+
buffer.unwrap(),
|
|
1652
|
+
(n: unknown, _: unknown, e: unknown) => {
|
|
1653
|
+
number = n;
|
|
1654
|
+
end = e;
|
|
1655
|
+
return false;
|
|
1656
|
+
},
|
|
1657
|
+
);
|
|
1647
1658
|
|
|
1648
1659
|
const endResult = NonNegativeInt.fromUnknown(end);
|
|
1649
1660
|
if (!endResult.ok) throw new ProtocolDecodeError(endResult.error.type);
|
|
@@ -1655,42 +1666,90 @@ export const decodeNumber = (buffer: Buffer): number => {
|
|
|
1655
1666
|
return numberResult.value;
|
|
1656
1667
|
};
|
|
1657
1668
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1669
|
+
/**
|
|
1670
|
+
* Encodes an array of boolean flags into a single byte.
|
|
1671
|
+
*
|
|
1672
|
+
* Each element in the array corresponds to a bit (0-7). Array can have 0-8
|
|
1673
|
+
* elements.
|
|
1674
|
+
*
|
|
1675
|
+
* ### Example
|
|
1676
|
+
*
|
|
1677
|
+
* ```ts
|
|
1678
|
+
* encodeFlags(buffer, [true, false, true]); // Encodes bits 0, 1, 2
|
|
1679
|
+
* ```
|
|
1680
|
+
*/
|
|
1681
|
+
export const encodeFlags = (
|
|
1682
|
+
buffer: Buffer,
|
|
1683
|
+
flags: ReadonlyArray<boolean>,
|
|
1684
|
+
): void => {
|
|
1685
|
+
let byte = 0;
|
|
1686
|
+
for (let i = 0; i < flags.length && i < 8; i++) {
|
|
1687
|
+
if (flags[i]) {
|
|
1688
|
+
byte |= 1 << i;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
buffer.extend([byte]);
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* Decodes a byte into an array of boolean flags.
|
|
1696
|
+
*
|
|
1697
|
+
* ### Example
|
|
1698
|
+
*
|
|
1699
|
+
* ```ts
|
|
1700
|
+
* const flags = decodeFlags(buffer, 3); // Decode 3 flags
|
|
1701
|
+
* ```
|
|
1702
|
+
*/
|
|
1703
|
+
export const decodeFlags = (
|
|
1704
|
+
buffer: Buffer,
|
|
1705
|
+
count: PositiveInt,
|
|
1706
|
+
): ReadonlyArray<boolean> => {
|
|
1707
|
+
const byte = buffer.shift();
|
|
1708
|
+
const flags: Array<boolean> = [];
|
|
1709
|
+
for (let i = 0; i < count && i < 8; i++) {
|
|
1710
|
+
flags.push((byte & (1 << i)) !== 0);
|
|
1711
|
+
}
|
|
1712
|
+
return flags;
|
|
1663
1713
|
};
|
|
1664
1714
|
|
|
1665
1715
|
/**
|
|
1666
1716
|
* Encodes and encrypts a {@link DbChange} using the provided owner's encryption
|
|
1667
1717
|
* key. Returns an encrypted binary representation as {@link EncryptedDbChange}.
|
|
1718
|
+
*
|
|
1719
|
+
* The format includes the protocol version for backward compatibility and the
|
|
1720
|
+
* timestamp for tamper-proof verification that the timestamp matches the change
|
|
1721
|
+
* data.
|
|
1668
1722
|
*/
|
|
1669
1723
|
export const encodeAndEncryptDbChange =
|
|
1670
1724
|
(deps: SymmetricCryptoDep) =>
|
|
1671
|
-
(
|
|
1725
|
+
(message: CrdtMessage, key: EncryptionKey): EncryptedDbChange => {
|
|
1672
1726
|
const buffer = createBuffer();
|
|
1673
1727
|
|
|
1674
|
-
|
|
1728
|
+
encodeNonNegativeInt(buffer, protocolVersion);
|
|
1675
1729
|
|
|
1676
|
-
|
|
1730
|
+
// Encode the timestamp to prevent tampering (e.g., a malicious relay
|
|
1731
|
+
// assigning this EncryptedDbChange to a different EncryptedCrdtMessage)
|
|
1732
|
+
buffer.extend(timestampToTimestampBytes(message.timestamp));
|
|
1677
1733
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
);
|
|
1734
|
+
encodeFlags(buffer, [
|
|
1735
|
+
message.change.isInsert,
|
|
1736
|
+
message.change.isDelete != null,
|
|
1737
|
+
message.change.isDelete ?? false,
|
|
1738
|
+
]);
|
|
1683
1739
|
|
|
1684
|
-
|
|
1740
|
+
encodeString(buffer, message.change.table);
|
|
1741
|
+
buffer.extend(idToIdBytes(message.change.id));
|
|
1685
1742
|
|
|
1743
|
+
const entries = objectToEntries(message.change.values);
|
|
1744
|
+
|
|
1745
|
+
encodeLength(buffer, entries);
|
|
1686
1746
|
for (const [column, value] of entries) {
|
|
1687
|
-
|
|
1747
|
+
encodeString(buffer, column);
|
|
1688
1748
|
encodeSqliteValue(buffer, value);
|
|
1689
1749
|
}
|
|
1690
1750
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
buffer.extend(new Uint8Array(paddingLength));
|
|
1751
|
+
// Add PADMÉ padding (ignored during decoding)
|
|
1752
|
+
buffer.extend(createPadmePadding(buffer.getLength()));
|
|
1694
1753
|
|
|
1695
1754
|
const { nonce, ciphertext } = deps.symmetricCrypto.encrypt(
|
|
1696
1755
|
buffer.unwrap(),
|
|
@@ -1706,50 +1765,82 @@ export const encodeAndEncryptDbChange =
|
|
|
1706
1765
|
};
|
|
1707
1766
|
|
|
1708
1767
|
/**
|
|
1709
|
-
* Decrypts and decodes an {@link
|
|
1710
|
-
* encryption key.
|
|
1768
|
+
* Decrypts and decodes an {@link EncryptedCrdtMessage} using the provided
|
|
1769
|
+
* owner's encryption key. Verifies that the embedded timestamp matches the
|
|
1770
|
+
* expected timestamp to ensure message integrity.
|
|
1711
1771
|
*/
|
|
1712
1772
|
export const decryptAndDecodeDbChange =
|
|
1713
1773
|
(deps: SymmetricCryptoDep) =>
|
|
1714
1774
|
(
|
|
1715
|
-
|
|
1775
|
+
message: EncryptedCrdtMessage,
|
|
1716
1776
|
key: EncryptionKey,
|
|
1717
|
-
): Result<
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1777
|
+
): Result<
|
|
1778
|
+
DbChange,
|
|
1779
|
+
| SymmetricCryptoDecryptError
|
|
1780
|
+
| ProtocolInvalidDataError
|
|
1781
|
+
| ProtocolTimestampMismatchError
|
|
1782
|
+
> => {
|
|
1783
|
+
try {
|
|
1784
|
+
const buffer = createBuffer(message.change);
|
|
1785
|
+
|
|
1786
|
+
const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
|
|
1787
|
+
const ciphertext = buffer.shiftN(decodeLength(buffer));
|
|
1788
|
+
|
|
1789
|
+
const plaintextBytes = deps.symmetricCrypto.decrypt(
|
|
1790
|
+
ciphertext,
|
|
1791
|
+
key,
|
|
1792
|
+
nonce,
|
|
1793
|
+
);
|
|
1794
|
+
if (!plaintextBytes.ok) return plaintextBytes;
|
|
1732
1795
|
|
|
1733
|
-
|
|
1734
|
-
|
|
1796
|
+
buffer.reset();
|
|
1797
|
+
buffer.extend(plaintextBytes.value);
|
|
1735
1798
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1799
|
+
// Decode version (for future compatibility, not need yet)
|
|
1800
|
+
decodeNonNegativeInt(buffer);
|
|
1738
1801
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1802
|
+
const timestamp = timestampBytesToTimestamp(
|
|
1803
|
+
buffer.shiftN(timestampBytesLength) as TimestampBytes,
|
|
1804
|
+
);
|
|
1741
1805
|
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1806
|
+
if (!eqTimestamp(timestamp, message.timestamp)) {
|
|
1807
|
+
return err<ProtocolTimestampMismatchError>({
|
|
1808
|
+
type: "ProtocolTimestampMismatchError",
|
|
1809
|
+
expected: message.timestamp,
|
|
1810
|
+
timestamp,
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1747
1813
|
|
|
1748
|
-
|
|
1814
|
+
const flags = decodeFlags(buffer, PositiveInt.orThrow(3));
|
|
1815
|
+
const table = decodeString(buffer);
|
|
1816
|
+
const id = decodeId(buffer);
|
|
1749
1817
|
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1818
|
+
const length = decodeLength(buffer);
|
|
1819
|
+
const values = Object.create(null) as Record<string, SqliteValue>;
|
|
1820
|
+
|
|
1821
|
+
for (let i = 0; i < length; i++) {
|
|
1822
|
+
const column = decodeString(buffer);
|
|
1823
|
+
const value = decodeSqliteValue(buffer);
|
|
1824
|
+
values[column] = value;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
const dbChange = DbChange.orThrow({
|
|
1828
|
+
table,
|
|
1829
|
+
id,
|
|
1830
|
+
values,
|
|
1831
|
+
isInsert: flags[0],
|
|
1832
|
+
isDelete: flags[1] ? flags[2] : null,
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
return ok(dbChange);
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
return err<ProtocolInvalidDataError>({
|
|
1838
|
+
type: "ProtocolInvalidDataError",
|
|
1839
|
+
data: message.change,
|
|
1840
|
+
error,
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1753
1844
|
|
|
1754
1845
|
/**
|
|
1755
1846
|
* Encodes a non-negative integer into a variable-length integer format. It's
|
|
@@ -1807,7 +1898,7 @@ export const decodeNonNegativeInt = (buffer: Buffer): NonNegativeInt => {
|
|
|
1807
1898
|
};
|
|
1808
1899
|
|
|
1809
1900
|
export const encodeLength = (buffer: Buffer, value: ArrayLike<any>): void => {
|
|
1810
|
-
encodeNonNegativeInt(buffer, value.length
|
|
1901
|
+
encodeNonNegativeInt(buffer, NonNegativeInt.orThrow(value.length));
|
|
1811
1902
|
};
|
|
1812
1903
|
|
|
1813
1904
|
export const decodeLength = decodeNonNegativeInt;
|
|
@@ -1829,23 +1920,10 @@ export const encodeNodeId = (buffer: Buffer, nodeId: NodeId): void => {
|
|
|
1829
1920
|
};
|
|
1830
1921
|
|
|
1831
1922
|
export const decodeNodeId = (buffer: Buffer): NodeId => {
|
|
1832
|
-
const bytes = buffer.shiftN(8
|
|
1923
|
+
const bytes = buffer.shiftN(NonNegativeInt.orThrow(8));
|
|
1833
1924
|
return bytesToHex(bytes) as NodeId;
|
|
1834
1925
|
};
|
|
1835
1926
|
|
|
1836
|
-
export const encodeBase64Url256 = (
|
|
1837
|
-
buffer: Buffer,
|
|
1838
|
-
string: Base64Url256Variant,
|
|
1839
|
-
): void => {
|
|
1840
|
-
encodeLength(buffer, string);
|
|
1841
|
-
buffer.extend(base64Url256ToBytes(string));
|
|
1842
|
-
};
|
|
1843
|
-
|
|
1844
|
-
export const decodeBase64Url256WithLength = (buffer: Buffer): Base64Url256 => {
|
|
1845
|
-
const length = decodeLength(buffer);
|
|
1846
|
-
return decodeBase64Url256(buffer, length) as Base64Url256;
|
|
1847
|
-
};
|
|
1848
|
-
|
|
1849
1927
|
// Small ints are encoded into ProtocolValueType, saving one byte per int.
|
|
1850
1928
|
const isSmallInt: Predicate<number> = (value: number) =>
|
|
1851
1929
|
value >= 0 && value < 20;
|
|
@@ -1854,23 +1932,26 @@ export const ProtocolValueType = {
|
|
|
1854
1932
|
// 0-19 small ints
|
|
1855
1933
|
|
|
1856
1934
|
// SQLite types
|
|
1857
|
-
String: 20
|
|
1858
|
-
Number: 21
|
|
1859
|
-
Null: 22
|
|
1860
|
-
|
|
1935
|
+
String: NonNegativeInt.orThrow(20),
|
|
1936
|
+
Number: NonNegativeInt.orThrow(21),
|
|
1937
|
+
Null: NonNegativeInt.orThrow(22),
|
|
1938
|
+
Bytes: NonNegativeInt.orThrow(23),
|
|
1861
1939
|
// We can add more types for other DBs or anything else later.
|
|
1862
1940
|
|
|
1863
1941
|
// Optimized types
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1942
|
+
NonNegativeInt: NonNegativeInt.orThrow(30),
|
|
1943
|
+
|
|
1944
|
+
// String optimizations
|
|
1945
|
+
EmptyString: NonNegativeInt.orThrow(31), // 1 byte vs 2 bytes (50% reduction)
|
|
1946
|
+
Base64Url: NonNegativeInt.orThrow(32),
|
|
1947
|
+
Id: NonNegativeInt.orThrow(33),
|
|
1948
|
+
Json: NonNegativeInt.orThrow(34),
|
|
1868
1949
|
|
|
1869
1950
|
// new Date().toISOString() - 24 bytes
|
|
1870
1951
|
// encoded with fixed length - 8 bytes
|
|
1871
1952
|
// encode as NonNegativeInt - 6 bytes (additional 25% reduction)
|
|
1872
|
-
DateIsoWithNonNegativeTime:
|
|
1873
|
-
DateIsoWithNegativeTime:
|
|
1953
|
+
DateIsoWithNonNegativeTime: NonNegativeInt.orThrow(35),
|
|
1954
|
+
DateIsoWithNegativeTime: NonNegativeInt.orThrow(36), // 9 bytes
|
|
1874
1955
|
|
|
1875
1956
|
// TODO: Operations (from 40)
|
|
1876
1957
|
// Increment, Decrement, Patch, whatever.
|
|
@@ -1884,9 +1965,14 @@ export const encodeSqliteValue = (buffer: Buffer, value: SqliteValue): void => {
|
|
|
1884
1965
|
|
|
1885
1966
|
switch (typeof value) {
|
|
1886
1967
|
case "string": {
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1968
|
+
if (value === "") {
|
|
1969
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.EmptyString);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
const dateIso = DateIso.fromParent(value);
|
|
1974
|
+
if (dateIso.ok) {
|
|
1975
|
+
const time = new Date(dateIso.value).getTime();
|
|
1890
1976
|
if (NonNegativeInt.is(time)) {
|
|
1891
1977
|
encodeNonNegativeInt(
|
|
1892
1978
|
buffer,
|
|
@@ -1903,27 +1989,34 @@ export const encodeSqliteValue = (buffer: Buffer, value: SqliteValue): void => {
|
|
|
1903
1989
|
return;
|
|
1904
1990
|
}
|
|
1905
1991
|
|
|
1906
|
-
const
|
|
1907
|
-
if (
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
buffer.extend(base64Url256ToBytes(base64Url256.value));
|
|
1911
|
-
return;
|
|
1912
|
-
}
|
|
1913
|
-
encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url256);
|
|
1914
|
-
encodeBase64Url256(buffer, base64Url256.value);
|
|
1992
|
+
const id = Id.fromParent(value);
|
|
1993
|
+
if (id.ok) {
|
|
1994
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Id);
|
|
1995
|
+
buffer.extend(idToIdBytes(id.value));
|
|
1915
1996
|
return;
|
|
1916
1997
|
}
|
|
1917
1998
|
|
|
1918
|
-
const
|
|
1919
|
-
if
|
|
1920
|
-
|
|
1999
|
+
const json = Json.fromParent(value);
|
|
2000
|
+
// Only encode as Json if it survives JSON.parse/JSON.stringify round-trip.
|
|
2001
|
+
// Some valid JSON strings like "-0E0" get normalized to "0" during parsing,
|
|
2002
|
+
// which would cause data corruption if we don't verify round-trip safety.
|
|
2003
|
+
if (json.ok && JSON.stringify(jsonToJsonValue(json.value)) === value) {
|
|
2004
|
+
const jsonBytes = packr.pack(jsonToJsonValue(json.value));
|
|
1921
2005
|
encodeNonNegativeInt(buffer, ProtocolValueType.Json);
|
|
1922
2006
|
encodeLength(buffer, jsonBytes);
|
|
1923
2007
|
buffer.extend(jsonBytes);
|
|
1924
2008
|
return;
|
|
1925
2009
|
}
|
|
1926
2010
|
|
|
2011
|
+
const base64Url = Base64Url.fromParent(value);
|
|
2012
|
+
if (base64Url.ok) {
|
|
2013
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url);
|
|
2014
|
+
const bytes = base64UrlToUint8Array(base64Url.value);
|
|
2015
|
+
encodeLength(buffer, bytes);
|
|
2016
|
+
buffer.extend(bytes);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1927
2020
|
encodeNonNegativeInt(buffer, ProtocolValueType.String);
|
|
1928
2021
|
encodeString(buffer, value);
|
|
1929
2022
|
return;
|
|
@@ -1945,7 +2038,7 @@ export const encodeSqliteValue = (buffer: Buffer, value: SqliteValue): void => {
|
|
|
1945
2038
|
}
|
|
1946
2039
|
}
|
|
1947
2040
|
|
|
1948
|
-
encodeNonNegativeInt(buffer, ProtocolValueType.
|
|
2041
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Bytes);
|
|
1949
2042
|
encodeLength(buffer, value);
|
|
1950
2043
|
buffer.extend(value);
|
|
1951
2044
|
};
|
|
@@ -1960,39 +2053,50 @@ export const decodeSqliteValue = (buffer: Buffer): SqliteValue => {
|
|
|
1960
2053
|
switch (type) {
|
|
1961
2054
|
case ProtocolValueType.String:
|
|
1962
2055
|
return decodeString(buffer);
|
|
2056
|
+
|
|
1963
2057
|
case ProtocolValueType.Number:
|
|
1964
2058
|
return decodeNumber(buffer);
|
|
2059
|
+
|
|
1965
2060
|
case ProtocolValueType.Null:
|
|
1966
2061
|
return null;
|
|
1967
|
-
|
|
2062
|
+
|
|
2063
|
+
case ProtocolValueType.Bytes: {
|
|
1968
2064
|
const length = decodeLength(buffer);
|
|
1969
2065
|
return buffer.shiftN(length);
|
|
1970
2066
|
}
|
|
1971
|
-
|
|
2067
|
+
|
|
2068
|
+
case ProtocolValueType.Id:
|
|
1972
2069
|
return decodeId(buffer);
|
|
1973
|
-
|
|
1974
|
-
case ProtocolValueType.Base64Url256:
|
|
1975
|
-
return decodeBase64Url256WithLength(buffer);
|
|
2070
|
+
|
|
1976
2071
|
case ProtocolValueType.NonNegativeInt:
|
|
1977
2072
|
return decodeNonNegativeInt(buffer);
|
|
2073
|
+
|
|
1978
2074
|
case ProtocolValueType.Json: {
|
|
1979
2075
|
const length = decodeLength(buffer);
|
|
1980
2076
|
const bytes = buffer.shiftN(length);
|
|
1981
|
-
return JSON.stringify(unpack(bytes));
|
|
2077
|
+
return JSON.stringify(packr.unpack(bytes));
|
|
1982
2078
|
}
|
|
2079
|
+
|
|
1983
2080
|
case ProtocolValueType.DateIsoWithNonNegativeTime:
|
|
1984
2081
|
case ProtocolValueType.DateIsoWithNegativeTime: {
|
|
1985
2082
|
const time =
|
|
1986
2083
|
type === ProtocolValueType.DateIsoWithNonNegativeTime
|
|
1987
2084
|
? decodeNonNegativeInt(buffer)
|
|
1988
2085
|
: decodeNumber(buffer);
|
|
1989
|
-
const
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2086
|
+
const dateIso = DateIso.fromParent(new Date(time).toISOString());
|
|
2087
|
+
if (!dateIso.ok) throw new ProtocolDecodeError(dateIso.error.type);
|
|
2088
|
+
return dateIso.value;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
case ProtocolValueType.EmptyString:
|
|
2092
|
+
return "";
|
|
2093
|
+
|
|
2094
|
+
case ProtocolValueType.Base64Url: {
|
|
2095
|
+
const length = decodeLength(buffer);
|
|
2096
|
+
const bytes = buffer.shiftN(length);
|
|
2097
|
+
return uint8ArrayToBase64Url(bytes);
|
|
1995
2098
|
}
|
|
2099
|
+
|
|
1996
2100
|
default:
|
|
1997
2101
|
throw new ProtocolDecodeError("invalid ProtocolValueType");
|
|
1998
2102
|
}
|