@evolu/common 8.12.0 → 8.14.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 (59) hide show
  1. package/dist/src/Callbacks.d.ts +12 -1
  2. package/dist/src/Callbacks.d.ts.map +1 -1
  3. package/dist/src/Callbacks.js +3 -0
  4. package/dist/src/Error.d.ts +10 -4
  5. package/dist/src/Error.d.ts.map +1 -1
  6. package/dist/src/Error.js +10 -4
  7. package/dist/src/Object.d.ts +65 -0
  8. package/dist/src/Object.d.ts.map +1 -1
  9. package/dist/src/Object.js +142 -0
  10. package/dist/src/Resource.d.ts +0 -5
  11. package/dist/src/Resource.d.ts.map +1 -1
  12. package/dist/src/Resource.js +6 -13
  13. package/dist/src/Sqlite.d.ts.map +1 -1
  14. package/dist/src/Sqlite.js +7 -0
  15. package/dist/src/Task.d.ts +10 -8
  16. package/dist/src/Task.d.ts.map +1 -1
  17. package/dist/src/Task.js +41 -5
  18. package/dist/src/Worker.d.ts +3 -3
  19. package/dist/src/local-first/Db.d.ts +8 -3
  20. package/dist/src/local-first/Db.d.ts.map +1 -1
  21. package/dist/src/local-first/Db.js +56 -19
  22. package/dist/src/local-first/Evolu.d.ts +147 -39
  23. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  24. package/dist/src/local-first/Evolu.js +53 -9
  25. package/dist/src/local-first/Owner.d.ts +9 -0
  26. package/dist/src/local-first/Owner.d.ts.map +1 -1
  27. package/dist/src/local-first/Owner.js +9 -0
  28. package/dist/src/local-first/Protocol.d.ts +6 -4
  29. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  30. package/dist/src/local-first/Protocol.js +1 -6
  31. package/dist/src/local-first/Schema.d.ts +7 -1
  32. package/dist/src/local-first/Schema.d.ts.map +1 -1
  33. package/dist/src/local-first/Shared.d.ts +550 -153
  34. package/dist/src/local-first/Shared.d.ts.map +1 -1
  35. package/dist/src/local-first/Shared.js +724 -273
  36. package/dist/src/local-first/Storage.d.ts +16 -12
  37. package/dist/src/local-first/Storage.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/Callbacks.test.ts +20 -0
  40. package/src/Callbacks.ts +17 -1
  41. package/src/Error.ts +10 -4
  42. package/src/Object.test.ts +296 -0
  43. package/src/Object.ts +163 -0
  44. package/src/Resource.test.ts +20 -16
  45. package/src/Resource.ts +6 -20
  46. package/src/Sqlite.ts +7 -0
  47. package/src/Task.test.ts +233 -62
  48. package/src/Task.ts +47 -13
  49. package/src/Worker.ts +3 -3
  50. package/src/local-first/Db.ts +88 -18
  51. package/src/local-first/Evolu.test.ts +381 -11
  52. package/src/local-first/Evolu.ts +219 -51
  53. package/src/local-first/Owner.ts +9 -0
  54. package/src/local-first/Protocol.test.ts +42 -60
  55. package/src/local-first/Protocol.ts +7 -9
  56. package/src/local-first/Schema.ts +8 -2
  57. package/src/local-first/Shared.test.ts +2714 -644
  58. package/src/local-first/Shared.ts +1245 -401
  59. package/src/local-first/Storage.ts +18 -19
@@ -53,7 +53,9 @@
53
53
  * still waiting after three seconds reports {@link OtherBuildRunningError} to
54
54
  * its tabs. Every build broadcasts console entries and errors on
55
55
  * {@link consoleEntryOrErrorBroadcastChannelName}, so a waiting tab also prints
56
- * the running build's output and reports its errors as its own `evoluError`.
56
+ * the running build's output, reports each {@link UnknownError} it sends as its
57
+ * own `evoluError`, and only logs any other error, such as an older build's
58
+ * sync error.
57
59
  *
58
60
  * The tabs of one worker elect the host of its DbWorkers among themselves, with
59
61
  * a lock scoped to the worker, so a tab of another worker never hosts them.
@@ -67,13 +69,20 @@
67
69
  *
68
70
  * ## Storage
69
71
  *
70
- * A platform that can lack persistent storage, as a browser does in Safari's
71
- * Private Browsing, provides {@link PersistentStorageDep}. The worker checks it
72
- * once, after it takes the build lock and before any DbWorker starts. Without
73
- * persistent storage, every DbWorker it starts keeps its database in memory,
74
- * replacements included, and each tab that connects is told with
75
- * `StorageUnavailable`. The decision holds for the worker's lifetime, so all
76
- * its tabs see one mode, and the next worker checks again.
72
+ * The platform tells the worker with {@link DevicePersistenceDep} what it can
73
+ * promise about stored databases. The worker asks once, after it takes the
74
+ * build lock and before any DbWorker starts. A browser checks whether it can
75
+ * open the origin private file system, which Safari's Private Browsing and
76
+ * Firefox's private windows refuse. Without persistent storage, every DbWorker
77
+ * the worker starts keeps its database in memory, replacements included. The
78
+ * decision holds for the worker's lifetime, so all its tabs see one mode, and
79
+ * the next worker checks again.
80
+ *
81
+ * A tenant tells each instance it adds its {@link Evolu.devicePersistence}:
82
+ * `NotPersisted` when its database is kept in memory, because of the platform
83
+ * or {@link EvoluConfig.memoryOnly}, otherwise what the platform promised.
84
+ * Instances of one name share the tenant, so a later instance gets the mode the
85
+ * first one chose.
77
86
  *
78
87
  * The check cannot tell a private session from a storage failure, but memory
79
88
  * loses nothing that refusing to start would have kept, and the persistent
@@ -100,7 +109,8 @@
100
109
  * writable registrations for its owner, whichever instance made it, even one
101
110
  * disposed before the database worker answered. When a relay frame stores new
102
111
  * messages, the tenant requests a round through each other transport claimed
103
- * for the owner, so data learned from one relay reaches the others. A closed
112
+ * for the owner, so data learned from one relay reaches the others, except
113
+ * through a route that skipped a message, as described below. A closed
104
114
  * transport reconciles when it opens, and a replacement leader reconciles every
105
115
  * transport again, because a response reporting stored messages may have been
106
116
  * lost.
