@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.
33
43
  *
34
- * Every protocol message belongs to an owner.
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 **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
  */
@@ -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, ReadonlyRecord } from "../Object.js";
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 interface DbChange {
275
- readonly table: Base64Url256;
276
- readonly id: Id;
277
- readonly values: ReadonlyRecord<Base64Url256, SqliteValue>;
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
- if (errorCode != null) buffers.header.extend([errorCode]);
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 = decodeNonNegativeInt(input);
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 = decodeNonNegativeInt(input);
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 messages = decodeMessages(input);
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 (isNonEmptyReadonlyArray(messages)) {
897
- const messagesEnd = inputMessage.length - input.getLength();
898
- const writeKey = input.shiftN(writeKeyLength) as WriteKey;
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
- const writeKeyIsValid = deps.storage.validateWriteKey(
901
- binaryOwnerId,
902
- writeKey,
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
- if (!writeKeyIsValid)
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
- if (broadcast) {
913
- // Instead of encoding a new protocol message, we reuse the inputMessage.
914
- const broadcastMessage = concatBytes(
915
- inputMessage.slice(0, 17),
916
- new Uint8Array([ProtocolErrorCode.NoError]),
917
- inputMessage.slice(17, messagesEnd),
918
- ) as ProtocolMessage;
919
-
920
- broadcast(ownerId, broadcastMessage);
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
- if (error instanceof ProtocolDecodeError || error instanceof BufferError)
953
- return err<ProtocolInvalidDataError>({
954
- type: "ProtocolInvalidDataError",
955
- data,
956
- error,
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
+ };