@evolu/common 8.17.0 → 8.19.0

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 (48) hide show
  1. package/dist/src/Bytes.d.ts +31 -14
  2. package/dist/src/Bytes.d.ts.map +1 -1
  3. package/dist/src/Bytes.js +48 -8
  4. package/dist/src/Error.d.ts.map +1 -1
  5. package/dist/src/Error.js +5 -1
  6. package/dist/src/Polyfills.d.ts.map +1 -1
  7. package/dist/src/Polyfills.js +3 -2
  8. package/dist/src/Sqlite.d.ts +1 -1
  9. package/dist/src/Task.d.ts +4 -3
  10. package/dist/src/Task.d.ts.map +1 -1
  11. package/dist/src/Task.js +4 -3
  12. package/dist/src/index.d.ts +1 -1
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/local-first/Db.d.ts +32 -3
  15. package/dist/src/local-first/Db.d.ts.map +1 -1
  16. package/dist/src/local-first/Db.js +30 -9
  17. package/dist/src/local-first/Evolu.d.ts +21 -6
  18. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  19. package/dist/src/local-first/Evolu.js +5 -1
  20. package/dist/src/local-first/Protocol.d.ts +215 -93
  21. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  22. package/dist/src/local-first/Protocol.js +799 -489
  23. package/dist/src/local-first/Relay.d.ts +9 -1
  24. package/dist/src/local-first/Relay.d.ts.map +1 -1
  25. package/dist/src/local-first/Relay.js +41 -36
  26. package/dist/src/local-first/Shared.d.ts +79 -53
  27. package/dist/src/local-first/Shared.d.ts.map +1 -1
  28. package/dist/src/local-first/Shared.js +67 -42
  29. package/dist/src/local-first/Storage.d.ts +80 -18
  30. package/dist/src/local-first/Storage.d.ts.map +1 -1
  31. package/dist/src/local-first/Storage.js +20 -4
  32. package/package.json +1 -1
  33. package/src/Bytes.test.ts +212 -0
  34. package/src/Bytes.ts +67 -17
  35. package/src/Error.ts +5 -1
  36. package/src/Polyfills.ts +3 -2
  37. package/src/Sqlite.ts +1 -1
  38. package/src/Task.ts +4 -3
  39. package/src/index.ts +4 -1
  40. package/src/local-first/Db.ts +76 -17
  41. package/src/local-first/Evolu.test.ts +43 -12
  42. package/src/local-first/Evolu.ts +34 -7
  43. package/src/local-first/Protocol.test.ts +1541 -44
  44. package/src/local-first/Protocol.ts +1070 -605
  45. package/src/local-first/Relay.ts +80 -69
  46. package/src/local-first/Shared.test.ts +37 -1
  47. package/src/local-first/Shared.ts +124 -73
  48. package/src/local-first/Storage.ts +100 -27
@@ -111,10 +111,10 @@
111
111
  * whichever instance made it, even one disposed before the database worker
112
112
  * answered. When a relay frame stores new messages, the tenant requests a round
113
113
  * through each other transport claimed for the owner, so data learned from one
114
- * relay reaches the others, except through a route that skipped a message, as
115
- * described below. A closed transport reconciles when it opens, and a
116
- * replacement leader reconciles every transport again, because a response
117
- * reporting stored messages may have been lost.
114
+ * relay reaches the others, except through a route that skipped a message it
115
+ * received or could not send, as described below. A closed transport reconciles
116
+ * when it opens, and a replacement leader reconciles every transport again,
117
+ * because a response reporting stored messages may have been lost.
118
118
  *
119
119
  * Relays omit the sending socket when broadcasting an upload, so the uploader
120
120
  * also delivers it as local Broadcast frames to every other tenant with
@@ -178,17 +178,17 @@
178
178
  * request, a replacement leader, or storing messages from another transport,
179
179
  * unless the route skipped a message. No failed or aborted result has arrived
180
180
  * on the route since.