@@ -109,26 +119,34 @@
109
119
  * also delivers it as local Broadcast frames to every other tenant with
110
120
  * writable access to the owner, even while sockets are closed. A copy keeps the
111
121
  * uploader's target, and a recipient that stores new continuation messages
112
- * reconciles them through the transports outside that target. Local delivery
113
- * forwards uploads, including historical messages sent during reconciliation,
114
- * but does not reconcile local database histories with each other. That waits
115
- * until replication scopes and retention semantics are defined.
122
+ * reconciles them through the transports outside that target, except through a
123
+ * route that skipped a message. Local delivery forwards uploads, including
124
+ * historical messages sent during reconciliation, but does not reconcile local
125
+ * database histories with each other. That waits until replication scopes and
126
+ * retention semantics are defined.
116
127
  *
117
128
  * ## Sync state
118
129
  *
119
130
  * The shared worker publishes one plain snapshot, {@link SyncState}, of every
120
131
  * transport it manages and every database and owner registration it holds, with
121
- * one route per writable registration and transport, as specified below.
122
- * {@link syncStateToOwnerSyncStates} derives one state per database and owner.
132
+ * one route per writable registration and transport, as specified below. Each
133
+ * part is a union of the states the worker keeps for it: a transport's
134
+ * connection, a database that is active or refused startup, a writable or
135
+ * readonly owner, and a pending, settled, or complete route. A transport's
136
+ * events drive its connection, so publishing never reads a socket.
137
+ *
138
+ * Apps show users an owner's {@link OwnerSyncStatus}, which
139
+ * {@link syncStateToOwnerSyncStatus} derives. A view of each relay pairs each
140
+ * route with its transport by {@link syncStateToRelaySyncStates} and tells the
141
+ * relay's status by {@link relaySyncStateToStatus}. Snapshots store neither, so
142
+ * a status never disagrees with the routes it comes from.
123
143
  *
124
144
  * Each worker broadcasts snapshots on its own channel, whose name a connecting
125
145
  * tab receives through its port, so a tab never hears another worker, such as
126
146
  * one of a different app version, and
127
147
  * {@link SyncStateDep.syncState | deps.syncState} keeps the last snapshot. The
128
- * worker publishes after every change it observes; a transition without an
129
- * event, such as a closed socket starting to reconnect, appears with the next
130
- * snapshot. The snapshot lives in worker memory only, so a new worker starts
131
- * empty.
148
+ * worker publishes after every change it observes. The snapshot lives in worker
149
+ * memory only, so a new worker starts empty.
132
150
  *
133
151
  * ## Synchronization completion
134
152
  *
@@ -156,27 +174,51 @@
156
174
  * - The tenant has sent a round through the transport since the last event that
157
175
  * requires one: its first use of the transport for the owner, the socket
158
176
  * opening, an explicit request, a replacement leader, or storing messages
159
- * from another transport. No failed or aborted result has arrived on the
160
- * route since.
177
+ * from another transport, unless the route skipped a message. No failed or
178
+ * aborted result has arrived on the route since.
179
+ * - No result that skipped a received message has arrived on the route since a
180
+ * round was last requested through it, and since that request, no messages
181
+ * have been stored from another transport, directly or through a sibling copy
182
+ * uploaded outside this transport, and no sibling copy has failed to apply or
183
+ * skipped a message.
184
+ *
185
+ * A route is settled when every condition except the last holds, so a complete
186
+ * route is settled too. A settled route that is incomplete has ended its
187
+ * reconciliation, but its relay may offer a message the database skipped, or
188
+ * messages stored elsewhere may not have reached it; it is published as
189
+ * {@link SettledSyncRoute}.
161
190
  *
162
191
  * A reconciliation chain ends only with a converged result, a failure, an
163
192
  * abort, a dropped frame, or a continuation that finds the socket closed, and
164
193
  * relay errors reach every applying tenant, so these conditions mean every
165
194
  * chain, including this tenant's, converged. A local Broadcast from a sibling
166
195
  * tenant holds every route of its owner until it is applied, because it arrives
167
- * without a request of its own; one that fails to apply requires a round
168
- * through every transport.
196
+ * without a request of its own; one that fails to apply or skips a message
197
+ * requires a round through every transport whose route has not skipped one.
169
198
  *
170
199
  * A failed result on a route requests one round through it. Any further failure
171
- * before the route completes waits for an explicit request or a reopen, so a
200
+ * before the route settles waits for an explicit request or a reopen, so a
172
201
  * persistent failure cannot loop; a converged reply in between does not end the
173
202
  * wait, because it may answer another tenant's round on the shared socket. An
174
203
  * aborted apply leaves its routes incomplete without a retry. An exception
175
204
  * while the database worker creates a round is logged there and fails the
176
- * round's routes with `SyncFailed` without a retry; other unexpected SQLite
177
- * exceptions remain unsupported and can panic the database worker. A frame the
178
- * relay silently drops, such as invalid data, leaves the count above zero until
179
- * the liveness rule below replaces the socket.
205
+ * round's routes with `SyncFailed` without a retry. A mutation that throws is
206
+ * rolled back and reported as an {@link UnknownError} to the tab that made it,
207
+ * or to every tab when its Evolu instance was disposed first. Other unexpected
208
+ * SQLite exceptions remain unsupported and can panic the database worker. A
209
+ * frame the relay silently drops, such as invalid data, leaves the count above
210
+ * zero until the liveness rule below replaces the socket.
211
+ *
212
+ * A result that skipped a received message the database could not decrypt,
213
+ * verify, or decode records the error on the route as its `skippedError`,
214
+ * without ending its chain, so it requests no round, and the relay offers the
215
+ * message again in every round through the route until the database stores a
216
+ * message with that timestamp. The messages received elsewhere that the last
217
+ * condition names request no round through such a route, even while a requested
218
+ * round checks it again, because each round would download every skipped
219
+ * message again. The next round that a first use of the transport for the
220
+ * owner, an explicit request, a reopen, a replacement leader, or a failure
221
+ * sends through the route reconciles them.
180
222
  *
181
223
  * ### Liveness
182
224
  *
@@ -196,11 +238,13 @@
196
238
  * The timeout belongs to the transport, not to an owner, because a small reply
197
239
  * for one owner can wait behind another owner's large frame on the socket. A
198
240
  * grown timeout lasts while a request is outstanding on the socket or a
