@evolu/common 6.0.1-preview.0 → 6.0.1-preview.10
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 +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +6 -3
- package/dist/src/Crypto.d.ts +11 -0
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Evolu/Config.d.ts +17 -4
- package/dist/src/Evolu/Config.d.ts.map +1 -1
- package/dist/src/Evolu/Config.js +1 -1
- package/dist/src/Evolu/Db.d.ts +21 -13
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +81 -64
- package/dist/src/Evolu/Evolu.d.ts +6 -6
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +19 -24
- package/dist/src/Evolu/Owner.d.ts +6 -1
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +5 -0
- package/dist/src/Evolu/Protocol.d.ts +134 -64
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +227 -77
- package/dist/src/Evolu/Relay.d.ts +3 -1
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +39 -3
- package/dist/src/Evolu/Schema.d.ts +24 -40
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +13 -72
- package/dist/src/Evolu/Storage.d.ts +1 -0
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +10 -0
- package/package.json +3 -3
- package/src/Assert.ts +6 -3
- package/src/Crypto.ts +13 -0
- package/src/Evolu/Config.ts +19 -5
- package/src/Evolu/Db.ts +92 -76
- package/src/Evolu/Evolu.ts +38 -35
- package/src/Evolu/Owner.ts +7 -1
- package/src/Evolu/Protocol.ts +276 -110
- package/src/Evolu/Relay.ts +45 -4
- package/src/Evolu/Schema.ts +102 -130
- package/src/Evolu/Storage.ts +12 -0
package/src/Evolu/Protocol.ts
CHANGED
|
@@ -7,31 +7,47 @@
|
|
|
7
7
|
* relays with each other.
|
|
8
8
|
*
|
|
9
9
|
* Evolu Protocol is designed for SQLite but can be extended to any database. It
|
|
10
|
-
* implements [Range-Based Set
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Negentropy but uses different encoding and also provides data transfer and
|
|
16
|
-
* ownership.
|
|
10
|
+
* implements [Range-Based Set
|
|
11
|
+
* Reconciliation](https://arxiv.org/abs/2212.13567). To learn how RBSR works,
|
|
12
|
+
* check [Negentropy](https://logperiodic.com/rbsr.html). Evolu Protocol is
|
|
13
|
+
* similar to Negentropy but uses different encoding and also provides data
|
|
14
|
+
* transfer and ownership.
|
|
17
15
|
*
|
|
18
16
|
* ### Message Structure
|
|
19
17
|
*
|
|
20
|
-
* | Field | Notes
|
|
21
|
-
* | :----------------------------- |
|
|
22
|
-
* | **Header** |
|
|
23
|
-
* | - {@link protocolVersion} |
|
|
24
|
-
* | - {@link OwnerId} |
|
|
25
|
-
* |
|
|
26
|
-
* |
|
|
27
|
-
* | - {@link
|
|
28
|
-
* | - {@link
|
|
29
|
-
* | -
|
|
30
|
-
* |
|
|
31
|
-
* |
|
|
32
|
-
* | - {@link
|
|
18
|
+
* | Field | Notes |
|
|
19
|
+
* | :----------------------------- | :------------------------ |
|
|
20
|
+
* | **Header** | |
|
|
21
|
+
* | - {@link protocolVersion} | |
|
|
22
|
+
* | - {@link OwnerId} | {@link Owner} |
|
|
23
|
+
* | **Initiator** | |
|
|
24
|
+
* | - {@link WriteKeyMode} | |
|
|
25
|
+
* | - {@link WriteKey} | If WriteKeyMode >= 1 |
|
|
26
|
+
* | - {@link WriteKey} | If WriteKeyMode = 2 (new) |
|
|
27
|
+
* | **Non-initiator** | |
|
|
28
|
+
* | - {@link ProtocolErrorCode} | |
|
|
29
|
+
* | **Messages** | |
|
|
30
|
+
* | - {@link NonNegativeInt} | A number of messages. |
|
|
31
|
+
* | - {@link EncryptedCrdtMessage} | |
|
|
32
|
+
* | **Ranges** | |
|
|
33
|
+
* | - {@link NonNegativeInt} | Number of ranges. |
|
|
34
|
+
* | - {@link Range} | |
|
|
35
|
+
*
|
|
36
|
+
* ### WriteKey Validation
|
|
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.
|
|
33
43
|
*
|
|
34
|
-
*
|
|
44
|
+
* ### WriteKey Rotation
|
|
45
|
+
*
|
|
46
|
+
* When initiator's {@link WriteKeyMode} is `Rotation`, two WriteKeys are
|
|
47
|
+
* present:
|
|
48
|
+
*
|
|
49
|
+
* 1. Current WriteKey (for validation)
|
|
50
|
+
* 2. New WriteKey (to be stored)
|
|
35
51
|
*
|
|
36
52
|
* ### Synchronization
|
|
37
53
|
*
|
|
@@ -48,21 +64,34 @@
|
|
|
48
64
|
* Both **Messages** and **Ranges** are optional, allowing each side to send,
|
|
49
65
|
* sync, or only subscribe data as needed.
|
|
50
66
|
*
|
|
51
|
-
* When the initiator sends data, the {@link WriteKey} is
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
67
|
+
* When the initiator sends data, the {@link WriteKey} is required in Messages as
|
|
68
|
+
* a secure token proving the initiator can write changes. The non-initiator
|
|
69
|
+
* responds without a {@link WriteKey}, since the initiator’s request already
|
|
70
|
+
* signals it wants data. If the non-initiator detects an issue, it sends an
|
|
71
|
+
* error code via the `Error` field in the header back to the initiator. In
|
|
72
|
+
* relay-to-relay or P2P sync, both sides may require the {@link WriteKey}
|
|
73
|
+
* depending on who is the initiator.
|
|
74
|
+
*
|
|
75
|
+
* ### Protocol Errors
|
|
76
|
+
*
|
|
77
|
+
* The protocol uses error codes in the header to signal issues:
|
|
78
|
+
*
|
|
79
|
+
* - {@link ProtocolWriteKeyError}: The provided WriteKey is invalid or missing.
|
|
80
|
+
* - {@link ProtocolWriteError}: A write operation failed (e.g., due to storage
|
|
81
|
+
* limits or billing).
|
|
82
|
+
* - {@link ProtocolSyncError}: A generic or unexpected synchronization failure
|
|
83
|
+
* occurred.
|
|
84
|
+
* - {@link ProtocolUnsupportedVersionError}: Protocol version mismatch.
|
|
85
|
+
* - {@link ProtocolInvalidDataError}: The message is malformed or corrupted.
|
|
86
|
+
*
|
|
87
|
+
* All protocol errors except `ProtocolInvalidDataError` include the `ownerId`
|
|
88
|
+
* to allow clients to associate errors with the correct owner.
|
|
60
89
|
*
|
|
61
90
|
* ### Message Size Limit
|
|
62
91
|
*
|
|
63
92
|
* The protocol enforces a strict maximum size for all messages, defined by
|
|
64
|
-
* {@link maxProtocolMessageSize}. This ensures every
|
|
65
|
-
* than or equal to this limit, eliminating the need for applications to
|
|
93
|
+
* {@link maxProtocolMessageSize}. This ensures every {@link ProtocolMessage} is
|
|
94
|
+
* less than or equal to this limit, eliminating the need for applications to
|
|
66
95
|
* fragment and reconstruct messages during transmission.
|
|
67
96
|
*
|
|
68
97
|
* ### Why Binary?
|
|
@@ -88,11 +117,24 @@
|
|
|
88
117
|
*
|
|
89
118
|
* ### Versioning
|
|
90
119
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
120
|
+
* Evolu Protocol uses explicit versioning to ensure compatibility between
|
|
121
|
+
* clients and relays (or peers). Each protocol message begins with a version
|
|
122
|
+
* number and an `ownerId` in its header.
|
|
123
|
+
*
|
|
124
|
+
* **How version negotiation works:**
|
|
125
|
+
*
|
|
126
|
+
* - The initiator (usually a client) sends a `ProtocolMessage` that includes its
|
|
127
|
+
* protocol version and the `ownerId`.
|
|
128
|
+
* - The non-initiator (usually a relay or peer) checks the version.
|
|
129
|
+
*
|
|
130
|
+
* - If the versions match, synchronization proceeds as normal.
|
|
131
|
+
* - If the versions do not match, the non-initiator responds with a message
|
|
132
|
+
* containing **its own protocol version and the same `ownerId`**.
|
|
133
|
+
* - The initiator can then detect the version mismatch for that specific owner
|
|
134
|
+
* and handle it appropriately (e.g., prompt for an update or halt sync).
|
|
135
|
+
*
|
|
136
|
+
* Version negotiation is per-owner, allowing Evolu Protocol to evolve safely
|
|
137
|
+
* over time and provide clear feedback about version mismatches.
|
|
96
138
|
*
|
|
97
139
|
* @module
|
|
98
140
|
*/
|
|
@@ -103,10 +145,8 @@ import { isNonEmptyReadonlyArray, NonEmptyReadonlyArray } from "../Array.js";
|
|
|
103
145
|
import { assert } from "../Assert.js";
|
|
104
146
|
import {
|
|
105
147
|
Buffer,
|
|
106
|
-
BufferError,
|
|
107
148
|
bytesToHex,
|
|
108
149
|
bytesToUtf8,
|
|
109
|
-
concatBytes,
|
|
110
150
|
createBuffer,
|
|
111
151
|
hexToBytes,
|
|
112
152
|
utf8ToBytes,
|
|
@@ -120,7 +160,7 @@ import {
|
|
|
120
160
|
} from "../Crypto.js";
|
|
121
161
|
import { eqArrayNumber } from "../Eq.js";
|
|
122
162
|
import { computeBalancedBuckets } from "../Number.js";
|
|
123
|
-
import { objectToEntries
|
|
163
|
+
import { objectToEntries } from "../Object.js";
|
|
124
164
|
import { err, ok, Result } from "../Result.js";
|
|
125
165
|
import { SqliteValue } from "../Sqlite.js";
|
|
126
166
|
import {
|
|
@@ -133,10 +173,13 @@ import {
|
|
|
133
173
|
NanoId,
|
|
134
174
|
NonNegativeInt,
|
|
135
175
|
Number,
|
|
176
|
+
object,
|
|
136
177
|
PositiveInt,
|
|
178
|
+
record,
|
|
137
179
|
} from "../Type.js";
|
|
138
180
|
import { Brand, Predicate } from "../Types.js";
|
|
139
181
|
import {
|
|
182
|
+
Owner,
|
|
140
183
|
OwnerId,
|
|
141
184
|
OwnerWithWriteAccess,
|
|
142
185
|
WriteKey,
|
|
@@ -177,6 +220,14 @@ export const ProtocolErrorCode = {
|
|
|
177
220
|
type ProtocolErrorCode =
|
|
178
221
|
(typeof ProtocolErrorCode)[keyof typeof ProtocolErrorCode];
|
|
179
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
|
+
|
|
180
231
|
/**
|
|
181
232
|
* Evolu Protocol Storage
|
|
182
233
|
*
|
|
@@ -221,17 +272,15 @@ export interface Storage {
|
|
|
221
272
|
callback: (timestamp: BinaryTimestamp, index: NonNegativeInt) => boolean,
|
|
222
273
|
) => void;
|
|
223
274
|
|
|
224
|
-
/**
|
|
225
|
-
* Authorizes the initiator's {@link WriteKey} for the given
|
|
226
|
-
* {@link BinaryOwnerId}.
|
|
227
|
-
*
|
|
228
|
-
* For a client that does not expect foreign writes, return `false`.
|
|
229
|
-
*/
|
|
275
|
+
/** Validates the {@link WriteKey} for the given {@link Owner}. */
|
|
230
276
|
readonly validateWriteKey: (
|
|
231
277
|
ownerId: BinaryOwnerId,
|
|
232
278
|
writeKey: WriteKey,
|
|
233
279
|
) => boolean;
|
|
234
280
|
|
|
281
|
+
/** Sets the {@link WriteKey} for the given {@link Owner}. */
|
|
282
|
+
readonly setWriteKey: (ownerId: BinaryOwnerId, writeKey: WriteKey) => boolean;
|
|
283
|
+
|
|
235
284
|
/** Write encrypted {@link CrdtMessage}s to storage. */
|
|
236
285
|
readonly writeMessages: (
|
|
237
286
|
ownerId: BinaryOwnerId,
|
|
@@ -243,6 +292,9 @@ export interface Storage {
|
|
|
243
292
|
ownerId: BinaryOwnerId,
|
|
244
293
|
timestamp: BinaryTimestamp,
|
|
245
294
|
) => EncryptedDbChange | null;
|
|
295
|
+
|
|
296
|
+
/** Delete all data for the given {@link Owner}. */
|
|
297
|
+
readonly deleteOwner: (ownerId: BinaryOwnerId) => boolean;
|
|
246
298
|
}
|
|
247
299
|
|
|
248
300
|
export interface StorageDep {
|
|
@@ -267,15 +319,23 @@ export interface CrdtMessage {
|
|
|
267
319
|
readonly change: DbChange;
|
|
268
320
|
}
|
|
269
321
|
|
|
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;
|
|
328
|
+
|
|
270
329
|
/**
|
|
271
330
|
* A DbChange is a change to a table row. Together with a unique
|
|
272
331
|
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
273
332
|
*/
|
|
274
|
-
export
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
333
|
+
export const DbChange = object({
|
|
334
|
+
table: Base64Url256,
|
|
335
|
+
id: Id,
|
|
336
|
+
values: record(Base64Url256, SqliteValue),
|
|
337
|
+
});
|
|
338
|
+
export type DbChange = typeof DbChange.Type;
|
|
279
339
|
|
|
280
340
|
export const RangeType = {
|
|
281
341
|
Fingerprint: 1,
|
|
@@ -340,11 +400,16 @@ export type ProtocolError =
|
|
|
340
400
|
| ProtocolWriteError
|
|
341
401
|
| ProtocolSyncError;
|
|
342
402
|
|
|
403
|
+
/** Base interface for all protocol errors. */
|
|
404
|
+
export interface ProtocolErrorBase {
|
|
405
|
+
readonly ownerId: OwnerId;
|
|
406
|
+
}
|
|
407
|
+
|
|
343
408
|
/**
|
|
344
409
|
* Represents a version mismatch in the Evolu Protocol. Occurs when the
|
|
345
410
|
* initiator and non-initiator are using incompatible protocol versions.
|
|
346
411
|
*/
|
|
347
|
-
export interface ProtocolUnsupportedVersionError {
|
|
412
|
+
export interface ProtocolUnsupportedVersionError extends ProtocolErrorBase {
|
|
348
413
|
readonly type: "ProtocolUnsupportedVersionError";
|
|
349
414
|
readonly unsupportedVersion: NonNegativeInt;
|
|
350
415
|
/** Indicates which side is obsolete and should update. */
|
|
@@ -359,7 +424,7 @@ export interface ProtocolInvalidDataError {
|
|
|
359
424
|
}
|
|
360
425
|
|
|
361
426
|
/** Error when a {@link WriteKey} is invalid, missing, or fails validation. */
|
|
362
|
-
export interface ProtocolWriteKeyError {
|
|
427
|
+
export interface ProtocolWriteKeyError extends ProtocolErrorBase {
|
|
363
428
|
readonly type: "ProtocolWriteKeyError";
|
|
364
429
|
}
|
|
365
430
|
|
|
@@ -367,7 +432,7 @@ export interface ProtocolWriteKeyError {
|
|
|
367
432
|
* Error when a write fails due to storage limits or billing requirements.
|
|
368
433
|
* Indicates the need to expand capacity or resolve payment issues.
|
|
369
434
|
*/
|
|
370
|
-
export interface ProtocolWriteError {
|
|
435
|
+
export interface ProtocolWriteError extends ProtocolErrorBase {
|
|
371
436
|
readonly type: "ProtocolWriteError";
|
|
372
437
|
}
|
|
373
438
|
|
|
@@ -375,7 +440,7 @@ export interface ProtocolWriteError {
|
|
|
375
440
|
* Error indicating a synchronization failure during the protocol exchange. Used
|
|
376
441
|
* for unexpected or generic sync errors not covered by other error types.
|
|
377
442
|
*/
|
|
378
|
-
export interface ProtocolSyncError {
|
|
443
|
+
export interface ProtocolSyncError extends ProtocolErrorBase {
|
|
379
444
|
readonly type: "ProtocolSyncError";
|
|
380
445
|
}
|
|
381
446
|
|
|
@@ -394,6 +459,7 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
394
459
|
maxSize?: PositiveInt,
|
|
395
460
|
): ProtocolMessage => {
|
|
396
461
|
const buffer = createProtocolMessageBuffer(owner.id, {
|
|
462
|
+
type: "initiator",
|
|
397
463
|
totalMaxSize: maxSize ?? maxProtocolMessageSize,
|
|
398
464
|
writeKey: owner.writeKey,
|
|
399
465
|
});
|
|
@@ -449,7 +515,7 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
449
515
|
export const createProtocolMessageForSync =
|
|
450
516
|
(deps: StorageDep) =>
|
|
451
517
|
(ownerId: OwnerId): ProtocolMessage | null => {
|
|
452
|
-
const buffer = createProtocolMessageBuffer(ownerId);
|
|
518
|
+
const buffer = createProtocolMessageBuffer(ownerId, { type: "initiator" });
|
|
453
519
|
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
454
520
|
|
|
455
521
|
const size = deps.storage.getSize(binaryOwnerId);
|
|
@@ -467,6 +533,19 @@ export const createProtocolMessageForSync =
|
|
|
467
533
|
return buffer.unwrap();
|
|
468
534
|
};
|
|
469
535
|
|
|
536
|
+
/** Creates a ProtocolMessage for {@link WriteKey} rotation. */
|
|
537
|
+
export const createProtocolMessageForWriteKeyRotation = (
|
|
538
|
+
ownerId: OwnerId,
|
|
539
|
+
currentWriteKey: WriteKey,
|
|
540
|
+
newWriteKey: WriteKey,
|
|
541
|
+
): ProtocolMessage => {
|
|
542
|
+
const buffer = createProtocolMessageBuffer(ownerId, {
|
|
543
|
+
type: "initiator",
|
|
544
|
+
writeKey: [currentWriteKey, newWriteKey],
|
|
545
|
+
});
|
|
546
|
+
return buffer.unwrap();
|
|
547
|
+
};
|
|
548
|
+
|
|
470
549
|
/**
|
|
471
550
|
* Mutable builder for constructing {@link ProtocolMessage} respecting size
|
|
472
551
|
* limits.
|
|
@@ -494,16 +573,22 @@ export interface ProtocolMessageBuffer {
|
|
|
494
573
|
export const createProtocolMessageBuffer = (
|
|
495
574
|
ownerId: OwnerId,
|
|
496
575
|
options: {
|
|
497
|
-
readonly errorCode?: ProtocolErrorCode;
|
|
498
|
-
readonly writeKey?: WriteKey;
|
|
499
576
|
readonly totalMaxSize?: PositiveInt | undefined;
|
|
500
577
|
readonly rangesMaxSize?: PositiveInt | undefined;
|
|
501
578
|
readonly version?: NonNegativeInt;
|
|
502
|
-
}
|
|
579
|
+
} & (
|
|
580
|
+
| {
|
|
581
|
+
readonly type: "initiator";
|
|
582
|
+
/** Single key or [current, new] for rotation. */
|
|
583
|
+
readonly writeKey?: WriteKey | readonly [WriteKey, WriteKey];
|
|
584
|
+
}
|
|
585
|
+
| {
|
|
586
|
+
readonly type: "non-initiator";
|
|
587
|
+
readonly errorCode: ProtocolErrorCode;
|
|
588
|
+
}
|
|
589
|
+
),
|
|
503
590
|
): ProtocolMessageBuffer => {
|
|
504
591
|
const {
|
|
505
|
-
errorCode,
|
|
506
|
-
writeKey,
|
|
507
592
|
totalMaxSize = maxProtocolMessageSize,
|
|
508
593
|
rangesMaxSize = maxProtocolMessageRangesSize,
|
|
509
594
|
version = protocolVersion,
|
|
@@ -524,7 +609,21 @@ export const createProtocolMessageBuffer = (
|
|
|
524
609
|
|
|
525
610
|
encodeNonNegativeInt(buffers.header, version);
|
|
526
611
|
buffers.header.extend(ownerIdToBinaryOwnerId(ownerId));
|
|
527
|
-
|
|
612
|
+
|
|
613
|
+
if (options.type === "initiator") {
|
|
614
|
+
if (!options.writeKey) {
|
|
615
|
+
buffers.header.extend([WriteKeyMode.None]);
|
|
616
|
+
} else if (!Array.isArray(options.writeKey)) {
|
|
617
|
+
buffers.header.extend([WriteKeyMode.Single]);
|
|
618
|
+
buffers.header.extend(options.writeKey as WriteKey);
|
|
619
|
+
} else {
|
|
620
|
+
buffers.header.extend([WriteKeyMode.Rotation]);
|
|
621
|
+
buffers.header.extend(options.writeKey[0] as WriteKey); // current
|
|
622
|
+
buffers.header.extend(options.writeKey[1] as WriteKey); // new
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
buffers.header.extend([options.errorCode]);
|
|
626
|
+
}
|
|
528
627
|
|
|
529
628
|
let isLastRangeInfinite = false;
|
|
530
629
|
|
|
@@ -536,10 +635,7 @@ export const createProtocolMessageBuffer = (
|
|
|
536
635
|
const getHeaderAndMessagesSize = () =>
|
|
537
636
|
buffers.header.getLength() +
|
|
538
637
|
buffers.messages.timestamps.getLength() +
|
|
539
|
-
buffers.messages.dbChanges.getLength()
|
|
540
|
-
(buffers.messages.timestamps.getCount() > 0 && writeKey
|
|
541
|
-
? writeKeyLength
|
|
542
|
-
: 0);
|
|
638
|
+
buffers.messages.dbChanges.getLength();
|
|
543
639
|
|
|
544
640
|
const getRangesSize = () =>
|
|
545
641
|
buffers.ranges.timestamps.getCount() > 0
|
|
@@ -654,8 +750,6 @@ export const createProtocolMessageBuffer = (
|
|
|
654
750
|
|
|
655
751
|
buffers.messages.timestamps.append(buffers.header);
|
|
656
752
|
buffers.header.extend(buffers.messages.dbChanges.unwrap());
|
|
657
|
-
if (buffers.messages.timestamps.getCount() > 0 && writeKey)
|
|
658
|
-
buffers.header.extend(writeKey);
|
|
659
753
|
|
|
660
754
|
if (buffers.ranges.timestamps.getCount() > 0) {
|
|
661
755
|
buffers.ranges.timestamps.append(buffers.header);
|
|
@@ -794,33 +888,34 @@ export const applyProtocolMessageAsClient =
|
|
|
794
888
|
tryDecodeProtocolData<ProtocolMessage | null, ProtocolError>(
|
|
795
889
|
inputMessage,
|
|
796
890
|
(input) => {
|
|
797
|
-
const requestedVersion =
|
|
891
|
+
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
798
892
|
|
|
799
893
|
if (requestedVersion !== version) {
|
|
800
894
|
return err<ProtocolUnsupportedVersionError>({
|
|
801
895
|
type: "ProtocolUnsupportedVersionError",
|
|
802
896
|
unsupportedVersion: requestedVersion,
|
|
803
897
|
isInitiator: version < requestedVersion,
|
|
898
|
+
ownerId,
|
|
804
899
|
});
|
|
805
900
|
}
|
|
806
901
|
|
|
807
|
-
const ownerId = decodeOwnerId(input);
|
|
808
|
-
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
809
|
-
|
|
810
902
|
const errorCode = input.shift() as ProtocolErrorCode;
|
|
811
903
|
if (errorCode !== ProtocolErrorCode.NoError) {
|
|
812
904
|
switch (errorCode) {
|
|
813
905
|
case ProtocolErrorCode.WriteKeyError:
|
|
814
906
|
return err<ProtocolWriteKeyError>({
|
|
815
907
|
type: "ProtocolWriteKeyError",
|
|
908
|
+
ownerId,
|
|
816
909
|
});
|
|
817
910
|
case ProtocolErrorCode.WriteError:
|
|
818
911
|
return err<ProtocolWriteError>({
|
|
819
912
|
type: "ProtocolWriteError",
|
|
913
|
+
ownerId,
|
|
820
914
|
});
|
|
821
915
|
case ProtocolErrorCode.SyncError:
|
|
822
916
|
return err<ProtocolSyncError>({
|
|
823
917
|
type: "ProtocolSyncError",
|
|
918
|
+
ownerId,
|
|
824
919
|
});
|
|
825
920
|
default:
|
|
826
921
|
throw new ProtocolDecodeError(
|
|
@@ -830,6 +925,7 @@ export const applyProtocolMessageAsClient =
|
|
|
830
925
|
}
|
|
831
926
|
|
|
832
927
|
const messages = decodeMessages(input);
|
|
928
|
+
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
833
929
|
|
|
834
930
|
if (
|
|
835
931
|
isNonEmptyReadonlyArray(messages) &&
|
|
@@ -843,6 +939,7 @@ export const applyProtocolMessageAsClient =
|
|
|
843
939
|
if (writeKey == null) return ok(null);
|
|
844
940
|
|
|
845
941
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
942
|
+
type: "initiator",
|
|
846
943
|
writeKey,
|
|
847
944
|
totalMaxSize,
|
|
848
945
|
rangesMaxSize,
|
|
@@ -877,58 +974,102 @@ export const applyProtocolMessageAsRelay =
|
|
|
877
974
|
version = protocolVersion,
|
|
878
975
|
): Result<ProtocolMessage | null, ProtocolInvalidDataError> =>
|
|
879
976
|
tryDecodeProtocolData(inputMessage, (input) => {
|
|
880
|
-
const requestedVersion =
|
|
977
|
+
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
978
|
+
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
881
979
|
|
|
882
980
|
if (requestedVersion !== version) {
|
|
883
|
-
// Non-initiator responds with its version.
|
|
981
|
+
// Non-initiator responds with its version and ownerId.
|
|
884
982
|
const output = createBuffer();
|
|
885
983
|
encodeNonNegativeInt(output, version);
|
|
984
|
+
output.extend(binaryOwnerId);
|
|
886
985
|
return ok(output.unwrap() as ProtocolMessage);
|
|
887
986
|
}
|
|
888
987
|
|
|
889
|
-
const ownerId = decodeOwnerId(input);
|
|
890
|
-
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
891
|
-
|
|
892
988
|
subscribe?.(ownerId);
|
|
893
989
|
|
|
894
|
-
const
|
|
990
|
+
const writeKeyMode = input.shift() as WriteKeyMode;
|
|
991
|
+
let writeKey: WriteKey | undefined;
|
|
992
|
+
let newWriteKey: WriteKey | undefined;
|
|
993
|
+
|
|
994
|
+
if (writeKeyMode !== WriteKeyMode.None) {
|
|
995
|
+
writeKey = input.shiftN(writeKeyLength) as WriteKey;
|
|
996
|
+
switch (writeKeyMode) {
|
|
997
|
+
case WriteKeyMode.Single:
|
|
998
|
+
break;
|
|
999
|
+
case WriteKeyMode.Rotation:
|
|
1000
|
+
newWriteKey = input.shiftN(writeKeyLength) as WriteKey;
|
|
1001
|
+
break;
|
|
1002
|
+
default:
|
|
1003
|
+
throw new ProtocolDecodeError(
|
|
1004
|
+
`Invalid WriteKeyMode: ${writeKeyMode}`,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
895
1008
|
|
|
896
|
-
if (
|
|
897
|
-
const
|
|
898
|
-
|
|
1009
|
+
if (writeKey) {
|
|
1010
|
+
const isValid = deps.storage.validateWriteKey(binaryOwnerId, writeKey);
|
|
1011
|
+
if (!isValid) {
|
|
1012
|
+
return ok(
|
|
1013
|
+
createProtocolMessageBuffer(ownerId, {
|
|
1014
|
+
type: "non-initiator",
|
|
1015
|
+
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
1016
|
+
}).unwrap(),
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
899
1019
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
904
1035
|
|
|
905
|
-
|
|
1036
|
+
const messages = decodeMessages(input);
|
|
1037
|
+
|
|
1038
|
+
if (isNonEmptyReadonlyArray(messages)) {
|
|
1039
|
+
if (!writeKey)
|
|
906
1040
|
return ok(
|
|
907
1041
|
createProtocolMessageBuffer(ownerId, {
|
|
1042
|
+
type: "non-initiator",
|
|
908
1043
|
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
909
1044
|
}).unwrap(),
|
|
910
1045
|
);
|
|
911
1046
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1047
|
+
// Only broadcast if there's no ranges.
|
|
1048
|
+
if (broadcast && input.getLength() === 0) {
|
|
1049
|
+
const broadcastBuffer = createProtocolMessageBuffer(ownerId, {
|
|
1050
|
+
type: "non-initiator",
|
|
1051
|
+
errorCode: ProtocolErrorCode.NoError,
|
|
1052
|
+
totalMaxSize,
|
|
1053
|
+
rangesMaxSize,
|
|
1054
|
+
version,
|
|
1055
|
+
});
|
|
1056
|
+
for (const message of messages) {
|
|
1057
|
+
broadcastBuffer.addMessage(message);
|
|
1058
|
+
}
|
|
1059
|
+
broadcast(ownerId, broadcastBuffer.unwrap());
|
|
921
1060
|
}
|
|
922
1061
|
|
|
923
1062
|
if (!deps.storage.writeMessages(binaryOwnerId, messages))
|
|
924
1063
|
return ok(
|
|
925
1064
|
createProtocolMessageBuffer(ownerId, {
|
|
1065
|
+
type: "non-initiator",
|
|
926
1066
|
errorCode: ProtocolErrorCode.WriteError,
|
|
927
1067
|
}).unwrap(),
|
|
928
1068
|
);
|
|
929
1069
|
}
|
|
930
1070
|
|
|
931
1071
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
1072
|
+
type: "non-initiator",
|
|
932
1073
|
errorCode: ProtocolErrorCode.NoError,
|
|
933
1074
|
totalMaxSize,
|
|
934
1075
|
rangesMaxSize,
|
|
@@ -949,17 +1090,24 @@ const tryDecodeProtocolData = <T, E>(
|
|
|
949
1090
|
try {
|
|
950
1091
|
return callback(createBuffer(data));
|
|
951
1092
|
} catch (error: unknown) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
throw error;
|
|
1093
|
+
return err<ProtocolInvalidDataError>({
|
|
1094
|
+
type: "ProtocolInvalidDataError",
|
|
1095
|
+
data,
|
|
1096
|
+
error,
|
|
1097
|
+
});
|
|
960
1098
|
}
|
|
961
1099
|
};
|
|
962
1100
|
|
|
1101
|
+
const decodeVersionAndOwner = (input: Buffer): [NonNegativeInt, OwnerId] => {
|
|
1102
|
+
// This structure must never change across protocol versions. The version
|
|
1103
|
+
// and owner ID must always be the first two fields in every protocol message
|
|
1104
|
+
// to enable version negotiation and owner identification before any other
|
|
1105
|
+
// processing occurs.
|
|
1106
|
+
const version = decodeNonNegativeInt(input);
|
|
1107
|
+
const ownerId = decodeOwnerId(input);
|
|
1108
|
+
return [version, ownerId];
|
|
1109
|
+
};
|
|
1110
|
+
|
|
963
1111
|
/**
|
|
964
1112
|
* Error thrown for internal protocol validation failures, such as invalid data
|
|
965
1113
|
* or type errors.
|
|
@@ -1012,6 +1160,7 @@ const sync =
|
|
|
1012
1160
|
return ok(null);
|
|
1013
1161
|
}
|
|
1014
1162
|
const message = createProtocolMessageBuffer(binaryOwnerId, {
|
|
1163
|
+
type: "non-initiator",
|
|
1015
1164
|
errorCode: ProtocolErrorCode.SyncError,
|
|
1016
1165
|
});
|
|
1017
1166
|
return ok(message.unwrap());
|
|
@@ -1394,13 +1543,6 @@ export const ownerIdToBinaryOwnerId = (ownerId: OwnerId): BinaryOwnerId =>
|
|
|
1394
1543
|
export const binaryOwnerIdToOwnerId = (binaryOwnerId: BinaryOwnerId): OwnerId =>
|
|
1395
1544
|
decodeOwnerId(createBuffer(binaryOwnerId));
|
|
1396
1545
|
|
|
1397
|
-
/**
|
|
1398
|
-
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
1399
|
-
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
1400
|
-
*/
|
|
1401
|
-
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
1402
|
-
export type Base64Url256 = typeof Base64Url256.Type;
|
|
1403
|
-
|
|
1404
1546
|
/**
|
|
1405
1547
|
* Union type for all variants of Base64Url strings with limited length. All
|
|
1406
1548
|
* these types use Base64Url alphabet and are < 256 characters.
|
|
@@ -1855,3 +1997,27 @@ export const decodeSqliteValue = (buffer: Buffer): SqliteValue => {
|
|
|
1855
1997
|
throw new ProtocolDecodeError("invalid ProtocolValueType");
|
|
1856
1998
|
}
|
|
1857
1999
|
};
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* Decodes a ProtocolMessage into a readable JSON object for debugging.
|
|
2003
|
+
*
|
|
2004
|
+
* Note: This is a stub for future implementation. It should use:
|
|
2005
|
+
*
|
|
2006
|
+
* - DecodeVersionAndOwner
|
|
2007
|
+
* - DecodeError or decodeWriteKeys (depending on context)
|
|
2008
|
+
* - DecodeMessages
|
|
2009
|
+
* - DecodeRanges
|
|
2010
|
+
*
|
|
2011
|
+
* If you want to help, please contribute to this function.
|
|
2012
|
+
*/
|
|
2013
|
+
export const decodeProtocolMessageToJson = (
|
|
2014
|
+
_protocolMessage: ProtocolMessage,
|
|
2015
|
+
_isInitiator: boolean,
|
|
2016
|
+
): unknown => {
|
|
2017
|
+
// TODO: Implement using
|
|
2018
|
+
// - decodeVersionAndOwner
|
|
2019
|
+
// -- decodeError or decodeWriteKeys (should be refactored out),
|
|
2020
|
+
// -- decodeMessages, and decodeRanges.
|
|
2021
|
+
// This is a stub for PRs and community contributions.
|
|
2022
|
+
throw new Error("decodeProtocolMessageToJson is not implemented yet.");
|
|
2023
|
+
};
|