181
- * - No result that skipped a received message has arrived on the route since a
182
- * round was last requested through it, and since that request, no messages
183
- * have been stored from another transport, directly or through a sibling copy
184
- * uploaded outside this transport, and no sibling copy has failed to apply or
185
- * skipped a message.
181
+ * - No result that skipped a received message or a stored change too large to
182
+ * send has arrived on the route since a round was last requested through it,
183
+ * and since that request, no messages have been stored from another
184
+ * transport, directly or through a sibling copy uploaded outside this
185
+ * transport, and no sibling copy has failed to apply or skipped a message.
186
186
  *
187
187
  * A route is settled when every condition except the last holds, so a complete
188
188
  * route is settled too. A settled route that is incomplete has ended its
189
- * reconciliation, but its relay may offer a message the database skipped, or
190
- * messages stored elsewhere may not have reached it; it is published as
191
- * {@link SettledSyncRoute}.
189
+ * reconciliation, but its relay may offer a message the database skipped, the
190
+ * database may store a change too large to send, or messages stored elsewhere
191
+ * may not have reached it; it is published as {@link SettledSyncRoute}.
192
192
  *
193
193
  * A reconciliation chain ends only with a converged result, a failure, an
194
194
  * abort, a dropped frame, or a continuation that finds the socket closed, and
@@ -207,20 +207,34 @@
207
207
  * while the database worker creates a round is logged there and fails the
208
208
  * round's routes with `SyncFailed` without a retry. A mutation that throws is
209
209
  * rolled back and reported as an {@link UnknownError} to the tab that made it,
210
- * or to every tab when its Evolu instance was disposed first. Other unexpected
211
- * SQLite exceptions remain unsupported and can panic the database worker. A
212
- * frame the relay silently drops, such as invalid data, leaves the count above
213
- * zero until the liveness rule below replaces the socket.
214
- *
215
- * A result that skipped a received message the database could not decrypt,
216
- * verify, or decode records the error on the route as its `skippedError`,
217
- * without ending its chain, so it requests no round, and the relay offers the
218
- * message again in every round through the route until the database stores a
219
- * message with that timestamp. The messages received elsewhere that the last
220
- * condition names request no round through such a route, even while a requested
221
- * round checks it again, because each round would download every skipped
222
- * message again. The next round that an explicit request, a reopen, a
223
- * replacement leader, or a failure sends through the route reconciles them.
210
+ * or to every tab when its Evolu instance was disposed first. A received batch
211
+ * SQLite fails to store, for example on a full disk, is rolled back and fails
212
+ * its route with an `UnknownError`. Other unexpected SQLite exceptions remain
213
+ * unsupported and can panic the database worker. A frame the relay silently
214
+ * drops, such as invalid data, leaves the count above zero until the liveness
215
+ * rule below replaces the socket.
216
+ *
217
+ * A result that skipped a message records the error on the route as its
218
+ * `skippedError`, without ending its chain, so it requests no round. A message
219
+ * is skipped in two cases:
220
+ *
221
+ * - The relay offers a message the database could not decrypt, verify, or decode.
222
+ * The relay offers it again in every round through the route until the
223
+ * database stores a message with that timestamp.
224
+ * - The database stores a change it cannot send, a
225
+ * {@link ProtocolChangeTooLargeError}: one saved before `maxMutationSize`
226
+ * existed, or a crafted one received from a relay. Every route through a
227
+ * relay that lacks it skips it on every sync, so each such route stays
228
+ * settled.
229
+ *
230
+ * The messages received elsewhere that the last condition names request no
231
+ * round through such a route, even while a requested round checks it again,
232
+ * because each round would download every skipped message again, or skip again
233
+ * a change too large to send. The next round that an explicit request, a
234
+ * reopen, a replacement leader, or a failure sends through the route reconciles
235
+ * them. So while the database stores a change too large to send, messages from
236
+ * other relays reach a route only through such a round, for example after a
237
+ * reconnect or {@link Evolu.requestSync}.
224
238
  *
225
239
  * ### Liveness
226
240
  *
@@ -338,7 +352,11 @@ import type {
338
352
  SharedWorkerSelf,
339
353
  WorkerDeps,
340
354
  } from "../Worker.ts";