199
- * database that has not refused startup has an incomplete route through it,
200
- * including one waiting after a failure, and ends with the transport. A reply's
201
- * speed proves nothing, because a recovery on a slow link starts with small
202
- * replies that arrive quickly. The reopen resets the counts and starts the open
203
- * rounds, so a dropped frame delays a route instead of stranding it.
241
+ * database that has not refused startup has an unsettled route through it,
242
+ * including one waiting after a failure, and ends with the transport. A settled
243
+ * route that is incomplete does not count, because its reconciliation has
244
+ * ended. A reply's speed proves nothing, because a recovery on a slow link
245
+ * starts with small replies that arrive quickly. The reopen resets the counts
246
+ * and starts the open rounds, so a dropped frame delays a route instead of
247
+ * stranding it.
204
248
  *
205
249
  * The shared worker sends nothing while idle, so a path that dies then may go
206
250
  * unnoticed until its next request; until then, changes from other devices stop
@@ -213,7 +257,8 @@
213
257
  import { type NonEmptyReadonlyArray } from "../Array.ts";
214
258
  import type { Brand } from "../Brand.ts";
215
259
  import type { ConsoleEntry, ConsoleLevel } from "../Console.ts";
216
- import type { EncryptionKey } from "../Crypto.ts";
260
+ import type { DecryptWithXChaCha20Poly1305Error, EncryptionKey } from "../Crypto.ts";
261
+ import { type UnknownError } from "../Error.ts";
217
262
  import { type LockManagerDep } from "../LockManager.ts";
218
263
  import { type Result } from "../Result.ts";
219
264
  import type { NonEmptyReadonlySet } from "../Set.ts";
@@ -221,12 +266,12 @@ import type { SqliteSchema } from "../Sqlite.ts";
221
266
  import { AbortError, type Task } from "../Task.ts";
222
267
  import { type Millis } from "../Time.ts";
223
268
  import { type ExtractTyped, type Id, type InferType, type LiteralType, type Name, type ObjectType, type Typed } from "../Type.ts";
224
- import type { CreateWebSocketDep, WebSocketError, WebSocketReadyState } from "../WebSocket.ts";
269
+ import type { CreateWebSocketDep, WebSocketError } from "../WebSocket.ts";
225
270
  import type { SharedWorker as CommonSharedWorker, CreateBroadcastChannelDep, CreateMessageChannelDep, NativeMessagePort, SharedWorkerSelf, WorkerDeps } from "../Worker.ts";
226
271
  import type { DbWorkerInit, UnsupportedDbVersionError } from "./Db.ts";
227
- import type { EvoluError } from "./Evolu.ts";
272
+ import type { DevicePersistence } from "./Evolu.ts";
228
273
  import type { Owner, OwnerId, SyncOwner } from "./Owner.ts";
229
- import { type ApplyProtocolMessageAsClientResult, type ProtocolError, type ProtocolMessage } from "./Protocol.ts";
274
+ import { type ApplyProtocolMessageAsClientResult, type ProtocolError, type ProtocolInvalidDataError, type ProtocolMessage, type ProtocolTimestampMismatchError } from "./Protocol.ts";
230
275
  import { type Patch, type Query, type RowsByQueryMap } from "./Query.ts";
231
276
  import { type MutationChange } from "./Schema.ts";
232
277
  import type { CrdtMessage, StorageWriteMessagesError } from "./Storage.ts";
@@ -256,11 +301,11 @@ export type SharedWorkerInput = {
256
301
  };
257
302
  export type SharedWorkerOutput = DbWorkerInit | {
258
303
  /**
259
- * Sent to one tab only: its database refused startup, or another build
260
- * keeps this worker waiting.
304
+ * Sent to one tab only: its database refused startup, a mutation it made
305
+ * could not be stored, or another build keeps this worker waiting.
261
306
  */
262
307
  readonly type: "Error";
263
- readonly error: UnsupportedDbVersionError | OtherBuildRunningError;
308
+ readonly error: OtherBuildRunningError | UnknownError | UnsupportedDbVersionError;
264
309
  } | {
265
310
  /**
266
311
  * Sent to a tab that connects while the worker waits for the build lock;
@@ -278,20 +323,13 @@ export type SharedWorkerOutput = DbWorkerInit | {
278
323
  readonly type: "Connected";
279
324
  readonly workerId: SharedWorkerId;
280
325
  readonly syncStateChannelName: string;
281
- } | {
282
- /**
283
- * Sent to a connecting tab after `Connected` when the platform offers no
284
- * persistent storage, so the worker keeps every database in memory; see
285
- * Storage in this module's documentation.
286
- */
287
- readonly type: "StorageUnavailable";
288
326
  };
289
327
  export type ConsoleEntryOrError = {
290
328
  readonly type: "ConsoleEntry";
291
329
  readonly entry: ConsoleEntry;
292
330
  } | {
293
331
  readonly type: "Error";
294
- readonly error: EvoluError;
332
+ readonly error: UnknownError;
295
333
  };
296
334
  export declare const consoleEntryOrErrorBroadcastChannelName = "evolu:console-entry-or-error";
297
335
  /** Identifies one running SharedWorker instance. */
@@ -337,126 +375,467 @@ export interface BuildWaitingRequest extends InferType<typeof BuildWaitingReques
337
375
  export interface OtherBuildRunningError extends Typed<"OtherBuildRunningError"> {
338
376
  }
339
377
  /**
340
- * A snapshot of the transports and databases the shared worker manages.
341
- *
342
- * See the Sync state section of this module's documentation.
378
+ * A snapshot of the transports and databases the shared worker manages. Each
379
+ * part is a union of the states the worker keeps for it, so each part holds
380
+ * only the fields valid for its state. Apps derive what to show with
381
+ * {@link syncStateToOwnerSyncStatus}; see Sync state in this module's
382
+ * documentation.
343
383
  */
344
384
  export interface SyncState {
345
385
  readonly transports: ReadonlyArray<SyncTransport>;
346
386
  readonly tenants: ReadonlyArray<SyncTenant>;
347
387
  }
