@evolu/common 8.16.0 → 8.18.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/Bytes.d.ts +31 -14
- package/dist/src/Bytes.d.ts.map +1 -1
- package/dist/src/Bytes.js +48 -8
- package/dist/src/Crypto.d.ts +21 -3
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +18 -2
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +5 -1
- package/dist/src/Polyfills.d.ts.map +1 -1
- package/dist/src/Polyfills.js +3 -2
- package/dist/src/Task.d.ts +4 -3
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +4 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +18 -7
- package/dist/src/local-first/Evolu.d.ts +10 -1
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +5 -1
- package/dist/src/local-first/Owner.d.ts +20 -8
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +23 -10
- package/dist/src/local-first/Protocol.d.ts +215 -93
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +799 -489
- package/dist/src/local-first/Relay.d.ts +9 -1
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +41 -36
- package/dist/src/local-first/Schema.d.ts +1 -1
- package/dist/src/local-first/Shared.d.ts +75 -49
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +62 -42
- package/dist/src/local-first/Storage.d.ts +80 -18
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +20 -4
- package/package.json +1 -1
- package/src/Bytes.test.ts +212 -0
- package/src/Bytes.ts +67 -17
- package/src/Crypto.ts +23 -3
- package/src/Error.ts +5 -1
- package/src/Polyfills.ts +3 -2
- package/src/Task.ts +4 -3
- package/src/local-first/Db.ts +29 -14
- package/src/local-first/Evolu.test.ts +43 -12
- package/src/local-first/Evolu.ts +16 -2
- package/src/local-first/Owner.test.ts +52 -1
- package/src/local-first/Owner.ts +26 -7
- package/src/local-first/Protocol.test.ts +1541 -44
- package/src/local-first/Protocol.ts +1070 -605
- package/src/local-first/Relay.ts +80 -69
- package/src/local-first/Shared.ts +106 -67
- package/src/local-first/Storage.ts +100 -27
package/src/local-first/Relay.ts
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
} from "../Array.ts";
|
|
14
14
|
import { assert } from "../Assert.ts";
|
|
15
15
|
import type { TimingSafeEqualDep } from "../Crypto.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { createUnknownError } from "../Error.ts";
|
|
17
|
+
import { err, ok, trySync } from "../Result.ts";
|
|
17
18
|
import type { SqliteDep } from "../Sqlite.ts";
|
|
18
19
|
import { sql } from "../Sqlite.ts";
|
|
19
20
|
import { createMutexByKey } from "../Task.ts";
|
|
@@ -25,12 +26,13 @@ import {
|
|
|
25
26
|
// OwnerTransport,
|
|
26
27
|
OwnerWriteKey,
|
|
27
28
|
} from "./Owner.ts";
|
|
29
|
+
import type { ProtocolWriteKeyError } from "./Protocol.ts";
|
|
28
30
|
import type {
|
|
29
31
|
EncryptedDbChange,
|
|
30
32
|
SqliteStorageDeps,
|
|
31
33
|
Storage,
|
|
32
34
|
StorageConfig,
|
|
33
|
-
|
|
35
|
+
StorageWriteMessagesError,
|
|
34
36
|
} from "./Storage.ts";
|
|
35
37
|
import {
|
|
36
38
|
createBaseSqliteStorage,
|
|
@@ -66,7 +68,15 @@ export interface RelayConfig extends StorageConfig {
|
|
|
66
68
|
*
|
|
67
69
|
* OwnerId is used rather than short-lived tokens because this only controls
|
|
68
70
|
* relay access, not write permissions. Since all data is encrypted on the
|
|
69
|
-
* relay, OwnerId exposure
|
|
71
|
+
* relay, OwnerId exposure reveals no data.
|
|
72
|
+
*
|
|
73
|
+
* It does allow write-key squatting. The relay stores the first write key
|
|
74
|
+
* presented for an owner, so anyone who learns an OwnerId before the owner's
|
|
75
|
+
* own write key reaches the relay can claim it. The owner then gets
|
|
76
|
+
* {@link ProtocolWriteKeyError} on this relay until the operator deletes the
|
|
77
|
+
* owner's row in `evolu_writeKey`. This callback does not prevent that: it
|
|
78
|
+
* receives only the OwnerId of a connection, which a squatter can name as
|
|
79
|
+
* well, and every message on the connection can name another owner.
|
|
70
80
|
*
|
|
71
81
|
* Owners specify which relays to connect to via `OwnerTransport`. In
|
|
72
82
|
* WebSocket-based implementations, this check occurs before accepting the
|
|
@@ -186,6 +196,9 @@ export const createRelaySqliteStorage =
|
|
|
186
196
|
return deps.timingSafeEqual(rows[0].writeKey, writeKey);
|
|
187
197
|
}
|
|
188
198
|
|
|
199
|
+
// When SQLite fails this insert, for example on a full disk, the throw
|
|
200
|
+
// reaches the protocol, which logs it and answers with WriteError,
|
|
201
|
+
// because a boolean has no room for the error.
|
|
189
202
|
deps.sqlite.exec(sql`
|
|
190
203
|
insert into evolu_writeKey (ownerId, writeKey)
|
|
191
204
|
values (${ownerId}, ${writeKey});
|
|
@@ -194,15 +207,6 @@ export const createRelaySqliteStorage =
|
|
|
194
207
|
return true;
|
|
195
208
|
},
|
|
196
209
|
|
|
197
|
-
setWriteKey: (ownerId, writeKey) => {
|
|
198
|
-
deps.sqlite.exec(sql`
|
|
199
|
-
insert into evolu_writeKey (ownerId, writeKey)
|
|
200
|
-
values (${ownerId}, ${writeKey})
|
|
201
|
-
on conflict (ownerId) do update
|
|
202
|
-
set writeKey = excluded.writeKey;
|
|
203
|
-
`);
|
|
204
|
-
},
|
|
205
|
-
|
|
206
210
|
writeMessages: (ownerIdBytes, messages) => async (run) => {
|
|
207
211
|
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
208
212
|
const uniqueMessagesWithTimestampBytes = dedupeArray(
|
|
@@ -215,32 +219,42 @@ export const createRelaySqliteStorage =
|
|
|
215
219
|
|
|
216
220
|
return run(
|
|
217
221
|
mutexByOwnerId.withLock(ownerId, async () => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
// SQLite can fail these reads, for example on a corrupt page, and
|
|
223
|
+
// a throw would panic the relay's shared Run.
|
|
224
|
+
const readResult = trySync(() => {
|
|
225
|
+
const existingTimestampsResult =
|
|
226
|
+
sqliteStorageBase.getExistingTimestamps(
|
|
227
|
+
ownerIdBytes,
|
|
228
|
+
mapArray(
|
|
229
|
+
uniqueMessagesWithTimestampBytes,
|
|
230
|
+
(m) => m.timestamp,
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const existingTimestampKeys = new Set(
|
|
235
|
+
mapArray(existingTimestampsResult, uint8ArrayToBase64Url),
|
|
236
|
+
);
|
|
237
|
+
const newMessages = filterArray(
|
|
238
|
+
uniqueMessagesWithTimestampBytes,
|
|
239
|
+
(message) =>
|
|
240
|
+
!existingTimestampKeys.has(
|
|
241
|
+
uint8ArrayToBase64Url(message.timestamp),
|
|
242
|
+
),
|
|
222
243
|
);
|
|
223
244
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
);
|
|
227
|
-
const newMessages = filterArray(
|
|
228
|
-
uniqueMessagesWithTimestampBytes,
|
|
229
|
-
(message) =>
|
|
230
|
-
!existingTimestampKeys.has(
|
|
231
|
-
uint8ArrayToBase64Url(message.timestamp),
|
|
232
|
-
),
|
|
233
|
-
);
|
|
245
|
+
// Nothing to write
|
|
246
|
+
if (!isNonEmptyArray(newMessages)) return null;
|
|
234
247
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
248
|
+
const usage = readOwnerUsageOrDefault(deps)(
|
|
249
|
+
ownerIdBytes,
|
|
250
|
+
firstInArray(newMessages).timestamp,
|
|
251
|
+
);
|
|
239
252
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
);
|
|
253
|
+
return { newMessages, usage };
|
|
254
|
+
}, createUnknownError);
|
|
255
|
+
if (!readResult.ok) return readResult;
|
|
256
|
+
if (readResult.value === null) return ok();
|
|
257
|
+
const { newMessages, usage } = readResult.value;
|
|
244
258
|
|
|
245
259
|
const incomingBytes = newMessages.reduce(
|
|
246
260
|
(sum, m) => sum + m.change.length,
|
|
@@ -260,7 +274,7 @@ export const createRelaySqliteStorage =
|
|
|
260
274
|
? await quotaResult
|
|
261
275
|
: quotaResult;
|
|
262
276
|
if (!isWithinQuota) {
|
|
263
|
-
return err<
|
|
277
|
+
return err<StorageWriteMessagesError>({
|
|
264
278
|
type: "StorageQuotaError",
|
|
265
279
|
ownerId,
|
|
266
280
|
});
|
|
@@ -268,39 +282,42 @@ export const createRelaySqliteStorage =
|
|
|
268
282
|
|
|
269
283
|
let { firstTimestamp, lastTimestamp } = usage;
|
|
270
284
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
285
|
+
// SQLite can fail the write, for example on a full disk. The
|
|
286
|
+
// transaction has rolled back, and a throw would panic the relay's
|
|
287
|
+
// shared Run.
|
|
288
|
+
return trySync(() => {
|
|
289
|
+
deps.sqlite.transaction(() => {
|
|
290
|
+
for (const { timestamp, change } of newMessages) {
|
|
291
|
+
let strategy;
|
|
292
|
+
[strategy, firstTimestamp, lastTimestamp] =
|
|
293
|
+
getTimestampInsertStrategy(
|
|
294
|
+
timestamp,
|
|
295
|
+
firstTimestamp,
|
|
296
|
+
lastTimestamp,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
sqliteStorageBase.insertTimestamp(
|
|
300
|
+
ownerIdBytes,
|
|
276
301
|
timestamp,
|
|
277
|
-
|
|
278
|
-
lastTimestamp,
|
|
302
|
+
strategy,
|
|
279
303
|
);
|
|
280
304
|
|
|
281
|
-
|
|
305
|
+
deps.sqlite.exec(sql`
|
|
306
|
+
insert into evolu_message
|
|
307
|
+
("ownerId", "timestamp", "change")
|
|
308
|
+
values (${ownerIdBytes}, ${timestamp}, ${change})
|
|
309
|
+
on conflict do nothing;
|
|
310
|
+
`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
updateOwnerUsage(deps)(
|
|
282
314
|
ownerIdBytes,
|
|
283
|
-
|
|
284
|
-
|
|
315
|
+
newStoredBytes,
|
|
316
|
+
firstTimestamp,
|
|
317
|
+
lastTimestamp,
|
|
285
318
|
);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
insert into evolu_message
|
|
289
|
-
("ownerId", "timestamp", "change")
|
|
290
|
-
values (${ownerIdBytes}, ${timestamp}, ${change})
|
|
291
|
-
on conflict do nothing;
|
|
292
|
-
`);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
updateOwnerUsage(deps)(
|
|
296
|
-
ownerIdBytes,
|
|
297
|
-
newStoredBytes,
|
|
298
|
-
firstTimestamp,
|
|
299
|
-
lastTimestamp,
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
return ok();
|
|
303
|
-
});
|
|
319
|
+
});
|
|
320
|
+
}, createUnknownError);
|
|
304
321
|
}),
|
|
305
322
|
);
|
|
306
323
|
},
|
|
@@ -329,13 +346,7 @@ export const createRelaySqliteStorage =
|
|
|
329
346
|
delete from evolu_message where ownerId = ${ownerId};
|
|
330
347
|
`);
|
|
331
348
|
|
|
332
|
-
deps.sqlite.exec(sql`
|
|
333
|
-
delete from evolu_usage where ownerId = ${ownerId};
|
|
334
|
-
`);
|
|
335
|
-
|
|
336
349
|
sqliteStorageBase.deleteOwner(ownerId);
|
|
337
|
-
|
|
338
|
-
return ok();
|
|
339
350
|
});
|
|
340
351
|
},
|
|
341
352
|
};
|
|
@@ -111,10 +111,10 @@
|
|
|
111
111
|
* whichever instance made it, even one disposed before the database worker
|
|
112
112
|
* answered. When a relay frame stores new messages, the tenant requests a round
|
|
113
113
|
* through each other transport claimed for the owner, so data learned from one
|
|
114
|
-
* relay reaches the others, except through a route that skipped a message
|
|
115
|
-
* described below. A closed transport reconciles
|
|
116
|
-
* replacement leader reconciles every transport again,
|
|
117
|
-
* reporting stored messages may have been lost.
|
|
114
|
+
* relay reaches the others, except through a route that skipped a message it
|
|
115
|
+
* received or could not send, as described below. A closed transport reconciles
|
|
116
|
+
* when it opens, and a replacement leader reconciles every transport again,
|
|
117
|
+
* because a response reporting stored messages may have been lost.
|
|
118
118
|
*
|
|
119
119
|
* Relays omit the sending socket when broadcasting an upload, so the uploader
|
|
120
120
|
* also delivers it as local Broadcast frames to every other tenant with
|
|
@@ -178,17 +178,17 @@
|
|
|
178
178
|
* request, a replacement leader, or storing messages from another transport,
|
|
179
179
|
* unless the route skipped a message. No failed or aborted result has arrived
|
|
180
180
|
* on the route since.
|
|
181
|
-
* - No result that skipped a received message
|
|
182
|
-
* round was last requested through it,
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* skipped a message.
|
|
181
|
+
* - No result that skipped a received message or a stored change too large to
|
|
182
|
+
* send has arrived on the route since a round was last requested through it,
|
|
183
|
+
* and since that request, no messages have been stored from another
|
|
184
|
+
* transport, directly or through a sibling copy uploaded outside this
|
|
185
|
+
* transport, and no sibling copy has failed to apply or skipped a message.
|
|
186
186
|
*
|
|
187
187
|
* A route is settled when every condition except the last holds, so a complete
|
|
188
188
|
* route is settled too. A settled route that is incomplete has ended its
|
|
189
|
-
* reconciliation, but its relay may offer a message the database skipped,
|
|
190
|
-
*
|
|
191
|
-
* {@link SettledSyncRoute}.
|
|
189
|
+
* reconciliation, but its relay may offer a message the database skipped, the
|
|
190
|
+
* database may store a change too large to send, or messages stored elsewhere
|
|
191
|
+
* may not have reached it; it is published as {@link SettledSyncRoute}.
|
|
192
192
|
*
|
|
193
193
|
* A reconciliation chain ends only with a converged result, a failure, an
|
|
194
194
|
* abort, a dropped frame, or a continuation that finds the socket closed, and
|
|
@@ -207,20 +207,34 @@
|
|
|
207
207
|
* while the database worker creates a round is logged there and fails the
|
|
208
208
|
* round's routes with `SyncFailed` without a retry. A mutation that throws is
|
|
209
209
|
* rolled back and reported as an {@link UnknownError} to the tab that made it,
|
|
210
|
-
* or to every tab when its Evolu instance was disposed first.
|
|
211
|
-
* SQLite
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
210
|
+
* or to every tab when its Evolu instance was disposed first. A received batch
|
|
211
|
+
* SQLite fails to store, for example on a full disk, is rolled back and fails
|
|
212
|
+
* its route with an `UnknownError`. Other unexpected SQLite exceptions remain
|
|
213
|
+
* unsupported and can panic the database worker. A frame the relay silently
|
|
214
|
+
* drops, such as invalid data, leaves the count above zero until the liveness
|
|
215
|
+
* rule below replaces the socket.
|
|
216
|
+
*
|
|
217
|
+
* A result that skipped a message records the error on the route as its
|
|
218
|
+
* `skippedError`, without ending its chain, so it requests no round. A message
|
|
219
|
+
* is skipped in two cases:
|
|
220
|
+
*
|
|
221
|
+
* - The relay offers a message the database could not decrypt, verify, or decode.
|
|
222
|
+
* The relay offers it again in every round through the route until the
|
|
223
|
+
* database stores a message with that timestamp.
|
|
224
|
+
* - The database stores a change it cannot send, a
|
|
225
|
+
* {@link ProtocolChangeTooLargeError}: one saved before `maxMutationSize`
|
|
226
|
+
* existed, or a crafted one received from a relay. Every route through a
|
|
227
|
+
* relay that lacks it skips it on every sync, so each such route stays
|
|
228
|
+
* settled.
|
|
229
|
+
*
|
|
230
|
+
* The messages received elsewhere that the last condition names request no
|
|
231
|
+
* round through such a route, even while a requested round checks it again,
|
|
232
|
+
* because each round would download every skipped message again, or skip again
|
|
233
|
+
* a change too large to send. The next round that an explicit request, a
|
|
234
|
+
* reopen, a replacement leader, or a failure sends through the route reconciles
|
|
235
|
+
* them. So while the database stores a change too large to send, messages from
|
|
236
|
+
* other relays reach a route only through such a round, for example after a
|
|
237
|
+
* reconnect or {@link Evolu.requestSync}.
|
|
224
238
|
*
|
|
225
239
|
* ### Liveness
|
|
226
240
|
*
|
|
@@ -353,6 +367,7 @@ import {
|
|
|
353
367
|
MessageType,
|
|
354
368
|
parseProtocolHeader,
|
|
355
369
|
type ApplyProtocolMessageAsClientResult,
|
|
370
|
+
type ProtocolChangeTooLargeError,
|
|
356
371
|
type ProtocolError,
|
|
357
372
|
type ProtocolInvalidDataError,
|
|
358
373
|
type ProtocolMessage,
|
|
@@ -659,8 +674,9 @@ export interface ReadonlySyncTenantOwner extends Typed<"Readonly"> {
|
|
|
659
674
|
|
|
660
675
|
/**
|
|
661
676
|
* One database's use of one owner through one transport: `Pending` until its
|
|
662
|
-
* reconciliation ends, then `Complete`, or `Settled` while
|
|
663
|
-
*
|
|
677
|
+
* reconciliation ends, then `Complete`, or `Settled` while it skips a change:
|
|
678
|
+
* one its relay offers that the database could not store, or one the database
|
|
679
|
+
* stores but cannot send. See Synchronization completion in this module's
|
|
664
680
|
* documentation.
|
|
665
681
|
*/
|
|
666
682
|
export type SyncRoute = PendingSyncRoute | SettledSyncRoute | CompleteSyncRoute;
|
|
@@ -680,9 +696,12 @@ export interface PendingSyncRoute extends Typed<"Pending"> {
|
|
|
680
696
|
*/
|
|
681
697
|
readonly failure: SyncRouteError | null;
|
|
682
698
|
/**
|
|
683
|
-
* The first change skipped in the latest reply that skipped one, or null.
|
|
684
|
-
* relay offers
|
|
685
|
-
* stores a change with that timestamp.
|
|
699
|
+
* The first change skipped in the latest reply that skipped one, or null. A
|
|
700
|
+
* change the relay offers is offered again in every round through the route
|
|
701
|
+
* until this database stores a change with that timestamp. A
|
|
702
|
+
* {@link ProtocolChangeTooLargeError} is a change this database stores but
|
|
703
|
+
* cannot send, which every route through a relay that lacks it skips on every
|
|
704
|
+
* sync.
|
|
686
705
|
*/
|
|
687
706
|
readonly skippedError: SyncRouteError | null;
|
|
688
707
|
/** When the route last became complete, or null. */
|
|
@@ -697,10 +716,15 @@ export interface PendingSyncRoute extends Typed<"Pending"> {
|
|
|
697
716
|
}
|
|
698
717
|
|
|
699
718
|
/**
|
|
700
|
-
* A route whose reconciliation ended
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
* {@link
|
|
719
|
+
* A route whose reconciliation ended with a skipped change, so it is
|
|
720
|
+
* incomplete: its relay offers a change the database could not store, or the
|
|
721
|
+
* database stores a change it cannot send, a
|
|
722
|
+
* {@link ProtocolChangeTooLargeError}. Changes stored from other relays request
|
|
723
|
+
* no round through it; the next round requested through it, such as by
|
|
724
|
+
* {@link Evolu.requestSync} or a reopen, checks it again. Every route through a
|
|
725
|
+
* relay that lacks a change too large to send skips it on every sync, so those
|
|
726
|
+
* routes stay settled, and messages from other relays reach them only through
|
|
727
|
+
* such rounds.
|
|
704
728
|
*/
|
|
705
729
|
export interface SettledSyncRoute extends Typed<"Settled"> {
|
|
706
730
|
readonly transportId: SyncTransportId;
|
|
@@ -734,11 +758,13 @@ export interface CompleteSyncRoute extends Typed<"Complete"> {
|
|
|
734
758
|
*
|
|
735
759
|
* It is the error itself, so it carries its details, such as the expected and
|
|
736
760
|
* actual timestamps of a {@link ProtocolTimestampMismatchError}. A skipped
|
|
737
|
-
* message adds {@link DecryptWithXChaCha20Poly1305Error}
|
|
738
|
-
* {@link
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
761
|
+
* message adds {@link DecryptWithXChaCha20Poly1305Error} and
|
|
762
|
+
* {@link ProtocolChangeTooLargeError}. A {@link ProtocolInvalidDataError} leaves
|
|
763
|
+
* out its data, which can be a whole frame. An {@link UnknownError} means SQLite
|
|
764
|
+
* failed to store received messages, for example on a full disk. `WriteFailed`
|
|
765
|
+
* means a `writeMessages` call that threw, logged by the protocol, and
|
|
766
|
+
* `SyncFailed` means a logged failure while creating a round or reconciling
|
|
767
|
+
* ranges.
|
|
742
768
|
*/
|
|
743
769
|
export type SyncRouteError = (
|
|
744
770
|
| Exclude<ProtocolError, ProtocolInvalidDataError>
|
|
@@ -749,6 +775,7 @@ export type SyncRouteError = (
|
|
|
749
775
|
| (Omit<DecryptWithXChaCha20Poly1305Error, "error"> & {
|
|
750
776
|
readonly error: UnknownError;
|
|
751
777
|
})
|
|
778
|
+
| ProtocolChangeTooLargeError
|
|
752
779
|
| Typed<"WriteFailed">
|
|
753
780
|
| Typed<"SyncFailed">
|
|
754
781
|
) & { readonly at: Millis };
|
|
@@ -779,10 +806,10 @@ export interface RelaySyncState {
|
|
|
779
806
|
* - `Synced`: a relay is up to date, and none syncs.
|
|
780
807
|
* - `Offline`: every relay is disconnected. Evolu keeps reconnecting, up to 30
|
|
781
808
|
* seconds apart, so `Offline` can briefly outlast the outage.
|
|
782
|
-
* - `Error`: a relay failed
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
* that change.
|
|
809
|
+
* - `Error`: a relay failed, a relay offers a change this database skipped, or
|
|
810
|
+
* this database stores a change too large to send. `error` is the newest
|
|
811
|
+
* failure, or without one, the newest skipped change: a failure stops syncing
|
|
812
|
+
* through its relay, while a skipped change leaves out only that change.
|
|
786
813
|
*
|
|
787
814
|
* Evolu stores changes in the local database before they sync, so sync needs no
|
|
788
815
|
* UI while it works: show nothing for `NoRelays`, `Syncing`, and `Synced`. An
|
|
@@ -1365,13 +1392,15 @@ export type DbWorkerQueuedResponse =
|
|
|
1365
1392
|
readonly ownerId: OwnerId;
|
|
1366
1393
|
readonly didWriteMessages: boolean;
|
|
1367
1394
|
/**
|
|
1368
|
-
* The first error of a
|
|
1369
|
-
* storing the rest, or
|
|
1395
|
+
* The first error of a message the DbWorker skipped, or null: a
|
|
1396
|
+
* received one it did not store while storing the rest, or a stored
|
|
1397
|
+
* change too large to send. It does not end the round.
|
|
1370
1398
|
*/
|
|
1371
1399
|
readonly skippedError:
|
|
1372
1400
|
| DecryptWithXChaCha20Poly1305Error
|
|
1373
1401
|
| ProtocolInvalidDataError
|
|
1374
1402
|
| ProtocolTimestampMismatchError
|
|
1403
|
+
| ProtocolChangeTooLargeError
|
|
1375
1404
|
| null;
|
|
1376
1405
|
readonly result: Result<
|
|
1377
1406
|
ApplyProtocolMessageAsClientResult,
|
|
@@ -1529,8 +1558,10 @@ interface PendingRoute extends Typed<"Pending"> {
|
|
|
1529
1558
|
}
|
|
1530
1559
|
|
|
1531
1560
|
/**
|
|
1532
|
-
* A message a route skipped
|
|
1533
|
-
*
|
|
1561
|
+
* A message a route skipped: one its relay offers that the database could not
|
|
1562
|
+
* store, which the relay offers again in every round, or one the database
|
|
1563
|
+
* stores but cannot send, which every round skips again. So messages received
|
|
1564
|
+
* elsewhere request no round through the route.
|
|
1534
1565
|
*/
|
|
1535
1566
|
interface RouteSkip {
|
|
1536
1567
|
readonly error: SyncRouteError;
|
|
@@ -1543,8 +1574,9 @@ interface RouteSkip {
|
|
|
1543
1574
|
}
|
|
1544
1575
|
|
|
1545
1576
|
/**
|
|
1546
|
-
* A route whose reconciliation has ended, but
|
|
1547
|
-
* message
|
|
1577
|
+
* A route whose reconciliation has ended, but which is incomplete: its relay
|
|
1578
|
+
* may offer a skipped message, the database may store a change too large to
|
|
1579
|
+
* send, or the relay may lack messages stored elsewhere.
|
|
1548
1580
|
*/
|
|
1549
1581
|
interface SettledRoute extends Typed<"Settled"> {
|
|
1550
1582
|
readonly skippedError: SyncRouteError;
|
|
@@ -1584,6 +1616,7 @@ const errorToSyncRouteError = (
|
|
|
1584
1616
|
| ProtocolError
|
|
1585
1617
|
| StorageWriteMessagesError
|
|
1586
1618
|
| DecryptWithXChaCha20Poly1305Error
|
|
1619
|
+
| ProtocolChangeTooLargeError
|
|
1587
1620
|
| Typed<"WriteFailed">
|
|
1588
1621
|
| Typed<"SyncFailed">,
|
|
1589
1622
|
at: Millis,
|
|
@@ -1773,8 +1806,9 @@ export const initSharedWorker =
|
|
|
1773
1806
|
|
|
1774
1807
|
// Held while this worker runs. Its DbWorkers stop once they can take it,
|
|
1775
1808
|
// because a Dispose posted right before this worker closes can be lost, as
|
|
1776
|
-
// in Firefox. Taken
|
|
1777
|
-
//
|
|
1809
|
+
// in Firefox (https://bugzilla.mozilla.org/show_bug.cgi?id=2077609). Taken
|
|
1810
|
+
// before the build lock, so nothing delays the end of starting once that
|
|
1811
|
+
// lock is held.
|
|
1778
1812
|
disposer.use(await run.ok(acquireLeaderLock(workerId)));
|
|
1779
1813
|
|
|
1780
1814
|
// Released after every tenant is disposed and has told its DbWorker to
|
|
@@ -2780,7 +2814,8 @@ const createEvoluTenant =
|
|
|
2780
2814
|
!hasQueuedWrite;
|
|
2781
2815
|
// Settling drops the failure, so the next one requests a round
|
|
2782
2816
|
// again. A route with a skip it is not rechecking settles without
|
|
2783
|
-
// completing, because its relay may offer the skipped message
|
|
2817
|
+
// completing, because its relay may offer the skipped message, the
|
|
2818
|
+
// database may store a change too large to send, or the relay may
|
|
2784
2819
|
// lack messages stored elsewhere.
|
|
2785
2820
|
const pending = routeToPending(route.progress);
|
|
2786
2821
|
route.progress =
|
|
@@ -2887,11 +2922,12 @@ const createEvoluTenant =
|
|
|
2887
2922
|
if (!isAborted) route.lastReceivedAt = now;
|
|
2888
2923
|
// A skipped message is recorded as the route's skip, not a
|
|
2889
2924
|
// failure, and does not end the round, whose response below is
|
|
2890
|
-
// still sent, so it requests no round. The relay offers
|
|
2891
|
-
//
|
|
2892
|
-
//
|
|
2893
|
-
//
|
|
2894
|
-
//
|
|
2925
|
+
// still sent, so it requests no round. The relay offers a message
|
|
2926
|
+
// the database could not store again in every later round, and
|
|
2927
|
+
// every round skips a stored change too large to send again, so
|
|
2928
|
+
// the route stays incomplete until a round requested through it
|
|
2929
|
+
// settles without skipping a message and without messages stored
|
|
2930
|
+
// elsewhere since that request.
|
|
2895
2931
|
if (skippedError !== null)
|
|
2896
2932
|
route.progress = {
|
|
2897
2933
|
...routeToPending(route.progress),
|
|
@@ -2918,12 +2954,14 @@ const createEvoluTenant =
|
|
|
2918
2954
|
}
|
|
2919
2955
|
} else if (failure !== null || skippedError !== null) {
|
|
2920
2956
|
// A sibling's copy comes from this worker, not from a relay, so no
|
|
2921
|
-
// route shows its failure.
|
|
2922
|
-
//
|
|
2923
|
-
//
|
|
2924
|
-
//
|
|
2925
|
-
//
|
|
2926
|
-
//
|
|
2957
|
+
// route shows its failure. SQLite can fail to store it, for example
|
|
2958
|
+
// on a full disk, which returns an UnknownError. It is a Broadcast,
|
|
2959
|
+
// which carries no relay error, so any other error or a skip means
|
|
2960
|
+
// a bug, such as databases holding different keys for the owner.
|
|
2961
|
+
// Each is reported as an unexpected failure. A Failed result was
|
|
2962
|
+
// logged, which reports it already. An error is the failure, which
|
|
2963
|
+
// is never an abort, and like a route, the report leaves out its
|
|
2964
|
+
// frame.
|
|
2927
2965
|
const unexpected = error !== null ? failure : skippedError;
|
|
2928
2966
|
if (unexpected !== null)
|
|
2929
2967
|
deps.postConsoleEntryOrError({
|
|
@@ -3043,9 +3081,10 @@ const createEvoluTenant =
|
|
|
3043
3081
|
* `except`, after messages from elsewhere were stored or a sibling copy was
|
|
3044
3082
|
* not fully stored. Rounds toward the same transport coalesce whatever
|
|
3045
3083
|
* their source. A route that skipped a message gets none, because its relay
|
|
3046
|
-
* would offer that message again
|
|
3047
|
-
*
|
|
3048
|
-
*
|
|
3084
|
+
* would offer that message again or the round would skip a stored change
|
|
3085
|
+
* too large to send again. A route rechecking one stops rechecking, because
|
|
3086
|
+
* its round may have read the database before this event. A later requested
|
|
3087
|
+
* round through such a route reconciles it.
|
|
3049
3088
|
*/
|
|
3050
3089
|
const requestRoundsForReceivedMessages = (
|
|
3051
3090
|
ownerId: OwnerId,
|