341
- import type { DbWorkerInit, UnsupportedDbVersionError } from "./Db.ts";
355
+ import type {
356
+ DatabaseHeldError,
357
+ DbWorkerInit,
358
+ UnsupportedDbVersionError,
359
+ } from "./Db.ts";
342
360
  import type {
343
361
  DevicePersistence,
344
362
  Evolu,
@@ -353,6 +371,7 @@ import {
353
371
  MessageType,
354
372
  parseProtocolHeader,
355
373
  type ApplyProtocolMessageAsClientResult,
374
+ type ProtocolChangeTooLargeError,
356
375
  type ProtocolError,
357
376
  type ProtocolInvalidDataError,
358
377
  type ProtocolMessage,
@@ -413,7 +432,10 @@ export type SharedWorkerOutput =
413
432
  */
414
433
  readonly type: "Error";
415
434
  readonly error:
416
- OtherBuildRunningError | UnknownError | UnsupportedDbVersionError;
435
+ | DatabaseHeldError
436
+ | OtherBuildRunningError
437
+ | UnknownError
438
+ | UnsupportedDbVersionError;
417
439
  }
418
440
  | {
419
441
  /**
@@ -627,7 +649,7 @@ export interface ActiveSyncTenant extends Typed<"Active"> {
627
649
  */
628
650
  export interface RefusedSyncTenant extends Typed<"Refused"> {
629
651
  readonly name: Name;
630
- readonly error: UnsupportedDbVersionError;
652
+ readonly error: DatabaseHeldError | UnsupportedDbVersionError;
631
653
  }
632
654
 
633
655
  /**
@@ -659,8 +681,9 @@ export interface ReadonlySyncTenantOwner extends Typed<"Readonly"> {
659
681
 
660
682
  /**
661
683
  * One database's use of one owner through one transport: `Pending` until its
662
- * reconciliation ends, then `Complete`, or `Settled` while its relay offers a
663
- * change the database skipped. See Synchronization completion in this module's
684
+ * reconciliation ends, then `Complete`, or `Settled` while it skips a change:
685
+ * one its relay offers that the database could not store, or one the database
686
+ * stores but cannot send. See Synchronization completion in this module's
664
687
  * documentation.
665
688
  */
666
689
  export type SyncRoute = PendingSyncRoute | SettledSyncRoute | CompleteSyncRoute;
@@ -680,9 +703,12 @@ export interface PendingSyncRoute extends Typed<"Pending"> {
680
703
  */
681
704
  readonly failure: SyncRouteError | null;
682
705
  /**
683
- * The first change skipped in the latest reply that skipped one, or null. The
684
- * relay offers it again in every round through the route until this database
685
- * stores a change with that timestamp.
706
+ * The first change skipped in the latest reply that skipped one, or null. A
707
+ * change the relay offers is offered again in every round through the route
708
+ * until this database stores a change with that timestamp. A
709
+ * {@link ProtocolChangeTooLargeError} is a change this database stores but
710
+ * cannot send, which every route through a relay that lacks it skips on every
711
+ * sync.
686
712
  */
687
713
  readonly skippedError: SyncRouteError | null;
688
714
  /** When the route last became complete, or null. */
@@ -697,10 +723,15 @@ export interface PendingSyncRoute extends Typed<"Pending"> {
697
723
  }
698
724
 
699
725
  /**
700
- * A route whose reconciliation ended while its relay offers a change the
701
- * database skipped, so it is incomplete. Changes stored from other relays
702
- * request no round through it; the next round requested through it, such as by
703
- * {@link Evolu.requestSync} or a reopen, checks it again.
726
+ * A route whose reconciliation ended with a skipped change, so it is
727
+ * incomplete: its relay offers a change the database could not store, or the
728
+ * database stores a change it cannot send, a
729
+ * {@link ProtocolChangeTooLargeError}. Changes stored from other relays request
730
+ * no round through it; the next round requested through it, such as by
731
+ * {@link Evolu.requestSync} or a reopen, checks it again. Every route through a
732
+ * relay that lacks a change too large to send skips it on every sync, so those
733
+ * routes stay settled, and messages from other relays reach them only through
734
+ * such rounds.
704
735
  */
705
736
  export interface SettledSyncRoute extends Typed<"Settled"> {
706
737
  readonly transportId: SyncTransportId;
@@ -734,11 +765,13 @@ export interface CompleteSyncRoute extends Typed<"Complete"> {
734
765
  *
735
766
  * It is the error itself, so it carries its details, such as the expected and
736
767
  * actual timestamps of a {@link ProtocolTimestampMismatchError}. A skipped
737
- * message adds {@link DecryptWithXChaCha20Poly1305Error}. A
738
- * {@link ProtocolInvalidDataError} leaves out its data, which can be a whole
739
- * frame. `WriteFailed` means a `writeMessages` call that threw, logged by the
740
- * protocol, and `SyncFailed` means a logged failure while creating a round or
741
- * reconciling ranges.
768
+ * message adds {@link DecryptWithXChaCha20Poly1305Error} and
769
+ * {@link ProtocolChangeTooLargeError}. A {@link ProtocolInvalidDataError} leaves
770
+ * out its data, which can be a whole frame. An {@link UnknownError} means SQLite
771
+ * failed to store received messages, for example on a full disk. `WriteFailed`
772
+ * means a `writeMessages` call that threw, logged by the protocol, and
773
+ * `SyncFailed` means a logged failure while creating a round or reconciling
774
+ * ranges.
742
775
  */
743
776
  export type SyncRouteError = (
744
777
  | Exclude<ProtocolError, ProtocolInvalidDataError>
@@ -749,6 +782,7 @@ export type SyncRouteError = (
749
782
  | (Omit<DecryptWithXChaCha20Poly1305Error, "error"> & {
750
783
  readonly error: UnknownError;
751
784
  })
785
+ | ProtocolChangeTooLargeError
752
786
  | Typed<"WriteFailed">
753
787
  | Typed<"SyncFailed">
754
788
  ) & { readonly at: Millis };
@@ -779,10 +813,10 @@ export interface RelaySyncState {
779
813
  * - `Synced`: a relay is up to date, and none syncs.
780
814
  * - `Offline`: every relay is disconnected. Evolu keeps reconnecting, up to 30
781
815
  * seconds apart, so `Offline` can briefly outlast the outage.
782
- * - `Error`: a relay failed or offers a change this database skipped. `error` is
783
- * the newest failure, or without one, the newest skipped change: a failure
784
- * stops syncing through its relay, while a skipped change leaves out only
785
- * that change.
816
+ * - `Error`: a relay failed, a relay offers a change this database skipped, or
817
+ * this database stores a change too large to send. `error` is the newest
818
+ * failure, or without one, the newest skipped change: a failure stops syncing
819
+ * through its relay, while a skipped change leaves out only that change.
786
820
  *
787
821
  * Evolu stores changes in the local database before they sync, so sync needs no
788
822
  * UI while it works: show nothing for `NoRelays`, `Syncing`, and `Synced`. An
@@ -1311,7 +1345,7 @@ export type DbWorkerOutput =
1311
1345
  | {
1312
1346
  /** Startup was refused; the worker is releasing its resources. */
1313
1347
  readonly type: "LeaderRefused";
1314
- readonly error: UnsupportedDbVersionError;
1348
+ readonly error: DatabaseHeldError | UnsupportedDbVersionError;
1315
1349
  }
1316
1350
  | {
1317
1351
  readonly type: "OnQueuedResponse";
@@ -1365,13 +1399,15 @@ export type DbWorkerQueuedResponse =
1365
1399
  readonly ownerId: OwnerId;
1366
1400
  readonly didWriteMessages: boolean;
1367
1401
  /**
1368
- * The first error of a received message the DbWorker skipped while
1369
- * storing the rest, or null. It does not end the round.
1402
+ * The first error of a message the DbWorker skipped, or null: a
1403
+ * received one it did not store while storing the rest, or a stored
1404
+ * change too large to send. It does not end the round.
1370
1405
  */
1371
1406
  readonly skippedError:
1372
1407
  | DecryptWithXChaCha20Poly1305Error
1373
1408
  | ProtocolInvalidDataError
1374
1409
  | ProtocolTimestampMismatchError
1410
+ | ProtocolChangeTooLargeError
1375
1411
  | null;
1376
1412
  readonly result: Result<
1377
1413
  ApplyProtocolMessageAsClientResult,
@@ -1529,8 +1565,10 @@ interface PendingRoute extends Typed<"Pending"> {
1529
1565
  }
1530
1566
 
1531
1567
  /**
1532
- * A message a route skipped. Its relay offers the message again in every round,
1533
- * so messages received elsewhere request no round through the route.
1568
+ * A message a route skipped: one its relay offers that the database could not
1569
+ * store, which the relay offers again in every round, or one the database
1570
+ * stores but cannot send, which every round skips again. So messages received
1571
+ * elsewhere request no round through the route.
1534
1572
  */
1535
1573
  interface RouteSkip {
1536
1574
  readonly error: SyncRouteError;
@@ -1543,8 +1581,9 @@ interface RouteSkip {
1543
1581
  }
1544
1582
 
1545
1583
  /**
1546
- * A route whose reconciliation has ended, but whose relay may offer a skipped
1547
- * message or lack messages stored elsewhere, so it is incomplete.
1584
+ * A route whose reconciliation has ended, but which is incomplete: its relay
1585
+ * may offer a skipped message, the database may store a change too large to
1586
+ * send, or the relay may lack messages stored elsewhere.
1548
1587
  */
1549
1588
  interface SettledRoute extends Typed<"Settled"> {
1550
1589
  readonly skippedError: SyncRouteError;
@@ -1584,6 +1623,7 @@ const errorToSyncRouteError = (
1584
1623
  | ProtocolError
1585
1624
  | StorageWriteMessagesError
1586
1625
  | DecryptWithXChaCha20Poly1305Error
1626
+ | ProtocolChangeTooLargeError
1587
1627
  | Typed<"WriteFailed">
1588
1628
  | Typed<"SyncFailed">,
1589
1629
  at: Millis,
@@ -1773,8 +1813,9 @@ export const initSharedWorker =
1773
1813
 
1774
1814
  // Held while this worker runs. Its DbWorkers stop once they can take it,
1775
1815
  // because a Dispose posted right before this worker closes can be lost, as
1776
- // in Firefox. Taken before the build lock, so nothing delays the end of
1777
- // starting once that lock is held.
1816
+ // in Firefox (https://bugzilla.mozilla.org/show_bug.cgi?id=2077609). Taken
1817
+ // before the build lock, so nothing delays the end of starting once that
1818
+ // lock is held.
1778
1819
  disposer.use(await run.ok(acquireLeaderLock(workerId)));
1779
1820
 
1780
1821
  // Released after every tenant is disposed and has told its DbWorker to
@@ -2302,7 +2343,7 @@ const createEvoluTenant =
2302
2343
  }
2303
2344
 
2304
2345
  interface RefusedDbWorker extends Typed<"Refused"> {
2305
- readonly error: UnsupportedDbVersionError;
2346
+ readonly error: DatabaseHeldError | UnsupportedDbVersionError;
2306
2347
  }
2307
2348
 
2308
2349
  let dbWorker: DbWorkerState = { type: "Starting" };
@@ -2405,6 +2446,11 @@ const createEvoluTenant =
2405
2446
  // tell tabs that connect later without starting another worker.
2406
2447
  if (dbWorker.type === "Leading") {
2407
2448
  assertNotSame(dbWorker.port, currentDbWorkerPort);
2449
+ // The leader may still hold the database lock: browsers deliver
2450
+ // a refusal before a worker requested later can lead, but the
2451
+ // tenant does not rely on that order. Tell it to dispose, so it
2452
+ // releases the lock.
2453
+ dbWorker.port.postMessage({ type: "Dispose" });
2408
2454
  dbWorker.port[Symbol.dispose]();
2409
2455
  }
2410
2456
  currentDbWorkerPort[Symbol.dispose]();
@@ -2546,7 +2592,7 @@ const createEvoluTenant =
2546
2592
 
2547
2593
  const reportRefusal = (
2548
2594
  tabPort: TabPort,
2549
- error: UnsupportedDbVersionError,
2595
+ error: DatabaseHeldError | UnsupportedDbVersionError,
2550
2596
  ): void => {
2551
2597
  if (refusedTabPorts.has(tabPort)) return;
2552
2598
  refusedTabPorts.add(tabPort);
@@ -2780,7 +2826,8 @@ const createEvoluTenant =
2780
2826
  !hasQueuedWrite;
2781
2827
  // Settling drops the failure, so the next one requests a round
2782
2828
  // again. A route with a skip it is not rechecking settles without
2783
- // completing, because its relay may offer the skipped message or
2829
+ // completing, because its relay may offer the skipped message, the
2830
+ // database may store a change too large to send, or the relay may
2784
2831
  // lack messages stored elsewhere.
2785
2832
  const pending = routeToPending(route.progress);
2786
2833
  route.progress =
@@ -2887,11 +2934,12 @@ const createEvoluTenant =
2887
2934
  if (!isAborted) route.lastReceivedAt = now;
2888
2935
  // A skipped message is recorded as the route's skip, not a
2889
2936
  // failure, and does not end the round, whose response below is
2890
- // still sent, so it requests no round. The relay offers the
2891
- // message again in every later round, so the route stays
2892
- // incomplete until a round requested through it settles without
2893
- // skipping a message and without messages stored elsewhere since
2894
- // that request.
2937
+ // still sent, so it requests no round. The relay offers a message
2938
+ // the database could not store again in every later round, and
2939
+ // every round skips a stored change too large to send again, so
2940
+ // the route stays incomplete until a round requested through it
2941
+ // settles without skipping a message and without messages stored
2942
+ // elsewhere since that request.
2895
2943
  if (skippedError !== null)
2896
2944
  route.progress = {
2897
2945
  ...routeToPending(route.progress),
@@ -2918,12 +2966,14 @@ const createEvoluTenant =
2918
2966
  }
2919
2967
  } else if (failure !== null || skippedError !== null) {
2920
2968
  // A sibling's copy comes from this worker, not from a relay, so no
2921
- // route shows its failure. It is a Broadcast, which carries no
2922
- // relay error, so an error or a skip means a bug, such as
2923
- // databases holding different keys for the owner, and is reported
2924
- // as an unexpected failure. A Failed result was logged, which
2925
- // reports it already. An error is the failure, which is never an
2926
- // abort, and like a route, the report leaves out its frame.
2969
+ // route shows its failure. SQLite can fail to store it, for example
2970
+ // on a full disk, which returns an UnknownError. It is a Broadcast,
2971
+ // which carries no relay error, so any other error or a skip means
2972
+ // a bug, such as databases holding different keys for the owner.
2973
+ // Each is reported as an unexpected failure. A Failed result was
2974
+ // logged, which reports it already. An error is the failure, which
2975
+ // is never an abort, and like a route, the report leaves out its
2976
+ // frame.
2927
2977
  const unexpected = error !== null ? failure : skippedError;
2928
2978
  if (unexpected !== null)
2929
2979
  deps.postConsoleEntryOrError({
@@ -3043,9 +3093,10 @@ const createEvoluTenant =
3043
3093
  * `except`, after messages from elsewhere were stored or a sibling copy was
3044
3094
  * not fully stored. Rounds toward the same transport coalesce whatever
3045
3095
  * their source. A route that skipped a message gets none, because its relay
3046
- * would offer that message again, and a route rechecking one stops
3047
- * rechecking, because its round may have read the database before this
3048
- * event. A later requested round through such a route reconciles it.
3096
+ * would offer that message again or the round would skip a stored change
3097
+ * too large to send again. A route rechecking one stops rechecking, because
3098
+ * its round may have read the database before this event. A later requested
3099
+ * round through such a route reconciles it.
3049
3100
  */
3050
3101
  const requestRoundsForReceivedMessages = (
3051
3102
  ownerId: OwnerId,