@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20
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/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +1 -1
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- 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 +48 -37
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +27 -50
- package/dist/src/Evolu/Db.d.ts +138 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +248 -645
- 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 +79 -116
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +275 -132
- 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 +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +129 -83
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +80 -89
- 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 +114 -191
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +409 -416
- 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.js +3 -3
- package/dist/src/Evolu/Relay.d.ts +1 -2
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +11 -9
- package/dist/src/Evolu/Schema.d.ts +88 -27
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +141 -24
- package/dist/src/Evolu/Storage.d.ts +158 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +32 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +29 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +20 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +144 -22
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +5 -2
- package/dist/src/Sqlite.d.ts +20 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +431 -341
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +458 -466
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- 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 +2 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/package.json +7 -7
- package/src/Assert.ts +2 -4
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +78 -91
- package/src/Evolu/Db.ts +455 -947
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +545 -307
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +191 -131
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +536 -653
- package/src/Evolu/Public.ts +7 -9
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Relay.ts +17 -12
- package/src/Evolu/Schema.ts +271 -66
- package/src/Evolu/Storage.ts +263 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +30 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +149 -23
- package/src/Sqlite.ts +59 -24
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +657 -695
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +2 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -82
- 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/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/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -97
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Protocol.ts
CHANGED
|
@@ -4,15 +4,22 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Evolu Protocol is a local-first, end-to-end encrypted binary synchronization
|
|
6
6
|
* protocol optimized for minimal size and maximum speed. It enables data sync
|
|
7
|
-
* between a client and a relay
|
|
8
|
-
*
|
|
7
|
+
* between a client and a relay. In the future, direct peer-to-peer (P2P) sync
|
|
8
|
+
* between clients will be possible without a relay.
|
|
9
|
+
*
|
|
10
|
+
* Relays don't need to sync with each other—clients using those relays will
|
|
11
|
+
* sync them eventually. If a relay is offline (e.g., for maintenance), it will
|
|
12
|
+
* sync automatically later via client sync logic. For relay backup using
|
|
13
|
+
* SQLite, see https://sqlite.org/rsync.html (uses a similar algorithm to Evolu
|
|
14
|
+
* RBSR).
|
|
9
15
|
*
|
|
10
16
|
* Evolu Protocol is designed for SQLite but can be extended to any database. It
|
|
11
17
|
* implements [Range-Based Set
|
|
12
18
|
* Reconciliation](https://arxiv.org/abs/2212.13567). To learn how RBSR works,
|
|
13
19
|
* check [Negentropy](https://logperiodic.com/rbsr.html). Evolu Protocol is
|
|
14
20
|
* similar to Negentropy but uses different encoding and also provides data
|
|
15
|
-
* transfer and
|
|
21
|
+
* transfer, ownership, real-time broadcasting, request-response semantics, and
|
|
22
|
+
* error handling.
|
|
16
23
|
*
|
|
17
24
|
* ### Message Structure
|
|
18
25
|
*
|
|
@@ -21,12 +28,15 @@
|
|
|
21
28
|
* | **Header** | |
|
|
22
29
|
* | - {@link protocolVersion} | |
|
|
23
30
|
* | - {@link OwnerId} | {@link Owner} |
|
|
24
|
-
* |
|
|
25
|
-
* |
|
|
26
|
-
* | -
|
|
27
|
-
* | - {@link
|
|
28
|
-
* |
|
|
31
|
+
* | - messageType | {@link MessageType} |
|
|
32
|
+
* | **Request (messageType=0)** | |
|
|
33
|
+
* | - hasWriteKey | 0 = no, 1 = yes |
|
|
34
|
+
* | - {@link OwnerWriteKey} | If hasWriteKey = 1 |
|
|
35
|
+
* | - subscriptionFlag | {@link SubscriptionFlags} |
|
|
36
|
+
* | **Response (messageType=1)** | |
|
|
29
37
|
* | - {@link ProtocolErrorCode} | |
|
|
38
|
+
* | **Broadcast (messageType=2)** | |
|
|
39
|
+
* | - (no additional fields) | |
|
|
30
40
|
* | **Messages** | |
|
|
31
41
|
* | - {@link NonNegativeInt} | A number of messages. |
|
|
32
42
|
* | - {@link EncryptedCrdtMessage} | |
|
|
@@ -36,19 +46,12 @@
|
|
|
36
46
|
*
|
|
37
47
|
* ### WriteKey Validation
|
|
38
48
|
*
|
|
39
|
-
* The initiator sends
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* ### WriteKey Rotation
|
|
46
|
-
*
|
|
47
|
-
* When initiator's {@link WriteKeyMode} is `Rotation`, two WriteKeys are
|
|
48
|
-
* present:
|
|
49
|
-
*
|
|
50
|
-
* 1. Current WriteKey (for validation)
|
|
51
|
-
* 2. New WriteKey (to be stored)
|
|
49
|
+
* The initiator sends a hasWriteKey flag and optionally a WriteKey. The
|
|
50
|
+
* WriteKey is required when sending messages as a secure token proving the
|
|
51
|
+
* initiator can write changes. It's ok to not send a WriteKey if the initiator
|
|
52
|
+
* is only syncing (read-only) and not sending messages. The non-initiator
|
|
53
|
+
* validates the WriteKey immediately after parsing the initiator header, before
|
|
54
|
+
* processing any messages or ranges.
|
|
52
55
|
*
|
|
53
56
|
* ### Synchronization
|
|
54
57
|
*
|
|
@@ -64,18 +67,18 @@
|
|
|
64
67
|
*
|
|
65
68
|
* The **non-initiator always responds** to provide sync completion feedback,
|
|
66
69
|
* even with empty messages containing only the header and no error. This allows
|
|
67
|
-
* the initiator to
|
|
70
|
+
* the initiator to detect when synchronization is complete.
|
|
68
71
|
*
|
|
69
72
|
* Both **Messages** and **Ranges** are optional, allowing each side to send,
|
|
70
73
|
* sync, or only subscribe data as needed.
|
|
71
74
|
*
|
|
72
|
-
* When the initiator sends data, the {@link
|
|
73
|
-
*
|
|
74
|
-
* responds without a {@link
|
|
75
|
-
* signals it wants data. If the non-initiator detects an issue, it
|
|
76
|
-
* error code via the `Error` field in the header back to the
|
|
77
|
-
* relay-to-relay or P2P sync, both sides may require the
|
|
78
|
-
* depending on who is the initiator.
|
|
75
|
+
* When the initiator sends data, the {@link OwnerWriteKey} is required as a
|
|
76
|
+
* secure token proving the initiator can write changes. The non-initiator
|
|
77
|
+
* responds without a {@link OwnerWriteKey}, since the initiator’s request
|
|
78
|
+
* already signals it wants data. If the non-initiator detects an issue, it
|
|
79
|
+
* sends an error code via the `Error` field in the header back to the
|
|
80
|
+
* initiator. In relay-to-relay or P2P sync, both sides may require the
|
|
81
|
+
* {@link OwnerWriteKey} depending on who is the initiator.
|
|
79
82
|
*
|
|
80
83
|
* ### Protocol Errors
|
|
81
84
|
*
|
|
@@ -89,15 +92,21 @@
|
|
|
89
92
|
* - {@link ProtocolUnsupportedVersionError}: Protocol version mismatch.
|
|
90
93
|
* - {@link ProtocolInvalidDataError}: The message is malformed or corrupted.
|
|
91
94
|
*
|
|
92
|
-
* All protocol errors except `ProtocolInvalidDataError` include the `
|
|
95
|
+
* All protocol errors except `ProtocolInvalidDataError` include the `OwnerId`
|
|
93
96
|
* to allow clients to associate errors with the correct owner.
|
|
94
97
|
*
|
|
95
98
|
* ### Message Size Limit
|
|
96
99
|
*
|
|
97
100
|
* The protocol enforces a strict maximum size for all messages, defined by
|
|
98
101
|
* {@link maxProtocolMessageSize}. This ensures every {@link ProtocolMessage} is
|
|
99
|
-
* less than or equal to this limit,
|
|
100
|
-
*
|
|
102
|
+
* less than or equal to this limit, enabling stateless transports, simplified
|
|
103
|
+
* relay implementation, and predictable memory usage. When all messages don't
|
|
104
|
+
* fit within the limit, the protocol automatically continues synchronization in
|
|
105
|
+
* subsequent rounds using range-based reconciliation.
|
|
106
|
+
*
|
|
107
|
+
* Individual database mutations are limited to `maxMutationSize` (640KB), which
|
|
108
|
+
* is smaller than the protocol message limit to ensure efficient sync with
|
|
109
|
+
* {@link maxProtocolMessageRangesSize}.
|
|
101
110
|
*
|
|
102
111
|
* ### Why Binary?
|
|
103
112
|
*
|
|
@@ -105,13 +114,12 @@
|
|
|
105
114
|
*
|
|
106
115
|
* - Encrypted data doesn’t compress well, unlike plain JSON.
|
|
107
116
|
* - Message size must be controlled during creation.
|
|
108
|
-
* - Sequential byte reading is faster than parsing and
|
|
117
|
+
* - Sequential byte reading is faster than parsing and avoids conversions.
|
|
109
118
|
*
|
|
110
119
|
* It uses structure-aware encoding, significantly outperforming generic binary
|
|
111
120
|
* serialization formats with the following optimizations:
|
|
112
121
|
*
|
|
113
122
|
* - **NonNegativeInt:** Up to 33% smaller than MessagePack.
|
|
114
|
-
* - **Base64Url Strings:** Up to 25% size reduction.
|
|
115
123
|
* - **DateIso:** Up to 75% smaller.
|
|
116
124
|
* - **Timestamp Encoding:** Delta encoding for milliseconds and run-length
|
|
117
125
|
* encoding (RLE) for counters and NodeIds.
|
|
@@ -144,10 +152,10 @@
|
|
|
144
152
|
* @module
|
|
145
153
|
*/
|
|
146
154
|
|
|
147
|
-
import {
|
|
148
|
-
import { pack, unpack, unpackMultiple } from "msgpackr";
|
|
155
|
+
import { Packr } from "msgpackr";
|
|
149
156
|
import { isNonEmptyReadonlyArray, NonEmptyReadonlyArray } from "../Array.js";
|
|
150
157
|
import { assert } from "../Assert.js";
|
|
158
|
+
import { Brand } from "../Brand.js";
|
|
151
159
|
import {
|
|
152
160
|
Buffer,
|
|
153
161
|
bytesToHex,
|
|
@@ -157,9 +165,9 @@ import {
|
|
|
157
165
|
utf8ToBytes,
|
|
158
166
|
} from "../Buffer.js";
|
|
159
167
|
import {
|
|
160
|
-
CreateRandomBytesDep,
|
|
161
168
|
EncryptionKey,
|
|
162
169
|
padmePaddingLength,
|
|
170
|
+
RandomBytesDep,
|
|
163
171
|
SymmetricCryptoDecryptError,
|
|
164
172
|
SymmetricCryptoDep,
|
|
165
173
|
} from "../Crypto.js";
|
|
@@ -170,34 +178,66 @@ import { err, ok, Result } from "../Result.js";
|
|
|
170
178
|
import { SqliteValue } from "../Sqlite.js";
|
|
171
179
|
import {
|
|
172
180
|
Base64Url,
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
base64UrlToUint8Array,
|
|
182
|
+
DateIso,
|
|
175
183
|
Id,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
IdBytes,
|
|
185
|
+
idBytesToId,
|
|
186
|
+
idBytesTypeValueLength,
|
|
187
|
+
idToIdBytes,
|
|
188
|
+
Json,
|
|
189
|
+
jsonToJsonValue,
|
|
180
190
|
NonNegativeInt,
|
|
181
191
|
Number,
|
|
182
|
-
object,
|
|
183
192
|
PositiveInt,
|
|
184
|
-
|
|
193
|
+
uint8ArrayToBase64Url,
|
|
185
194
|
} from "../Type.js";
|
|
186
195
|
import { Predicate } from "../Types.js";
|
|
187
|
-
import { Brand } from "../Brand.js";
|
|
188
|
-
import { Owner, OwnerId, WriteKey, writeKeyLength } from "./Owner.js";
|
|
189
196
|
import {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
Owner,
|
|
198
|
+
OwnerId,
|
|
199
|
+
OwnerIdBytes,
|
|
200
|
+
ownerIdToOwnerIdBytes,
|
|
201
|
+
OwnerWriteKey,
|
|
202
|
+
ownerWriteKeyLength,
|
|
203
|
+
} from "./Owner.js";
|
|
204
|
+
import {
|
|
205
|
+
BaseRange,
|
|
206
|
+
CrdtMessage,
|
|
207
|
+
DbChange,
|
|
208
|
+
EncryptedCrdtMessage,
|
|
209
|
+
EncryptedDbChange,
|
|
210
|
+
Fingerprint,
|
|
211
|
+
FingerprintRange,
|
|
212
|
+
fingerprintSize,
|
|
213
|
+
InfiniteUpperBound,
|
|
214
|
+
Range,
|
|
215
|
+
RangeType,
|
|
216
|
+
RangeUpperBound,
|
|
217
|
+
SkipRange,
|
|
218
|
+
StorageDep,
|
|
219
|
+
TimestampsRange,
|
|
220
|
+
} from "./Storage.js";
|
|
221
|
+
import {
|
|
193
222
|
Counter,
|
|
194
223
|
eqTimestamp,
|
|
195
224
|
Millis,
|
|
196
225
|
NodeId,
|
|
197
226
|
Timestamp,
|
|
198
|
-
|
|
227
|
+
TimestampBytes,
|
|
228
|
+
timestampBytesLength,
|
|
229
|
+
timestampBytesToTimestamp,
|
|
230
|
+
timestampToTimestampBytes,
|
|
199
231
|
} from "./Timestamp.js";
|
|
200
232
|
|
|
233
|
+
/**
|
|
234
|
+
* MessagePack serializer for standard compatibility and compact encoding.
|
|
235
|
+
*
|
|
236
|
+
* - `variableMapSize: true` - More compact maps, ~5-10% slower encoding
|
|
237
|
+
* - `useRecords: false` - Standard MessagePack without extensions
|
|
238
|
+
*/
|
|
239
|
+
const packr = new Packr({ variableMapSize: true, useRecords: false });
|
|
240
|
+
|
|
201
241
|
/** Maximum size of the entire protocol message in bytes. */
|
|
202
242
|
export const maxProtocolMessageSize = 1_000_000 as PositiveInt;
|
|
203
243
|
|
|
@@ -210,6 +250,29 @@ export type ProtocolMessage = Uint8Array & Brand<"ProtocolMessage">;
|
|
|
210
250
|
/** Evolu Protocol version. */
|
|
211
251
|
export const protocolVersion = 0 as NonNegativeInt;
|
|
212
252
|
|
|
253
|
+
export const MessageType = {
|
|
254
|
+
/** Request message from initiator (client) to non-initiator (relay). */
|
|
255
|
+
Request: 0,
|
|
256
|
+
/** Response message from non-initiator (relay) to initiator (client). */
|
|
257
|
+
Response: 1,
|
|
258
|
+
/** Broadcast message from non-initiator (relay) to subscribed clients. */
|
|
259
|
+
Broadcast: 2,
|
|
260
|
+
} as const;
|
|
261
|
+
|
|
262
|
+
export type MessageType = (typeof MessageType)[keyof typeof MessageType];
|
|
263
|
+
|
|
264
|
+
export const SubscriptionFlags = {
|
|
265
|
+
/** No subscription changes for this owner. */
|
|
266
|
+
None: 0,
|
|
267
|
+
/** Subscribe to updates for this owner. */
|
|
268
|
+
Subscribe: 1,
|
|
269
|
+
/** Unsubscribe from updates for this owner. */
|
|
270
|
+
Unsubscribe: 2,
|
|
271
|
+
} as const;
|
|
272
|
+
|
|
273
|
+
export type SubscriptionFlag =
|
|
274
|
+
(typeof SubscriptionFlags)[keyof typeof SubscriptionFlags];
|
|
275
|
+
|
|
213
276
|
export const ProtocolErrorCode = {
|
|
214
277
|
NoError: 0,
|
|
215
278
|
/** A code for {@link ProtocolWriteKeyError}. */
|
|
@@ -223,179 +286,6 @@ export const ProtocolErrorCode = {
|
|
|
223
286
|
type ProtocolErrorCode =
|
|
224
287
|
(typeof ProtocolErrorCode)[keyof typeof ProtocolErrorCode];
|
|
225
288
|
|
|
226
|
-
export const WriteKeyMode = {
|
|
227
|
-
None: 0,
|
|
228
|
-
Single: 1,
|
|
229
|
-
Rotation: 2,
|
|
230
|
-
} as const;
|
|
231
|
-
|
|
232
|
-
type WriteKeyMode = (typeof WriteKeyMode)[keyof typeof WriteKeyMode];
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Evolu Protocol Storage
|
|
236
|
-
*
|
|
237
|
-
* The protocol is agnostic to storage implementation details—any storage can be
|
|
238
|
-
* plugged in, as long as it implements this interface. Implementations must
|
|
239
|
-
* handle their own errors; return values only indicates overall success or
|
|
240
|
-
* failure.
|
|
241
|
-
*/
|
|
242
|
-
export interface Storage {
|
|
243
|
-
readonly getSize: (ownerId: BinaryOwnerId) => NonNegativeInt | null;
|
|
244
|
-
|
|
245
|
-
readonly fingerprint: (
|
|
246
|
-
ownerId: BinaryOwnerId,
|
|
247
|
-
begin: NonNegativeInt,
|
|
248
|
-
end: NonNegativeInt,
|
|
249
|
-
) => Fingerprint | null;
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Computes fingerprints with their upper bounds in one call.
|
|
253
|
-
*
|
|
254
|
-
* This function can be replaced with many fingerprint/findLowerBound calls,
|
|
255
|
-
* but implementations can leverage it for batching and more efficient
|
|
256
|
-
* fingerprint computation.
|
|
257
|
-
*/
|
|
258
|
-
readonly fingerprintRanges: (
|
|
259
|
-
ownerId: BinaryOwnerId,
|
|
260
|
-
buckets: ReadonlyArray<NonNegativeInt>,
|
|
261
|
-
upperBound?: RangeUpperBound,
|
|
262
|
-
) => ReadonlyArray<FingerprintRange> | null;
|
|
263
|
-
|
|
264
|
-
readonly findLowerBound: (
|
|
265
|
-
ownerId: BinaryOwnerId,
|
|
266
|
-
begin: NonNegativeInt,
|
|
267
|
-
end: NonNegativeInt,
|
|
268
|
-
upperBound: RangeUpperBound,
|
|
269
|
-
) => NonNegativeInt | null;
|
|
270
|
-
|
|
271
|
-
readonly iterate: (
|
|
272
|
-
ownerId: BinaryOwnerId,
|
|
273
|
-
begin: NonNegativeInt,
|
|
274
|
-
end: NonNegativeInt,
|
|
275
|
-
callback: (timestamp: BinaryTimestamp, index: NonNegativeInt) => boolean,
|
|
276
|
-
) => void;
|
|
277
|
-
|
|
278
|
-
/** Validates the {@link WriteKey} for the given {@link Owner}. */
|
|
279
|
-
readonly validateWriteKey: (
|
|
280
|
-
ownerId: BinaryOwnerId,
|
|
281
|
-
writeKey: WriteKey,
|
|
282
|
-
) => boolean;
|
|
283
|
-
|
|
284
|
-
/** Sets the {@link WriteKey} for the given {@link Owner}. */
|
|
285
|
-
readonly setWriteKey: (ownerId: BinaryOwnerId, writeKey: WriteKey) => boolean;
|
|
286
|
-
|
|
287
|
-
/** Write encrypted {@link CrdtMessage}s to storage. */
|
|
288
|
-
readonly writeMessages: (
|
|
289
|
-
ownerId: BinaryOwnerId,
|
|
290
|
-
messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
|
|
291
|
-
) => boolean;
|
|
292
|
-
|
|
293
|
-
/** Read encrypted {@link DbChange}s from storage. */
|
|
294
|
-
readonly readDbChange: (
|
|
295
|
-
ownerId: BinaryOwnerId,
|
|
296
|
-
timestamp: BinaryTimestamp,
|
|
297
|
-
) => EncryptedDbChange | null;
|
|
298
|
-
|
|
299
|
-
/** Delete all data for the given {@link Owner}. */
|
|
300
|
-
readonly deleteOwner: (ownerId: BinaryOwnerId) => boolean;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export interface StorageDep {
|
|
304
|
-
readonly storage: Storage;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** An encrypted {@link CrdtMessage}. */
|
|
308
|
-
export interface EncryptedCrdtMessage {
|
|
309
|
-
readonly timestamp: Timestamp;
|
|
310
|
-
readonly change: EncryptedDbChange;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/** Encrypted DbChange */
|
|
314
|
-
export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* A CRDT message that combines a unique {@link Timestamp} with a
|
|
318
|
-
* {@link DbChange}.
|
|
319
|
-
*/
|
|
320
|
-
export interface CrdtMessage {
|
|
321
|
-
readonly timestamp: Timestamp;
|
|
322
|
-
readonly change: DbChange;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
327
|
-
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
328
|
-
*/
|
|
329
|
-
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
330
|
-
export type Base64Url256 = typeof Base64Url256.Type;
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* A DbChange is a change to a table row. Together with a unique
|
|
334
|
-
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
335
|
-
*/
|
|
336
|
-
export const DbChange = object({
|
|
337
|
-
table: Base64Url256,
|
|
338
|
-
id: Id,
|
|
339
|
-
values: record(Base64Url256, SqliteValue),
|
|
340
|
-
});
|
|
341
|
-
export type DbChange = typeof DbChange.Type;
|
|
342
|
-
|
|
343
|
-
export const RangeType = {
|
|
344
|
-
Fingerprint: 1,
|
|
345
|
-
Skip: 0,
|
|
346
|
-
Timestamps: 2,
|
|
347
|
-
} as const;
|
|
348
|
-
|
|
349
|
-
export type RangeType = (typeof RangeType)[keyof typeof RangeType];
|
|
350
|
-
|
|
351
|
-
export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
|
|
352
|
-
export type InfiniteUpperBound = typeof InfiniteUpperBound;
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Union type for Range's upperBound: either a {@link BinaryTimestamp} or
|
|
356
|
-
* {@link InfiniteUpperBound}.
|
|
357
|
-
*/
|
|
358
|
-
export type RangeUpperBound = BinaryTimestamp | InfiniteUpperBound;
|
|
359
|
-
|
|
360
|
-
interface BaseRange {
|
|
361
|
-
readonly upperBound: RangeUpperBound;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export interface SkipRange extends BaseRange {
|
|
365
|
-
readonly type: typeof RangeType.Skip;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export interface FingerprintRange extends BaseRange {
|
|
369
|
-
readonly type: typeof RangeType.Fingerprint;
|
|
370
|
-
readonly fingerprint: Fingerprint;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* A cryptographic hash used for efficiently comparing collections of
|
|
375
|
-
* {@link BinaryTimestamp}s.
|
|
376
|
-
*
|
|
377
|
-
* It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
|
|
378
|
-
* one or more timestamps.
|
|
379
|
-
*/
|
|
380
|
-
export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
|
|
381
|
-
|
|
382
|
-
export const fingerprintSize = 12 as NonNegativeInt;
|
|
383
|
-
|
|
384
|
-
/** A fingerprint of an empty range. */
|
|
385
|
-
export const zeroFingerprint = new Uint8Array(fingerprintSize) as Fingerprint;
|
|
386
|
-
|
|
387
|
-
export interface TimestampsRange extends BaseRange {
|
|
388
|
-
readonly type: typeof RangeType.Timestamps;
|
|
389
|
-
readonly timestamps: ReadonlyArray<BinaryTimestamp>;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export interface TimestampsRangeWithTimestampsBuffer extends BaseRange {
|
|
393
|
-
readonly type: typeof RangeType.Timestamps;
|
|
394
|
-
readonly timestamps: TimestampsBuffer;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
export type Range = SkipRange | FingerprintRange | TimestampsRange;
|
|
398
|
-
|
|
399
289
|
export type ProtocolError =
|
|
400
290
|
| ProtocolUnsupportedVersionError
|
|
401
291
|
| ProtocolInvalidDataError
|
|
@@ -427,7 +317,7 @@ export interface ProtocolInvalidDataError {
|
|
|
427
317
|
readonly error: unknown;
|
|
428
318
|
}
|
|
429
319
|
|
|
430
|
-
/** Error when a {@link
|
|
320
|
+
/** Error when a {@link OwnerWriteKey} is invalid, missing, or fails validation. */
|
|
431
321
|
export interface ProtocolWriteKeyError extends ProtocolErrorBase {
|
|
432
322
|
readonly type: "ProtocolWriteKeyError";
|
|
433
323
|
}
|
|
@@ -467,14 +357,14 @@ export interface ProtocolTimestampMismatchError {
|
|
|
467
357
|
* unidirectional and stateless transports.
|
|
468
358
|
*/
|
|
469
359
|
export const createProtocolMessageFromCrdtMessages =
|
|
470
|
-
(deps:
|
|
360
|
+
(deps: RandomBytesDep & SymmetricCryptoDep) =>
|
|
471
361
|
(
|
|
472
362
|
owner: Owner,
|
|
473
363
|
messages: NonEmptyReadonlyArray<CrdtMessage>,
|
|
474
364
|
maxSize?: PositiveInt,
|
|
475
365
|
): ProtocolMessage => {
|
|
476
366
|
const buffer = createProtocolMessageBuffer(owner.id, {
|
|
477
|
-
|
|
367
|
+
messageType: MessageType.Request,
|
|
478
368
|
totalMaxSize: maxSize ?? maxProtocolMessageSize,
|
|
479
369
|
writeKey: owner.writeKey,
|
|
480
370
|
});
|
|
@@ -511,7 +401,7 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
511
401
|
* For now, using a random fingerprint avoids extra complexity and is good
|
|
512
402
|
* enough for this case.
|
|
513
403
|
*/
|
|
514
|
-
const randomFingerprint = deps.
|
|
404
|
+
const randomFingerprint = deps.randomBytes.create(
|
|
515
405
|
fingerprintSize,
|
|
516
406
|
) as unknown as Fingerprint;
|
|
517
407
|
|
|
@@ -529,16 +419,22 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
529
419
|
/** Creates a {@link ProtocolMessage} for sync. */
|
|
530
420
|
export const createProtocolMessageForSync =
|
|
531
421
|
(deps: StorageDep) =>
|
|
532
|
-
(
|
|
533
|
-
|
|
534
|
-
|
|
422
|
+
(
|
|
423
|
+
ownerId: OwnerId,
|
|
424
|
+
subscriptionFlag?: SubscriptionFlag,
|
|
425
|
+
): ProtocolMessage | null => {
|
|
426
|
+
const buffer = createProtocolMessageBuffer(ownerId, {
|
|
427
|
+
messageType: MessageType.Request,
|
|
428
|
+
subscriptionFlag: subscriptionFlag ?? SubscriptionFlags.None,
|
|
429
|
+
});
|
|
430
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
535
431
|
|
|
536
|
-
const size = deps.storage.getSize(
|
|
432
|
+
const size = deps.storage.getSize(ownerIdBytes);
|
|
537
433
|
// Errors are handled by the storage.
|
|
538
434
|
if (size == null) return null;
|
|
539
435
|
|
|
540
436
|
splitRange(deps)(
|
|
541
|
-
|
|
437
|
+
ownerIdBytes,
|
|
542
438
|
0 as NonNegativeInt,
|
|
543
439
|
size,
|
|
544
440
|
InfiniteUpperBound,
|
|
@@ -548,18 +444,13 @@ export const createProtocolMessageForSync =
|
|
|
548
444
|
return buffer.unwrap();
|
|
549
445
|
};
|
|
550
446
|
|
|
551
|
-
|
|
552
|
-
export const createProtocolMessageForWriteKeyRotation = (
|
|
447
|
+
export const createProtocolMessageForUnsubscribe = (
|
|
553
448
|
ownerId: OwnerId,
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
writeKey: [currentWriteKey, newWriteKey],
|
|
560
|
-
});
|
|
561
|
-
return buffer.unwrap();
|
|
562
|
-
};
|
|
449
|
+
): ProtocolMessage =>
|
|
450
|
+
createProtocolMessageBuffer(ownerId, {
|
|
451
|
+
messageType: MessageType.Request,
|
|
452
|
+
subscriptionFlag: SubscriptionFlags.Unsubscribe,
|
|
453
|
+
}).unwrap();
|
|
563
454
|
|
|
564
455
|
/**
|
|
565
456
|
* Mutable builder for constructing {@link ProtocolMessage} respecting size
|
|
@@ -593,14 +484,17 @@ export const createProtocolMessageBuffer = (
|
|
|
593
484
|
readonly version?: NonNegativeInt;
|
|
594
485
|
} & (
|
|
595
486
|
| {
|
|
596
|
-
readonly
|
|
597
|
-
|
|
598
|
-
readonly
|
|
487
|
+
readonly messageType: typeof MessageType.Request;
|
|
488
|
+
readonly writeKey?: OwnerWriteKey;
|
|
489
|
+
readonly subscriptionFlag?: SubscriptionFlag;
|
|
599
490
|
}
|
|
600
491
|
| {
|
|
601
|
-
readonly
|
|
492
|
+
readonly messageType: typeof MessageType.Response;
|
|
602
493
|
readonly errorCode: ProtocolErrorCode;
|
|
603
494
|
}
|
|
495
|
+
| {
|
|
496
|
+
readonly messageType: typeof MessageType.Broadcast;
|
|
497
|
+
}
|
|
604
498
|
),
|
|
605
499
|
): ProtocolMessageBuffer => {
|
|
606
500
|
const {
|
|
@@ -623,20 +517,19 @@ export const createProtocolMessageBuffer = (
|
|
|
623
517
|
};
|
|
624
518
|
|
|
625
519
|
encodeNonNegativeInt(buffers.header, version);
|
|
626
|
-
buffers.header.extend(
|
|
520
|
+
buffers.header.extend(ownerIdToOwnerIdBytes(ownerId));
|
|
521
|
+
buffers.header.extend([options.messageType]);
|
|
627
522
|
|
|
628
|
-
if (options.
|
|
523
|
+
if (options.messageType === MessageType.Request) {
|
|
629
524
|
if (!options.writeKey) {
|
|
630
|
-
buffers.header.extend([
|
|
631
|
-
} else if (!Array.isArray(options.writeKey)) {
|
|
632
|
-
buffers.header.extend([WriteKeyMode.Single]);
|
|
633
|
-
buffers.header.extend(options.writeKey as WriteKey);
|
|
525
|
+
buffers.header.extend([0]);
|
|
634
526
|
} else {
|
|
635
|
-
buffers.header.extend([
|
|
636
|
-
buffers.header.extend(options.writeKey
|
|
637
|
-
buffers.header.extend(options.writeKey[1] as WriteKey); // new
|
|
527
|
+
buffers.header.extend([1]);
|
|
528
|
+
buffers.header.extend(options.writeKey);
|
|
638
529
|
}
|
|
639
|
-
|
|
530
|
+
const subscriptionFlag = options.subscriptionFlag ?? SubscriptionFlags.None;
|
|
531
|
+
buffers.header.extend([subscriptionFlag]);
|
|
532
|
+
} else if (options.messageType === MessageType.Response) {
|
|
640
533
|
buffers.header.extend([options.errorCode]);
|
|
641
534
|
}
|
|
642
535
|
|
|
@@ -719,10 +612,15 @@ export const createProtocolMessageBuffer = (
|
|
|
719
612
|
},
|
|
720
613
|
|
|
721
614
|
addRange: (range) => {
|
|
615
|
+
assert(
|
|
616
|
+
options.messageType !== MessageType.Broadcast,
|
|
617
|
+
"Cannot add a range into broadcast message",
|
|
618
|
+
);
|
|
722
619
|
assert(
|
|
723
620
|
!isLastRangeInfinite,
|
|
724
621
|
"Cannot add a range after an InfiniteUpperBound range",
|
|
725
622
|
);
|
|
623
|
+
|
|
726
624
|
isLastRangeInfinite = range.upperBound === InfiniteUpperBound;
|
|
727
625
|
|
|
728
626
|
/**
|
|
@@ -732,7 +630,7 @@ export const createProtocolMessageBuffer = (
|
|
|
732
630
|
*/
|
|
733
631
|
if (range.upperBound !== InfiniteUpperBound)
|
|
734
632
|
buffers.ranges.timestamps.add(
|
|
735
|
-
|
|
633
|
+
timestampBytesToTimestamp(range.upperBound),
|
|
736
634
|
);
|
|
737
635
|
else {
|
|
738
636
|
buffers.ranges.timestamps.addInfinite();
|
|
@@ -779,6 +677,11 @@ export const createProtocolMessageBuffer = (
|
|
|
779
677
|
};
|
|
780
678
|
};
|
|
781
679
|
|
|
680
|
+
export interface TimestampsRangeWithTimestampsBuffer extends BaseRange {
|
|
681
|
+
readonly type: typeof RangeType.Timestamps;
|
|
682
|
+
readonly timestamps: TimestampsBuffer;
|
|
683
|
+
}
|
|
684
|
+
|
|
782
685
|
export interface TimestampsBuffer {
|
|
783
686
|
readonly add: (timestamp: Timestamp) => void;
|
|
784
687
|
readonly addInfinite: () => void;
|
|
@@ -880,7 +783,7 @@ const createRunLengthEncoder = <T>(
|
|
|
880
783
|
};
|
|
881
784
|
|
|
882
785
|
export interface ApplyProtocolMessageAsClientOptions {
|
|
883
|
-
getWriteKey?: (ownerId: OwnerId) =>
|
|
786
|
+
getWriteKey?: (ownerId: OwnerId) => OwnerWriteKey | null;
|
|
884
787
|
|
|
885
788
|
/** For testing purposes only; should not be used in production. */
|
|
886
789
|
version?: NonNegativeInt;
|
|
@@ -889,31 +792,53 @@ export interface ApplyProtocolMessageAsClientOptions {
|
|
|
889
792
|
rangesMaxSize?: PositiveInt;
|
|
890
793
|
}
|
|
891
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Result type for {@link applyProtocolMessageAsClient} that distinguishes
|
|
797
|
+
* between responses to client requests and broadcast messages.
|
|
798
|
+
*/
|
|
799
|
+
export type ApplyProtocolMessageAsClientResult =
|
|
800
|
+
| { readonly type: "response"; readonly message: ProtocolMessage }
|
|
801
|
+
| { readonly type: "no-response" }
|
|
802
|
+
| { readonly type: "broadcast" };
|
|
803
|
+
|
|
892
804
|
export const applyProtocolMessageAsClient =
|
|
893
805
|
(deps: StorageDep) =>
|
|
894
|
-
(
|
|
806
|
+
async (
|
|
895
807
|
inputMessage: Uint8Array,
|
|
896
|
-
{
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
isInitiator: version < requestedVersion,
|
|
913
|
-
ownerId,
|
|
914
|
-
});
|
|
915
|
-
}
|
|
808
|
+
options: ApplyProtocolMessageAsClientOptions = {},
|
|
809
|
+
): Promise<
|
|
810
|
+
Result<
|
|
811
|
+
ApplyProtocolMessageAsClientResult,
|
|
812
|
+
| ProtocolInvalidDataError
|
|
813
|
+
| ProtocolSyncError
|
|
814
|
+
| ProtocolUnsupportedVersionError
|
|
815
|
+
| ProtocolWriteError
|
|
816
|
+
| ProtocolWriteKeyError
|
|
817
|
+
>
|
|
818
|
+
> => {
|
|
819
|
+
// try-catch instead of Result for performance and stacktraces
|
|
820
|
+
try {
|
|
821
|
+
const input = createBuffer(inputMessage);
|
|
822
|
+
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
823
|
+
const version = options.version ?? protocolVersion;
|
|
916
824
|
|
|
825
|
+
if (requestedVersion !== version) {
|
|
826
|
+
return err<ProtocolUnsupportedVersionError>({
|
|
827
|
+
type: "ProtocolUnsupportedVersionError",
|
|
828
|
+
unsupportedVersion: requestedVersion,
|
|
829
|
+
isInitiator: version < requestedVersion,
|
|
830
|
+
ownerId,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const messageType = input.shift() as MessageType;
|
|
835
|
+
assert(
|
|
836
|
+
messageType === MessageType.Response ||
|
|
837
|
+
messageType === MessageType.Broadcast,
|
|
838
|
+
"Invalid MessageType",
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
if (messageType === MessageType.Response) {
|
|
917
842
|
const errorCode = input.shift() as ProtocolErrorCode;
|
|
918
843
|
if (errorCode !== ProtocolErrorCode.NoError) {
|
|
919
844
|
switch (errorCode) {
|
|
@@ -938,36 +863,69 @@ export const applyProtocolMessageAsClient =
|
|
|
938
863
|
);
|
|
939
864
|
}
|
|
940
865
|
}
|
|
866
|
+
}
|
|
941
867
|
|
|
942
|
-
|
|
943
|
-
|
|
868
|
+
const messages = decodeMessages(input);
|
|
869
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
944
870
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
871
|
+
if (
|
|
872
|
+
isNonEmptyReadonlyArray(messages) &&
|
|
873
|
+
!(await deps.storage.writeMessages(ownerIdBytes, messages))
|
|
874
|
+
) {
|
|
875
|
+
return ok({ type: "no-response" });
|
|
876
|
+
}
|
|
951
877
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
878
|
+
// Now: No writeKey, no sync.
|
|
879
|
+
// TODO: Allow to sync SharedReadonlyOwner
|
|
880
|
+
// Without local changes, writeKey will not be required.
|
|
881
|
+
// With local changes, writeKey will be required and if not provided,
|
|
882
|
+
// the sync will stop.
|
|
883
|
+
const writeKey = options.getWriteKey?.(ownerId);
|
|
884
|
+
if (writeKey == null) {
|
|
885
|
+
return ok({ type: "no-response" });
|
|
886
|
+
}
|
|
955
887
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
totalMaxSize,
|
|
960
|
-
rangesMaxSize,
|
|
961
|
-
});
|
|
888
|
+
if (messageType === MessageType.Broadcast) {
|
|
889
|
+
return ok({ type: "broadcast" });
|
|
890
|
+
}
|
|
962
891
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
892
|
+
const ranges = decodeRanges(input);
|
|
893
|
+
|
|
894
|
+
if (!isNonEmptyReadonlyArray(ranges)) {
|
|
895
|
+
return ok({ type: "no-response" });
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const output = createProtocolMessageBuffer(ownerId, {
|
|
899
|
+
messageType: MessageType.Request,
|
|
900
|
+
writeKey,
|
|
901
|
+
totalMaxSize: options.totalMaxSize,
|
|
902
|
+
rangesMaxSize: options.rangesMaxSize,
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
const syncResult = sync(deps)(ranges, output, ownerIdBytes);
|
|
906
|
+
|
|
907
|
+
// Client sync error (handled via Storage) or no changes.
|
|
908
|
+
if (!syncResult.ok || !syncResult.value) {
|
|
909
|
+
return ok({ type: "no-response" });
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return ok({ type: "response", message: output.unwrap() });
|
|
913
|
+
} catch (error) {
|
|
914
|
+
return err<ProtocolInvalidDataError>({
|
|
915
|
+
type: "ProtocolInvalidDataError",
|
|
916
|
+
data: inputMessage,
|
|
917
|
+
error,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
};
|
|
966
921
|
|
|
967
922
|
export interface ApplyProtocolMessageAsRelayOptions {
|
|
968
923
|
/** To subscribe an owner for broadcasting. */
|
|
969
924
|
subscribe?: (ownerId: OwnerId) => void;
|
|
970
925
|
|
|
926
|
+
/** To unsubscribe an owner from broadcasting. */
|
|
927
|
+
unsubscribe?: (ownerId: OwnerId) => void;
|
|
928
|
+
|
|
971
929
|
/** To broadcast a protocol message to all subscribers. */
|
|
972
930
|
broadcast?: (ownerId: OwnerId, message: ProtocolMessage) => void;
|
|
973
931
|
|
|
@@ -975,143 +933,166 @@ export interface ApplyProtocolMessageAsRelayOptions {
|
|
|
975
933
|
rangesMaxSize?: PositiveInt;
|
|
976
934
|
}
|
|
977
935
|
|
|
936
|
+
/**
|
|
937
|
+
* Result type for {@link applyProtocolMessageAsRelay}.
|
|
938
|
+
*
|
|
939
|
+
* Unlike {@link ApplyProtocolMessageAsClientResult}, relays always respond with
|
|
940
|
+
* a message to provide sync completion feedback. This ensures the initiator can
|
|
941
|
+
* reliably detect when synchronization is complete, even when there's nothing
|
|
942
|
+
* to sync. Clients may choose not to respond in certain cases (like when they
|
|
943
|
+
* receive broadcast messages or when they lack a write key for syncing).
|
|
944
|
+
*/
|
|
945
|
+
export interface ApplyProtocolMessageAsRelayResult {
|
|
946
|
+
readonly type: "response";
|
|
947
|
+
readonly message: ProtocolMessage;
|
|
948
|
+
}
|
|
949
|
+
|
|
978
950
|
export const applyProtocolMessageAsRelay =
|
|
979
951
|
(deps: StorageDep) =>
|
|
980
|
-
(
|
|
952
|
+
async (
|
|
981
953
|
inputMessage: Uint8Array,
|
|
982
|
-
{
|
|
983
|
-
subscribe,
|
|
984
|
-
broadcast,
|
|
985
|
-
totalMaxSize,
|
|
986
|
-
rangesMaxSize,
|
|
987
|
-
}: ApplyProtocolMessageAsRelayOptions = {},
|
|
954
|
+
options: ApplyProtocolMessageAsRelayOptions = {},
|
|
988
955
|
/** For testing purposes only; should not be used in production. */
|
|
989
956
|
version = protocolVersion,
|
|
990
|
-
):
|
|
991
|
-
|
|
957
|
+
): Promise<
|
|
958
|
+
Result<ApplyProtocolMessageAsRelayResult, ProtocolInvalidDataError>
|
|
959
|
+
> => {
|
|
960
|
+
// try-catch instead of Result for performance and stacktraces
|
|
961
|
+
try {
|
|
962
|
+
const input = createBuffer(inputMessage);
|
|
992
963
|
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
993
|
-
const
|
|
964
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
994
965
|
|
|
995
966
|
if (requestedVersion !== version) {
|
|
996
967
|
// Non-initiator responds with its version and ownerId.
|
|
997
968
|
const output = createBuffer();
|
|
998
969
|
encodeNonNegativeInt(output, version);
|
|
999
|
-
output.extend(
|
|
1000
|
-
return ok(
|
|
970
|
+
output.extend(ownerIdBytes);
|
|
971
|
+
return ok({
|
|
972
|
+
type: "response",
|
|
973
|
+
message: output.unwrap() as ProtocolMessage,
|
|
974
|
+
});
|
|
1001
975
|
}
|
|
1002
976
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
let
|
|
1008
|
-
|
|
1009
|
-
if (
|
|
1010
|
-
writeKey = input.shiftN(
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
977
|
+
const messageType = input.shift() as MessageType;
|
|
978
|
+
assert(messageType === MessageType.Request, "Invalid MessageType");
|
|
979
|
+
|
|
980
|
+
const hasWriteKey = input.shift();
|
|
981
|
+
let writeKey: OwnerWriteKey | undefined;
|
|
982
|
+
|
|
983
|
+
if (hasWriteKey === 1) {
|
|
984
|
+
writeKey = input.shiftN(ownerWriteKeyLength) as OwnerWriteKey;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const subscriptionFlag = input.shift() as SubscriptionFlag;
|
|
988
|
+
|
|
989
|
+
switch (subscriptionFlag) {
|
|
990
|
+
case SubscriptionFlags.Subscribe:
|
|
991
|
+
options.subscribe?.(ownerId);
|
|
992
|
+
break;
|
|
993
|
+
case SubscriptionFlags.Unsubscribe:
|
|
994
|
+
options.unsubscribe?.(ownerId);
|
|
995
|
+
break;
|
|
996
|
+
case SubscriptionFlags.None:
|
|
997
|
+
break;
|
|
1022
998
|
}
|
|
1023
999
|
|
|
1024
1000
|
if (writeKey) {
|
|
1025
|
-
const isValid = deps.storage.validateWriteKey(
|
|
1001
|
+
const isValid = deps.storage.validateWriteKey(ownerIdBytes, writeKey);
|
|
1026
1002
|
if (!isValid) {
|
|
1027
|
-
return ok(
|
|
1028
|
-
|
|
1029
|
-
|
|
1003
|
+
return ok({
|
|
1004
|
+
type: "response",
|
|
1005
|
+
message: createProtocolMessageBuffer(ownerId, {
|
|
1006
|
+
messageType: MessageType.Response,
|
|
1030
1007
|
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
1031
1008
|
}).unwrap(),
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (newWriteKey) {
|
|
1036
|
-
const rotationSuccess = deps.storage.setWriteKey(
|
|
1037
|
-
binaryOwnerId,
|
|
1038
|
-
newWriteKey,
|
|
1039
|
-
);
|
|
1040
|
-
if (!rotationSuccess) {
|
|
1041
|
-
return ok(
|
|
1042
|
-
createProtocolMessageBuffer(ownerId, {
|
|
1043
|
-
type: "non-initiator",
|
|
1044
|
-
errorCode: ProtocolErrorCode.WriteError,
|
|
1045
|
-
}).unwrap(),
|
|
1046
|
-
);
|
|
1047
|
-
}
|
|
1009
|
+
});
|
|
1048
1010
|
}
|
|
1049
1011
|
}
|
|
1050
1012
|
|
|
1051
1013
|
const messages = decodeMessages(input);
|
|
1052
1014
|
|
|
1053
1015
|
if (isNonEmptyReadonlyArray(messages)) {
|
|
1054
|
-
if (!writeKey)
|
|
1055
|
-
return ok(
|
|
1056
|
-
|
|
1057
|
-
|
|
1016
|
+
if (!writeKey) {
|
|
1017
|
+
return ok({
|
|
1018
|
+
type: "response",
|
|
1019
|
+
message: createProtocolMessageBuffer(ownerId, {
|
|
1020
|
+
messageType: MessageType.Response,
|
|
1058
1021
|
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
1059
1022
|
}).unwrap(),
|
|
1060
|
-
);
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1061
1025
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1026
|
+
/**
|
|
1027
|
+
* Broadcast messages to all subscribed devices. This ensures real-time
|
|
1028
|
+
* synchronization between clients.
|
|
1029
|
+
*
|
|
1030
|
+
* When a relay's database is deleted or clients migrate to a new relay
|
|
1031
|
+
* (without data migration), clients will sync their data to the relay,
|
|
1032
|
+
* and the relay will broadcast those messages to other connected
|
|
1033
|
+
* clients. Those clients may receive messages they already have, but
|
|
1034
|
+
* this is safe because `applyMessages` is idempotent. As the relay
|
|
1035
|
+
* becomes more synchronized with clients over time, fewer duplicate
|
|
1036
|
+
* messages will be broadcasted.
|
|
1037
|
+
*/
|
|
1038
|
+
if (options.broadcast) {
|
|
1064
1039
|
const broadcastBuffer = createProtocolMessageBuffer(ownerId, {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
rangesMaxSize,
|
|
1040
|
+
messageType: MessageType.Broadcast,
|
|
1041
|
+
totalMaxSize: options.totalMaxSize,
|
|
1042
|
+
rangesMaxSize: options.rangesMaxSize,
|
|
1069
1043
|
version,
|
|
1070
1044
|
});
|
|
1071
1045
|
for (const message of messages) {
|
|
1072
1046
|
broadcastBuffer.addMessage(message);
|
|
1073
1047
|
}
|
|
1074
|
-
broadcast(ownerId, broadcastBuffer.unwrap());
|
|
1048
|
+
options.broadcast(ownerId, broadcastBuffer.unwrap());
|
|
1075
1049
|
}
|
|
1076
1050
|
|
|
1077
|
-
if (!deps.storage.writeMessages(
|
|
1078
|
-
return ok(
|
|
1079
|
-
|
|
1080
|
-
|
|
1051
|
+
if (!(await deps.storage.writeMessages(ownerIdBytes, messages))) {
|
|
1052
|
+
return ok({
|
|
1053
|
+
type: "response",
|
|
1054
|
+
message: createProtocolMessageBuffer(ownerId, {
|
|
1055
|
+
messageType: MessageType.Response,
|
|
1081
1056
|
errorCode: ProtocolErrorCode.WriteError,
|
|
1082
1057
|
}).unwrap(),
|
|
1083
|
-
);
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1084
1060
|
}
|
|
1085
1061
|
|
|
1062
|
+
const ranges = decodeRanges(input);
|
|
1063
|
+
|
|
1086
1064
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
1087
|
-
|
|
1065
|
+
messageType: MessageType.Response,
|
|
1088
1066
|
errorCode: ProtocolErrorCode.NoError,
|
|
1089
|
-
totalMaxSize,
|
|
1090
|
-
rangesMaxSize,
|
|
1067
|
+
totalMaxSize: options.totalMaxSize,
|
|
1068
|
+
rangesMaxSize: options.rangesMaxSize,
|
|
1091
1069
|
});
|
|
1092
1070
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1071
|
+
// Non-initiators always respond to provide sync completion feedback,
|
|
1072
|
+
// even when there's nothing to sync.
|
|
1073
|
+
if (!isNonEmptyReadonlyArray(ranges)) {
|
|
1074
|
+
return ok({ type: "response", message: output.unwrap() });
|
|
1075
|
+
}
|
|
1095
1076
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
};
|
|
1077
|
+
const syncResult = sync(deps)(ranges, output, ownerIdBytes);
|
|
1078
|
+
|
|
1079
|
+
const message = syncResult.ok
|
|
1080
|
+
? output.unwrap()
|
|
1081
|
+
: createProtocolMessageBuffer(ownerId, {
|
|
1082
|
+
messageType: MessageType.Response,
|
|
1083
|
+
errorCode: syncResult.error,
|
|
1084
|
+
}).unwrap();
|
|
1085
|
+
|
|
1086
|
+
// Non-initiators always respond to provide sync completion feedback,
|
|
1087
|
+
return ok({ type: "response", message });
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
return err<ProtocolInvalidDataError>({
|
|
1090
|
+
type: "ProtocolInvalidDataError",
|
|
1091
|
+
data: inputMessage,
|
|
1092
|
+
error,
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1115
1096
|
|
|
1116
1097
|
const decodeVersionAndOwner = (input: Buffer): [NonNegativeInt, OwnerId] => {
|
|
1117
1098
|
// This structure must never change across protocol versions. The version
|
|
@@ -1119,7 +1100,7 @@ const decodeVersionAndOwner = (input: Buffer): [NonNegativeInt, OwnerId] => {
|
|
|
1119
1100
|
// to enable version negotiation and owner identification before any other
|
|
1120
1101
|
// processing occurs.
|
|
1121
1102
|
const version = decodeNonNegativeInt(input);
|
|
1122
|
-
const ownerId =
|
|
1103
|
+
const ownerId = decodeId(input) as OwnerId;
|
|
1123
1104
|
return [version, ownerId];
|
|
1124
1105
|
};
|
|
1125
1106
|
|
|
@@ -1154,40 +1135,14 @@ const decodeMessages = (
|
|
|
1154
1135
|
const sync =
|
|
1155
1136
|
(deps: StorageDep) =>
|
|
1156
1137
|
(
|
|
1157
|
-
|
|
1158
|
-
input: Buffer,
|
|
1138
|
+
ranges: NonEmptyReadonlyArray<Range>,
|
|
1159
1139
|
output: ProtocolMessageBuffer,
|
|
1160
|
-
|
|
1161
|
-
): Result<
|
|
1162
|
-
const ranges = decodeRanges(input);
|
|
1163
|
-
|
|
1164
|
-
if (!isNonEmptyReadonlyArray(ranges)) {
|
|
1165
|
-
// Non-initiators always respond to provide sync completion feedback,
|
|
1166
|
-
// even when there's nothing to sync.
|
|
1167
|
-
if (role === "non-initiator") {
|
|
1168
|
-
return ok(output.unwrap());
|
|
1169
|
-
}
|
|
1170
|
-
// Nothing to sync.
|
|
1171
|
-
return ok(null);
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
const binaryOwnerId = binaryOwnerIdToOwnerId(ownerId);
|
|
1140
|
+
ownerIdBytes: OwnerIdBytes,
|
|
1141
|
+
): Result<boolean, typeof ProtocolErrorCode.SyncError> => {
|
|
1175
1142
|
const outputInitialSize = output.getSize();
|
|
1176
1143
|
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
if (role === "initiator") {
|
|
1180
|
-
return ok(null);
|
|
1181
|
-
}
|
|
1182
|
-
const message = createProtocolMessageBuffer(binaryOwnerId, {
|
|
1183
|
-
type: "non-initiator",
|
|
1184
|
-
errorCode: ProtocolErrorCode.SyncError,
|
|
1185
|
-
});
|
|
1186
|
-
return ok(message.unwrap());
|
|
1187
|
-
};
|
|
1188
|
-
|
|
1189
|
-
const storageSize = deps.storage.getSize(ownerId);
|
|
1190
|
-
if (storageSize == null) return syncFail();
|
|
1144
|
+
const storageSize = deps.storage.getSize(ownerIdBytes);
|
|
1145
|
+
if (storageSize == null) return err(ProtocolErrorCode.SyncError);
|
|
1191
1146
|
|
|
1192
1147
|
let prevUpperBound: RangeUpperBound | null = null;
|
|
1193
1148
|
let prevIndex = 0 as NonNegativeInt;
|
|
@@ -1227,7 +1182,11 @@ const sync =
|
|
|
1227
1182
|
const addFingerprintForRemainingRange = (
|
|
1228
1183
|
begin: NonNegativeInt,
|
|
1229
1184
|
): boolean => {
|
|
1230
|
-
const fingerprint = deps.storage.fingerprint(
|
|
1185
|
+
const fingerprint = deps.storage.fingerprint(
|
|
1186
|
+
ownerIdBytes,
|
|
1187
|
+
begin,
|
|
1188
|
+
storageSize,
|
|
1189
|
+
);
|
|
1231
1190
|
if (!fingerprint) return false;
|
|
1232
1191
|
// There is always a space for a ramaining range.
|
|
1233
1192
|
output.addRange({
|
|
@@ -1243,12 +1202,12 @@ const sync =
|
|
|
1243
1202
|
|
|
1244
1203
|
const lower = prevIndex;
|
|
1245
1204
|
let upper = deps.storage.findLowerBound(
|
|
1246
|
-
|
|
1205
|
+
ownerIdBytes,
|
|
1247
1206
|
prevIndex,
|
|
1248
1207
|
storageSize,
|
|
1249
1208
|
currentUpperBound,
|
|
1250
1209
|
);
|
|
1251
|
-
if (upper == null) return
|
|
1210
|
+
if (upper == null) return err(ProtocolErrorCode.SyncError);
|
|
1252
1211
|
|
|
1253
1212
|
switch (range.type) {
|
|
1254
1213
|
case RangeType.Skip: {
|
|
@@ -1258,11 +1217,11 @@ const sync =
|
|
|
1258
1217
|
|
|
1259
1218
|
case RangeType.Fingerprint: {
|
|
1260
1219
|
const ourFingerprint = deps.storage.fingerprint(
|
|
1261
|
-
|
|
1220
|
+
ownerIdBytes,
|
|
1262
1221
|
lower,
|
|
1263
1222
|
upper,
|
|
1264
1223
|
);
|
|
1265
|
-
if (ourFingerprint == null) return
|
|
1224
|
+
if (ourFingerprint == null) return err(ProtocolErrorCode.SyncError);
|
|
1266
1225
|
|
|
1267
1226
|
if (eqArrayNumber(range.fingerprint, ourFingerprint)) {
|
|
1268
1227
|
skipRange(range);
|
|
@@ -1270,15 +1229,16 @@ const sync =
|
|
|
1270
1229
|
if (output.canSplitRange()) {
|
|
1271
1230
|
coalesceSkipsBeforeAdd();
|
|
1272
1231
|
splitRange(deps)(
|
|
1273
|
-
|
|
1232
|
+
ownerIdBytes,
|
|
1274
1233
|
lower,
|
|
1275
1234
|
upper,
|
|
1276
1235
|
currentUpperBound,
|
|
1277
1236
|
output,
|
|
1278
1237
|
);
|
|
1279
1238
|
} else {
|
|
1280
|
-
|
|
1281
|
-
|
|
1239
|
+
return addFingerprintForRemainingRange(upper)
|
|
1240
|
+
? ok(true)
|
|
1241
|
+
: err(ProtocolErrorCode.SyncError);
|
|
1282
1242
|
}
|
|
1283
1243
|
}
|
|
1284
1244
|
break;
|
|
@@ -1292,45 +1252,53 @@ const sync =
|
|
|
1292
1252
|
);
|
|
1293
1253
|
const ourTimestamps = createTimestampsBuffer();
|
|
1294
1254
|
|
|
1295
|
-
let
|
|
1255
|
+
let cantReadDbChange = false as boolean;
|
|
1296
1256
|
let exceeded = false as boolean;
|
|
1297
1257
|
|
|
1298
|
-
deps.storage.iterate(
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1258
|
+
deps.storage.iterate(
|
|
1259
|
+
ownerIdBytes,
|
|
1260
|
+
lower,
|
|
1261
|
+
upper,
|
|
1262
|
+
(timestamp, index) => {
|
|
1263
|
+
const timestampString = timestamp.join();
|
|
1264
|
+
const timestampBinary = timestampBytesToTimestamp(timestamp);
|
|
1265
|
+
|
|
1266
|
+
let message: EncryptedCrdtMessage | null = null;
|
|
1267
|
+
|
|
1268
|
+
if (timestampsWeNeed.has(timestampString)) {
|
|
1269
|
+
timestampsWeNeed.delete(timestampString);
|
|
1270
|
+
} else {
|
|
1271
|
+
const dbChange = deps.storage.readDbChange(
|
|
1272
|
+
ownerIdBytes,
|
|
1273
|
+
timestamp,
|
|
1274
|
+
);
|
|
1275
|
+
if (dbChange == null) {
|
|
1276
|
+
cantReadDbChange = true;
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
message = {
|
|
1280
|
+
timestamp: timestampBinary,
|
|
1281
|
+
change: dbChange,
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1303
1284
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1285
|
+
if (
|
|
1286
|
+
!output.canAddTimestampsRangeAndMessage(ourTimestamps, message)
|
|
1287
|
+
) {
|
|
1288
|
+
exceeded = true;
|
|
1289
|
+
endBound = timestamp;
|
|
1290
|
+
upper = index;
|
|
1310
1291
|
return false;
|
|
1311
1292
|
}
|
|
1312
|
-
message = {
|
|
1313
|
-
timestamp: timestampBinary,
|
|
1314
|
-
change: dbChange,
|
|
1315
|
-
};
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
if (
|
|
1319
|
-
!output.canAddTimestampsRangeAndMessage(ourTimestamps, message)
|
|
1320
|
-
) {
|
|
1321
|
-
exceeded = true;
|
|
1322
|
-
endBound = timestamp;
|
|
1323
|
-
upper = index;
|
|
1324
|
-
return false;
|
|
1325
|
-
}
|
|
1326
1293
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1294
|
+
ourTimestamps.add(timestampBinary);
|
|
1295
|
+
if (message) output.addMessage(message);
|
|
1296
|
+
return true;
|
|
1297
|
+
},
|
|
1298
|
+
);
|
|
1331
1299
|
|
|
1332
|
-
if (
|
|
1333
|
-
return
|
|
1300
|
+
if (cantReadDbChange) {
|
|
1301
|
+
return err(ProtocolErrorCode.SyncError);
|
|
1334
1302
|
}
|
|
1335
1303
|
|
|
1336
1304
|
const addRange = () => {
|
|
@@ -1345,9 +1313,9 @@ const sync =
|
|
|
1345
1313
|
if (exceeded) {
|
|
1346
1314
|
addRange();
|
|
1347
1315
|
if (!addFingerprintForRemainingRange(upper)) {
|
|
1348
|
-
return
|
|
1316
|
+
return err(ProtocolErrorCode.SyncError);
|
|
1349
1317
|
}
|
|
1350
|
-
return ok(
|
|
1318
|
+
return ok(true);
|
|
1351
1319
|
}
|
|
1352
1320
|
|
|
1353
1321
|
// If we need something, we have to respond with our timestamps.
|
|
@@ -1368,19 +1336,13 @@ const sync =
|
|
|
1368
1336
|
// If all ranges were skipped, there are no changes and sync is complete.
|
|
1369
1337
|
const hasChange = output.getSize() > outputInitialSize;
|
|
1370
1338
|
|
|
1371
|
-
|
|
1372
|
-
// even with empty messages. This allows clients to detect sync completion.
|
|
1373
|
-
if (role === "non-initiator" && !hasChange) {
|
|
1374
|
-
return ok(output.unwrap());
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
return ok(hasChange ? output.unwrap() : null);
|
|
1339
|
+
return ok(hasChange);
|
|
1378
1340
|
};
|
|
1379
1341
|
|
|
1380
1342
|
const splitRange =
|
|
1381
1343
|
(deps: StorageDep) =>
|
|
1382
1344
|
(
|
|
1383
|
-
ownerId:
|
|
1345
|
+
ownerId: OwnerIdBytes,
|
|
1384
1346
|
lower: NonNegativeInt,
|
|
1385
1347
|
upper: NonNegativeInt,
|
|
1386
1348
|
upperBound: RangeUpperBound,
|
|
@@ -1401,7 +1363,7 @@ const splitRange =
|
|
|
1401
1363
|
0 as NonNegativeInt,
|
|
1402
1364
|
itemCount,
|
|
1403
1365
|
(timestamp) => {
|
|
1404
|
-
range.timestamps.add(
|
|
1366
|
+
range.timestamps.add(timestampBytesToTimestamp(timestamp));
|
|
1405
1367
|
return true;
|
|
1406
1368
|
},
|
|
1407
1369
|
);
|
|
@@ -1460,7 +1422,7 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
|
|
|
1460
1422
|
for (let i = 0; i < rangesCount; i++) {
|
|
1461
1423
|
const upperBound =
|
|
1462
1424
|
i < timestampsCount
|
|
1463
|
-
?
|
|
1425
|
+
? timestampToTimestampBytes(timestamps[i])
|
|
1464
1426
|
: InfiniteUpperBound;
|
|
1465
1427
|
|
|
1466
1428
|
const rangeType = rangeTypes[i];
|
|
@@ -1482,7 +1444,7 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
|
|
|
1482
1444
|
|
|
1483
1445
|
case RangeType.Timestamps: {
|
|
1484
1446
|
const timestamps = decodeTimestamps(buffer).map(
|
|
1485
|
-
|
|
1447
|
+
timestampToTimestampBytes,
|
|
1486
1448
|
);
|
|
1487
1449
|
ranges.push({
|
|
1488
1450
|
type: RangeType.Timestamps,
|
|
@@ -1550,120 +1512,31 @@ const decodeTimestamps = (
|
|
|
1550
1512
|
return timestamps;
|
|
1551
1513
|
};
|
|
1552
1514
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
export const binaryIdLength = 16 as NonNegativeInt;
|
|
1557
|
-
|
|
1558
|
-
export const idToBinaryId = (id: Id): BinaryId =>
|
|
1559
|
-
base64Url256ToBytes(id) as BinaryId;
|
|
1560
|
-
|
|
1561
|
-
export const binaryIdToId = (binaryId: BinaryId): Id =>
|
|
1562
|
-
decodeId(createBuffer(binaryId));
|
|
1563
|
-
|
|
1564
|
-
/** Binary representation of {@link OwnerId}. */
|
|
1565
|
-
export type BinaryOwnerId = Uint8Array & Brand<"BinaryOwnerId">;
|
|
1566
|
-
|
|
1567
|
-
export const ownerIdToBinaryOwnerId = (ownerId: OwnerId): BinaryOwnerId =>
|
|
1568
|
-
base64Url256ToBytes(ownerId) as BinaryOwnerId;
|
|
1569
|
-
|
|
1570
|
-
export const binaryOwnerIdToOwnerId = (binaryOwnerId: BinaryOwnerId): OwnerId =>
|
|
1571
|
-
decodeOwnerId(createBuffer(binaryOwnerId));
|
|
1572
|
-
|
|
1573
|
-
/**
|
|
1574
|
-
* Union type for all variants of Base64Url strings with limited length. All
|
|
1575
|
-
* these types use Base64Url alphabet and are < 256 characters.
|
|
1576
|
-
*/
|
|
1577
|
-
export type Base64Url256Variant = Base64Url256 | Id | NanoId | OwnerId;
|
|
1578
|
-
|
|
1579
|
-
/**
|
|
1580
|
-
* Converts a Base64Url string to a Uint8Array for binary storage. This encoding
|
|
1581
|
-
* is more space-efficient than UTF-8 for Base64Url strings.
|
|
1582
|
-
*/
|
|
1583
|
-
export const base64Url256ToBytes = (
|
|
1584
|
-
string: Base64Url256Variant,
|
|
1585
|
-
): globalThis.Uint8Array => {
|
|
1586
|
-
const totalBits = string.length * 6; // 6 bits per character
|
|
1587
|
-
const byteLength = Math.ceil(totalBits / 8);
|
|
1588
|
-
const value = new globalThis.Uint8Array(byteLength);
|
|
1589
|
-
|
|
1590
|
-
let bitBuffer = 0;
|
|
1591
|
-
let bitsInBuffer = 0;
|
|
1592
|
-
let byteIndex = 0;
|
|
1593
|
-
|
|
1594
|
-
for (const char of string) {
|
|
1595
|
-
const charValue = base64UrlAlphabet.indexOf(char);
|
|
1596
|
-
bitBuffer = (bitBuffer << 6) | charValue;
|
|
1597
|
-
bitsInBuffer += 6;
|
|
1598
|
-
while (bitsInBuffer >= 8) {
|
|
1599
|
-
bitsInBuffer -= 8;
|
|
1600
|
-
value[byteIndex++] = (bitBuffer >> bitsInBuffer) & 0xff;
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
if (bitsInBuffer > 0 && byteIndex < byteLength) {
|
|
1605
|
-
value[byteIndex] = (bitBuffer << (8 - bitsInBuffer)) & 0xff;
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
return value;
|
|
1609
|
-
};
|
|
1610
|
-
|
|
1611
|
-
export const decodeBase64Url256 = (
|
|
1612
|
-
buffer: Buffer,
|
|
1613
|
-
stringLength: number,
|
|
1614
|
-
): Base64Url256Variant => {
|
|
1615
|
-
const bytes = buffer.shiftN(
|
|
1616
|
-
Math.ceil((stringLength * 6) / 8) as NonNegativeInt,
|
|
1617
|
-
);
|
|
1618
|
-
|
|
1619
|
-
let bitBuffer = 0;
|
|
1620
|
-
let bitsInBuffer = 0;
|
|
1621
|
-
let string = "";
|
|
1622
|
-
|
|
1623
|
-
for (const byte of bytes) {
|
|
1624
|
-
bitBuffer = (bitBuffer << 8) | byte;
|
|
1625
|
-
bitsInBuffer += 8;
|
|
1626
|
-
while (bitsInBuffer >= 6) {
|
|
1627
|
-
bitsInBuffer -= 6;
|
|
1628
|
-
if (string.length < stringLength) {
|
|
1629
|
-
const charValue = (bitBuffer >> bitsInBuffer) & 0x3f;
|
|
1630
|
-
if (charValue < 0 || charValue >= base64UrlAlphabet.length) {
|
|
1631
|
-
throw new ProtocolDecodeError("invalid charValue");
|
|
1632
|
-
}
|
|
1633
|
-
string += base64UrlAlphabet[charValue];
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
const result = Base64Url256.from(string);
|
|
1639
|
-
if (!result.ok) throw new ProtocolDecodeError(result.error.type);
|
|
1640
|
-
|
|
1641
|
-
return result.value;
|
|
1515
|
+
const decodeId = (buffer: Buffer): Id => {
|
|
1516
|
+
const bytes = buffer.shiftN(idBytesTypeValueLength);
|
|
1517
|
+
return idBytesToId(bytes as IdBytes);
|
|
1642
1518
|
};
|
|
1643
1519
|
|
|
1644
|
-
const decodeId = (buffer: Buffer): Id =>
|
|
1645
|
-
decodeBase64Url256(buffer, idTypeValueLength) as Id;
|
|
1646
|
-
|
|
1647
|
-
/** Not all 16 bytes are valid {@link OwnerId}. */
|
|
1648
|
-
const decodeOwnerId = (buffer: Buffer): OwnerId => decodeId(buffer) as OwnerId;
|
|
1649
|
-
|
|
1650
1520
|
/**
|
|
1651
1521
|
* Evolu uses MessagePack to handle all number variants except for
|
|
1652
1522
|
* NonNegativeInt. For NonNegativeInt, Evolu provides more efficient encoding.
|
|
1653
1523
|
*/
|
|
1654
1524
|
export const encodeNumber = (buffer: Buffer, number: number): void => {
|
|
1655
|
-
buffer.extend(pack(number));
|
|
1525
|
+
buffer.extend(packr.pack(number));
|
|
1656
1526
|
};
|
|
1657
1527
|
|
|
1658
1528
|
export const decodeNumber = (buffer: Buffer): number => {
|
|
1659
1529
|
let number: unknown;
|
|
1660
1530
|
let end: unknown;
|
|
1661
1531
|
|
|
1662
|
-
unpackMultiple(
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1532
|
+
packr.unpackMultiple(
|
|
1533
|
+
buffer.unwrap(),
|
|
1534
|
+
(n: unknown, _: unknown, e: unknown) => {
|
|
1535
|
+
number = n;
|
|
1536
|
+
end = e;
|
|
1537
|
+
return false;
|
|
1538
|
+
},
|
|
1539
|
+
);
|
|
1667
1540
|
|
|
1668
1541
|
const endResult = NonNegativeInt.fromUnknown(end);
|
|
1669
1542
|
if (!endResult.ok) throw new ProtocolDecodeError(endResult.error.type);
|
|
@@ -1675,13 +1548,6 @@ export const decodeNumber = (buffer: Buffer): number => {
|
|
|
1675
1548
|
return numberResult.value;
|
|
1676
1549
|
};
|
|
1677
1550
|
|
|
1678
|
-
export const binaryTimestampToFingerprint = (
|
|
1679
|
-
timestamp: BinaryTimestamp,
|
|
1680
|
-
): Fingerprint => {
|
|
1681
|
-
const hash = sha256(timestamp).slice(0, fingerprintSize);
|
|
1682
|
-
return hash as Fingerprint;
|
|
1683
|
-
};
|
|
1684
|
-
|
|
1685
1551
|
/**
|
|
1686
1552
|
* Encodes and encrypts a {@link DbChange} using the provided owner's encryption
|
|
1687
1553
|
* key. Returns an encrypted binary representation as {@link EncryptedDbChange}.
|
|
@@ -1700,15 +1566,15 @@ export const encodeAndEncryptDbChange =
|
|
|
1700
1566
|
encodeNonNegativeInt(buffer, protocolVersion);
|
|
1701
1567
|
|
|
1702
1568
|
// Encode the timestamp (after version) for tamper verification
|
|
1703
|
-
const
|
|
1704
|
-
buffer.extend(
|
|
1569
|
+
const timestampBytes = timestampToTimestampBytes(message.timestamp);
|
|
1570
|
+
buffer.extend(timestampBytes);
|
|
1705
1571
|
|
|
1706
|
-
|
|
1572
|
+
encodeString(buffer, change.table);
|
|
1707
1573
|
|
|
1708
|
-
buffer.extend(
|
|
1574
|
+
buffer.extend(idToIdBytes(change.id));
|
|
1709
1575
|
|
|
1710
1576
|
const entries = objectToEntries(change.values).map(
|
|
1711
|
-
([column, value]): [
|
|
1577
|
+
([column, value]): [string, SqliteValue] => {
|
|
1712
1578
|
return [column, value];
|
|
1713
1579
|
},
|
|
1714
1580
|
);
|
|
@@ -1716,7 +1582,7 @@ export const encodeAndEncryptDbChange =
|
|
|
1716
1582
|
encodeLength(buffer, entries);
|
|
1717
1583
|
|
|
1718
1584
|
for (const [column, value] of entries) {
|
|
1719
|
-
|
|
1585
|
+
encodeString(buffer, column);
|
|
1720
1586
|
encodeSqliteValue(buffer, value);
|
|
1721
1587
|
}
|
|
1722
1588
|
|
|
@@ -1752,11 +1618,10 @@ export const decryptAndDecodeDbChange =
|
|
|
1752
1618
|
| SymmetricCryptoDecryptError
|
|
1753
1619
|
| ProtocolInvalidDataError
|
|
1754
1620
|
| ProtocolTimestampMismatchError
|
|
1755
|
-
> =>
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
>(message.change, (buffer) => {
|
|
1621
|
+
> => {
|
|
1622
|
+
// try-catch instead of Result for performance and stacktraces
|
|
1623
|
+
try {
|
|
1624
|
+
const buffer = createBuffer(message.change);
|
|
1760
1625
|
const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
|
|
1761
1626
|
|
|
1762
1627
|
const ciphertextLength = decodeLength(buffer);
|
|
@@ -1776,11 +1641,9 @@ export const decryptAndDecodeDbChange =
|
|
|
1776
1641
|
decodeNonNegativeInt(buffer);
|
|
1777
1642
|
|
|
1778
1643
|
// Decode and verify the embedded timestamp
|
|
1779
|
-
const
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
const embeddedTimestamp = binaryTimestampToTimestamp(
|
|
1783
|
-
embeddedBinaryTimestamp,
|
|
1644
|
+
const embeddedTimestampBytes = buffer.shiftN(timestampBytesLength);
|
|
1645
|
+
const embeddedTimestamp = timestampBytesToTimestamp(
|
|
1646
|
+
embeddedTimestampBytes as TimestampBytes,
|
|
1784
1647
|
);
|
|
1785
1648
|
|
|
1786
1649
|
// Verify timestamp integrity
|
|
@@ -1792,14 +1655,14 @@ export const decryptAndDecodeDbChange =
|
|
|
1792
1655
|
});
|
|
1793
1656
|
}
|
|
1794
1657
|
|
|
1795
|
-
const table =
|
|
1658
|
+
const table = decodeString(buffer);
|
|
1796
1659
|
const id = decodeId(buffer);
|
|
1797
1660
|
|
|
1798
1661
|
const length = decodeLength(buffer);
|
|
1799
1662
|
const values = Object.create(null) as Record<string, SqliteValue>;
|
|
1800
1663
|
|
|
1801
1664
|
for (let i = 0; i < length; i++) {
|
|
1802
|
-
const column =
|
|
1665
|
+
const column = decodeString(buffer);
|
|
1803
1666
|
const value = decodeSqliteValue(buffer);
|
|
1804
1667
|
values[column] = value;
|
|
1805
1668
|
}
|
|
@@ -1807,7 +1670,14 @@ export const decryptAndDecodeDbChange =
|
|
|
1807
1670
|
const dbChange = { table, id, values };
|
|
1808
1671
|
|
|
1809
1672
|
return ok(dbChange);
|
|
1810
|
-
})
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
return err<ProtocolInvalidDataError>({
|
|
1675
|
+
type: "ProtocolInvalidDataError",
|
|
1676
|
+
data: message.change,
|
|
1677
|
+
error,
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1811
1681
|
|
|
1812
1682
|
/**
|
|
1813
1683
|
* Encodes a non-negative integer into a variable-length integer format. It's
|
|
@@ -1891,19 +1761,6 @@ export const decodeNodeId = (buffer: Buffer): NodeId => {
|
|
|
1891
1761
|
return bytesToHex(bytes) as NodeId;
|
|
1892
1762
|
};
|
|
1893
1763
|
|
|
1894
|
-
export const encodeBase64Url256 = (
|
|
1895
|
-
buffer: Buffer,
|
|
1896
|
-
string: Base64Url256Variant,
|
|
1897
|
-
): void => {
|
|
1898
|
-
encodeLength(buffer, string);
|
|
1899
|
-
buffer.extend(base64Url256ToBytes(string));
|
|
1900
|
-
};
|
|
1901
|
-
|
|
1902
|
-
export const decodeBase64Url256WithLength = (buffer: Buffer): Base64Url256 => {
|
|
1903
|
-
const length = decodeLength(buffer);
|
|
1904
|
-
return decodeBase64Url256(buffer, length) as Base64Url256;
|
|
1905
|
-
};
|
|
1906
|
-
|
|
1907
1764
|
// Small ints are encoded into ProtocolValueType, saving one byte per int.
|
|
1908
1765
|
const isSmallInt: Predicate<number> = (value: number) =>
|
|
1909
1766
|
value >= 0 && value < 20;
|
|
@@ -1915,20 +1772,23 @@ export const ProtocolValueType = {
|
|
|
1915
1772
|
String: 20 as NonNegativeInt,
|
|
1916
1773
|
Number: 21 as NonNegativeInt,
|
|
1917
1774
|
Null: 22 as NonNegativeInt,
|
|
1918
|
-
|
|
1775
|
+
Bytes: 23 as NonNegativeInt,
|
|
1919
1776
|
// We can add more types for other DBs or anything else later.
|
|
1920
1777
|
|
|
1921
1778
|
// Optimized types
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1779
|
+
NonNegativeInt: 30 as NonNegativeInt,
|
|
1780
|
+
|
|
1781
|
+
// String optimizations
|
|
1782
|
+
EmptyString: 31 as NonNegativeInt, // 1 byte vs 2 bytes (50% reduction)
|
|
1783
|
+
Base64Url: 32 as NonNegativeInt,
|
|
1784
|
+
Id: 33 as NonNegativeInt,
|
|
1785
|
+
Json: 34 as NonNegativeInt,
|
|
1926
1786
|
|
|
1927
1787
|
// new Date().toISOString() - 24 bytes
|
|
1928
1788
|
// encoded with fixed length - 8 bytes
|
|
1929
1789
|
// encode as NonNegativeInt - 6 bytes (additional 25% reduction)
|
|
1930
|
-
DateIsoWithNonNegativeTime:
|
|
1931
|
-
DateIsoWithNegativeTime:
|
|
1790
|
+
DateIsoWithNonNegativeTime: 35 as NonNegativeInt,
|
|
1791
|
+
DateIsoWithNegativeTime: 36 as NonNegativeInt, // 9 bytes
|
|
1932
1792
|
|
|
1933
1793
|
// TODO: Operations (from 40)
|
|
1934
1794
|
// Increment, Decrement, Patch, whatever.
|
|
@@ -1942,9 +1802,14 @@ export const encodeSqliteValue = (buffer: Buffer, value: SqliteValue): void => {
|
|
|
1942
1802
|
|
|
1943
1803
|
switch (typeof value) {
|
|
1944
1804
|
case "string": {
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1805
|
+
if (value === "") {
|
|
1806
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.EmptyString);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
const dateIso = DateIso.fromParent(value);
|
|
1811
|
+
if (dateIso.ok) {
|
|
1812
|
+
const time = new Date(dateIso.value).getTime();
|
|
1948
1813
|
if (NonNegativeInt.is(time)) {
|
|
1949
1814
|
encodeNonNegativeInt(
|
|
1950
1815
|
buffer,
|
|
@@ -1961,27 +1826,34 @@ export const encodeSqliteValue = (buffer: Buffer, value: SqliteValue): void => {
|
|
|
1961
1826
|
return;
|
|
1962
1827
|
}
|
|
1963
1828
|
|
|
1964
|
-
const
|
|
1965
|
-
if (
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
buffer.extend(base64Url256ToBytes(base64Url256.value));
|
|
1969
|
-
return;
|
|
1970
|
-
}
|
|
1971
|
-
encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url256);
|
|
1972
|
-
encodeBase64Url256(buffer, base64Url256.value);
|
|
1829
|
+
const id = Id.fromParent(value);
|
|
1830
|
+
if (id.ok) {
|
|
1831
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Id);
|
|
1832
|
+
buffer.extend(idToIdBytes(id.value));
|
|
1973
1833
|
return;
|
|
1974
1834
|
}
|
|
1975
1835
|
|
|
1976
|
-
const
|
|
1977
|
-
if
|
|
1978
|
-
|
|
1836
|
+
const json = Json.fromParent(value);
|
|
1837
|
+
// Only encode as Json if it survives JSON.parse/JSON.stringify round-trip.
|
|
1838
|
+
// Some valid JSON strings like "-0E0" get normalized to "0" during parsing,
|
|
1839
|
+
// which would cause data corruption if we don't verify round-trip safety.
|
|
1840
|
+
if (json.ok && JSON.stringify(jsonToJsonValue(json.value)) === value) {
|
|
1841
|
+
const jsonBytes = packr.pack(jsonToJsonValue(json.value));
|
|
1979
1842
|
encodeNonNegativeInt(buffer, ProtocolValueType.Json);
|
|
1980
1843
|
encodeLength(buffer, jsonBytes);
|
|
1981
1844
|
buffer.extend(jsonBytes);
|
|
1982
1845
|
return;
|
|
1983
1846
|
}
|
|
1984
1847
|
|
|
1848
|
+
const base64Url = Base64Url.fromParent(value);
|
|
1849
|
+
if (base64Url.ok) {
|
|
1850
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url);
|
|
1851
|
+
const bytes = base64UrlToUint8Array(base64Url.value);
|
|
1852
|
+
encodeLength(buffer, bytes);
|
|
1853
|
+
buffer.extend(bytes);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1985
1857
|
encodeNonNegativeInt(buffer, ProtocolValueType.String);
|
|
1986
1858
|
encodeString(buffer, value);
|
|
1987
1859
|
return;
|
|
@@ -2003,7 +1875,7 @@ export const encodeSqliteValue = (buffer: Buffer, value: SqliteValue): void => {
|
|
|
2003
1875
|
}
|
|
2004
1876
|
}
|
|
2005
1877
|
|
|
2006
|
-
encodeNonNegativeInt(buffer, ProtocolValueType.
|
|
1878
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Bytes);
|
|
2007
1879
|
encodeLength(buffer, value);
|
|
2008
1880
|
buffer.extend(value);
|
|
2009
1881
|
};
|
|
@@ -2018,39 +1890,50 @@ export const decodeSqliteValue = (buffer: Buffer): SqliteValue => {
|
|
|
2018
1890
|
switch (type) {
|
|
2019
1891
|
case ProtocolValueType.String:
|
|
2020
1892
|
return decodeString(buffer);
|
|
1893
|
+
|
|
2021
1894
|
case ProtocolValueType.Number:
|
|
2022
1895
|
return decodeNumber(buffer);
|
|
1896
|
+
|
|
2023
1897
|
case ProtocolValueType.Null:
|
|
2024
1898
|
return null;
|
|
2025
|
-
|
|
1899
|
+
|
|
1900
|
+
case ProtocolValueType.Bytes: {
|
|
2026
1901
|
const length = decodeLength(buffer);
|
|
2027
1902
|
return buffer.shiftN(length);
|
|
2028
1903
|
}
|
|
2029
|
-
|
|
1904
|
+
|
|
1905
|
+
case ProtocolValueType.Id:
|
|
2030
1906
|
return decodeId(buffer);
|
|
2031
|
-
|
|
2032
|
-
case ProtocolValueType.Base64Url256:
|
|
2033
|
-
return decodeBase64Url256WithLength(buffer);
|
|
1907
|
+
|
|
2034
1908
|
case ProtocolValueType.NonNegativeInt:
|
|
2035
1909
|
return decodeNonNegativeInt(buffer);
|
|
1910
|
+
|
|
2036
1911
|
case ProtocolValueType.Json: {
|
|
2037
1912
|
const length = decodeLength(buffer);
|
|
2038
1913
|
const bytes = buffer.shiftN(length);
|
|
2039
|
-
return JSON.stringify(unpack(bytes));
|
|
1914
|
+
return JSON.stringify(packr.unpack(bytes));
|
|
2040
1915
|
}
|
|
1916
|
+
|
|
2041
1917
|
case ProtocolValueType.DateIsoWithNonNegativeTime:
|
|
2042
1918
|
case ProtocolValueType.DateIsoWithNegativeTime: {
|
|
2043
1919
|
const time =
|
|
2044
1920
|
type === ProtocolValueType.DateIsoWithNonNegativeTime
|
|
2045
1921
|
? decodeNonNegativeInt(buffer)
|
|
2046
1922
|
: decodeNumber(buffer);
|
|
2047
|
-
const
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
1923
|
+
const dateIso = DateIso.fromParent(new Date(time).toISOString());
|
|
1924
|
+
if (!dateIso.ok) throw new ProtocolDecodeError(dateIso.error.type);
|
|
1925
|
+
return dateIso.value;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
case ProtocolValueType.EmptyString:
|
|
1929
|
+
return "";
|
|
1930
|
+
|
|
1931
|
+
case ProtocolValueType.Base64Url: {
|
|
1932
|
+
const length = decodeLength(buffer);
|
|
1933
|
+
const bytes = buffer.shiftN(length);
|
|
1934
|
+
return uint8ArrayToBase64Url(bytes);
|
|
2053
1935
|
}
|
|
1936
|
+
|
|
2054
1937
|
default:
|
|
2055
1938
|
throw new ProtocolDecodeError("invalid ProtocolValueType");
|
|
2056
1939
|
}
|