@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
|
@@ -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.
|
|
43
|
+
*
|
|
44
|
+
* ### WriteKey Rotation
|
|
45
|
+
*
|
|
46
|
+
* When initiator's {@link WriteKeyMode} is `Rotation`, two WriteKeys are
|
|
47
|
+
* present:
|
|
33
48
|
*
|
|
34
|
-
*
|
|
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
|
*/
|
|
@@ -100,13 +142,14 @@ import { sha256 } from "@noble/hashes/sha2";
|
|
|
100
142
|
import { pack, unpack, unpackMultiple } from "msgpackr";
|
|
101
143
|
import { isNonEmptyReadonlyArray } from "../Array.js";
|
|
102
144
|
import { assert } from "../Assert.js";
|
|
103
|
-
import {
|
|
145
|
+
import { bytesToHex, bytesToUtf8, createBuffer, hexToBytes, utf8ToBytes, } from "../Buffer.js";
|
|
104
146
|
import { padmePaddingLength, } from "../Crypto.js";
|
|
105
147
|
import { eqArrayNumber } from "../Eq.js";
|
|
106
148
|
import { computeBalancedBuckets } from "../Number.js";
|
|
107
149
|
import { objectToEntries } from "../Object.js";
|
|
108
150
|
import { err, ok } from "../Result.js";
|
|
109
|
-
import {
|
|
151
|
+
import { SqliteValue } from "../Sqlite.js";
|
|
152
|
+
import { Base64Url, DateIsoString, Id, idTypeValueLength, JsonValueFromString, maxLength, NonNegativeInt, Number, object, record, } from "../Type.js";
|
|
110
153
|
import { writeKeyLength, } from "./Owner.js";
|
|
111
154
|
import { binaryTimestampToTimestamp, Counter, Millis, timestampToBinaryTimestamp, } from "./Timestamp.js";
|
|
112
155
|
/** Maximum size of the entire protocol message in bytes. */
|
|
@@ -124,6 +167,25 @@ export const ProtocolErrorCode = {
|
|
|
124
167
|
/** A code for {@link ProtocolSyncError}. */
|
|
125
168
|
SyncError: 3,
|
|
126
169
|
};
|
|
170
|
+
export const WriteKeyMode = {
|
|
171
|
+
None: 0,
|
|
172
|
+
Single: 1,
|
|
173
|
+
Rotation: 2,
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
177
|
+
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
178
|
+
*/
|
|
179
|
+
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
180
|
+
/**
|
|
181
|
+
* A DbChange is a change to a table row. Together with a unique
|
|
182
|
+
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
183
|
+
*/
|
|
184
|
+
export const DbChange = object({
|
|
185
|
+
table: Base64Url256,
|
|
186
|
+
id: Id,
|
|
187
|
+
values: record(Base64Url256, SqliteValue),
|
|
188
|
+
});
|
|
127
189
|
export const RangeType = {
|
|
128
190
|
Fingerprint: 1,
|
|
129
191
|
Skip: 0,
|
|
@@ -142,6 +204,7 @@ export const zeroFingerprint = new Uint8Array(fingerprintSize);
|
|
|
142
204
|
*/
|
|
143
205
|
export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages, maxSize) => {
|
|
144
206
|
const buffer = createProtocolMessageBuffer(owner.id, {
|
|
207
|
+
type: "initiator",
|
|
145
208
|
totalMaxSize: maxSize ?? maxProtocolMessageSize,
|
|
146
209
|
writeKey: owner.writeKey,
|
|
147
210
|
});
|
|
@@ -185,7 +248,7 @@ export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages,
|
|
|
185
248
|
};
|
|
186
249
|
/** Creates a {@link ProtocolMessage} for sync. */
|
|
187
250
|
export const createProtocolMessageForSync = (deps) => (ownerId) => {
|
|
188
|
-
const buffer = createProtocolMessageBuffer(ownerId);
|
|
251
|
+
const buffer = createProtocolMessageBuffer(ownerId, { type: "initiator" });
|
|
189
252
|
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
190
253
|
const size = deps.storage.getSize(binaryOwnerId);
|
|
191
254
|
// Errors are handled by the storage.
|
|
@@ -194,8 +257,16 @@ export const createProtocolMessageForSync = (deps) => (ownerId) => {
|
|
|
194
257
|
splitRange(deps)(binaryOwnerId, 0, size, InfiniteUpperBound, buffer);
|
|
195
258
|
return buffer.unwrap();
|
|
196
259
|
};
|
|
197
|
-
|
|
198
|
-
|
|
260
|
+
/** Creates a ProtocolMessage for {@link WriteKey} rotation. */
|
|
261
|
+
export const createProtocolMessageForWriteKeyRotation = (ownerId, currentWriteKey, newWriteKey) => {
|
|
262
|
+
const buffer = createProtocolMessageBuffer(ownerId, {
|
|
263
|
+
type: "initiator",
|
|
264
|
+
writeKey: [currentWriteKey, newWriteKey],
|
|
265
|
+
});
|
|
266
|
+
return buffer.unwrap();
|
|
267
|
+
};
|
|
268
|
+
export const createProtocolMessageBuffer = (ownerId, options) => {
|
|
269
|
+
const { totalMaxSize = maxProtocolMessageSize, rangesMaxSize = maxProtocolMessageRangesSize, version = protocolVersion, } = options;
|
|
199
270
|
const buffers = {
|
|
200
271
|
header: createBuffer(),
|
|
201
272
|
messages: {
|
|
@@ -210,17 +281,29 @@ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
|
|
|
210
281
|
};
|
|
211
282
|
encodeNonNegativeInt(buffers.header, version);
|
|
212
283
|
buffers.header.extend(ownerIdToBinaryOwnerId(ownerId));
|
|
213
|
-
if (
|
|
214
|
-
|
|
284
|
+
if (options.type === "initiator") {
|
|
285
|
+
if (!options.writeKey) {
|
|
286
|
+
buffers.header.extend([WriteKeyMode.None]);
|
|
287
|
+
}
|
|
288
|
+
else if (!Array.isArray(options.writeKey)) {
|
|
289
|
+
buffers.header.extend([WriteKeyMode.Single]);
|
|
290
|
+
buffers.header.extend(options.writeKey);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
buffers.header.extend([WriteKeyMode.Rotation]);
|
|
294
|
+
buffers.header.extend(options.writeKey[0]); // current
|
|
295
|
+
buffers.header.extend(options.writeKey[1]); // new
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
buffers.header.extend([options.errorCode]);
|
|
300
|
+
}
|
|
215
301
|
let isLastRangeInfinite = false;
|
|
216
302
|
const isWithinSizeLimits = () => getSize() <= totalMaxSize;
|
|
217
303
|
const getSize = () => (getHeaderAndMessagesSize() + getRangesSize());
|
|
218
304
|
const getHeaderAndMessagesSize = () => buffers.header.getLength() +
|
|
219
305
|
buffers.messages.timestamps.getLength() +
|
|
220
|
-
buffers.messages.dbChanges.getLength()
|
|
221
|
-
(buffers.messages.timestamps.getCount() > 0 && writeKey
|
|
222
|
-
? writeKeyLength
|
|
223
|
-
: 0);
|
|
306
|
+
buffers.messages.dbChanges.getLength();
|
|
224
307
|
const getRangesSize = () => buffers.ranges.timestamps.getCount() > 0
|
|
225
308
|
? buffers.ranges.timestamps.getLength() +
|
|
226
309
|
buffers.ranges.types.getLength() +
|
|
@@ -306,8 +389,6 @@ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
|
|
|
306
389
|
}
|
|
307
390
|
buffers.messages.timestamps.append(buffers.header);
|
|
308
391
|
buffers.header.extend(buffers.messages.dbChanges.unwrap());
|
|
309
|
-
if (buffers.messages.timestamps.getCount() > 0 && writeKey)
|
|
310
|
-
buffers.header.extend(writeKey);
|
|
311
392
|
if (buffers.ranges.timestamps.getCount() > 0) {
|
|
312
393
|
buffers.ranges.timestamps.append(buffers.header);
|
|
313
394
|
buffers.header.extend(buffers.ranges.types.unwrap());
|
|
@@ -386,36 +467,39 @@ const createRunLengthEncoder = (encodeValue) => {
|
|
|
386
467
|
};
|
|
387
468
|
};
|
|
388
469
|
export const applyProtocolMessageAsClient = (deps) => (inputMessage, { getWriteKey, version = protocolVersion, totalMaxSize, rangesMaxSize, } = {}) => tryDecodeProtocolData(inputMessage, (input) => {
|
|
389
|
-
const requestedVersion =
|
|
470
|
+
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
390
471
|
if (requestedVersion !== version) {
|
|
391
472
|
return err({
|
|
392
473
|
type: "ProtocolUnsupportedVersionError",
|
|
393
474
|
unsupportedVersion: requestedVersion,
|
|
394
475
|
isInitiator: version < requestedVersion,
|
|
476
|
+
ownerId,
|
|
395
477
|
});
|
|
396
478
|
}
|
|
397
|
-
const ownerId = decodeOwnerId(input);
|
|
398
|
-
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
399
479
|
const errorCode = input.shift();
|
|
400
480
|
if (errorCode !== ProtocolErrorCode.NoError) {
|
|
401
481
|
switch (errorCode) {
|
|
402
482
|
case ProtocolErrorCode.WriteKeyError:
|
|
403
483
|
return err({
|
|
404
484
|
type: "ProtocolWriteKeyError",
|
|
485
|
+
ownerId,
|
|
405
486
|
});
|
|
406
487
|
case ProtocolErrorCode.WriteError:
|
|
407
488
|
return err({
|
|
408
489
|
type: "ProtocolWriteError",
|
|
490
|
+
ownerId,
|
|
409
491
|
});
|
|
410
492
|
case ProtocolErrorCode.SyncError:
|
|
411
493
|
return err({
|
|
412
494
|
type: "ProtocolSyncError",
|
|
495
|
+
ownerId,
|
|
413
496
|
});
|
|
414
497
|
default:
|
|
415
498
|
throw new ProtocolDecodeError(`Invalid ProtocolErrorCode: ${errorCode}`);
|
|
416
499
|
}
|
|
417
500
|
}
|
|
418
501
|
const messages = decodeMessages(input);
|
|
502
|
+
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
419
503
|
if (isNonEmptyReadonlyArray(messages) &&
|
|
420
504
|
!deps.storage.writeMessages(binaryOwnerId, messages)) {
|
|
421
505
|
return ok(null);
|
|
@@ -426,6 +510,7 @@ export const applyProtocolMessageAsClient = (deps) => (inputMessage, { getWriteK
|
|
|
426
510
|
if (writeKey == null)
|
|
427
511
|
return ok(null);
|
|
428
512
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
513
|
+
type: "initiator",
|
|
429
514
|
writeKey,
|
|
430
515
|
totalMaxSize,
|
|
431
516
|
rangesMaxSize,
|
|
@@ -435,36 +520,78 @@ export const applyProtocolMessageAsClient = (deps) => (inputMessage, { getWriteK
|
|
|
435
520
|
export const applyProtocolMessageAsRelay = (deps) => (inputMessage, { subscribe, broadcast, totalMaxSize, rangesMaxSize, } = {},
|
|
436
521
|
/** For testing purposes only; should not be used in production. */
|
|
437
522
|
version = protocolVersion) => tryDecodeProtocolData(inputMessage, (input) => {
|
|
438
|
-
const requestedVersion =
|
|
523
|
+
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
524
|
+
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
439
525
|
if (requestedVersion !== version) {
|
|
440
|
-
// Non-initiator responds with its version.
|
|
526
|
+
// Non-initiator responds with its version and ownerId.
|
|
441
527
|
const output = createBuffer();
|
|
442
528
|
encodeNonNegativeInt(output, version);
|
|
529
|
+
output.extend(binaryOwnerId);
|
|
443
530
|
return ok(output.unwrap());
|
|
444
531
|
}
|
|
445
|
-
const ownerId = decodeOwnerId(input);
|
|
446
|
-
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
447
532
|
subscribe?.(ownerId);
|
|
533
|
+
const writeKeyMode = input.shift();
|
|
534
|
+
let writeKey;
|
|
535
|
+
let newWriteKey;
|
|
536
|
+
if (writeKeyMode !== WriteKeyMode.None) {
|
|
537
|
+
writeKey = input.shiftN(writeKeyLength);
|
|
538
|
+
switch (writeKeyMode) {
|
|
539
|
+
case WriteKeyMode.Single:
|
|
540
|
+
break;
|
|
541
|
+
case WriteKeyMode.Rotation:
|
|
542
|
+
newWriteKey = input.shiftN(writeKeyLength);
|
|
543
|
+
break;
|
|
544
|
+
default:
|
|
545
|
+
throw new ProtocolDecodeError(`Invalid WriteKeyMode: ${writeKeyMode}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (writeKey) {
|
|
549
|
+
const isValid = deps.storage.validateWriteKey(binaryOwnerId, writeKey);
|
|
550
|
+
if (!isValid) {
|
|
551
|
+
return ok(createProtocolMessageBuffer(ownerId, {
|
|
552
|
+
type: "non-initiator",
|
|
553
|
+
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
554
|
+
}).unwrap());
|
|
555
|
+
}
|
|
556
|
+
if (newWriteKey) {
|
|
557
|
+
const rotationSuccess = deps.storage.setWriteKey(binaryOwnerId, newWriteKey);
|
|
558
|
+
if (!rotationSuccess) {
|
|
559
|
+
return ok(createProtocolMessageBuffer(ownerId, {
|
|
560
|
+
type: "non-initiator",
|
|
561
|
+
errorCode: ProtocolErrorCode.WriteError,
|
|
562
|
+
}).unwrap());
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
448
566
|
const messages = decodeMessages(input);
|
|
449
567
|
if (isNonEmptyReadonlyArray(messages)) {
|
|
450
|
-
|
|
451
|
-
const writeKey = input.shiftN(writeKeyLength);
|
|
452
|
-
const writeKeyIsValid = deps.storage.validateWriteKey(binaryOwnerId, writeKey);
|
|
453
|
-
if (!writeKeyIsValid)
|
|
568
|
+
if (!writeKey)
|
|
454
569
|
return ok(createProtocolMessageBuffer(ownerId, {
|
|
570
|
+
type: "non-initiator",
|
|
455
571
|
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
456
572
|
}).unwrap());
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
573
|
+
// Only broadcast if there's no ranges.
|
|
574
|
+
if (broadcast && input.getLength() === 0) {
|
|
575
|
+
const broadcastBuffer = createProtocolMessageBuffer(ownerId, {
|
|
576
|
+
type: "non-initiator",
|
|
577
|
+
errorCode: ProtocolErrorCode.NoError,
|
|
578
|
+
totalMaxSize,
|
|
579
|
+
rangesMaxSize,
|
|
580
|
+
version,
|
|
581
|
+
});
|
|
582
|
+
for (const message of messages) {
|
|
583
|
+
broadcastBuffer.addMessage(message);
|
|
584
|
+
}
|
|
585
|
+
broadcast(ownerId, broadcastBuffer.unwrap());
|
|
461
586
|
}
|
|
462
587
|
if (!deps.storage.writeMessages(binaryOwnerId, messages))
|
|
463
588
|
return ok(createProtocolMessageBuffer(ownerId, {
|
|
589
|
+
type: "non-initiator",
|
|
464
590
|
errorCode: ProtocolErrorCode.WriteError,
|
|
465
591
|
}).unwrap());
|
|
466
592
|
}
|
|
467
593
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
594
|
+
type: "non-initiator",
|
|
468
595
|
errorCode: ProtocolErrorCode.NoError,
|
|
469
596
|
totalMaxSize,
|
|
470
597
|
rangesMaxSize,
|
|
@@ -481,15 +608,22 @@ const tryDecodeProtocolData = (data, callback) => {
|
|
|
481
608
|
return callback(createBuffer(data));
|
|
482
609
|
}
|
|
483
610
|
catch (error) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
});
|
|
490
|
-
throw error;
|
|
611
|
+
return err({
|
|
612
|
+
type: "ProtocolInvalidDataError",
|
|
613
|
+
data,
|
|
614
|
+
error,
|
|
615
|
+
});
|
|
491
616
|
}
|
|
492
617
|
};
|
|
618
|
+
const decodeVersionAndOwner = (input) => {
|
|
619
|
+
// This structure must never change across protocol versions. The version
|
|
620
|
+
// and owner ID must always be the first two fields in every protocol message
|
|
621
|
+
// to enable version negotiation and owner identification before any other
|
|
622
|
+
// processing occurs.
|
|
623
|
+
const version = decodeNonNegativeInt(input);
|
|
624
|
+
const ownerId = decodeOwnerId(input);
|
|
625
|
+
return [version, ownerId];
|
|
626
|
+
};
|
|
493
627
|
/**
|
|
494
628
|
* Error thrown for internal protocol validation failures, such as invalid data
|
|
495
629
|
* or type errors.
|
|
@@ -525,6 +659,7 @@ const sync = (deps) => (role, input, output, ownerId) => {
|
|
|
525
659
|
return ok(null);
|
|
526
660
|
}
|
|
527
661
|
const message = createProtocolMessageBuffer(binaryOwnerId, {
|
|
662
|
+
type: "non-initiator",
|
|
528
663
|
errorCode: ProtocolErrorCode.SyncError,
|
|
529
664
|
});
|
|
530
665
|
return ok(message.unwrap());
|
|
@@ -807,11 +942,6 @@ export const idToBinaryId = (id) => base64Url256ToBytes(id);
|
|
|
807
942
|
export const binaryIdToId = (binaryId) => decodeId(createBuffer(binaryId));
|
|
808
943
|
export const ownerIdToBinaryOwnerId = (ownerId) => base64Url256ToBytes(ownerId);
|
|
809
944
|
export const binaryOwnerIdToOwnerId = (binaryOwnerId) => decodeOwnerId(createBuffer(binaryOwnerId));
|
|
810
|
-
/**
|
|
811
|
-
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
812
|
-
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
813
|
-
*/
|
|
814
|
-
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
815
945
|
/**
|
|
816
946
|
* Alphabet used for Base64Url encoding. This is copied from the `nanoid`
|
|
817
947
|
* library to avoid dependency on a specific version of `nanoid`.
|
|
@@ -1149,3 +1279,23 @@ export const decodeSqliteValue = (buffer) => {
|
|
|
1149
1279
|
throw new ProtocolDecodeError("invalid ProtocolValueType");
|
|
1150
1280
|
}
|
|
1151
1281
|
};
|
|
1282
|
+
/**
|
|
1283
|
+
* Decodes a ProtocolMessage into a readable JSON object for debugging.
|
|
1284
|
+
*
|
|
1285
|
+
* Note: This is a stub for future implementation. It should use:
|
|
1286
|
+
*
|
|
1287
|
+
* - DecodeVersionAndOwner
|
|
1288
|
+
* - DecodeError or decodeWriteKeys (depending on context)
|
|
1289
|
+
* - DecodeMessages
|
|
1290
|
+
* - DecodeRanges
|
|
1291
|
+
*
|
|
1292
|
+
* If you want to help, please contribute to this function.
|
|
1293
|
+
*/
|
|
1294
|
+
export const decodeProtocolMessageToJson = (_protocolMessage, _isInitiator) => {
|
|
1295
|
+
// TODO: Implement using
|
|
1296
|
+
// - decodeVersionAndOwner
|
|
1297
|
+
// -- decodeError or decodeWriteKeys (should be refactored out),
|
|
1298
|
+
// -- decodeMessages, and decodeRanges.
|
|
1299
|
+
// This is a stub for PRs and community contributions.
|
|
1300
|
+
throw new Error("decodeProtocolMessageToJson is not implemented yet.");
|
|
1301
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConsoleConfig } from "../Console.js";
|
|
2
|
+
import { TimingSafeEqualDep } from "../Crypto.js";
|
|
2
3
|
import { Result } from "../Result.js";
|
|
3
4
|
import { SqliteError } from "../Sqlite.js";
|
|
4
5
|
import { SimpleName } from "../Type.js";
|
|
@@ -9,5 +10,6 @@ export interface Relay extends Disposable {
|
|
|
9
10
|
export interface RelayConfig extends ConsoleConfig {
|
|
10
11
|
readonly name?: SimpleName;
|
|
11
12
|
}
|
|
12
|
-
export
|
|
13
|
+
export type RelaySqliteStorageDeps = SqliteStorageDeps & TimingSafeEqualDep;
|
|
14
|
+
export declare const createRelayStorage: (deps: RelaySqliteStorageDeps) => (options: CreateSqliteStorageBaseOptions) => Result<Storage, SqliteError>;
|
|
13
15
|
//# sourceMappingURL=Relay.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Relay.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Relay.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAW,MAAM,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAO,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAqB,OAAO,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAEL,8BAA8B,EAC9B,iBAAiB,EAClB,MAAM,cAAc,CAAC;AAGtB,MAAM,WAAW,KAAM,SAAQ,UAAU;CAAG;AAE5C,MAAM,WAAW,WAAY,SAAQ,aAAa;IAChD,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;AAE5E,eAAO,MAAM,kBAAkB,GAC5B,MAAM,sBAAsB,MAC5B,SAAS,8BAA8B,KAAG,MAAM,CAAC,OAAO,EAAE,WAAW,CA6JrE,CAAC"}
|
package/dist/src/Evolu/Relay.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isNonEmptyReadonlyArray } from "../Array.js";
|
|
2
|
-
import {
|
|
3
|
-
import { ok } from "../Result.js";
|
|
2
|
+
import { err, ok } from "../Result.js";
|
|
4
3
|
import { sql } from "../Sqlite.js";
|
|
5
4
|
import { createSqliteStorageBase, } from "./Storage.js";
|
|
6
5
|
import { timestampToBinaryTimestamp } from "./Timestamp.js";
|
|
@@ -64,7 +63,20 @@ export const createRelayStorage = (deps) => (options) => {
|
|
|
64
63
|
}
|
|
65
64
|
return true;
|
|
66
65
|
}
|
|
67
|
-
return
|
|
66
|
+
return deps.timingSafeEqual(rows[0].writeKey, writeKey);
|
|
67
|
+
},
|
|
68
|
+
setWriteKey: (ownerId, writeKey) => {
|
|
69
|
+
const upsertWriteKey = deps.sqlite.exec(sql `
|
|
70
|
+
insert into evolu_writeKey (ownerId, writeKey)
|
|
71
|
+
values (${ownerId}, ${writeKey})
|
|
72
|
+
on conflict (ownerId) do update
|
|
73
|
+
set writeKey = excluded.writeKey;
|
|
74
|
+
`);
|
|
75
|
+
if (!upsertWriteKey.ok) {
|
|
76
|
+
options.onStorageError(upsertWriteKey.error);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
68
80
|
},
|
|
69
81
|
writeMessages: (ownerId, messages) => {
|
|
70
82
|
const result = deps.sqlite.transaction(() => {
|
|
@@ -105,5 +117,29 @@ export const createRelayStorage = (deps) => (options) => {
|
|
|
105
117
|
}
|
|
106
118
|
return result.value.rows[0]?.change;
|
|
107
119
|
},
|
|
120
|
+
deleteOwner: (ownerId) => {
|
|
121
|
+
const result = deps.sqlite.transaction(() => {
|
|
122
|
+
const del1 = deps.sqlite.exec(sql `
|
|
123
|
+
delete from evolu_writeKey where ownerId = ${ownerId};
|
|
124
|
+
`);
|
|
125
|
+
if (!del1.ok)
|
|
126
|
+
return del1;
|
|
127
|
+
const del2 = deps.sqlite.exec(sql `
|
|
128
|
+
delete from evolu_message where ownerId = ${ownerId};
|
|
129
|
+
`);
|
|
130
|
+
if (!del2.ok)
|
|
131
|
+
return del2;
|
|
132
|
+
const del3 = sqliteStorageBase.value.deleteOwner(ownerId);
|
|
133
|
+
if (!del3)
|
|
134
|
+
return err(null);
|
|
135
|
+
return ok();
|
|
136
|
+
});
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
if (result.error)
|
|
139
|
+
options.onStorageError(result.error);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
},
|
|
108
144
|
});
|
|
109
145
|
};
|