@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.
- package/dist/src/Callbacks.d.ts +12 -1
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +3 -0
- package/dist/src/Error.d.ts +10 -4
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +10 -4
- package/dist/src/Object.d.ts +65 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +142 -0
- package/dist/src/Resource.d.ts +0 -5
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +6 -13
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +7 -0
- package/dist/src/Task.d.ts +10 -8
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -5
- package/dist/src/Worker.d.ts +3 -3
- package/dist/src/local-first/Db.d.ts +8 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +56 -19
- package/dist/src/local-first/Evolu.d.ts +147 -39
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +53 -9
- package/dist/src/local-first/Owner.d.ts +9 -0
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +9 -0
- package/dist/src/local-first/Protocol.d.ts +6 -4
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +1 -6
- package/dist/src/local-first/Schema.d.ts +7 -1
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Shared.d.ts +550 -153
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +724 -273
- package/dist/src/local-first/Storage.d.ts +16 -12
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Callbacks.test.ts +20 -0
- package/src/Callbacks.ts +17 -1
- package/src/Error.ts +10 -4
- package/src/Object.test.ts +296 -0
- package/src/Object.ts +163 -0
- package/src/Resource.test.ts +20 -16
- package/src/Resource.ts +6 -20
- package/src/Sqlite.ts +7 -0
- package/src/Task.test.ts +233 -62
- package/src/Task.ts +47 -13
- package/src/Worker.ts +3 -3
- package/src/local-first/Db.ts +88 -18
- package/src/local-first/Evolu.test.ts +381 -11
- package/src/local-first/Evolu.ts +219 -51
- package/src/local-first/Owner.ts +9 -0
- package/src/local-first/Protocol.test.ts +42 -60
- package/src/local-first/Protocol.ts +7 -9
- package/src/local-first/Schema.ts +8 -2
- package/src/local-first/Shared.test.ts +2714 -644
- package/src/local-first/Shared.ts +1245 -401
- 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
|
|
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
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* its tabs see one mode, and
|
|
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
|
|
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
|
|
113
|
-
*
|
|
114
|
-
* but does not reconcile local
|
|
115
|
-
* until replication scopes and
|
|
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
|
-
*
|
|
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
|
|
129
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
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
|
|
200
|
-
* including one waiting after a failure, and ends with the transport. A
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
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
|
|
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 {
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
398
|
+
/** The relay URL without the owner-specific query. */
|
|
353
399
|
readonly label: string;
|
|
354
|
-
readonly
|
|
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
|
-
/**
|
|
371
|
-
|
|
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
|
-
|
|
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
|
|
388
|
-
*
|
|
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
|
|
499
|
+
export interface PendingSyncRoute extends Typed<"Pending"> {
|
|
391
500
|
readonly transportId: SyncTransportId;
|
|
392
|
-
/**
|
|
393
|
-
|
|
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
|
|
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
|
-
*
|
|
419
|
-
*
|
|
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
|
|
422
|
-
readonly
|
|
423
|
-
|
|
424
|
-
readonly
|
|
425
|
-
/** When
|
|
426
|
-
readonly
|
|
427
|
-
/**
|
|
428
|
-
readonly
|
|
429
|
-
/**
|
|
430
|
-
readonly
|
|
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
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
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
|
|
834
|
+
export declare const syncStateToOwnerSyncStatus: (state: SyncState | null, name: Name, ownerId: OwnerId) => OwnerSyncStatus;
|
|
454
835
|
/**
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
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
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
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
|
|
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
|
|
1031
|
+
* Tells what the platform can promise about the databases it stores.
|
|
636
1032
|
*
|
|
637
|
-
*
|
|
638
|
-
* does in Safari's Private Browsing
|
|
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
|
|
641
|
-
readonly
|
|
1037
|
+
export interface DevicePersistenceDep {
|
|
1038
|
+
readonly getDevicePersistence: () => Promise<DevicePersistence>;
|
|
642
1039
|
}
|
|
643
|
-
export type SharedWorkerDeps = WorkerDeps & CreateBroadcastChannelDep & CreateMessageChannelDep & CreateWebSocketDep &
|
|
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.
|