348
- /** One WebSocket, shared by every owner and database claiming it. */
349
- export interface SyncTransport {
350
- /** Opaque and stable for the transport's lifetime, across socket replacements. */
388
+ /**
389
+ * One transport, shared by every owner and database claiming it. Its `type` is
390
+ * the kind of transport, and its {@link SyncConnection} tells whether it is
391
+ * connected.
392
+ */
393
+ export type SyncTransport = WebSocketSyncTransport;
394
+ /** A WebSocket {@link SyncTransport}. */
395
+ export interface WebSocketSyncTransport extends Typed<"WebSocket"> {
396
+ /** Opaque and stable for the transport's lifetime, across reconnects. */
351
397
  readonly id: SyncTransportId;
352
- /** The URL without its query, which carries the owner ID. */
398
+ /** The relay URL without the owner-specific query. */
353
399
  readonly label: string;
354
- readonly readyState: WebSocketReadyState;
355
- /** When the connection last opened, or null before its first open. */
356
- readonly openedAt: Millis | null;
357
- /** When the connection last closed, or null before its first close. */
358
- readonly closedAt: Millis | null;
359
- /**
360
- * The last error, retained after a successful reconnect; null if none. Errors
361
- * while reconnecting are routine.
362
- */
363
- readonly error: SyncTransportError | null;
400
+ readonly connection: SyncConnection;
364
401
  }
365
402
  export type SyncTransportId = Id & Brand<"SyncTransport">;
366
403
  export interface SyncTransportError {
367
404
  readonly type: WebSocketError["type"];
368
405
  readonly at: Millis;
369
406
  }
370
- /** One named local database and the owners it registered. */
371
- export interface SyncTenant {
407
+ /**
408
+ * The connection of a {@link SyncTransport}: `Connecting` until its first
409
+ * connection opens or fails, `Open` while a connection is open, and
410
+ * `Disconnected` from a close, a failure, or an unanswered request that
411
+ * replaced the connection, until a connection opens again. It never returns to
412
+ * `Connecting`. The transport's events drive these states.
413
+ */
414
+ export type SyncConnection = ConnectingSyncConnection | OpenSyncConnection | DisconnectedSyncConnection;
415
+ /**
416
+ * A first connection, which has neither opened nor failed. A connection to a
417
+ * host that drops packets stays here until the operating system or browser
418
+ * gives up on it, which can take a minute or more.
419
+ */
420
+ export interface ConnectingSyncConnection extends Typed<"Connecting"> {
421
+ }
422
+ /** An open connection. */
423
+ export interface OpenSyncConnection extends Typed<"Open"> {
424
+ /** When this connection opened. */
425
+ readonly openedAt: Millis;
426
+ /** The last error of an earlier connection or attempt, or null. */
427
+ readonly error: SyncTransportError | null;
428
+ }
429
+ /**
430
+ * No connection: a connection closed or failed, or the relay did not answer a
431
+ * request in time. The transport reconnects by itself.
432
+ */
433
+ export interface DisconnectedSyncConnection extends Typed<"Disconnected"> {
434
+ /** When it disconnected. Failed reconnect attempts leave it unchanged. */
435
+ readonly disconnectedAt: Millis;
436
+ /** When the last connection opened, or null when none has. */
437
+ readonly openedAt: Millis | null;
438
+ /**
439
+ * The last error, or null. A close or an unanswered request records none, so
440
+ * it can come from an earlier connection. Errors while reconnecting are
441
+ * routine.
442
+ */
443
+ readonly error: SyncTransportError | null;
444
+ }
445
+ /** One named local database: `Active`, or `Refused` when it refused startup. */
446
+ export type SyncTenant = ActiveSyncTenant | RefusedSyncTenant;
447
+ /**
448
+ * A database that has not refused startup, including one still starting, with
449
+ * the owners its instances registered.
450
+ */
451
+ export interface ActiveSyncTenant extends Typed<"Active"> {
372
452
  readonly name: Name;
373
- /** The database refused startup, so nothing it holds synchronizes. */
374
- readonly refused: boolean;
375
453
  readonly owners: ReadonlyArray<SyncTenantOwner>;
376
454
  }
377
- export interface SyncTenantOwner {
455
+ /**
456
+ * A database that refused startup, so nothing it holds syncs. Its tabs also
457
+ * receive the error through `evoluError`.
458
+ */
459
+ export interface RefusedSyncTenant extends Typed<"Refused"> {
460
+ readonly name: Name;
461
+ readonly error: UnsupportedDbVersionError;
462
+ }
463
+ /**
464
+ * An owner registered by any instance of an {@link ActiveSyncTenant}: `Writable`
465
+ * when any registration holds its write key, otherwise `Readonly`.
466
+ */
467
+ export type SyncTenantOwner = WritableSyncTenantOwner | ReadonlySyncTenantOwner;
468
+ /**
469
+ * An owner the database syncs, with one route per transport claimed for it by
470
+ * this or any other database. Routes are briefly empty while the owner's
471
+ * transports are being claimed.
472
+ */
473
+ export interface WritableSyncTenantOwner extends Typed<"Writable"> {
474
+ readonly ownerId: OwnerId;
475
+ readonly routes: ReadonlyArray<SyncRoute>;
476
+ }
477
+ /**
478
+ * An owner registered only without its write key. Its registrations hold
479
+ * transports, which other databases' routes for the owner use, but this
480
+ * database does not sync it.
481
+ */
482
+ export interface ReadonlySyncTenantOwner extends Typed<"Readonly"> {
378
483
  readonly ownerId: OwnerId;
379
- /** A readonly registration holds transports but never synchronizes. */
380
- readonly writable: boolean;
381
484
  /** Every transport claimed for the owner, by any database. */
382
485
  readonly transportIds: ReadonlyArray<SyncTransportId>;
383
- /** One route per transport for a writable owner; none for a readonly one. */
384
- readonly routes: ReadonlyArray<SyncRoute>;
385
486
  }
386
487
  /**
387
- * One database's use of one owner through one transport. See the
388
- * Synchronization completion section of this module's documentation.
488
+ * One database's use of one owner through one transport: `Pending` until its
489
+ * reconciliation ends, then `Complete`, or `Settled` while its relay offers a
490
+ * change the database skipped. See Synchronization completion in this module's
491
+ * documentation.
492
+ */
493
+ export type SyncRoute = PendingSyncRoute | SettledSyncRoute | CompleteSyncRoute;
494
+ /**
495
+ * A route whose reconciliation has not ended: its transport is not open, a
496
+ * request or a received frame is outstanding, a replicated write is queued, or
497
+ * a round must still be sent through it.
389
498
  */
390
- export interface SyncRoute {
499
+ export interface PendingSyncRoute extends Typed<"Pending"> {
391
500
  readonly transportId: SyncTransportId;
392
- /** Whether the database is reconciled with the relay for the owner. */
393
- readonly complete: boolean;
501
+ /**
502
+ * The failure since the route last settled, or null. The first one requests a
503
+ * round; a further one waits for {@link Evolu.requestSync} or a reopen.
504
+ */
505
+ readonly failure: SyncRouteError | null;
506
+ /**
507
+ * The first change skipped in the latest reply that skipped one, or null. The
508
+ * relay offers it again in every round through the route until this database
509
+ * stores a change with that timestamp.
510
+ */
511
+ readonly skippedError: SyncRouteError | null;
394
512
  /** When the route last became complete, or null. */
395
513
  readonly completeAt: Millis | null;
396
514
  /** When this database last sent a request through the route, or null. */
397
515
  readonly lastSentAt: Millis | null;
398
516
  /**
399
517
  * When processing a frame from the route last finished, successfully or with
400
- * a failure, or null. Aborted processing does not update this timestamp.
518
+ * a failure, or null. Aborted processing does not update it.
401
519
  */
402
520
  readonly lastReceivedAt: Millis | null;
403
- /** The last failed result on the route; cleared when the route completes. */
404
- readonly error: SyncRouteError | null;
405
521
  }
406
- export interface SyncRouteError {
407
- readonly type: SyncRouteErrorType;
408
- readonly at: Millis;
409
- }
410
- /**
411
- * A {@link ProtocolError} or the original {@link StorageWriteMessagesError} type
412
- * for a rejected write. `WriteFailed` means a `writeMessages` call that threw,
413
- * logged by the protocol, and `SyncFailed` means a logged failure while
414
- * creating a round or reconciling ranges.
415
- */
416
- export type SyncRouteErrorType = ProtocolError["type"] | StorageWriteMessagesError["type"] | "WriteFailed" | "SyncFailed";
417
522
  /**
418
- * One owner's standing with its relays in one database, derived from
419
- * {@link SyncState} by {@link syncStateToOwnerSyncStates}.
523
+ * A route whose reconciliation ended while its relay offers a change the
524
+ * database skipped, so it is incomplete. Changes stored from other relays
525
+ * request no round through it; the next round requested through it, such as by
526
+ * {@link Evolu.requestSync} or a reopen, checks it again.
420
527
  */
421
- export interface OwnerSyncState {
422
- readonly name: Name;
423
- readonly ownerId: OwnerId;
424
- readonly status: OwnerSyncStatus;
425
- /** When a route of the owner last became complete, or null. */
426
- readonly syncedAt: Millis | null;
427
- /** The newest route error of the owner, or null. */
428
- readonly error: SyncRouteError | null;
429
- /** Each relay of the owner, in the order of its routes. */
430
- readonly relays: ReadonlyArray<RelaySyncState>;
528
+ export interface SettledSyncRoute extends Typed<"Settled"> {
529
+ readonly transportId: SyncTransportId;
530
+ /** The first change skipped in the latest reply that skipped one. */
531
+ readonly skippedError: SyncRouteError;
532
+ /** When the route last became complete, or null. */
533
+ readonly completeAt: Millis | null;
534
+ /** When this database last sent a request through the route. */
535
+ readonly lastSentAt: Millis;
536
+ /** When processing a frame from the route last finished, or null. */
537
+ readonly lastReceivedAt: Millis | null;
538
+ }
539
+ /** A route on which the database is reconciled with the relay for the owner. */
540
+ export interface CompleteSyncRoute extends Typed<"Complete"> {
541
+ readonly transportId: SyncTransportId;
542
+ /** When the route became complete. */
543
+ readonly completeAt: Millis;
544
+ /** When this database last sent a request through the route. */
545
+ readonly lastSentAt: Millis;
546
+ /** When processing a frame from the route last finished, or null. */
547
+ readonly lastReceivedAt: Millis | null;
431
548
  }
432
549
  /**
433
- * The status of an {@link OwnerSyncState}: the first of `error`, `syncing`,
434
- * `synced`, and `offline` that one of its relays has, or `initial` before the
435
- * owner has a relay. Work in progress on an open transport shows before a relay
436
- * that is already up to date.
550
+ * A failure or a skipped change of a {@link SyncRoute}, with the time it
551
+ * arrived.
552
+ *
553
+ * It is the error itself, so it carries its details, such as the expected and
554
+ * actual timestamps of a {@link ProtocolTimestampMismatchError}. A skipped
555
+ * message adds {@link DecryptWithXChaCha20Poly1305Error}. A
556
+ * {@link ProtocolInvalidDataError} leaves out its data, which can be a whole
557
+ * frame. The caught value in the `error` of either is an {@link UnknownError}.
558
+ * `WriteFailed` means a `writeMessages` call that threw, logged by the
559
+ * protocol, and `SyncFailed` means a logged failure while creating a round or
560
+ * reconciling ranges.
437
561
  */
438
- export type OwnerSyncStatus = "initial" | RelaySyncStatus;
562
+ export type SyncRouteError = (Exclude<ProtocolError, ProtocolInvalidDataError> | Omit<ProtocolInvalidDataError, "data"> | StorageWriteMessagesError | DecryptWithXChaCha20Poly1305Error | Typed<"WriteFailed"> | Typed<"SyncFailed">) & {
563
+ readonly at: Millis;
564
+ };
565
+ /** The type of a {@link SyncRouteError}. */
566
+ export type SyncRouteErrorType = SyncRouteError["type"];
439
567
  /**
440
- * One relay of an {@link OwnerSyncState}: a transport and the database's route
441
- * through it.
568
+ * One relay of an owner in one database: a transport and the database's route
569
+ * through it, from {@link syncStateToRelaySyncStates}. Its status comes from
570
+ * {@link relaySyncStateToStatus} and is not stored beside them.
442
571
  */
443
572
  export interface RelaySyncState {
444
573
  readonly transport: SyncTransport;
445
574
  readonly route: SyncRoute;
446
- readonly status: RelaySyncStatus;
447
575
  }
448
576
  /**
449
- * The status of a {@link RelaySyncState}: `error` when its route failed and has
450
- * not completed since, `synced` when the route is complete, `syncing` while the
451
- * transport is open, and `offline` otherwise.
577
+ * What an app shows users about syncing one owner of one database, from
578
+ * {@link syncStateToOwnerSyncStatus} or the React and Vue `useOwnerSyncStatus`.
579
+ *
580
+ * Its variants:
581
+ *
582
+ * - `NoRelays`: nothing syncs the owner here. There is no snapshot yet, the
583
+ * owner's transports are still being set up, the app uses no relays for it,
584
+ * it is registered only as readonly, or the database refused startup, which
585
+ * `evoluError` reports.
586
+ * - `Syncing`: a relay connects for the first time or reconciles. A first
587
+ * connection to a host that drops packets stays `Syncing` until the platform
588
+ * gives up on it, which can take a minute.
589
+ * - `Synced`: a relay is up to date, and none syncs.
590
+ * - `Offline`: every relay is disconnected. Evolu keeps reconnecting, up to 30
591
+ * seconds apart, so `Offline` can briefly outlast the outage.
592
+ * - `Error`: a relay failed or offers a change this database skipped. `error` is
593
+ * the newest failure, or without one, the newest skipped change: a failure
594
+ * stops syncing through its relay, while a skipped change leaves out only
595
+ * that change.
596
+ *
597
+ * Evolu stores changes in the local database before they sync, so sync needs no
598
+ * UI while it works: show nothing for `NoRelays`, `Syncing`, and `Synced`. An
599
+ * indicator that changes with every edit distracts, and screen readers announce
600
+ * each change.
601
+ *
602
+ * For `Offline` and `Error`, show one quiet line that lasts as long as the
603
+ * status, not a dialog, which interrupts, or a toast, which disappears while
604
+ * the problem lasts. Do not say that changes are saved on this device unless
605
+ * {@link Evolu.devicePersistence} is `Persisted`: a browser may keep them only
606
+ * for a private session or delete them later. Evolu reports `Offline` at once;
607
+ * an app may wait a few seconds before showing it, because brief
608
+ * disconnections, such as waking from sleep, reconnect quickly. For `Error`,
609
+ * write actionable text for the error types the app can act on, such as a
610
+ * {@link ProtocolQuotaError}: the relay stores no more data for the owner, so
611
+ * offer more quota, such as a plan upgrade, then call {@link Evolu.requestSync}
612
+ * with the owner's ID. For any other error, show generic text that names the
613
+ * error type, which helps when the user reports it.
614
+ *
615
+ * Render the line inside one element with `role="status"` that stays mounted:
616
+ * screen readers announce changes only in a live region that already exists,
617
+ * and a polite announcement fits a status that loses nothing. Do not use
618
+ * `role="alert"`.
619
+ *
620
+ * Show the app owner's status once for the whole app, such as below the header,
621
+ * and another owner's status where the app shows that owner's data. Each Evolu
622
+ * instance finds its own status by {@link Evolu.name}, so an app with several
623
+ * databases shows the status of the one the user works in.
624
+ *
625
+ * ### Example
626
+ *
627
+ * ```ts
628
+ * import { assertEqual, Millis } from "@evolu/common";
629
+ * import {
630
+ * testAppOwner,
631
+ * type OwnerSyncStatus,
632
+ * } from "@evolu/common/local-first";
633
+ *
634
+ * // What to tell the user, or null while sync works or is not used.
635
+ * const syncStatusToMessage = (status: OwnerSyncStatus): string | null => {
636
+ * switch (status.type) {
637
+ * case "NoRelays":
638
+ * case "Syncing":
639
+ * case "Synced":
640
+ * return null;
641
+ * case "Offline":
642
+ * return "Offline. Changes will sync when you're back online.";
643
+ * case "Error":
644
+ * return status.error.type === "ProtocolQuotaError"
645
+ * ? "Sync is paused because the sync server is full."
646
+ * : `Sync error: ${status.error.type}.`;
647
+ * }
648
+ * };
649
+ *
650
+ * assertEqual(syncStatusToMessage({ type: "Synced" }), null);
651
+ * assertEqual(
652
+ * syncStatusToMessage({ type: "Offline" }),
653
+ * "Offline. Changes will sync when you're back online.",
654
+ * );
655
+ * assertEqual(
656
+ * syncStatusToMessage({
657
+ * type: "Error",
658
+ * error: {
659
+ * type: "ProtocolQuotaError",
660
+ * ownerId: testAppOwner.id,
661
+ * at: Millis.orThrow(1000),
662
+ * },
663
+ * }),
664
+ * "Sync is paused because the sync server is full.",
665
+ * );
666
+ * assertEqual(
667
+ * syncStatusToMessage({
668
+ * type: "Error",
669
+ * error: { type: "SyncFailed", at: Millis.orThrow(1000) },
670
+ * }),
671
+ * "Sync error: SyncFailed.",
672
+ * );
673
+ * ```
674
+ */
675
+ export type OwnerSyncStatus = NoRelaysSyncStatus | RelaySyncStatus;
676
+ /**
677
+ * The status of a {@link RelaySyncState}, from {@link relaySyncStateToStatus}:
678
+ * `Error` when its route has a failure or a skipped change, the failure first;
679
+ * otherwise `Synced` when the route is complete, `Syncing` while the
680
+ * transport's connection is `Connecting` or `Open`, and `Offline` while it is
681
+ * `Disconnected`.
682
+ */
683
+ export type RelaySyncStatus = SyncingSyncStatus | SyncedSyncStatus | OfflineSyncStatus | ErrorSyncStatus;
684
+ /** Evolu reports no relay syncing the owner in the database. */
685
+ export interface NoRelaysSyncStatus extends Typed<"NoRelays"> {
686
+ }
687
+ /** A relay makes its first connection or reconciles. */
688
+ export interface SyncingSyncStatus extends Typed<"Syncing"> {
689
+ }
690
+ /** A relay is reconciled with the database for the owner. */
691
+ export interface SyncedSyncStatus extends Typed<"Synced"> {
692
+ }
693
+ /** Relays are disconnected and reconnecting, so changes wait on this device. */
694
+ export interface OfflineSyncStatus extends Typed<"Offline"> {
695
+ }
696
+ /** A route failed or holds a change the database skipped. */
697
+ export interface ErrorSyncStatus extends Typed<"Error"> {
698
+ /**
699
+ * The failure, or without one, the skipped change. For an owner, the newest
700
+ * failure of any relay, or without one, the newest skipped change.
701
+ */
702
+ readonly error: SyncRouteError;
703
+ }
704
+ /**
705
+ * Tells what an app shows about syncing an owner in a database: `Error` when a
706
+ * relay has one, holding the newest failure of any relay or, without one, the
707
+ * newest skipped change; otherwise the first of `Syncing`, `Synced`, and
708
+ * `Offline` that a relay has, or `NoRelays`. Accepts null, the store's value
709
+ * before the first snapshot. See {@link OwnerSyncStatus} for what to show.
710
+ *
711
+ * The same status is the same object: statuses other than `Error` are shared
712
+ * constants, and an `Error` status is the same object for the same error
713
+ * object, which {@link SyncStateDep.syncState} keeps between snapshots while it
714
+ * is unchanged. So compare statuses with `===`.
715
+ *
716
+ * ### Example
717
+ *
718
+ * ```ts
719
+ * import {
720
+ * assertEqual,
721
+ * assertSame,
722
+ * createId,
723
+ * createUnknownError,
724
+ * Millis,
725
+ * testCreateDeps,
726
+ * testName,
727
+ * } from "@evolu/common";
728
+ * import {
729
+ * syncStateToOwnerSyncStatus,
730
+ * testAppOwner,
731
+ * type PendingSyncRoute,
732
+ * type SyncConnection,
733
+ * type SyncRoute,
734
+ * type SyncState,
735
+ * type SyncTransportId,
736
+ * } from "@evolu/common/local-first";
737
+ *
738
+ * const deps = testCreateDeps();
739
+ * const primaryId = createId<"SyncTransport">(deps);
740
+ * const backupId = createId<"SyncTransport">(deps);
741
+ *
742
+ * const stateOf = (
743
+ * connections: ReadonlyArray<SyncConnection>,
744
+ * routes: ReadonlyArray<SyncRoute>,
745
+ * ): SyncState => ({
746
+ * transports: connections.map((connection, index) => ({
747
+ * type: "WebSocket",
748
+ * id: index === 0 ? primaryId : backupId,
749
+ * label: "wss://relay.example",
750
+ * connection,
751
+ * })),
752
+ * tenants: [
753
+ * {
754
+ * type: "Active",
755
+ * name: testName,
756
+ * owners: [{ type: "Writable", ownerId: testAppOwner.id, routes }],
757
+ * },
758
+ * ],
759
+ * });
760
+ * const pending = (transportId: SyncTransportId): PendingSyncRoute => ({
761
+ * type: "Pending",
762
+ * transportId,
763
+ * failure: null,
764
+ * skippedError: null,
765
+ * completeAt: null,
766
+ * lastSentAt: null,
767
+ * lastReceivedAt: null,
768
+ * });
769
+ * const statusOf = (state: SyncState | null) =>
770
+ * syncStateToOwnerSyncStatus(state, testName, testAppOwner.id);
771
+ *
772
+ * // Before the first snapshot, nothing syncs the owner.
773
+ * assertEqual(statusOf(null), { type: "NoRelays" });
774
+ *
775
+ * // A first connection is syncing; a lost one is offline.
776
+ * const connecting: SyncConnection = { type: "Connecting" };
777
+ * assertEqual(statusOf(stateOf([connecting], [pending(primaryId)])), {
778
+ * type: "Syncing",
779
+ * });
780
+ * const disconnected: SyncConnection = {
781
+ * type: "Disconnected",
782
+ * disconnectedAt: Millis.orThrow(2000),
783
+ * openedAt: Millis.orThrow(1000),
784
+ * error: null,
785
+ * };
786
+ * assertEqual(statusOf(stateOf([disconnected], [pending(primaryId)])), {
787
+ * type: "Offline",
788
+ * });
789
+ *
790
+ * // A quota failure on one relay shows before a newer skipped change on
791
+ * // another, because the app can act on it.
792
+ * const quotaError = {
793
+ * type: "ProtocolQuotaError",
794
+ * ownerId: testAppOwner.id,
795
+ * at: Millis.orThrow(3000),
796
+ * } as const;
797
+ * const open: SyncConnection = {
798
+ * type: "Open",
799
+ * openedAt: Millis.orThrow(3400),
800
+ * error: null,
801
+ * };
802
+ * assertEqual(
803
+ * statusOf(
804
+ * stateOf(
805
+ * [disconnected, open],
806
+ * [
807
+ * { ...pending(primaryId), failure: quotaError },
808
+ * {
809
+ * type: "Settled",
810
+ * transportId: backupId,
811
+ * skippedError: {
812
+ * type: "DecryptWithXChaCha20Poly1305Error",
813
+ * error: createUnknownError(new Error("invalid tag")),
814
+ * at: Millis.orThrow(4000),
815
+ * },
816
+ * completeAt: null,
817
+ * lastSentAt: Millis.orThrow(3500),
818
+ * lastReceivedAt: Millis.orThrow(4000),
819
+ * },
820
+ * ],
821
+ * ),
822
+ * ),
823
+ * { type: "Error", error: quotaError },
824
+ * );
825
+ *
826
+ * // The same error gives the same status object.
827
+ * const quotaState = stateOf(
828
+ * [disconnected],
829
+ * [{ ...pending(primaryId), failure: quotaError }],
830
+ * );
831
+ * assertSame(statusOf(quotaState), statusOf(quotaState));
832
+ * ```
452
833
  */
453
- export type RelaySyncStatus = "syncing" | "synced" | "offline" | "error";
834
+ export declare const syncStateToOwnerSyncStatus: (state: SyncState | null, name: Name, ownerId: OwnerId) => OwnerSyncStatus;
454
835
  /**
455
- * Folds the routes of every writable owner registration of every running
456
- * database in a {@link SyncState} into one {@link OwnerSyncState} per database
457
- * and owner, which pairs each route with its transport as a
458
- * {@link RelaySyncState}. A database that refused startup and a readonly
459
- * registration synchronize nothing, so they are left out.
836
+ * Pairs each route of an owner in a database with its transport, in route
837
+ * order. Returns none for a null snapshot, a missing or refused database, and a
838
+ * missing or readonly owner.
460
839
  *
461
840
  * ### Example
462
841
  *
@@ -469,7 +848,8 @@ export type RelaySyncStatus = "syncing" | "synced" | "offline" | "error";
469
848
  * testName,
470
849
  * } from "@evolu/common";
471
850
  * import {
472
- * syncStateToOwnerSyncStates,
851
+ * relaySyncStateToStatus,
852
+ * syncStateToRelaySyncStates,
473
853
  * testAppOwner,
474
854
  * type SyncRoute,
475
855
  * type SyncState,
@@ -478,52 +858,51 @@ export type RelaySyncStatus = "syncing" | "synced" | "offline" | "error";
478
858
  *
479
859
  * const deps = testCreateDeps();
480
860
  * const transport: SyncTransport = {
861
+ * type: "WebSocket",
481
862
  * id: createId<"SyncTransport">(deps),
482
863
  * label: "wss://relay.example",
483
- * readyState: "open",
484
- * openedAt: null,
485
- * closedAt: null,
486
- * error: null,
864
+ * connection: {
865
+ * type: "Open",
866
+ * openedAt: Millis.orThrow(800),
867
+ * error: null,
868
+ * },
487
869
  * };
488
870
  * const route: SyncRoute = {
871
+ * type: "Complete",
489
872
  * transportId: transport.id,
490
- * complete: true,
491
873
  * completeAt: Millis.orThrow(1000),
492
874
  * lastSentAt: Millis.orThrow(900),
493
875
  * lastReceivedAt: Millis.orThrow(1000),
494
- * error: null,
495
876
  * };
496
877
  * const state: SyncState = {
497
878
  * transports: [transport],
498
879
  * tenants: [
499
880
  * {
881
+ * type: "Active",
500
882
  * name: testName,
501
- * refused: false,
502
883
  * owners: [
503
- * {
504
- * ownerId: testAppOwner.id,
505
- * writable: true,
506
- * transportIds: [transport.id],
507
- * routes: [route],
508
- * },
884
+ * { type: "Writable", ownerId: testAppOwner.id, routes: [route] },
509
885
  * ],
510
886
  * },
511
887
  * ],
512
888
  * };
513
889
  *
514
- * assertEqual(syncStateToOwnerSyncStates(state), [
515
- * {
516
- * name: testName,
517
- * ownerId: testAppOwner.id,
518
- * status: "synced",
519
- * syncedAt: Millis.orThrow(1000),
520
- * error: null,
521
- * relays: [{ transport, route, status: "synced" }],
522
- * },
523
- * ]);
890
+ * const relays = syncStateToRelaySyncStates(
891
+ * state,
892
+ * testName,
893
+ * testAppOwner.id,
894
+ * );
895
+ * assertEqual(relays, [{ transport, route }]);
896
+ * assertEqual(relays.map(relaySyncStateToStatus), [{ type: "Synced" }]);
897
+ * assertEqual(
898
+ * syncStateToRelaySyncStates(null, testName, testAppOwner.id),
899
+ * [],
900
+ * );
524
901
  * ```
525
902
  */
526
- export declare const syncStateToOwnerSyncStates: (state: SyncState) => ReadonlyArray<OwnerSyncState>;
903
+ export declare const syncStateToRelaySyncStates: (state: SyncState | null, name: Name, ownerId: OwnerId) => ReadonlyArray<RelaySyncState>;
904
+ /** Tells the status of one relay; a failure shows before a skipped change. */
905
+ export declare const relaySyncStateToStatus: ({ transport, route, }: RelaySyncState) => RelaySyncStatus;
527
906
  export type EvoluInput = {
528
907
  readonly type: "Mutate";
529
908
  readonly changes: NonEmptyReadonlyArray<MutationChange>;
@@ -553,6 +932,14 @@ export type EvoluOutput = {
553
932
  } | {
554
933
  readonly type: "OnExport";
555
934
  readonly file: Uint8Array<ArrayBuffer>;
935
+ } | {
936
+ /** The mutation with these onComplete callbacks could not be stored. */
937
+ readonly type: "OnMutateFailed";
938
+ readonly onCompleteIds: ReadonlyArray<Id>;
939
+ } | {
940
+ /** Sent once, when the tenant adds the instance; see Storage. */
941
+ readonly type: "OnDevicePersistence";
942
+ readonly devicePersistence: DevicePersistence;
556
943
  };
557
944
  export type DbWorkerInput = (Typed<"Request"> & {
558
945
  readonly attemptId: Id;
@@ -609,6 +996,10 @@ export type DbWorkerQueuedResponse = {
609
996
  readonly clock: Timestamp;
610
997
  readonly messagesByOwnerId: ReadonlyMap<OwnerId, NonEmptyReadonlyArray<CrdtMessage>>;
611
998
  readonly rowsByQuery: RowsByQueryMap;
999
+ } | {
1000
+ /** The mutation threw, so it rolled back and nothing was stored. */
1001
+ readonly type: "MutateFailed";
1002
+ readonly error: UnknownError;
612
1003
  } | {
613
1004
  readonly type: "Query";
614
1005
  readonly rowsByQuery: RowsByQueryMap;
@@ -628,19 +1019,25 @@ export type DbWorkerQueuedResponse = {
628
1019
  readonly clock: Timestamp;
629
1020
  readonly ownerId: OwnerId;
630
1021
  readonly didWriteMessages: boolean;
1022
+ /**
1023
+ * The first error of a received message the DbWorker skipped while
1024
+ * storing the rest, or null. It does not end the round.
1025
+ */
1026
+ readonly skippedError: DecryptWithXChaCha20Poly1305Error | ProtocolInvalidDataError | ProtocolTimestampMismatchError | null;
631
1027
  readonly result: Result<ApplyProtocolMessageAsClientResult, ProtocolError | StorageWriteMessagesError | AbortError>;
632
1028
  };
633
1029
  };
634
1030
  /**
635
- * Tells whether the platform can store databases persistently.
1031
+ * Tells what the platform can promise about the databases it stores.
636
1032
  *
637
- * Only a platform that can lack persistent storage provides it, as a browser
638
- * does in Safari's Private Browsing; see Storage in the Shared module.
1033
+ * `NotPersisted` means the platform offers no persistent storage now, as a
1034
+ * browser does in Safari's Private Browsing or a Firefox private window, so the
1035
+ * worker keeps every database in memory; see Storage in the Shared module.
639
1036
  */
640
- export interface PersistentStorageDep {
641
- readonly isPersistentStorageAvailable: () => Promise<boolean>;
1037
+ export interface DevicePersistenceDep {
1038
+ readonly getDevicePersistence: () => Promise<DevicePersistence>;
642
1039
  }
643
- export type SharedWorkerDeps = WorkerDeps & CreateBroadcastChannelDep & CreateMessageChannelDep & CreateWebSocketDep & LockManagerDep & Partial<PersistentStorageDep>;
1040
+ export type SharedWorkerDeps = WorkerDeps & CreateBroadcastChannelDep & CreateMessageChannelDep & CreateWebSocketDep & DevicePersistenceDep & LockManagerDep;
644
1041
  export type EvoluInstanceId = Id & Brand<"EvoluInstance">;
645
1042
  /**
646
1043
  * Initializes the platform-agnostic Evolu SharedWorker.