@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.
Files changed (40) hide show
  1. package/dist/src/Assert.d.ts +6 -3
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +6 -3
  4. package/dist/src/Crypto.d.ts +11 -0
  5. package/dist/src/Crypto.d.ts.map +1 -1
  6. package/dist/src/Evolu/Config.d.ts +17 -4
  7. package/dist/src/Evolu/Config.d.ts.map +1 -1
  8. package/dist/src/Evolu/Config.js +1 -1
  9. package/dist/src/Evolu/Db.d.ts +21 -13
  10. package/dist/src/Evolu/Db.d.ts.map +1 -1
  11. package/dist/src/Evolu/Db.js +81 -64
  12. package/dist/src/Evolu/Evolu.d.ts +6 -6
  13. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  14. package/dist/src/Evolu/Evolu.js +19 -24
  15. package/dist/src/Evolu/Owner.d.ts +6 -1
  16. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  17. package/dist/src/Evolu/Owner.js +5 -0
  18. package/dist/src/Evolu/Protocol.d.ts +134 -64
  19. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  20. package/dist/src/Evolu/Protocol.js +227 -77
  21. package/dist/src/Evolu/Relay.d.ts +3 -1
  22. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  23. package/dist/src/Evolu/Relay.js +39 -3
  24. package/dist/src/Evolu/Schema.d.ts +24 -40
  25. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  26. package/dist/src/Evolu/Schema.js +13 -72
  27. package/dist/src/Evolu/Storage.d.ts +1 -0
  28. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  29. package/dist/src/Evolu/Storage.js +10 -0
  30. package/package.json +3 -3
  31. package/src/Assert.ts +6 -3
  32. package/src/Crypto.ts +13 -0
  33. package/src/Evolu/Config.ts +19 -5
  34. package/src/Evolu/Db.ts +92 -76
  35. package/src/Evolu/Evolu.ts +38 -35
  36. package/src/Evolu/Owner.ts +7 -1
  37. package/src/Evolu/Protocol.ts +276 -110
  38. package/src/Evolu/Relay.ts +45 -4
  39. package/src/Evolu/Schema.ts +102 -130
  40. 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 Reconciliation](https://arxiv.org/abs/2212.13567)
11
- * by Aljoscha Meyer.
12
- *
13
- * To learn how RBSR works, check
14
- * [Negentropy](https://logperiodic.com/rbsr.html). Evolu Protocol is similar to
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
- * | - {@link ProtocolErrorCode} | In non-initiator response. |
26
- * | **Messages** | |
27
- * | - {@link NonNegativeInt} | A number of messages. |
28
- * | - {@link EncryptedCrdtMessage} | |
29
- * | - {@link WriteKey} | In initiator request. |
30
- * | **Ranges** | |
31
- * | - {@link NonNegativeInt} | Number of ranges. |
32
- * | - {@link Range} | |
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
- * Every protocol message belongs to an owner.
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 **required** in
52
- * **Messages** as a secure token proving the initiator can write changes. The
53
- * non-initiator responds without a {@link WriteKey}, since the initiator’s
54
- * request already signals it wants data. If the non-initiator detects an issue
55
- * (e.g., an invalid {@link WriteKey} causing a {@link ProtocolWriteKeyError}, or
56
- * a write failure causing a {@link ProtocolWriteError}), it sends an error code
57
- * via the `Error` field in the header back to the initiator. In relay-to-relay
58
- * or P2P sync, both sides may require the {@link WriteKey} depending on who is
59
- * the initiator.
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 `ProtocolMessage` is less
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
- * The initiator sends a versioned `ProtocolMessage`. If the non-initiator uses
92
- * a different version, it responds with a message containing only its protocol
93
- * version—**without an `ownerId`**. This allows the initiator to check protocol
94
- * compatibility, for example, by sending version-only messages to multiple
95
- * relays before starting synchronization.
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 { BufferError, bytesToHex, bytesToUtf8, concatBytes, createBuffer, hexToBytes, utf8ToBytes, } from "../Buffer.js";
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 { Base64Url, DateIsoString, idTypeValueLength, JsonValueFromString, maxLength, NonNegativeInt, Number, } from "../Type.js";
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
- export const createProtocolMessageBuffer = (ownerId, options = {}) => {
198
- const { errorCode, writeKey, totalMaxSize = maxProtocolMessageSize, rangesMaxSize = maxProtocolMessageRangesSize, version = protocolVersion, } = options;
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 (errorCode != null)
214
- buffers.header.extend([errorCode]);
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 = decodeNonNegativeInt(input);
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 = decodeNonNegativeInt(input);
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
- const messagesEnd = inputMessage.length - input.getLength();
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
- if (broadcast) {
458
- // Instead of encoding a new protocol message, we reuse the inputMessage.
459
- const broadcastMessage = concatBytes(inputMessage.slice(0, 17), new Uint8Array([ProtocolErrorCode.NoError]), inputMessage.slice(17, messagesEnd));
460
- broadcast(ownerId, broadcastMessage);
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
- if (error instanceof ProtocolDecodeError || error instanceof BufferError)
485
- return err({
486
- type: "ProtocolInvalidDataError",
487
- data,
488
- error,
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 declare const createRelayStorage: (deps: SqliteStorageDeps) => (options: CreateSqliteStorageBaseOptions) => Result<Storage, SqliteError>;
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;AAE9C,OAAO,EAAM,MAAM,EAAE,MAAM,cAAc,CAAC;AAC1C,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,eAAO,MAAM,kBAAkB,GAC5B,MAAM,iBAAiB,MACvB,SAAS,8BAA8B,KAAG,MAAM,CAAC,OAAO,EAAE,WAAW,CAsHrE,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"}
@@ -1,6 +1,5 @@
1
1
  import { isNonEmptyReadonlyArray } from "../Array.js";
2
- import { eqArrayNumber } from "../Eq.js";
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 eqArrayNumber(rows[0].writeKey, writeKey);
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
  };