@evolu/common 8.15.0 → 8.16.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/Console.d.ts +24 -46
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +34 -29
- package/dist/src/Error.d.ts +10 -6
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +52 -20
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +16 -12
- package/dist/src/Task.d.ts +3 -1
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +15 -13
- package/dist/src/Worker.d.ts +3 -0
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +6 -1
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +7 -7
- package/dist/src/local-first/Evolu.d.ts +4 -0
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Schema.d.ts +3 -0
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +9 -5
- package/dist/src/local-first/Shared.d.ts +55 -39
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +264 -320
- package/package.json +1 -1
- package/src/Console.test.ts +75 -247
- package/src/Console.ts +68 -110
- package/src/Error.test.ts +85 -0
- package/src/Error.ts +48 -22
- package/src/Sqlite.ts +19 -11
- package/src/Task.test.ts +46 -0
- package/src/Task.ts +17 -13
- package/src/Worker.ts +6 -1
- package/src/local-first/Db.ts +12 -8
- package/src/local-first/Evolu.ts +4 -0
- package/src/local-first/Schema.test.ts +80 -1
- package/src/local-first/Schema.ts +15 -5
- package/src/local-first/Shared.test.ts +414 -39
- package/src/local-first/Shared.ts +386 -440
|
@@ -102,18 +102,19 @@
|
|
|
102
102
|
* another tenant's request is still a valid reconciliation step.
|
|
103
103
|
*
|
|
104
104
|
* Traffic goes only where it is needed. A continuation returns to the transport
|
|
105
|
-
* that produced the response,
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* transport claimed for
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* through
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
105
|
+
* that produced the response, a round started by a socket opening or by a
|
|
106
|
+
* transport's first claim for an owner goes through that transport, and a
|
|
107
|
+
* tenant's first writable registration of an owner starts one through each
|
|
108
|
+
* transport already claimed for it. Explicit synchronization requests and
|
|
109
|
+
* mutation uploads go to every open transport claimed for the owner. A write
|
|
110
|
+
* uploads through the database's writable registrations for its owner,
|
|
111
|
+
* whichever instance made it, even one disposed before the database worker
|
|
112
|
+
* answered. When a relay frame stores new messages, the tenant requests a round
|
|
113
|
+
* through each other transport claimed for the owner, so data learned from one
|
|
114
|
+
* relay reaches the others, except through a route that skipped a message, as
|
|
115
|
+
* described below. A closed transport reconciles when it opens, and a
|
|
116
|
+
* replacement leader reconciles every transport again, because a response
|
|
117
|
+
* reporting stored messages may have been lost.
|
|
117
118
|
*
|
|
118
119
|
* Relays omit the sending socket when broadcasting an upload, so the uploader
|
|
119
120
|
* also delivers it as local Broadcast frames to every other tenant with
|
|
@@ -128,12 +129,13 @@
|
|
|
128
129
|
* ## Sync state
|
|
129
130
|
*
|
|
130
131
|
* The shared worker publishes one plain snapshot, {@link SyncState}, of every
|
|
131
|
-
* transport it manages and every database
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
132
|
+
* transport it manages and every database it holds, with the owners its
|
|
133
|
+
* instances registered and, for each writable owner, one route per transport
|
|
134
|
+
* claimed for the owner by any database, as specified below. Each part is a
|
|
135
|
+
* union of the states the worker keeps for it: a transport's connection, a
|
|
136
|
+
* database that is active or refused startup, a writable or readonly owner, and
|
|
137
|
+
* a pending, settled, or complete route. A transport's events drive its
|
|
138
|
+
* connection, so publishing never reads a socket.
|
|
137
139
|
*
|
|
138
140
|
* Apps show users an owner's {@link OwnerSyncStatus}, which
|
|
139
141
|
* {@link syncStateToOwnerSyncStatus} derives. A view of each relay pairs each
|
|
@@ -172,10 +174,10 @@
|
|
|
172
174
|
* - The tenant has no replicated write for the owner queued, because its upload
|
|
173
175
|
* is sent only after the database worker answers it.
|
|
174
176
|
* - The tenant has sent a round through the transport since the last event that
|
|
175
|
-
* requires one:
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
177
|
+
* requires one: the route's creation, the socket opening, an explicit
|
|
178
|
+
* request, a replacement leader, or storing messages from another transport,
|
|
179
|
+
* unless the route skipped a message. No failed or aborted result has arrived
|
|
180
|
+
* on the route since.
|
|
179
181
|
* - No result that skipped a received message has arrived on the route since a
|
|
180
182
|
* round was last requested through it, and since that request, no messages
|
|
181
183
|
* have been stored from another transport, directly or through a sibling copy
|
|
@@ -197,7 +199,8 @@
|
|
|
197
199
|
* requires a round through every transport whose route has not skipped one.
|
|
198
200
|
*
|
|
199
201
|
* A failed result on a route requests one round through it. Any further failure
|
|
200
|
-
* before the route settles
|
|
202
|
+
* before the route settles requests no round of its own and waits for the next
|
|
203
|
+
* round another event requests, such as an explicit request or a reopen, so a
|
|
201
204
|
* persistent failure cannot loop; a converged reply in between does not end the
|
|
202
205
|
* wait, because it may answer another tenant's round on the shared socket. An
|
|
203
206
|
* aborted apply leaves its routes incomplete without a retry. An exception
|
|
@@ -216,9 +219,8 @@
|
|
|
216
219
|
* message with that timestamp. The messages received elsewhere that the last
|
|
217
220
|
* condition names request no round through such a route, even while a requested
|
|
218
221
|
* round checks it again, because each round would download every skipped
|
|
219
|
-
* message again. The next round that
|
|
220
|
-
*
|
|
221
|
-
* sends through the route reconciles them.
|
|
222
|
+
* message again. The next round that an explicit request, a reopen, a
|
|
223
|
+
* replacement leader, or a failure sends through the route reconciles them.
|
|
222
224
|
*
|
|
223
225
|
* ### Liveness
|
|
224
226
|
*
|
|
@@ -292,17 +294,12 @@ import {
|
|
|
292
294
|
type ClaimLease,
|
|
293
295
|
type SharedResourceByKeyWithClaims,
|
|
294
296
|
} from "../Resource.ts";
|
|
295
|
-
import { ok, type Result } from "../Result.ts";
|
|
297
|
+
import { ok, trySync, type Result } from "../Result.ts";
|
|
296
298
|
import type { NonEmptyReadonlySet } from "../Set.ts";
|
|
297
299
|
import type { SqliteSchema } from "../Sqlite.ts";
|
|
298
300
|
import { createStore, type Store } from "../Store.ts";
|
|
299
|
-
import {
|
|
300
|
-
|
|
301
|
-
createMutex,
|
|
302
|
-
unabortable,
|
|
303
|
-
type Mutex,
|
|
304
|
-
type Task,
|
|
305
|
-
} from "../Task.ts";
|
|
301
|
+
import { safelyStringifyUnknownValue } from "../String.ts";
|
|
302
|
+
import { AbortError, createMutex, unabortable, type Task } from "../Task.ts";
|
|
306
303
|
import {
|
|
307
304
|
performanceDurationBetween,
|
|
308
305
|
PositiveMillis,
|
|
@@ -331,9 +328,11 @@ import type {
|
|
|
331
328
|
WebSocketError,
|
|
332
329
|
} from "../WebSocket.ts";
|
|
333
330
|
import type {
|
|
331
|
+
BroadcastChannel,
|
|
334
332
|
SharedWorker as CommonSharedWorker,
|
|
335
333
|
CreateBroadcastChannelDep,
|
|
336
334
|
CreateMessageChannelDep,
|
|
335
|
+
CreateMessagePortDep,
|
|
337
336
|
MessagePort,
|
|
338
337
|
NativeMessagePort,
|
|
339
338
|
SharedWorkerSelf,
|
|
@@ -403,6 +402,8 @@ export type SharedWorkerInput =
|
|
|
403
402
|
readonly evoluPort: NativeMessagePort<EvoluOutput, EvoluInput>;
|
|
404
403
|
};
|
|
405
404
|
|
|
405
|
+
export type EvoluInstanceId = Id & Brand<"EvoluInstance">;
|
|
406
|
+
|
|
406
407
|
export type SharedWorkerOutput =
|
|
407
408
|
| DbWorkerInit
|
|
408
409
|
| {
|
|
@@ -447,6 +448,39 @@ export type ConsoleEntryOrError =
|
|
|
447
448
|
export const consoleEntryOrErrorBroadcastChannelName =
|
|
448
449
|
"evolu:console-entry-or-error";
|
|
449
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Posts a {@link ConsoleEntry} to tabs on a channel named
|
|
453
|
+
* {@link consoleEntryOrErrorBroadcastChannelName}.
|
|
454
|
+
*
|
|
455
|
+
* An argument that cannot be structured-cloned is replaced: an `Error` by the
|
|
456
|
+
* plain object {@link createUnknownError} makes of it, and any other value, such
|
|
457
|
+
* as an object holding a function, by a string. Logging such a value in a
|
|
458
|
+
* worker therefore does not throw.
|
|
459
|
+
*/
|
|
460
|
+
export const postConsoleEntry = (
|
|
461
|
+
channel: BroadcastChannel<ConsoleEntryOrError>,
|
|
462
|
+
entry: ConsoleEntry,
|
|
463
|
+
): void => {
|
|
464
|
+
// A native BroadcastChannel throws a DataCloneError synchronously, into
|
|
465
|
+
// whatever called the console.
|
|
466
|
+
const posted = trySync(() =>
|
|
467
|
+
channel.postMessage({ type: "ConsoleEntry", entry }),
|
|
468
|
+
);
|
|
469
|
+
if (posted.ok) return;
|
|
470
|
+
channel.postMessage({
|
|
471
|
+
type: "ConsoleEntry",
|
|
472
|
+
entry: {
|
|
473
|
+
...entry,
|
|
474
|
+
args: entry.args.map((arg) => {
|
|
475
|
+
if (trySync(() => structuredClone(arg)).ok) return arg;
|
|
476
|
+
return arg instanceof Error
|
|
477
|
+
? createUnknownError(arg).error
|
|
478
|
+
: safelyStringifyUnknownValue(arg);
|
|
479
|
+
}),
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
|
|
450
484
|
/** Identifies one running SharedWorker instance. */
|
|
451
485
|
export const SharedWorkerId = /*#__PURE__*/ id("SharedWorker");
|
|
452
486
|
export type SharedWorkerId = typeof SharedWorkerId.Output;
|
|
@@ -639,8 +673,10 @@ export type SyncRoute = PendingSyncRoute | SettledSyncRoute | CompleteSyncRoute;
|
|
|
639
673
|
export interface PendingSyncRoute extends Typed<"Pending"> {
|
|
640
674
|
readonly transportId: SyncTransportId;
|
|
641
675
|
/**
|
|
642
|
-
* The failure since the route last settled, or null. The first
|
|
643
|
-
* round; a
|
|
676
|
+
* The failure since the route last settled, or null. The first failure in a
|
|
677
|
+
* reply requests one round through the route; a later one, or a failure to
|
|
678
|
+
* create a round, waits for the next round another event requests, such as
|
|
679
|
+
* {@link Evolu.requestSync} or a reopen.
|
|
644
680
|
*/
|
|
645
681
|
readonly failure: SyncRouteError | null;
|
|
646
682
|
/**
|
|
@@ -674,8 +710,11 @@ export interface SettledSyncRoute extends Typed<"Settled"> {
|
|
|
674
710
|
readonly completeAt: Millis | null;
|
|
675
711
|
/** When this database last sent a request through the route. */
|
|
676
712
|
readonly lastSentAt: Millis;
|
|
677
|
-
/**
|
|
678
|
-
|
|
713
|
+
/**
|
|
714
|
+
* When processing a frame from the route last finished. Never null, because
|
|
715
|
+
* the reply that skipped the change set it.
|
|
716
|
+
*/
|
|
717
|
+
readonly lastReceivedAt: Millis;
|
|
679
718
|
}
|
|
680
719
|
|
|
681
720
|
/** A route on which the database is reconciled with the relay for the owner. */
|
|
@@ -697,23 +736,23 @@ export interface CompleteSyncRoute extends Typed<"Complete"> {
|
|
|
697
736
|
* actual timestamps of a {@link ProtocolTimestampMismatchError}. A skipped
|
|
698
737
|
* message adds {@link DecryptWithXChaCha20Poly1305Error}. A
|
|
699
738
|
* {@link ProtocolInvalidDataError} leaves out its data, which can be a whole
|
|
700
|
-
* frame.
|
|
701
|
-
* `WriteFailed` means a `writeMessages` call that threw, logged by the
|
|
739
|
+
* frame. `WriteFailed` means a `writeMessages` call that threw, logged by the
|
|
702
740
|
* protocol, and `SyncFailed` means a logged failure while creating a round or
|
|
703
741
|
* reconciling ranges.
|
|
704
742
|
*/
|
|
705
743
|
export type SyncRouteError = (
|
|
706
744
|
| Exclude<ProtocolError, ProtocolInvalidDataError>
|
|
707
|
-
| Omit<ProtocolInvalidDataError, "data">
|
|
745
|
+
| (Omit<ProtocolInvalidDataError, "data" | "error"> & {
|
|
746
|
+
readonly error: UnknownError;
|
|
747
|
+
})
|
|
708
748
|
| StorageWriteMessagesError
|
|
709
|
-
| DecryptWithXChaCha20Poly1305Error
|
|
749
|
+
| (Omit<DecryptWithXChaCha20Poly1305Error, "error"> & {
|
|
750
|
+
readonly error: UnknownError;
|
|
751
|
+
})
|
|
710
752
|
| Typed<"WriteFailed">
|
|
711
753
|
| Typed<"SyncFailed">
|
|
712
754
|
) & { readonly at: Millis };
|
|
713
755
|
|
|
714
|
-
/** The type of a {@link SyncRouteError}. */
|
|
715
|
-
export type SyncRouteErrorType = SyncRouteError["type"];
|
|
716
|
-
|
|
717
756
|
/**
|
|
718
757
|
* One relay of an owner in one database: a transport and the database's route
|
|
719
758
|
* through it, from {@link syncStateToRelaySyncStates}. Its status comes from
|
|
@@ -1267,13 +1306,11 @@ export type DbWorkerReadRequest =
|
|
|
1267
1306
|
export type DbWorkerOutput =
|
|
1268
1307
|
| {
|
|
1269
1308
|
readonly type: "LeaderAcquired";
|
|
1270
|
-
readonly name: Name;
|
|
1271
1309
|
readonly clock: Timestamp;
|
|
1272
1310
|
}
|
|
1273
1311
|
| {
|
|
1274
1312
|
/** Startup was refused; the worker is releasing its resources. */
|
|
1275
1313
|
readonly type: "LeaderRefused";
|
|
1276
|
-
readonly name: Name;
|
|
1277
1314
|
readonly error: UnsupportedDbVersionError;
|
|
1278
1315
|
}
|
|
1279
1316
|
| {
|
|
@@ -1372,7 +1409,7 @@ interface EvoluTenant extends AsyncDisposable {
|
|
|
1372
1409
|
onDisposed: () => void,
|
|
1373
1410
|
) => void;
|
|
1374
1411
|
|
|
1375
|
-
readonly getSyncTenant: () =>
|
|
1412
|
+
readonly getSyncTenant: () => SyncTenant;
|
|
1376
1413
|
|
|
1377
1414
|
readonly refreshSyncRoutes: () => void;
|
|
1378
1415
|
|
|
@@ -1388,34 +1425,6 @@ interface EvoluTenant extends AsyncDisposable {
|
|
|
1388
1425
|
) => void;
|
|
1389
1426
|
}
|
|
1390
1427
|
|
|
1391
|
-
/** A tenant's part of {@link SyncState}, with its transports still keyed. */
|
|
1392
|
-
type TenantSyncState = ActiveTenantSyncState | RefusedSyncTenant;
|
|
1393
|
-
|
|
1394
|
-
interface ActiveTenantSyncState extends Typed<"Active"> {
|
|
1395
|
-
readonly name: Name;
|
|
1396
|
-
readonly owners: ReadonlyArray<TenantSyncOwner>;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
type TenantSyncOwner = WritableTenantSyncOwner | ReadonlyTenantSyncOwner;
|
|
1400
|
-
|
|
1401
|
-
interface WritableTenantSyncOwner extends Typed<"Writable"> {
|
|
1402
|
-
readonly ownerId: OwnerId;
|
|
1403
|
-
readonly routes: ReadonlyArray<TenantSyncRoute>;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
interface ReadonlyTenantSyncOwner extends Typed<"Readonly"> {
|
|
1407
|
-
readonly ownerId: OwnerId;
|
|
1408
|
-
readonly transportKeys: ReadonlyArray<StructuralLookupKey>;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
/** A tenant's route, with its transport still keyed. */
|
|
1412
|
-
interface TenantSyncRoute {
|
|
1413
|
-
readonly transportKey: StructuralLookupKey;
|
|
1414
|
-
readonly progress: RouteProgress;
|
|
1415
|
-
readonly lastSentAt: Millis | null;
|
|
1416
|
-
readonly lastReceivedAt: Millis | null;
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
1428
|
/**
|
|
1420
1429
|
* How long an unanswered request keeps a socket before it is replaced.
|
|
1421
1430
|
*
|
|
@@ -1538,7 +1547,7 @@ interface RouteSkip {
|
|
|
1538
1547
|
* message or lack messages stored elsewhere, so it is incomplete.
|
|
1539
1548
|
*/
|
|
1540
1549
|
interface SettledRoute extends Typed<"Settled"> {
|
|
1541
|
-
readonly
|
|
1550
|
+
readonly skippedError: SyncRouteError;
|
|
1542
1551
|
readonly completeAt: Millis | null;
|
|
1543
1552
|
}
|
|
1544
1553
|
|
|
@@ -1551,19 +1560,15 @@ const routeToPending = (progress: RouteProgress): PendingRoute => {
|
|
|
1551
1560
|
case "Pending":
|
|
1552
1561
|
return progress;
|
|
1553
1562
|
case "Settled":
|
|
1554
|
-
return {
|
|
1555
|
-
type: "Pending",
|
|
1556
|
-
roundRequired: false,
|
|
1557
|
-
failure: null,
|
|
1558
|
-
skip: { error: progress.error, isRechecking: false },
|
|
1559
|
-
completeAt: progress.completeAt,
|
|
1560
|
-
};
|
|
1561
1563
|
case "Complete":
|
|
1562
1564
|
return {
|
|
1563
1565
|
type: "Pending",
|
|
1564
1566
|
roundRequired: false,
|
|
1565
1567
|
failure: null,
|
|
1566
|
-
skip:
|
|
1568
|
+
skip:
|
|
1569
|
+
progress.type === "Settled"
|
|
1570
|
+
? { error: progress.skippedError, isRechecking: false }
|
|
1571
|
+
: null,
|
|
1567
1572
|
completeAt: progress.completeAt,
|
|
1568
1573
|
};
|
|
1569
1574
|
}
|
|
@@ -1572,7 +1577,7 @@ const routeToPending = (progress: RouteProgress): PendingRoute => {
|
|
|
1572
1577
|
/**
|
|
1573
1578
|
* Converts an error to a {@link SyncRouteError}: a caught value becomes an
|
|
1574
1579
|
* {@link UnknownError}, and a {@link ProtocolInvalidDataError} leaves out its
|
|
1575
|
-
* data.
|
|
1580
|
+
* data, which can be a whole frame.
|
|
1576
1581
|
*/
|
|
1577
1582
|
const errorToSyncRouteError = (
|
|
1578
1583
|
error:
|
|
@@ -1583,8 +1588,6 @@ const errorToSyncRouteError = (
|
|
|
1583
1588
|
| Typed<"SyncFailed">,
|
|
1584
1589
|
at: Millis,
|
|
1585
1590
|
): SyncRouteError => {
|
|
1586
|
-
// A caught value inside an error becomes an UnknownError, and a
|
|
1587
|
-
// ProtocolInvalidDataError leaves out its data, which can be a whole frame.
|
|
1588
1591
|
if (error.type === "ProtocolInvalidDataError") {
|
|
1589
1592
|
const { data: _data, ...rest } = error;
|
|
1590
1593
|
return { ...rest, error: createUnknownError(rest.error), at };
|
|
@@ -1603,7 +1606,10 @@ const failRoute = (
|
|
|
1603
1606
|
failure,
|
|
1604
1607
|
});
|
|
1605
1608
|
|
|
1606
|
-
type EvoluTenantDeps =
|
|
1609
|
+
type EvoluTenantDeps = CreateMessageChannelDep &
|
|
1610
|
+
CreateMessagePortDep &
|
|
1611
|
+
GetSyncTransportIdDep &
|
|
1612
|
+
LockManagerDep &
|
|
1607
1613
|
PostConsoleEntryOrErrorDep &
|
|
1608
1614
|
PublishSyncStateDep &
|
|
1609
1615
|
RefreshAllSyncRoutesDep &
|
|
@@ -1611,6 +1617,11 @@ type EvoluTenantDeps = SharedWorkerDeps &
|
|
|
1611
1617
|
TabLeaderPortStoreDep &
|
|
1612
1618
|
TransportsDep;
|
|
1613
1619
|
|
|
1620
|
+
interface GetSyncTransportIdDep {
|
|
1621
|
+
/** Returns the ID of a claimed transport, which always exists. */
|
|
1622
|
+
readonly getSyncTransportId: (key: StructuralLookupKey) => SyncTransportId;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1614
1625
|
interface PostConsoleEntryOrErrorDep {
|
|
1615
1626
|
readonly postConsoleEntryOrError: Callback<ConsoleEntryOrError>;
|
|
1616
1627
|
}
|
|
@@ -1655,8 +1666,6 @@ interface TransportsDep {
|
|
|
1655
1666
|
>;
|
|
1656
1667
|
}
|
|
1657
1668
|
|
|
1658
|
-
export type EvoluInstanceId = Id & Brand<"EvoluInstance">;
|
|
1659
|
-
|
|
1660
1669
|
// Long enough for another build's tabs to reload and its worker to end.
|
|
1661
1670
|
const otherBuildRunningReportDelay: PositiveDuration = "3s";
|
|
1662
1671
|
|
|
@@ -1727,7 +1736,7 @@ export const initSharedWorker =
|
|
|
1727
1736
|
port.onMessage = (message) => {
|
|
1728
1737
|
switch (message.type) {
|
|
1729
1738
|
case "AnnounceTabLeader": {
|
|
1730
|
-
console.setLevel(message.consoleLevel);
|
|
1739
|
+
deps.console.setLevel(message.consoleLevel);
|
|
1731
1740
|
tabLeaderPortStore.set(port);
|
|
1732
1741
|
console.info("tabLeaderAnnounced");
|
|
1733
1742
|
break;
|
|
@@ -1782,7 +1791,7 @@ export const initSharedWorker =
|
|
|
1782
1791
|
disposer.defer(
|
|
1783
1792
|
deps.consoleStoreOutputEntry.subscribe(() => {
|
|
1784
1793
|
const entry = deps.consoleStoreOutputEntry.get();
|
|
1785
|
-
if (entry)
|
|
1794
|
+
if (entry) postConsoleEntry(consoleEntryOrErrorBroadcastChannel, entry);
|
|
1786
1795
|
}),
|
|
1787
1796
|
);
|
|
1788
1797
|
|
|
@@ -1837,85 +1846,23 @@ export const initSharedWorker =
|
|
|
1837
1846
|
connection,
|
|
1838
1847
|
}),
|
|
1839
1848
|
);
|
|
1849
|
+
const tenants = [...currentTenantsByName.values()].map((tenant) =>
|
|
1850
|
+
tenant.getSyncTenant(),
|
|
1851
|
+
);
|
|
1840
1852
|
// A grown timeout lasts while a request is outstanding on the socket
|
|
1841
1853
|
// or a database that has not refused startup has an unsettled route
|
|
1842
1854
|
// through it. Every change to either publishes, including a route that
|
|
1843
1855
|
// goes away without settling.
|
|
1844
1856
|
const unsettledTransportIds = new Set<SyncTransportId>();
|
|
1845
|
-
const
|
|
1846
|
-
(tenant
|
|
1847
|
-
|
|
1848
|
-
if (
|
|
1849
|
-
|
|
1850
|
-
type
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
? {
|
|
1855
|
-
type: "Readonly",
|
|
1856
|
-
ownerId: owner.ownerId,
|
|
1857
|
-
transportIds: owner.transportKeys.flatMap((key) => {
|
|
1858
|
-
const entry = transportsByKey.get(key);
|
|
1859
|
-
return entry ? [entry.id] : [];
|
|
1860
|
-
}),
|
|
1861
|
-
}
|
|
1862
|
-
: {
|
|
1863
|
-
type: "Writable",
|
|
1864
|
-
ownerId: owner.ownerId,
|
|
1865
|
-
routes: owner.routes.flatMap(
|
|
1866
|
-
({
|
|
1867
|
-
transportKey,
|
|
1868
|
-
progress,
|
|
1869
|
-
lastSentAt,
|
|
1870
|
-
lastReceivedAt,
|
|
1871
|
-
}): Array<SyncRoute> => {
|
|
1872
|
-
const entry = transportsByKey.get(transportKey);
|
|
1873
|
-
if (!entry) return [];
|
|
1874
|
-
const transportId = entry.id;
|
|
1875
|
-
if (progress.type !== "Pending") {
|
|
1876
|
-
// Only a sent round settles a route or completes
|
|
1877
|
-
// it.
|
|
1878
|
-
assertNonNullable(lastSentAt);
|
|
1879
|
-
return [
|
|
1880
|
-
progress.type === "Complete"
|
|
1881
|
-
? {
|
|
1882
|
-
type: "Complete",
|
|
1883
|
-
transportId,
|
|
1884
|
-
completeAt: progress.completeAt,
|
|
1885
|
-
lastSentAt,
|
|
1886
|
-
lastReceivedAt,
|
|
1887
|
-
}
|
|
1888
|
-
: {
|
|
1889
|
-
type: "Settled",
|
|
1890
|
-
transportId,
|
|
1891
|
-
skippedError: progress.error,
|
|
1892
|
-
completeAt: progress.completeAt,
|
|
1893
|
-
lastSentAt,
|
|
1894
|
-
lastReceivedAt,
|
|
1895
|
-
},
|
|
1896
|
-
];
|
|
1897
|
-
}
|
|
1898
|
-
// Only a database that has not refused startup
|
|
1899
|
-
// publishes routes.
|
|
1900
|
-
unsettledTransportIds.add(transportId);
|
|
1901
|
-
return [
|
|
1902
|
-
{
|
|
1903
|
-
type: "Pending",
|
|
1904
|
-
transportId,
|
|
1905
|
-
failure: progress.failure,
|
|
1906
|
-
skippedError: progress.skip?.error ?? null,
|
|
1907
|
-
completeAt: progress.completeAt,
|
|
1908
|
-
lastSentAt,
|
|
1909
|
-
lastReceivedAt,
|
|
1910
|
-
},
|
|
1911
|
-
];
|
|
1912
|
-
},
|
|
1913
|
-
),
|
|
1914
|
-
},
|
|
1915
|
-
),
|
|
1916
|
-
};
|
|
1917
|
-
},
|
|
1918
|
-
);
|
|
1857
|
+
for (const tenant of tenants) {
|
|
1858
|
+
if (tenant.type !== "Active") continue;
|
|
1859
|
+
for (const owner of tenant.owners) {
|
|
1860
|
+
if (owner.type !== "Writable") continue;
|
|
1861
|
+
for (const route of owner.routes)
|
|
1862
|
+
if (route.type === "Pending")
|
|
1863
|
+
unsettledTransportIds.add(route.transportId);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1919
1866
|
for (const entry of transportsByKey.values())
|
|
1920
1867
|
if (
|
|
1921
1868
|
entry.outstandingByOwnerId.size === 0 &&
|
|
@@ -1959,7 +1906,9 @@ export const initSharedWorker =
|
|
|
1959
1906
|
shortestRemaining = remaining;
|
|
1960
1907
|
}
|
|
1961
1908
|
if (!isTimedOut) {
|
|
1962
|
-
|
|
1909
|
+
// Emptying outstandingByOwnerId clears this timer, so an owner is
|
|
1910
|
+
// still outstanding.
|
|
1911
|
+
assertNonNullable(shortestRemaining);
|
|
1963
1912
|
// Timer delays use integer milliseconds; round up to wait at least
|
|
1964
1913
|
// the remaining fractional duration.
|
|
1965
1914
|
armSyncRequestTimeout(
|
|
@@ -1994,7 +1943,7 @@ export const initSharedWorker =
|
|
|
1994
1943
|
const syncRequests: SyncRequests = {
|
|
1995
1944
|
noteSent: (ownerId, key) => {
|
|
1996
1945
|
const entry = transportsByKey.get(key);
|
|
1997
|
-
|
|
1946
|
+
assertNonNullable(entry);
|
|
1998
1947
|
const outstanding = entry.outstandingByOwnerId.get(ownerId);
|
|
1999
1948
|
if (outstanding) outstanding.count++;
|
|
2000
1949
|
else
|
|
@@ -2021,6 +1970,7 @@ export const initSharedWorker =
|
|
|
2021
1970
|
>(
|
|
2022
1971
|
(transport) => async (run) => {
|
|
2023
1972
|
const key = structuralLookup(transport);
|
|
1973
|
+
const target: SyncTarget = { type: "Transport", key };
|
|
2024
1974
|
// The query carries the owner ID; WebSocket accepts URLs that
|
|
2025
1975
|
// URL() rejects, so the label is derived without parsing.
|
|
2026
1976
|
const queryIndex = transport.url.indexOf("?");
|
|
@@ -2038,10 +1988,10 @@ export const initSharedWorker =
|
|
|
2038
1988
|
};
|
|
2039
1989
|
await using disposer = new AsyncDisposableStack();
|
|
2040
1990
|
// Registered before the socket, so an aborted creation also
|
|
2041
|
-
// forgets the transport.
|
|
1991
|
+
// forgets the transport. No claim exists while a transport is
|
|
1992
|
+
// created or torn down, so neither changes a route.
|
|
2042
1993
|
disposer.defer(() => {
|
|
2043
1994
|
transportsByKey.delete(key);
|
|
2044
|
-
refreshAllSyncRoutes();
|
|
2045
1995
|
publishSyncState();
|
|
2046
1996
|
});
|
|
2047
1997
|
transportsByKey.set(key, entry);
|
|
@@ -2073,13 +2023,8 @@ export const initSharedWorker =
|
|
|
2073
2023
|
ownerIds: [...ownerIds],
|
|
2074
2024
|
});
|
|
2075
2025
|
|
|
2076
|
-
const
|
|
2077
|
-
type: "Transport",
|
|
2078
|
-
key: structuralLookup(transport),
|
|
2079
|
-
};
|
|
2080
|
-
forEachTenant((tenant) => {
|
|
2026
|
+
for (const tenant of currentTenantsByName.values())
|
|
2081
2027
|
tenant.requestCreateSyncMessages(ownerIds, target);
|
|
2082
|
-
});
|
|
2083
2028
|
refreshAllSyncRoutes();
|
|
2084
2029
|
},
|
|
2085
2030
|
|
|
@@ -2119,7 +2064,7 @@ export const initSharedWorker =
|
|
|
2119
2064
|
publishSyncState();
|
|
2120
2065
|
},
|
|
2121
2066
|
|
|
2122
|
-
onMessage(data) {
|
|
2067
|
+
onMessage: (data) => {
|
|
2123
2068
|
if (!(data instanceof ArrayBuffer)) return;
|
|
2124
2069
|
|
|
2125
2070
|
const message = new Uint8Array(data);
|
|
@@ -2159,16 +2104,12 @@ export const initSharedWorker =
|
|
|
2159
2104
|
publishSyncState();
|
|
2160
2105
|
}
|
|
2161
2106
|
|
|
2162
|
-
|
|
2107
|
+
for (const tenant of currentTenantsByName.values())
|
|
2163
2108
|
tenant.requestApplySyncMessage(
|
|
2164
2109
|
headerResult.value.ownerId,
|
|
2165
2110
|
message,
|
|
2166
|
-
|
|
2167
|
-
type: "Transport",
|
|
2168
|
-
key: structuralLookup(transport),
|
|
2169
|
-
},
|
|
2111
|
+
target,
|
|
2170
2112
|
);
|
|
2171
|
-
});
|
|
2172
2113
|
// Do not complete a sibling after decrementing the shared
|
|
2173
2114
|
// counter but before its apply has been queued.
|
|
2174
2115
|
refreshAllSyncRoutes();
|
|
@@ -2178,18 +2119,15 @@ export const initSharedWorker =
|
|
|
2178
2119
|
disposer.use(socket);
|
|
2179
2120
|
// LIFO: the transport drops its timer and its socket reference
|
|
2180
2121
|
// before the socket is disposed. `disposable` guards every method
|
|
2181
|
-
// of the socket
|
|
2182
|
-
//
|
|
2122
|
+
// of the socket lent to claims, and disposing the socket awaits its
|
|
2123
|
+
// retry, so this entry stays registered meanwhile. It keeps its
|
|
2183
2124
|
// last connection and holds no socket to reconnect.
|
|
2184
2125
|
disposer.defer(() => {
|
|
2185
2126
|
clearSyncRequestTimeout(entry);
|
|
2186
2127
|
entry.socket = null;
|
|
2187
|
-
refreshAllSyncRoutes();
|
|
2188
2128
|
});
|
|
2189
2129
|
const disposables = disposer.move();
|
|
2190
2130
|
entry.socket = socket;
|
|
2191
|
-
refreshAllSyncRoutes();
|
|
2192
|
-
publishSyncState();
|
|
2193
2131
|
// `disposable` replaces the socket's disposal method in place, but
|
|
2194
2132
|
// `disposer.use` above already captured the original, so disposing
|
|
2195
2133
|
// `disposables` disposes the socket rather than recursing.
|
|
@@ -2202,9 +2140,8 @@ export const initSharedWorker =
|
|
|
2202
2140
|
type: "Transport",
|
|
2203
2141
|
key: structuralLookup(transport),
|
|
2204
2142
|
};
|
|
2205
|
-
|
|
2143
|
+
for (const tenant of currentTenantsByName.values())
|
|
2206
2144
|
tenant.requestCreateSyncMessages(new Set([ownerId]), target);
|
|
2207
|
-
});
|
|
2208
2145
|
}
|
|
2209
2146
|
refreshAllSyncRoutes();
|
|
2210
2147
|
},
|
|
@@ -2227,6 +2164,11 @@ export const initSharedWorker =
|
|
|
2227
2164
|
const sharedWorkerRun = disposer.use(
|
|
2228
2165
|
run.create({
|
|
2229
2166
|
...deps,
|
|
2167
|
+
getSyncTransportId: (key: StructuralLookupKey) => {
|
|
2168
|
+
const entry = transportsByKey.get(key);
|
|
2169
|
+
assertNonNullable(entry);
|
|
2170
|
+
return entry.id;
|
|
2171
|
+
},
|
|
2230
2172
|
postConsoleEntryOrError,
|
|
2231
2173
|
publishSyncState,
|
|
2232
2174
|
refreshAllSyncRoutes,
|
|
@@ -2239,17 +2181,13 @@ export const initSharedWorker =
|
|
|
2239
2181
|
const tenantsByName = disposer.use(
|
|
2240
2182
|
await sharedWorkerRun.ok(
|
|
2241
2183
|
createSharedResourceByKey(
|
|
2242
|
-
(message: ExtractTyped<SharedWorkerInput, "CreateEvolu">) =>
|
|
2243
|
-
|
|
2244
|
-
message
|
|
2245
|
-
|
|
2246
|
-
return createEvoluTenant(
|
|
2247
|
-
{ ...message, memoryOnly },
|
|
2248
|
-
memoryOnly ? "NotPersisted" : platformDevicePersistence,
|
|
2184
|
+
(message: ExtractTyped<SharedWorkerInput, "CreateEvolu">) =>
|
|
2185
|
+
createEvoluTenant(
|
|
2186
|
+
message,
|
|
2187
|
+
message.memoryOnly ? "NotPersisted" : platformDevicePersistence,
|
|
2249
2188
|
currentTenantsByName,
|
|
2250
2189
|
workerId,
|
|
2251
|
-
)
|
|
2252
|
-
},
|
|
2190
|
+
),
|
|
2253
2191
|
{
|
|
2254
2192
|
idleDisposeAfter: "3s",
|
|
2255
2193
|
lookup: (message) => message.name,
|
|
@@ -2260,12 +2198,6 @@ export const initSharedWorker =
|
|
|
2260
2198
|
|
|
2261
2199
|
sharedWorkerReady.resolve();
|
|
2262
2200
|
|
|
2263
|
-
const forEachTenant = (
|
|
2264
|
-
callback: Callback<BorrowedResource<EvoluTenant>>,
|
|
2265
|
-
): void => {
|
|
2266
|
-
for (const tenant of currentTenantsByName.values()) callback(tenant);
|
|
2267
|
-
};
|
|
2268
|
-
|
|
2269
2201
|
return ok(disposer.move());
|
|
2270
2202
|
};
|
|
2271
2203
|
|
|
@@ -2276,7 +2208,6 @@ const createEvoluTenant =
|
|
|
2276
2208
|
consoleLevel,
|
|
2277
2209
|
sqliteSchema,
|
|
2278
2210
|
encryptionKey,
|
|
2279
|
-
memoryOnly,
|
|
2280
2211
|
}: ExtractTyped<SharedWorkerInput, "CreateEvolu">,
|
|
2281
2212
|
devicePersistence: DevicePersistence,
|
|
2282
2213
|
currentTenantsByName: Map<Name, BorrowedResource<EvoluTenant>>,
|
|
@@ -2288,6 +2219,7 @@ const createEvoluTenant =
|
|
|
2288
2219
|
|
|
2289
2220
|
const { deps } = run;
|
|
2290
2221
|
const console = deps.console.child(name).child("SharedWorker");
|
|
2222
|
+
console.setLevel(consoleLevel);
|
|
2291
2223
|
|
|
2292
2224
|
interface EvoluInstance extends AsyncDisposable {
|
|
2293
2225
|
readonly id: EvoluInstanceId;
|
|
@@ -2296,11 +2228,9 @@ const createEvoluTenant =
|
|
|
2296
2228
|
SyncOwner,
|
|
2297
2229
|
Array<ClaimLease | null>
|
|
2298
2230
|
>;
|
|
2299
|
-
readonly onDisposed: () => void;
|
|
2300
2231
|
readonly port: MessagePort<EvoluOutput, EvoluInput>;
|
|
2301
2232
|
readonly tabPort: TabPort;
|
|
2302
|
-
readonly
|
|
2303
|
-
rowsByQuery: Map<Query, ReadonlyArray<Row>>;
|
|
2233
|
+
readonly rowsByQuery: Map<Query, ReadonlyArray<Row>>;
|
|
2304
2234
|
}
|
|
2305
2235
|
|
|
2306
2236
|
const instancesById = disposer.adopt(
|
|
@@ -2313,17 +2243,110 @@ const createEvoluTenant =
|
|
|
2313
2243
|
},
|
|
2314
2244
|
);
|
|
2315
2245
|
|
|
2316
|
-
let dbWorkerPort = null as MessagePort<
|
|
2317
|
-
DbWorkerInput,
|
|
2318
|
-
DbWorkerOutput
|
|
2319
|
-
> | null;
|
|
2320
2246
|
const dbWorkerInited = Promise.withResolvers<void>();
|
|
2321
2247
|
|
|
2248
|
+
// Sync requests keep their target or source on the entry: the DbWorker
|
|
2249
|
+
// does not need it, and a replacement leader replays the same entry.
|
|
2250
|
+
type QueueEntry =
|
|
2251
|
+
| {
|
|
2252
|
+
readonly type: "Read";
|
|
2253
|
+
readonly request: ExtractTyped<DbWorkerReadRequest, "ForEvolu">;
|
|
2254
|
+
}
|
|
2255
|
+
| {
|
|
2256
|
+
readonly type: "Write";
|
|
2257
|
+
readonly request: ExtractTyped<DbWorkerWriteRequest, "ForEvolu">;
|
|
2258
|
+
/** Owners of its changes to replicated tables. */
|
|
2259
|
+
readonly replicatedOwnerIds: ReadonlySet<OwnerId>;
|
|
2260
|
+
capturedInputs?: CapturedWriteInputs;
|
|
2261
|
+
}
|
|
2262
|
+
| {
|
|
2263
|
+
readonly type: "CreateSyncMessages";
|
|
2264
|
+
readonly request: ExtractTyped<
|
|
2265
|
+
DbWorkerReadRequest,
|
|
2266
|
+
"ForSharedWorker"
|
|
2267
|
+
>;
|
|
2268
|
+
readonly target: SyncTarget;
|
|
2269
|
+
}
|
|
2270
|
+
| {
|
|
2271
|
+
readonly type: "ApplySyncMessage";
|
|
2272
|
+
readonly request: ExtractTyped<
|
|
2273
|
+
DbWorkerWriteRequest,
|
|
2274
|
+
"ForSharedWorker"
|
|
2275
|
+
>;
|
|
2276
|
+
readonly source: ApplySyncMessageSource;
|
|
2277
|
+
capturedInputs?: CapturedWriteInputs;
|
|
2278
|
+
};
|
|
2279
|
+
interface CapturedWriteInputs {
|
|
2280
|
+
readonly clock: Timestamp;
|
|
2281
|
+
readonly now: Millis;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/**
|
|
2285
|
+
* The tenant's database worker: `Starting` until one leads, then `Leading`,
|
|
2286
|
+
* which a replacement leader replaces. `Refused` is final, and `Disposed`
|
|
2287
|
+
* changes only to `Refused`, when a worker requested earlier refuses
|
|
2288
|
+
* startup.
|
|
2289
|
+
*/
|
|
2290
|
+
type DbWorkerState =
|
|
2291
|
+
Typed<"Starting"> | LeadingDbWorker | RefusedDbWorker | Typed<"Disposed">;
|
|
2292
|
+
|
|
2293
|
+
interface LeadingDbWorker extends Typed<"Leading"> {
|
|
2294
|
+
readonly port: MessagePort<DbWorkerInput, DbWorkerOutput>;
|
|
2295
|
+
/** The greatest clock the tenant's leading database workers reported. */
|
|
2296
|
+
clock: Timestamp;
|
|
2297
|
+
/** The dispatched queue head, which stays queued until it is answered. */
|
|
2298
|
+
activeDispatch: {
|
|
2299
|
+
readonly entry: QueueEntry;
|
|
2300
|
+
readonly attemptId: Id;
|
|
2301
|
+
} | null;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
interface RefusedDbWorker extends Typed<"Refused"> {
|
|
2305
|
+
readonly error: UnsupportedDbVersionError;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
let dbWorker: DbWorkerState = { type: "Starting" };
|
|
2309
|
+
const queue: Array<QueueEntry> = [];
|
|
2310
|
+
const pendingWriteCountByOwnerId = new Map<OwnerId, number>();
|
|
2311
|
+
const pendingApplyCountForAllRoutesByOwnerId = new Map<OwnerId, number>();
|
|
2312
|
+
// Queued applies of relay frames, counted per owner and source transport.
|
|
2313
|
+
const ownerTransportApplyRelation = createRefCountedRelation<
|
|
2314
|
+
OwnerId,
|
|
2315
|
+
StructuralLookupKey
|
|
2316
|
+
>();
|
|
2317
|
+
|
|
2318
|
+
// Each tab is told once during this tenant's lifetime, through its own
|
|
2319
|
+
// connection. Recreating the tenant after idle disposal retries startup
|
|
2320
|
+
// and may report the refusal again.
|
|
2321
|
+
const refusedTabPorts = new WeakSet<TabPort>();
|
|
2322
|
+
|
|
2323
|
+
interface RouteState {
|
|
2324
|
+
progress: RouteProgress;
|
|
2325
|
+
lastSentAt: Millis | null;
|
|
2326
|
+
lastReceivedAt: Millis | null;
|
|
2327
|
+
}
|
|
2328
|
+
const routesByOwnerIdByKey = new Map<
|
|
2329
|
+
StructuralLookupKey,
|
|
2330
|
+
Map<OwnerId, RouteState>
|
|
2331
|
+
>();
|
|
2332
|
+
|
|
2333
|
+
// Disposal does not wait for the DbWorker. It holds the database lock
|
|
2334
|
+
// until Dispose arrives, its tab closes, or this worker ends, and the next
|
|
2335
|
+
// DbWorker for this database, of this worker or another build, waits for
|
|
2336
|
+
// that lock before it reads the clock. A requested DbWorker that reports
|
|
2337
|
+
// in later gets Dispose, and a refused tenant stays refused, so it keeps
|
|
2338
|
+
// dropping requests.
|
|
2339
|
+
disposer.defer(() => {
|
|
2340
|
+
if (dbWorker.type === "Leading")
|
|
2341
|
+
dbWorker.port.postMessage({ type: "Dispose" });
|
|
2342
|
+
if (dbWorker.type !== "Refused") dbWorker = { type: "Disposed" };
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2322
2345
|
const initDbWorker = (): void => {
|
|
2323
2346
|
// Without a tab leader yet, the store subscription starts the DbWorker
|
|
2324
2347
|
// once a tab announces itself.
|
|
2325
2348
|
const tabLeaderPort = deps.tabLeaderPortStore.get();
|
|
2326
|
-
if (
|
|
2349
|
+
if (dbWorker.type === "Refused" || !tabLeaderPort) return;
|
|
2327
2350
|
|
|
2328
2351
|
const dbWorkerChannel = deps.createMessageChannel<
|
|
2329
2352
|
DbWorkerOutput,
|
|
@@ -2334,8 +2357,10 @@ const createEvoluTenant =
|
|
|
2334
2357
|
currentDbWorkerPort.onMessage = (message) => {
|
|
2335
2358
|
switch (message.type) {
|
|
2336
2359
|
case "LeaderAcquired": {
|
|
2337
|
-
|
|
2338
|
-
if (
|
|
2360
|
+
const previous = dbWorker;
|
|
2361
|
+
if (previous.type === "Leading")
|
|
2362
|
+
assertNotSame(previous.port, currentDbWorkerPort);
|
|
2363
|
+
if (previous.type === "Refused" || previous.type === "Disposed") {
|
|
2339
2364
|
// This worker was requested before the refusal or before
|
|
2340
2365
|
// disposal started. The tenant will not use it, so let it
|
|
2341
2366
|
// release the database lock.
|
|
@@ -2343,32 +2368,30 @@ const createEvoluTenant =
|
|
|
2343
2368
|
currentDbWorkerPort[Symbol.dispose]();
|
|
2344
2369
|
break;
|
|
2345
2370
|
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2371
|
+
if (previous.type === "Leading") previous.port[Symbol.dispose]();
|
|
2372
|
+
dbWorker = {
|
|
2373
|
+
type: "Leading",
|
|
2374
|
+
port: currentDbWorkerPort,
|
|
2375
|
+
// A replacement may advance the clock by releasing quarantine,
|
|
2376
|
+
// or start behind it with an empty memoryOnly database. Keep the
|
|
2377
|
+
// greater clock; pending writes retain their captured inputs.
|
|
2378
|
+
clock:
|
|
2379
|
+
previous.type !== "Leading" ||
|
|
2380
|
+
orderTimestamp(previous.clock, message.clock) === -1
|
|
2381
|
+
? message.clock
|
|
2382
|
+
: previous.clock,
|
|
2383
|
+
activeDispatch: null,
|
|
2384
|
+
};
|
|
2359
2385
|
// A replacement leader may have committed writes whose responses
|
|
2360
2386
|
// were lost, and may have released quarantine at startup. A lost
|
|
2361
2387
|
// response may also have reported stored owner messages, so the
|
|
2362
2388
|
// owners reconcile through every transport again.
|
|
2363
|
-
if (
|
|
2389
|
+
if (previous.type === "Leading") {
|
|
2364
2390
|
refreshQueries();
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
requestCreateSyncMessages(usedOwnerIds, allTransports);
|
|
2391
|
+
requestCreateSyncMessages(
|
|
2392
|
+
new Set(getSyncOwners().map(({ ownerId }) => ownerId)),
|
|
2393
|
+
allTransports,
|
|
2394
|
+
);
|
|
2372
2395
|
}
|
|
2373
2396
|
console.info("leaderAcquired");
|
|
2374
2397
|
dbWorkerInited.resolve();
|
|
@@ -2376,21 +2399,20 @@ const createEvoluTenant =
|
|
|
2376
2399
|
break;
|
|
2377
2400
|
}
|
|
2378
2401
|
case "LeaderRefused": {
|
|
2379
|
-
assertNotSame(dbWorkerPort, currentDbWorkerPort);
|
|
2380
2402
|
// The worker refused startup and is releasing its resources.
|
|
2381
2403
|
// Requests stay unanswered until their instances are disposed.
|
|
2382
2404
|
// Keep the tenant unavailable, tell each connected tab once, and
|
|
2383
2405
|
// tell tabs that connect later without starting another worker.
|
|
2384
|
-
|
|
2385
|
-
|
|
2406
|
+
if (dbWorker.type === "Leading") {
|
|
2407
|
+
assertNotSame(dbWorker.port, currentDbWorkerPort);
|
|
2408
|
+
dbWorker.port[Symbol.dispose]();
|
|
2409
|
+
}
|
|
2386
2410
|
currentDbWorkerPort[Symbol.dispose]();
|
|
2387
|
-
activeDispatch = null;
|
|
2388
2411
|
queue.length = 0;
|
|
2389
2412
|
pendingWriteCountByOwnerId.clear();
|
|
2390
2413
|
pendingApplyCountForAllRoutesByOwnerId.clear();
|
|
2391
2414
|
ownerTransportApplyRelation.clear();
|
|
2392
|
-
|
|
2393
|
-
refreshSyncRoutes();
|
|
2415
|
+
dbWorker = { type: "Refused", error: message.error };
|
|
2394
2416
|
deps.publishSyncState();
|
|
2395
2417
|
console.info("leaderRefused", message.error);
|
|
2396
2418
|
for (const instance of instancesById.values()) {
|
|
@@ -2400,20 +2422,22 @@ const createEvoluTenant =
|
|
|
2400
2422
|
break;
|
|
2401
2423
|
}
|
|
2402
2424
|
case "OnQueuedResponse": {
|
|
2403
|
-
|
|
2404
|
-
|
|
2425
|
+
const leading = dbWorker;
|
|
2426
|
+
if (
|
|
2427
|
+
leading.type !== "Leading" ||
|
|
2428
|
+
leading.activeDispatch?.attemptId !== message.attemptId
|
|
2429
|
+
)
|
|
2430
|
+
return;
|
|
2431
|
+
const { entry } = leading.activeDispatch;
|
|
2405
2432
|
const { response } = message;
|
|
2433
|
+
// Replays report their computed clock, which a replacement leader
|
|
2434
|
+
// may have passed at startup. Keep the greater clock.
|
|
2406
2435
|
if (
|
|
2407
|
-
response.message.type === "Mutate" ||
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
assertNonNullable(sessionClock);
|
|
2413
|
-
if (orderTimestamp(sessionClock, response.message.clock) === -1) {
|
|
2414
|
-
sessionClock = response.message.clock;
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2436
|
+
(response.message.type === "Mutate" ||
|
|
2437
|
+
response.message.type === "ApplySyncMessage") &&
|
|
2438
|
+
orderTimestamp(leading.clock, response.message.clock) === -1
|
|
2439
|
+
)
|
|
2440
|
+
leading.clock = response.message.clock;
|
|
2417
2441
|
switch (entry.type) {
|
|
2418
2442
|
case "Read":
|
|
2419
2443
|
case "Write":
|
|
@@ -2431,7 +2455,7 @@ const createEvoluTenant =
|
|
|
2431
2455
|
const head = queue.shift();
|
|
2432
2456
|
assertNonNullable(head);
|
|
2433
2457
|
updatePendingWork(head, -1);
|
|
2434
|
-
activeDispatch = null;
|
|
2458
|
+
leading.activeDispatch = null;
|
|
2435
2459
|
// Follow-up uploads and retries are already queued or sent. Only
|
|
2436
2460
|
// now can removing the completed head establish convergence.
|
|
2437
2461
|
refreshSyncRoutes();
|
|
@@ -2440,6 +2464,8 @@ const createEvoluTenant =
|
|
|
2440
2464
|
runQueue();
|
|
2441
2465
|
break;
|
|
2442
2466
|
}
|
|
2467
|
+
default:
|
|
2468
|
+
exhaustiveCheck(message);
|
|
2443
2469
|
}
|
|
2444
2470
|
};
|
|
2445
2471
|
|
|
@@ -2450,7 +2476,7 @@ const createEvoluTenant =
|
|
|
2450
2476
|
consoleLevel,
|
|
2451
2477
|
sqliteSchema,
|
|
2452
2478
|
encryptionKey,
|
|
2453
|
-
memoryOnly,
|
|
2479
|
+
memoryOnly: devicePersistence === "NotPersisted",
|
|
2454
2480
|
sharedWorkerId: workerId,
|
|
2455
2481
|
port: dbWorkerChannel.port1.native,
|
|
2456
2482
|
},
|
|
@@ -2465,48 +2491,6 @@ const createEvoluTenant =
|
|
|
2465
2491
|
}
|
|
2466
2492
|
};
|
|
2467
2493
|
|
|
2468
|
-
// Sync requests keep their target or source on the entry: the DbWorker
|
|
2469
|
-
// does not need it, and a replacement leader replays the same entry.
|
|
2470
|
-
type QueueEntry =
|
|
2471
|
-
| {
|
|
2472
|
-
readonly type: "Read";
|
|
2473
|
-
readonly request: ExtractTyped<DbWorkerReadRequest, "ForEvolu">;
|
|
2474
|
-
}
|
|
2475
|
-
| {
|
|
2476
|
-
readonly type: "Write";
|
|
2477
|
-
readonly request: ExtractTyped<DbWorkerWriteRequest, "ForEvolu">;
|
|
2478
|
-
/** Owners of its changes to replicated tables. */
|
|
2479
|
-
readonly replicatedOwnerIds: ReadonlySet<OwnerId>;
|
|
2480
|
-
now?: Millis;
|
|
2481
|
-
clock?: Timestamp;
|
|
2482
|
-
}
|
|
2483
|
-
| {
|
|
2484
|
-
readonly type: "CreateSyncMessages";
|
|
2485
|
-
readonly request: ExtractTyped<
|
|
2486
|
-
DbWorkerReadRequest,
|
|
2487
|
-
"ForSharedWorker"
|
|
2488
|
-
>;
|
|
2489
|
-
readonly target: SyncTarget;
|
|
2490
|
-
}
|
|
2491
|
-
| {
|
|
2492
|
-
readonly type: "ApplySyncMessage";
|
|
2493
|
-
readonly request: ExtractTyped<
|
|
2494
|
-
DbWorkerWriteRequest,
|
|
2495
|
-
"ForSharedWorker"
|
|
2496
|
-
>;
|
|
2497
|
-
readonly source: ApplySyncMessageSource;
|
|
2498
|
-
now?: Millis;
|
|
2499
|
-
clock?: Timestamp;
|
|
2500
|
-
};
|
|
2501
|
-
const queue: Array<QueueEntry> = [];
|
|
2502
|
-
const pendingWriteCountByOwnerId = new Map<OwnerId, number>();
|
|
2503
|
-
const pendingApplyCountForAllRoutesByOwnerId = new Map<OwnerId, number>();
|
|
2504
|
-
// Queued applies of relay frames, counted per owner and source transport.
|
|
2505
|
-
const ownerTransportApplyRelation = createRefCountedRelation<
|
|
2506
|
-
OwnerId,
|
|
2507
|
-
StructuralLookupKey
|
|
2508
|
-
>();
|
|
2509
|
-
|
|
2510
2494
|
const updatePendingCount = <K>(
|
|
2511
2495
|
counts: Map<K, number>,
|
|
2512
2496
|
key: K,
|
|
@@ -2560,13 +2544,6 @@ const createEvoluTenant =
|
|
|
2560
2544
|
queue.push(entry);
|
|
2561
2545
|
};
|
|
2562
2546
|
|
|
2563
|
-
let sessionClock: Timestamp | null = null;
|
|
2564
|
-
let startupError: UnsupportedDbVersionError | null = null;
|
|
2565
|
-
let isDisposing = false;
|
|
2566
|
-
// Each tab is told once during this tenant's lifetime, through its own
|
|
2567
|
-
// connection. Recreating the tenant after idle disposal retries startup
|
|
2568
|
-
// and may report the refusal again.
|
|
2569
|
-
const refusedTabPorts = new WeakSet<TabPort>();
|
|
2570
2547
|
const reportRefusal = (
|
|
2571
2548
|
tabPort: TabPort,
|
|
2572
2549
|
error: UnsupportedDbVersionError,
|
|
@@ -2575,31 +2552,31 @@ const createEvoluTenant =
|
|
|
2575
2552
|
refusedTabPorts.add(tabPort);
|
|
2576
2553
|
tabPort.postMessage({ type: "Error", error });
|
|
2577
2554
|
};
|
|
2578
|
-
let activeDispatch: {
|
|
2579
|
-
readonly entry: QueueEntry;
|
|
2580
|
-
readonly attemptId: Id;
|
|
2581
|
-
} | null = null;
|
|
2582
|
-
|
|
2583
2555
|
const runQueue = (): void => {
|
|
2584
|
-
if (
|
|
2585
|
-
|
|
2556
|
+
if (
|
|
2557
|
+
dbWorker.type !== "Leading" ||
|
|
2558
|
+
dbWorker.activeDispatch ||
|
|
2559
|
+
!isNonEmptyArray(queue)
|
|
2560
|
+
)
|
|
2561
|
+
return;
|
|
2586
2562
|
const entry = firstInArray(queue);
|
|
2587
2563
|
const attemptId = createId(run.deps);
|
|
2588
|
-
activeDispatch = { entry, attemptId };
|
|
2564
|
+
dbWorker.activeDispatch = { entry, attemptId };
|
|
2589
2565
|
if (entry.type === "Write" || entry.type === "ApplySyncMessage") {
|
|
2590
2566
|
// A write captures its inputs on first dispatch, so a retry after
|
|
2591
2567
|
// leader replacement reproduces the same timestamps.
|
|
2592
|
-
entry.
|
|
2593
|
-
|
|
2594
|
-
|
|
2568
|
+
entry.capturedInputs ??= {
|
|
2569
|
+
clock: dbWorker.clock,
|
|
2570
|
+
now: run.deps.time.now(),
|
|
2571
|
+
};
|
|
2572
|
+
dbWorker.port.postMessage({
|
|
2595
2573
|
type: "Request",
|
|
2596
2574
|
attemptId,
|
|
2597
2575
|
request: entry.request,
|
|
2598
|
-
|
|
2599
|
-
now: entry.now,
|
|
2576
|
+
...entry.capturedInputs,
|
|
2600
2577
|
});
|
|
2601
2578
|
} else {
|
|
2602
|
-
|
|
2579
|
+
dbWorker.port.postMessage({
|
|
2603
2580
|
type: "Request",
|
|
2604
2581
|
attemptId,
|
|
2605
2582
|
request: entry.request,
|
|
@@ -2607,18 +2584,6 @@ const createEvoluTenant =
|
|
|
2607
2584
|
}
|
|
2608
2585
|
};
|
|
2609
2586
|
|
|
2610
|
-
// Disposal does not wait for the DbWorker. It holds the database lock
|
|
2611
|
-
// until Dispose arrives, its tab closes, or this worker ends, and the next
|
|
2612
|
-
// DbWorker for this database, of this worker or another build, waits for
|
|
2613
|
-
// that lock before it reads the clock. A requested DbWorker that reports
|
|
2614
|
-
// in later gets Dispose from the isDisposing check.
|
|
2615
|
-
disposer.defer(() => {
|
|
2616
|
-
isDisposing = true;
|
|
2617
|
-
dbWorkerPort?.postMessage({ type: "Dispose" });
|
|
2618
|
-
dbWorkerPort = null;
|
|
2619
|
-
activeDispatch = null;
|
|
2620
|
-
});
|
|
2621
|
-
|
|
2622
2587
|
const handleResponseForEvolu = (
|
|
2623
2588
|
response: ExtractTyped<DbWorkerQueuedResponse, "ForEvolu">,
|
|
2624
2589
|
first: DbWorkerRequest,
|
|
@@ -2631,19 +2596,16 @@ const createEvoluTenant =
|
|
|
2631
2596
|
case "Mutate":
|
|
2632
2597
|
case "Query": {
|
|
2633
2598
|
if (instance) {
|
|
2634
|
-
const nextRowsByQuery = new Map(instance.rowsByQuery);
|
|
2635
2599
|
const patchesByQuery = new Map<Query, ReadonlyArray<Patch>>();
|
|
2636
2600
|
|
|
2637
2601
|
for (const [query, rows] of response.message.rowsByQuery) {
|
|
2638
|
-
nextRowsByQuery.set(query, rows);
|
|
2639
2602
|
patchesByQuery.set(
|
|
2640
2603
|
query,
|
|
2641
2604
|
makePatches(instance.rowsByQuery.get(query), rows),
|
|
2642
2605
|
);
|
|
2606
|
+
instance.rowsByQuery.set(query, rows);
|
|
2643
2607
|
}
|
|
2644
2608
|
|
|
2645
|
-
instance.rowsByQuery = nextRowsByQuery;
|
|
2646
|
-
|
|
2647
2609
|
instance.port.postMessage({
|
|
2648
2610
|
type: "OnPatchesByQuery",
|
|
2649
2611
|
patchesByQuery,
|
|
@@ -2740,28 +2702,10 @@ const createEvoluTenant =
|
|
|
2740
2702
|
}
|
|
2741
2703
|
};
|
|
2742
2704
|
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
}
|
|
2748
|
-
const routesByOwnerIdByKey = new Map<
|
|
2749
|
-
StructuralLookupKey,
|
|
2750
|
-
Map<OwnerId, RouteState>
|
|
2751
|
-
>();
|
|
2752
|
-
|
|
2753
|
-
const getRoute = (
|
|
2754
|
-
ownerId: OwnerId,
|
|
2755
|
-
key: StructuralLookupKey,
|
|
2756
|
-
): RouteState => {
|
|
2757
|
-
let routesByOwnerId = routesByOwnerIdByKey.get(key);
|
|
2758
|
-
if (!routesByOwnerId) {
|
|
2759
|
-
routesByOwnerId = new Map();
|
|
2760
|
-
routesByOwnerIdByKey.set(key, routesByOwnerId);
|
|
2761
|
-
}
|
|
2762
|
-
let route = routesByOwnerId.get(ownerId);
|
|
2763
|
-
if (!route) {
|
|
2764
|
-
route = {
|
|
2705
|
+
const getRoute = (ownerId: OwnerId, key: StructuralLookupKey): RouteState =>
|
|
2706
|
+
routesByOwnerIdByKey
|
|
2707
|
+
.getOrInsertComputed(key, () => new Map())
|
|
2708
|
+
.getOrInsertComputed(ownerId, () => ({
|
|
2765
2709
|
progress: {
|
|
2766
2710
|
type: "Pending",
|
|
2767
2711
|
roundRequired: true,
|
|
@@ -2771,11 +2715,7 @@ const createEvoluTenant =
|
|
|
2771
2715
|
},
|
|
2772
2716
|
lastSentAt: null,
|
|
2773
2717
|
lastReceivedAt: null,
|
|
2774
|
-
};
|
|
2775
|
-
routesByOwnerId.set(ownerId, route);
|
|
2776
|
-
}
|
|
2777
|
-
return route;
|
|
2778
|
-
};
|
|
2718
|
+
}));
|
|
2779
2719
|
|
|
2780
2720
|
const getSyncOwners = () => {
|
|
2781
2721
|
const ownersById = new Map<OwnerId, { writable: boolean }>();
|
|
@@ -2849,7 +2789,7 @@ const createEvoluTenant =
|
|
|
2849
2789
|
: pending.skip && !pending.skip.isRechecking
|
|
2850
2790
|
? {
|
|
2851
2791
|
type: "Settled",
|
|
2852
|
-
|
|
2792
|
+
skippedError: pending.skip.error,
|
|
2853
2793
|
completeAt: pending.completeAt,
|
|
2854
2794
|
}
|
|
2855
2795
|
: route.progress.type === "Complete"
|
|
@@ -2876,8 +2816,9 @@ const createEvoluTenant =
|
|
|
2876
2816
|
);
|
|
2877
2817
|
const { failedOwnerIds } = response.message;
|
|
2878
2818
|
if (failedOwnerIds.size === 0) break;
|
|
2879
|
-
// A retry would likely fail the same way, so the routes wait for
|
|
2880
|
-
// explicit request or
|
|
2819
|
+
// A retry would likely fail the same way, so the routes wait for the
|
|
2820
|
+
// next round another event requests, such as an explicit request or
|
|
2821
|
+
// a reopen.
|
|
2881
2822
|
const now = deps.time.now();
|
|
2882
2823
|
for (const ownerId of failedOwnerIds) {
|
|
2883
2824
|
deps.transports.forEachResourceForClaim(ownerId, (_, transport) => {
|
|
@@ -2961,9 +2902,9 @@ const createEvoluTenant =
|
|
|
2961
2902
|
};
|
|
2962
2903
|
if (failure !== null) {
|
|
2963
2904
|
// The first failure since the route settled requests one
|
|
2964
|
-
// round. Further failures wait for
|
|
2965
|
-
//
|
|
2966
|
-
// another request.
|
|
2905
|
+
// round. Further failures wait for the next round another event
|
|
2906
|
+
// requests, such as an explicit request or a reopen, even after
|
|
2907
|
+
// a converged reply, which may answer another request.
|
|
2967
2908
|
const requestsRetry =
|
|
2968
2909
|
route.progress.type !== "Pending" ||
|
|
2969
2910
|
route.progress.failure === null;
|
|
@@ -3170,7 +3111,7 @@ const createEvoluTenant =
|
|
|
3170
3111
|
afterQueuedWrites?: boolean;
|
|
3171
3112
|
} = {},
|
|
3172
3113
|
): void => {
|
|
3173
|
-
if (
|
|
3114
|
+
if (dbWorker.type === "Refused") return;
|
|
3174
3115
|
const usedOwnersById = getUsedOwnersById(ownerIds);
|
|
3175
3116
|
// Opening, storing messages from another transport, a failure, and an
|
|
3176
3117
|
// explicit request each require a new round before completion. The
|
|
@@ -3209,10 +3150,14 @@ const createEvoluTenant =
|
|
|
3209
3150
|
({ type }) => type === "Write" || type === "ApplySyncMessage",
|
|
3210
3151
|
)
|
|
3211
3152
|
: -1;
|
|
3153
|
+
const dispatchedEntry =
|
|
3154
|
+
dbWorker.type === "Leading"
|
|
3155
|
+
? dbWorker.activeDispatch?.entry
|
|
3156
|
+
: undefined;
|
|
3212
3157
|
const isQueued = queue.some(
|
|
3213
3158
|
(entry, index) =>
|
|
3214
3159
|
index > lastWriteIndex &&
|
|
3215
|
-
entry !==
|
|
3160
|
+
entry !== dispatchedEntry &&
|
|
3216
3161
|
entry.type === "CreateSyncMessages" &&
|
|
3217
3162
|
(entry.target.type === "AllTransports" ||
|
|
3218
3163
|
(target.type === "Transport" && entry.target.key === target.key)) &&
|
|
@@ -3255,21 +3200,12 @@ const createEvoluTenant =
|
|
|
3255
3200
|
"writeKey" in syncOwner.owner &&
|
|
3256
3201
|
!getUsedOwnersById(new Set([ownerId])).has(ownerId);
|
|
3257
3202
|
// A transport first claimed for the owner starts every tenant's
|
|
3258
|
-
// round through onFirstClaimAdded. A
|
|
3259
|
-
// its existing history through the
|
|
3260
|
-
//
|
|
3261
|
-
//
|
|
3262
|
-
|
|
3263
|
-
const
|
|
3264
|
-
for (const { ownerRegistrations } of instancesById.values()) {
|
|
3265
|
-
for (const [usedSyncOwner, leases] of ownerRegistrations) {
|
|
3266
|
-
if (usedSyncOwner.owner.id !== ownerId) continue;
|
|
3267
|
-
if (!leases.some((lease) => lease !== null)) continue;
|
|
3268
|
-
for (const transport of usedSyncOwner.transports) {
|
|
3269
|
-
usedKeys.add(structuralLookup(transport));
|
|
3270
|
-
}
|
|
3271
|
-
}
|
|
3272
|
-
}
|
|
3203
|
+
// round through onFirstClaimAdded. A tenant's first writable
|
|
3204
|
+
// registration reconciles its existing history through the
|
|
3205
|
+
// transports already claimed for the owner. Later writes upload
|
|
3206
|
+
// through every claimed transport, whichever instance made them, so
|
|
3207
|
+
// later registrations need no round.
|
|
3208
|
+
const claimedKeys = getClaimedKeys(ownerId);
|
|
3273
3209
|
const leases = instance.ownerRegistrations.getOrInsertComputed(
|
|
3274
3210
|
syncOwner,
|
|
3275
3211
|
() => [],
|
|
@@ -3289,17 +3225,12 @@ const createEvoluTenant =
|
|
|
3289
3225
|
}
|
|
3290
3226
|
deps.refreshAllSyncRoutes();
|
|
3291
3227
|
}
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
requestCreateSyncMessages(new Set([ownerId]), {
|
|
3299
|
-
type: "Transport",
|
|
3300
|
-
key,
|
|
3301
|
-
});
|
|
3302
|
-
}
|
|
3228
|
+
if (isFirstWritableUse)
|
|
3229
|
+
for (const key of claimedKeys)
|
|
3230
|
+
requestCreateSyncMessages(new Set([ownerId]), {
|
|
3231
|
+
type: "Transport",
|
|
3232
|
+
key,
|
|
3233
|
+
});
|
|
3303
3234
|
} else {
|
|
3304
3235
|
const claimLeases = instance.ownerRegistrations.get(syncOwner);
|
|
3305
3236
|
assertNotUndefined(claimLeases);
|
|
@@ -3333,18 +3264,18 @@ const createEvoluTenant =
|
|
|
3333
3264
|
const tenant = disposable<EvoluTenant>(
|
|
3334
3265
|
{
|
|
3335
3266
|
getSyncTenant: () =>
|
|
3336
|
-
|
|
3337
|
-
? { type: "Refused", name, error:
|
|
3267
|
+
dbWorker.type === "Refused"
|
|
3268
|
+
? { type: "Refused", name, error: dbWorker.error }
|
|
3338
3269
|
: {
|
|
3339
3270
|
type: "Active",
|
|
3340
3271
|
name,
|
|
3341
3272
|
owners: getSyncOwners().map(
|
|
3342
|
-
({ ownerId, writable, transportKeys }):
|
|
3273
|
+
({ ownerId, writable, transportKeys }): SyncTenantOwner =>
|
|
3343
3274
|
writable
|
|
3344
3275
|
? {
|
|
3345
3276
|
type: "Writable",
|
|
3346
3277
|
ownerId,
|
|
3347
|
-
routes: transportKeys.map((key):
|
|
3278
|
+
routes: transportKeys.map((key): SyncRoute => {
|
|
3348
3279
|
const route = routesByOwnerIdByKey
|
|
3349
3280
|
.get(key)
|
|
3350
3281
|
?.get(ownerId);
|
|
@@ -3352,10 +3283,49 @@ const createEvoluTenant =
|
|
|
3352
3283
|
// before yielding. Snapshot reads must not create
|
|
3353
3284
|
// missing routes.
|
|
3354
3285
|
assertNotUndefined(route);
|
|
3355
|
-
|
|
3286
|
+
const transportId = deps.getSyncTransportId(key);
|
|
3287
|
+
const { progress, lastSentAt, lastReceivedAt } =
|
|
3288
|
+
route;
|
|
3289
|
+
if (progress.type === "Pending")
|
|
3290
|
+
return {
|
|
3291
|
+
type: "Pending",
|
|
3292
|
+
transportId,
|
|
3293
|
+
failure: progress.failure,
|
|
3294
|
+
skippedError: progress.skip?.error ?? null,
|
|
3295
|
+
completeAt: progress.completeAt,
|
|
3296
|
+
lastSentAt,
|
|
3297
|
+
lastReceivedAt,
|
|
3298
|
+
};
|
|
3299
|
+
// Only a sent round settles a route or completes
|
|
3300
|
+
// it.
|
|
3301
|
+
assertNonNullable(lastSentAt);
|
|
3302
|
+
if (progress.type === "Complete")
|
|
3303
|
+
return {
|
|
3304
|
+
type: "Complete",
|
|
3305
|
+
transportId,
|
|
3306
|
+
completeAt: progress.completeAt,
|
|
3307
|
+
lastSentAt,
|
|
3308
|
+
lastReceivedAt,
|
|
3309
|
+
};
|
|
3310
|
+
// Only a received reply records a skip.
|
|
3311
|
+
assertNonNullable(lastReceivedAt);
|
|
3312
|
+
return {
|
|
3313
|
+
type: "Settled",
|
|
3314
|
+
transportId,
|
|
3315
|
+
skippedError: progress.skippedError,
|
|
3316
|
+
completeAt: progress.completeAt,
|
|
3317
|
+
lastSentAt,
|
|
3318
|
+
lastReceivedAt,
|
|
3319
|
+
};
|
|
3356
3320
|
}),
|
|
3357
3321
|
}
|
|
3358
|
-
: {
|
|
3322
|
+
: {
|
|
3323
|
+
type: "Readonly",
|
|
3324
|
+
ownerId,
|
|
3325
|
+
transportIds: transportKeys.map(
|
|
3326
|
+
deps.getSyncTransportId,
|
|
3327
|
+
),
|
|
3328
|
+
},
|
|
3359
3329
|
),
|
|
3360
3330
|
},
|
|
3361
3331
|
|
|
@@ -3381,15 +3351,13 @@ const createEvoluTenant =
|
|
|
3381
3351
|
message.evoluPort,
|
|
3382
3352
|
),
|
|
3383
3353
|
tabPort,
|
|
3384
|
-
onDisposed,
|
|
3385
3354
|
rowsByQuery: new Map<Query, ReadonlyArray<Row>>(),
|
|
3386
|
-
useOwnerMutex: createMutex(),
|
|
3387
3355
|
[Symbol.asyncDispose]: () => disposer.disposeAsync(),
|
|
3388
3356
|
};
|
|
3389
3357
|
|
|
3390
3358
|
instancesById.set(instance.id, instance);
|
|
3391
3359
|
|
|
3392
|
-
disposer.defer(
|
|
3360
|
+
disposer.defer(onDisposed);
|
|
3393
3361
|
|
|
3394
3362
|
disposer.defer(() => {
|
|
3395
3363
|
for (const leases of instance.ownerRegistrations.values()) {
|
|
@@ -3404,6 +3372,7 @@ const createEvoluTenant =
|
|
|
3404
3372
|
// aborts queued UseOwner batches and waits for the running one, whose
|
|
3405
3373
|
// transport claim cannot be aborted, so the release sees every lease.
|
|
3406
3374
|
const instanceRun = disposer.use(tenantRun.create());
|
|
3375
|
+
const useOwnerMutex = createMutex();
|
|
3407
3376
|
|
|
3408
3377
|
disposer.defer(() => {
|
|
3409
3378
|
instancesById.delete(instance.id);
|
|
@@ -3435,7 +3404,7 @@ const createEvoluTenant =
|
|
|
3435
3404
|
});
|
|
3436
3405
|
|
|
3437
3406
|
instance.port.onMessage = (message) => {
|
|
3438
|
-
if (
|
|
3407
|
+
if (dbWorker.type === "Refused") return;
|
|
3439
3408
|
switch (message.type) {
|
|
3440
3409
|
case "Query":
|
|
3441
3410
|
case "Export": {
|
|
@@ -3467,7 +3436,7 @@ const createEvoluTenant =
|
|
|
3467
3436
|
}
|
|
3468
3437
|
case "UseOwner": {
|
|
3469
3438
|
void instanceRun(
|
|
3470
|
-
|
|
3439
|
+
useOwnerMutex.withLock(async (run) => {
|
|
3471
3440
|
for (const action of message.actions) {
|
|
3472
3441
|
switch (action.action) {
|
|
3473
3442
|
case "sync":
|
|
@@ -3508,16 +3477,19 @@ const createEvoluTenant =
|
|
|
3508
3477
|
);
|
|
3509
3478
|
break;
|
|
3510
3479
|
}
|
|
3480
|
+
default:
|
|
3481
|
+
exhaustiveCheck(message);
|
|
3511
3482
|
}
|
|
3512
3483
|
};
|
|
3513
3484
|
|
|
3514
|
-
if (
|
|
3485
|
+
if (dbWorker.type === "Refused")
|
|
3486
|
+
reportRefusal(instance.tabPort, dbWorker.error);
|
|
3515
3487
|
},
|
|
3516
3488
|
|
|
3517
3489
|
requestCreateSyncMessages,
|
|
3518
3490
|
|
|
3519
3491
|
requestApplySyncMessage: (ownerId, inputMessage, source): void => {
|
|
3520
|
-
if (
|
|
3492
|
+
if (dbWorker.type === "Refused") return;
|
|
3521
3493
|
const owner = getUsedOwnersById(new Set([ownerId])).get(ownerId);
|
|
3522
3494
|
if (!owner) return;
|
|
3523
3495
|
|
|
@@ -3549,42 +3521,16 @@ const createEvoluTenant =
|
|
|
3549
3521
|
return ok(tenant);
|
|
3550
3522
|
};
|
|
3551
3523
|
|
|
3552
|
-
// | (Typed<"reset"> & {
|
|
3553
|
-
// readonly onCompleteId: CallbackId;
|
|
3554
|
-
// readonly reload: boolean;
|
|
3555
|
-
// readonly restore?: {
|
|
3556
|
-
// readonly sqliteSchema: SqliteSchema;
|
|
3557
|
-
// readonly mnemonic: Mnemonic;
|
|
3558
|
-
// };
|
|
3559
|
-
// })
|
|
3560
|
-
// | (Typed<"ensureSqliteSchema"> & {
|
|
3561
|
-
// readonly sqliteSchema: SqliteSchema;
|
|
3562
|
-
// })
|
|
3563
|
-
// | (Typed<"export"> & {
|
|
3564
|
-
// readonly onCompleteId: CallbackId;
|
|
3565
|
-
// })
|
|
3566
|
-
// | (Typed<"useOwner"> & {
|
|
3567
|
-
// readonly use: boolean;
|
|
3568
|
-
// readonly owner: SyncOwner;
|
|
3569
|
-
// });
|
|
3570
|
-
// | (Typed<"onReset"> & {
|
|
3571
|
-
// readonly onCompleteId: CallbackId;
|
|
3572
|
-
// readonly reload: boolean;
|
|
3573
|
-
// })
|
|
3574
|
-
|
|
3575
3524
|
// TODO: SharedWorker follow-ups.
|
|
3576
3525
|
// - Rotate the node ID when a copied database is detected; see the Duplicate
|
|
3577
3526
|
// node IDs section in the Timestamp module.
|
|
3578
3527
|
// - Detect DbWorker and port liveness so a worker-only crash resumes the queue.
|
|
3579
3528
|
// Defer panicked-worker restart until failure detection and recovery are
|
|
3580
|
-
// defined, accounting for SQLite WASM's detection limits.
|
|
3581
|
-
//
|
|
3582
|
-
//
|
|
3583
|
-
// planned to be forbidden.
|
|
3529
|
+
// defined, accounting for SQLite WASM's detection limits. Mutations, round
|
|
3530
|
+
// creation, and frame application are answered when SQLite throws; a query
|
|
3531
|
+
// or an export that throws is not, and stops the database's queue.
|
|
3584
3532
|
// - Split worker protocol types and the EvoluTenant implementation into focused
|
|
3585
3533
|
// modules.
|
|
3586
|
-
// - Remove the obsolete commented protocol block above.
|
|
3587
|
-
// - Propagate invalid protocol messages to sync state.
|
|
3588
3534
|
// - Bound sync state publishing during a bulk catch-up: every sent and applied
|
|
3589
3535
|
// frame changes a route timestamp, so each frame broadcasts a full snapshot
|
|
3590
3536
|
// to every tab. Throttling needs a wall-clock policy and a deterministic way
|