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