@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
|
*
|
|
@@ -314,9 +316,10 @@ import { acquireLeaderLock } from "../LockManager.js";
|
|
|
314
316
|
import { createLookupMap, structuralLookup, } from "../Lookup.js";
|
|
315
317
|
import { createRefCountedRelation } from "../Relation.js";
|
|
316
318
|
import { createSharedResourceByKey, createSharedResourceByKeyWithClaims, } from "../Resource.js";
|
|
317
|
-
import { ok } from "../Result.js";
|
|
319
|
+
import { ok, trySync } from "../Result.js";
|
|
318
320
|
import { createStore } from "../Store.js";
|
|
319
|
-
import {
|
|
321
|
+
import { safelyStringifyUnknownValue } from "../String.js";
|
|
322
|
+
import { AbortError, createMutex, unabortable } from "../Task.js";
|
|
320
323
|
import { performanceDurationBetween, PositiveMillis, } from "../Time.js";
|
|
321
324
|
import { createId, id, literal, object, } from "../Type.js";
|
|
322
325
|
import { createProtocolBroadcastMessagesFromCrdtMessages, createProtocolMessageForUnsubscribe, createProtocolMessageFromCrdtMessages, MessageType, parseProtocolHeader, } from "./Protocol.js";
|
|
@@ -324,6 +327,35 @@ import { makePatches, } from "./Query.js";
|
|
|
324
327
|
import { isLocalOnlyTable } from "./Schema.js";
|
|
325
328
|
import { orderTimestamp } from "./Timestamp.js";
|
|
326
329
|
export const consoleEntryOrErrorBroadcastChannelName = "evolu:console-entry-or-error";
|
|
330
|
+
/**
|
|
331
|
+
* Posts a {@link ConsoleEntry} to tabs on a channel named
|
|
332
|
+
* {@link consoleEntryOrErrorBroadcastChannelName}.
|
|
333
|
+
*
|
|
334
|
+
* An argument that cannot be structured-cloned is replaced: an `Error` by the
|
|
335
|
+
* plain object {@link createUnknownError} makes of it, and any other value, such
|
|
336
|
+
* as an object holding a function, by a string. Logging such a value in a
|
|
337
|
+
* worker therefore does not throw.
|
|
338
|
+
*/
|
|
339
|
+
export const postConsoleEntry = (channel, entry) => {
|
|
340
|
+
// A native BroadcastChannel throws a DataCloneError synchronously, into
|
|
341
|
+
// whatever called the console.
|
|
342
|
+
const posted = trySync(() => channel.postMessage({ type: "ConsoleEntry", entry }));
|
|
343
|
+
if (posted.ok)
|
|
344
|
+
return;
|
|
345
|
+
channel.postMessage({
|
|
346
|
+
type: "ConsoleEntry",
|
|
347
|
+
entry: {
|
|
348
|
+
...entry,
|
|
349
|
+
args: entry.args.map((arg) => {
|
|
350
|
+
if (trySync(() => structuredClone(arg)).ok)
|
|
351
|
+
return arg;
|
|
352
|
+
return arg instanceof Error
|
|
353
|
+
? createUnknownError(arg).error
|
|
354
|
+
: safelyStringifyUnknownValue(arg);
|
|
355
|
+
}),
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
};
|
|
327
359
|
/** Identifies one running SharedWorker instance. */
|
|
328
360
|
export const SharedWorkerId = /*#__PURE__*/ id("SharedWorker");
|
|
329
361
|
/**
|
|
@@ -689,19 +721,14 @@ const routeToPending = (progress) => {
|
|
|
689
721
|
case "Pending":
|
|
690
722
|
return progress;
|
|
691
723
|
case "Settled":
|
|
692
|
-
return {
|
|
693
|
-
type: "Pending",
|
|
694
|
-
roundRequired: false,
|
|
695
|
-
failure: null,
|
|
696
|
-
skip: { error: progress.error, isRechecking: false },
|
|
697
|
-
completeAt: progress.completeAt,
|
|
698
|
-
};
|
|
699
724
|
case "Complete":
|
|
700
725
|
return {
|
|
701
726
|
type: "Pending",
|
|
702
727
|
roundRequired: false,
|
|
703
728
|
failure: null,
|
|
704
|
-
skip:
|
|
729
|
+
skip: progress.type === "Settled"
|
|
730
|
+
? { error: progress.skippedError, isRechecking: false }
|
|
731
|
+
: null,
|
|
705
732
|
completeAt: progress.completeAt,
|
|
706
733
|
};
|
|
707
734
|
}
|
|
@@ -709,11 +736,9 @@ const routeToPending = (progress) => {
|
|
|
709
736
|
/**
|
|
710
737
|
* Converts an error to a {@link SyncRouteError}: a caught value becomes an
|
|
711
738
|
* {@link UnknownError}, and a {@link ProtocolInvalidDataError} leaves out its
|
|
712
|
-
* data.
|
|
739
|
+
* data, which can be a whole frame.
|
|
713
740
|
*/
|
|
714
741
|
const errorToSyncRouteError = (error, at) => {
|
|
715
|
-
// A caught value inside an error becomes an UnknownError, and a
|
|
716
|
-
// ProtocolInvalidDataError leaves out its data, which can be a whole frame.
|
|
717
742
|
if (error.type === "ProtocolInvalidDataError") {
|
|
718
743
|
const { data: _data, ...rest } = error;
|
|
719
744
|
return { ...rest, error: createUnknownError(rest.error), at };
|
|
@@ -785,7 +810,7 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
785
810
|
port.onMessage = (message) => {
|
|
786
811
|
switch (message.type) {
|
|
787
812
|
case "AnnounceTabLeader": {
|
|
788
|
-
console.setLevel(message.consoleLevel);
|
|
813
|
+
deps.console.setLevel(message.consoleLevel);
|
|
789
814
|
tabLeaderPortStore.set(port);
|
|
790
815
|
console.info("tabLeaderAnnounced");
|
|
791
816
|
break;
|
|
@@ -832,7 +857,7 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
832
857
|
disposer.defer(deps.consoleStoreOutputEntry.subscribe(() => {
|
|
833
858
|
const entry = deps.consoleStoreOutputEntry.get();
|
|
834
859
|
if (entry)
|
|
835
|
-
|
|
860
|
+
postConsoleEntry(consoleEntryOrErrorBroadcastChannel, entry);
|
|
836
861
|
}));
|
|
837
862
|
const currentTenantsByName = new Map();
|
|
838
863
|
const transportsByKey = new Map();
|
|
@@ -856,76 +881,23 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
856
881
|
label,
|
|
857
882
|
connection,
|
|
858
883
|
}));
|
|
884
|
+
const tenants = [...currentTenantsByName.values()].map((tenant) => tenant.getSyncTenant());
|
|
859
885
|
// A grown timeout lasts while a request is outstanding on the socket
|
|
860
886
|
// or a database that has not refused startup has an unsettled route
|
|
861
887
|
// through it. Every change to either publishes, including a route that
|
|
862
888
|
// goes away without settling.
|
|
863
889
|
const unsettledTransportIds = new Set();
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
transportIds: owner.transportKeys.flatMap((key) => {
|
|
876
|
-
const entry = transportsByKey.get(key);
|
|
877
|
-
return entry ? [entry.id] : [];
|
|
878
|
-
}),
|
|
879
|
-
}
|
|
880
|
-
: {
|
|
881
|
-
type: "Writable",
|
|
882
|
-
ownerId: owner.ownerId,
|
|
883
|
-
routes: owner.routes.flatMap(({ transportKey, progress, lastSentAt, lastReceivedAt, }) => {
|
|
884
|
-
const entry = transportsByKey.get(transportKey);
|
|
885
|
-
if (!entry)
|
|
886
|
-
return [];
|
|
887
|
-
const transportId = entry.id;
|
|
888
|
-
if (progress.type !== "Pending") {
|
|
889
|
-
// Only a sent round settles a route or completes
|
|
890
|
-
// it.
|
|
891
|
-
assertNonNullable(lastSentAt);
|
|
892
|
-
return [
|
|
893
|
-
progress.type === "Complete"
|
|
894
|
-
? {
|
|
895
|
-
type: "Complete",
|
|
896
|
-
transportId,
|
|
897
|
-
completeAt: progress.completeAt,
|
|
898
|
-
lastSentAt,
|
|
899
|
-
lastReceivedAt,
|
|
900
|
-
}
|
|
901
|
-
: {
|
|
902
|
-
type: "Settled",
|
|
903
|
-
transportId,
|
|
904
|
-
skippedError: progress.error,
|
|
905
|
-
completeAt: progress.completeAt,
|
|
906
|
-
lastSentAt,
|
|
907
|
-
lastReceivedAt,
|
|
908
|
-
},
|
|
909
|
-
];
|
|
910
|
-
}
|
|
911
|
-
// Only a database that has not refused startup
|
|
912
|
-
// publishes routes.
|
|
913
|
-
unsettledTransportIds.add(transportId);
|
|
914
|
-
return [
|
|
915
|
-
{
|
|
916
|
-
type: "Pending",
|
|
917
|
-
transportId,
|
|
918
|
-
failure: progress.failure,
|
|
919
|
-
skippedError: progress.skip?.error ?? null,
|
|
920
|
-
completeAt: progress.completeAt,
|
|
921
|
-
lastSentAt,
|
|
922
|
-
lastReceivedAt,
|
|
923
|
-
},
|
|
924
|
-
];
|
|
925
|
-
}),
|
|
926
|
-
}),
|
|
927
|
-
};
|
|
928
|
-
});
|
|
890
|
+
for (const tenant of tenants) {
|
|
891
|
+
if (tenant.type !== "Active")
|
|
892
|
+
continue;
|
|
893
|
+
for (const owner of tenant.owners) {
|
|
894
|
+
if (owner.type !== "Writable")
|
|
895
|
+
continue;
|
|
896
|
+
for (const route of owner.routes)
|
|
897
|
+
if (route.type === "Pending")
|
|
898
|
+
unsettledTransportIds.add(route.transportId);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
929
901
|
for (const entry of transportsByKey.values())
|
|
930
902
|
if (entry.outstandingByOwnerId.size === 0 &&
|
|
931
903
|
!unsettledTransportIds.has(entry.id))
|
|
@@ -962,8 +934,9 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
962
934
|
shortestRemaining = remaining;
|
|
963
935
|
}
|
|
964
936
|
if (!isTimedOut) {
|
|
965
|
-
|
|
966
|
-
|
|
937
|
+
// Emptying outstandingByOwnerId clears this timer, so an owner is
|
|
938
|
+
// still outstanding.
|
|
939
|
+
assertNonNullable(shortestRemaining);
|
|
967
940
|
// Timer delays use integer milliseconds; round up to wait at least
|
|
968
941
|
// the remaining fractional duration.
|
|
969
942
|
armSyncRequestTimeout(entry, PositiveMillis.orThrow(Math.ceil(shortestRemaining)));
|
|
@@ -988,8 +961,7 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
988
961
|
const syncRequests = {
|
|
989
962
|
noteSent: (ownerId, key) => {
|
|
990
963
|
const entry = transportsByKey.get(key);
|
|
991
|
-
|
|
992
|
-
return;
|
|
964
|
+
assertNonNullable(entry);
|
|
993
965
|
const outstanding = entry.outstandingByOwnerId.get(ownerId);
|
|
994
966
|
if (outstanding)
|
|
995
967
|
outstanding.count++;
|
|
@@ -1008,6 +980,7 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
1008
980
|
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
1009
981
|
try {
|
|
1010
982
|
const key = structuralLookup(transport);
|
|
983
|
+
const target = { type: "Transport", key };
|
|
1011
984
|
// The query carries the owner ID; WebSocket accepts URLs that
|
|
1012
985
|
// URL() rejects, so the label is derived without parsing.
|
|
1013
986
|
const queryIndex = transport.url.indexOf("?");
|
|
@@ -1024,10 +997,10 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
1024
997
|
};
|
|
1025
998
|
const disposer = __addDisposableResource(env_2, new AsyncDisposableStack(), true);
|
|
1026
999
|
// Registered before the socket, so an aborted creation also
|
|
1027
|
-
// forgets the transport.
|
|
1000
|
+
// forgets the transport. No claim exists while a transport is
|
|
1001
|
+
// created or torn down, so neither changes a route.
|
|
1028
1002
|
disposer.defer(() => {
|
|
1029
1003
|
transportsByKey.delete(key);
|
|
1030
|
-
refreshAllSyncRoutes();
|
|
1031
1004
|
publishSyncState();
|
|
1032
1005
|
});
|
|
1033
1006
|
transportsByKey.set(key, entry);
|
|
@@ -1054,13 +1027,8 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
1054
1027
|
url: transport.url,
|
|
1055
1028
|
ownerIds: [...ownerIds],
|
|
1056
1029
|
});
|
|
1057
|
-
const
|
|
1058
|
-
type: "Transport",
|
|
1059
|
-
key: structuralLookup(transport),
|
|
1060
|
-
};
|
|
1061
|
-
forEachTenant((tenant) => {
|
|
1030
|
+
for (const tenant of currentTenantsByName.values())
|
|
1062
1031
|
tenant.requestCreateSyncMessages(ownerIds, target);
|
|
1063
|
-
});
|
|
1064
1032
|
refreshAllSyncRoutes();
|
|
1065
1033
|
},
|
|
1066
1034
|
onClose: (event) => {
|
|
@@ -1089,7 +1057,7 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
1089
1057
|
refreshAllSyncRoutes();
|
|
1090
1058
|
publishSyncState();
|
|
1091
1059
|
},
|
|
1092
|
-
onMessage(data) {
|
|
1060
|
+
onMessage: (data) => {
|
|
1093
1061
|
if (!(data instanceof ArrayBuffer))
|
|
1094
1062
|
return;
|
|
1095
1063
|
const message = new Uint8Array(data);
|
|
@@ -1123,12 +1091,8 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
1123
1091
|
clearSyncRequestTimeout(entry);
|
|
1124
1092
|
publishSyncState();
|
|
1125
1093
|
}
|
|
1126
|
-
|
|
1127
|
-
tenant.requestApplySyncMessage(headerResult.value.ownerId, message,
|
|
1128
|
-
type: "Transport",
|
|
1129
|
-
key: structuralLookup(transport),
|
|
1130
|
-
});
|
|
1131
|
-
});
|
|
1094
|
+
for (const tenant of currentTenantsByName.values())
|
|
1095
|
+
tenant.requestApplySyncMessage(headerResult.value.ownerId, message, target);
|
|
1132
1096
|
// Do not complete a sibling after decrementing the shared
|
|
1133
1097
|
// counter but before its apply has been queued.
|
|
1134
1098
|
refreshAllSyncRoutes();
|
|
@@ -1137,18 +1101,15 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
1137
1101
|
disposer.use(socket);
|
|
1138
1102
|
// LIFO: the transport drops its timer and its socket reference
|
|
1139
1103
|
// before the socket is disposed. `disposable` guards every method
|
|
1140
|
-
// of the socket
|
|
1141
|
-
//
|
|
1104
|
+
// of the socket lent to claims, and disposing the socket awaits its
|
|
1105
|
+
// retry, so this entry stays registered meanwhile. It keeps its
|
|
1142
1106
|
// last connection and holds no socket to reconnect.
|
|
1143
1107
|
disposer.defer(() => {
|
|
1144
1108
|
clearSyncRequestTimeout(entry);
|
|
1145
1109
|
entry.socket = null;
|
|
1146
|
-
refreshAllSyncRoutes();
|
|
1147
1110
|
});
|
|
1148
1111
|
const disposables = disposer.move();
|
|
1149
1112
|
entry.socket = socket;
|
|
1150
|
-
refreshAllSyncRoutes();
|
|
1151
|
-
publishSyncState();
|
|
1152
1113
|
// `disposable` replaces the socket's disposal method in place, but
|
|
1153
1114
|
// `disposer.use` above already captured the original, so disposing
|
|
1154
1115
|
// `disposables` disposes the socket rather than recursing.
|
|
@@ -1170,9 +1131,8 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
1170
1131
|
type: "Transport",
|
|
1171
1132
|
key: structuralLookup(transport),
|
|
1172
1133
|
};
|
|
1173
|
-
|
|
1134
|
+
for (const tenant of currentTenantsByName.values())
|
|
1174
1135
|
tenant.requestCreateSyncMessages(new Set([ownerId]), target);
|
|
1175
|
-
});
|
|
1176
1136
|
}
|
|
1177
1137
|
refreshAllSyncRoutes();
|
|
1178
1138
|
},
|
|
@@ -1189,6 +1149,11 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
1189
1149
|
})));
|
|
1190
1150
|
const sharedWorkerRun = disposer.use(run.create({
|
|
1191
1151
|
...deps,
|
|
1152
|
+
getSyncTransportId: (key) => {
|
|
1153
|
+
const entry = transportsByKey.get(key);
|
|
1154
|
+
assertNonNullable(entry);
|
|
1155
|
+
return entry.id;
|
|
1156
|
+
},
|
|
1192
1157
|
postConsoleEntryOrError,
|
|
1193
1158
|
publishSyncState,
|
|
1194
1159
|
refreshAllSyncRoutes,
|
|
@@ -1196,19 +1161,11 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
1196
1161
|
tabLeaderPortStore,
|
|
1197
1162
|
transports,
|
|
1198
1163
|
}));
|
|
1199
|
-
const tenantsByName = disposer.use(await sharedWorkerRun.ok(createSharedResourceByKey((message) => {
|
|
1200
|
-
const memoryOnly = message.memoryOnly ||
|
|
1201
|
-
platformDevicePersistence === "NotPersisted";
|
|
1202
|
-
return createEvoluTenant({ ...message, memoryOnly }, memoryOnly ? "NotPersisted" : platformDevicePersistence, currentTenantsByName, workerId);
|
|
1203
|
-
}, {
|
|
1164
|
+
const tenantsByName = disposer.use(await sharedWorkerRun.ok(createSharedResourceByKey((message) => createEvoluTenant(message, message.memoryOnly ? "NotPersisted" : platformDevicePersistence, currentTenantsByName, workerId), {
|
|
1204
1165
|
idleDisposeAfter: "3s",
|
|
1205
1166
|
lookup: (message) => message.name,
|
|
1206
1167
|
})));
|
|
1207
1168
|
sharedWorkerReady.resolve();
|
|
1208
|
-
const forEachTenant = (callback) => {
|
|
1209
|
-
for (const tenant of currentTenantsByName.values())
|
|
1210
|
-
callback(tenant);
|
|
1211
|
-
};
|
|
1212
1169
|
return ok(disposer.move());
|
|
1213
1170
|
}
|
|
1214
1171
|
catch (e_1) {
|
|
@@ -1221,13 +1178,14 @@ export const initSharedWorker = (self) => async (run) => {
|
|
|
1221
1178
|
await result_1;
|
|
1222
1179
|
}
|
|
1223
1180
|
};
|
|
1224
|
-
const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey,
|
|
1181
|
+
const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, }, devicePersistence, currentTenantsByName, workerId) => async (run) => {
|
|
1225
1182
|
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1226
1183
|
try {
|
|
1227
1184
|
const disposer = __addDisposableResource(env_3, new AsyncDisposableStack(), true);
|
|
1228
1185
|
const tenantRun = disposer.use(run.create());
|
|
1229
1186
|
const { deps } = run;
|
|
1230
1187
|
const console = deps.console.child(name).child("SharedWorker");
|
|
1188
|
+
console.setLevel(consoleLevel);
|
|
1231
1189
|
const instancesById = disposer.adopt(new Map(), async (instancesById) => {
|
|
1232
1190
|
const env_4 = { stack: [], error: void 0, hasError: false };
|
|
1233
1191
|
try {
|
|
@@ -1246,21 +1204,45 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1246
1204
|
await result_4;
|
|
1247
1205
|
}
|
|
1248
1206
|
});
|
|
1249
|
-
let dbWorkerPort = null;
|
|
1250
1207
|
const dbWorkerInited = Promise.withResolvers();
|
|
1208
|
+
let dbWorker = { type: "Starting" };
|
|
1209
|
+
const queue = [];
|
|
1210
|
+
const pendingWriteCountByOwnerId = new Map();
|
|
1211
|
+
const pendingApplyCountForAllRoutesByOwnerId = new Map();
|
|
1212
|
+
// Queued applies of relay frames, counted per owner and source transport.
|
|
1213
|
+
const ownerTransportApplyRelation = createRefCountedRelation();
|
|
1214
|
+
// Each tab is told once during this tenant's lifetime, through its own
|
|
1215
|
+
// connection. Recreating the tenant after idle disposal retries startup
|
|
1216
|
+
// and may report the refusal again.
|
|
1217
|
+
const refusedTabPorts = new WeakSet();
|
|
1218
|
+
const routesByOwnerIdByKey = new Map();
|
|
1219
|
+
// Disposal does not wait for the DbWorker. It holds the database lock
|
|
1220
|
+
// until Dispose arrives, its tab closes, or this worker ends, and the next
|
|
1221
|
+
// DbWorker for this database, of this worker or another build, waits for
|
|
1222
|
+
// that lock before it reads the clock. A requested DbWorker that reports
|
|
1223
|
+
// in later gets Dispose, and a refused tenant stays refused, so it keeps
|
|
1224
|
+
// dropping requests.
|
|
1225
|
+
disposer.defer(() => {
|
|
1226
|
+
if (dbWorker.type === "Leading")
|
|
1227
|
+
dbWorker.port.postMessage({ type: "Dispose" });
|
|
1228
|
+
if (dbWorker.type !== "Refused")
|
|
1229
|
+
dbWorker = { type: "Disposed" };
|
|
1230
|
+
});
|
|
1251
1231
|
const initDbWorker = () => {
|
|
1252
1232
|
// Without a tab leader yet, the store subscription starts the DbWorker
|
|
1253
1233
|
// once a tab announces itself.
|
|
1254
1234
|
const tabLeaderPort = deps.tabLeaderPortStore.get();
|
|
1255
|
-
if (
|
|
1235
|
+
if (dbWorker.type === "Refused" || !tabLeaderPort)
|
|
1256
1236
|
return;
|
|
1257
1237
|
const dbWorkerChannel = deps.createMessageChannel();
|
|
1258
1238
|
const currentDbWorkerPort = dbWorkerChannel.port2;
|
|
1259
1239
|
currentDbWorkerPort.onMessage = (message) => {
|
|
1260
1240
|
switch (message.type) {
|
|
1261
1241
|
case "LeaderAcquired": {
|
|
1262
|
-
|
|
1263
|
-
if (
|
|
1242
|
+
const previous = dbWorker;
|
|
1243
|
+
if (previous.type === "Leading")
|
|
1244
|
+
assertNotSame(previous.port, currentDbWorkerPort);
|
|
1245
|
+
if (previous.type === "Refused" || previous.type === "Disposed") {
|
|
1264
1246
|
// This worker was requested before the refusal or before
|
|
1265
1247
|
// disposal started. The tenant will not use it, so let it
|
|
1266
1248
|
// release the database lock.
|
|
@@ -1268,30 +1250,27 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1268
1250
|
currentDbWorkerPort[Symbol.dispose]();
|
|
1269
1251
|
break;
|
|
1270
1252
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1253
|
+
if (previous.type === "Leading")
|
|
1254
|
+
previous.port[Symbol.dispose]();
|
|
1255
|
+
dbWorker = {
|
|
1256
|
+
type: "Leading",
|
|
1257
|
+
port: currentDbWorkerPort,
|
|
1258
|
+
// A replacement may advance the clock by releasing quarantine,
|
|
1259
|
+
// or start behind it with an empty memoryOnly database. Keep the
|
|
1260
|
+
// greater clock; pending writes retain their captured inputs.
|
|
1261
|
+
clock: previous.type !== "Leading" ||
|
|
1262
|
+
orderTimestamp(previous.clock, message.clock) === -1
|
|
1263
|
+
? message.clock
|
|
1264
|
+
: previous.clock,
|
|
1265
|
+
activeDispatch: null,
|
|
1266
|
+
};
|
|
1282
1267
|
// A replacement leader may have committed writes whose responses
|
|
1283
1268
|
// were lost, and may have released quarantine at startup. A lost
|
|
1284
1269
|
// response may also have reported stored owner messages, so the
|
|
1285
1270
|
// owners reconcile through every transport again.
|
|
1286
|
-
if (
|
|
1271
|
+
if (previous.type === "Leading") {
|
|
1287
1272
|
refreshQueries();
|
|
1288
|
-
|
|
1289
|
-
for (const instance of instancesById.values()) {
|
|
1290
|
-
for (const { owner } of instance.ownerRegistrations.keys()) {
|
|
1291
|
-
usedOwnerIds.add(owner.id);
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
requestCreateSyncMessages(usedOwnerIds, allTransports);
|
|
1273
|
+
requestCreateSyncMessages(new Set(getSyncOwners().map(({ ownerId }) => ownerId)), allTransports);
|
|
1295
1274
|
}
|
|
1296
1275
|
console.info("leaderAcquired");
|
|
1297
1276
|
dbWorkerInited.resolve();
|
|
@@ -1299,21 +1278,20 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1299
1278
|
break;
|
|
1300
1279
|
}
|
|
1301
1280
|
case "LeaderRefused": {
|
|
1302
|
-
assertNotSame(dbWorkerPort, currentDbWorkerPort);
|
|
1303
1281
|
// The worker refused startup and is releasing its resources.
|
|
1304
1282
|
// Requests stay unanswered until their instances are disposed.
|
|
1305
1283
|
// Keep the tenant unavailable, tell each connected tab once, and
|
|
1306
1284
|
// tell tabs that connect later without starting another worker.
|
|
1307
|
-
|
|
1308
|
-
|
|
1285
|
+
if (dbWorker.type === "Leading") {
|
|
1286
|
+
assertNotSame(dbWorker.port, currentDbWorkerPort);
|
|
1287
|
+
dbWorker.port[Symbol.dispose]();
|
|
1288
|
+
}
|
|
1309
1289
|
currentDbWorkerPort[Symbol.dispose]();
|
|
1310
|
-
activeDispatch = null;
|
|
1311
1290
|
queue.length = 0;
|
|
1312
1291
|
pendingWriteCountByOwnerId.clear();
|
|
1313
1292
|
pendingApplyCountForAllRoutesByOwnerId.clear();
|
|
1314
1293
|
ownerTransportApplyRelation.clear();
|
|
1315
|
-
|
|
1316
|
-
refreshSyncRoutes();
|
|
1294
|
+
dbWorker = { type: "Refused", error: message.error };
|
|
1317
1295
|
deps.publishSyncState();
|
|
1318
1296
|
console.info("leaderRefused", message.error);
|
|
1319
1297
|
for (const instance of instancesById.values()) {
|
|
@@ -1323,19 +1301,18 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1323
1301
|
break;
|
|
1324
1302
|
}
|
|
1325
1303
|
case "OnQueuedResponse": {
|
|
1326
|
-
|
|
1304
|
+
const leading = dbWorker;
|
|
1305
|
+
if (leading.type !== "Leading" ||
|
|
1306
|
+
leading.activeDispatch?.attemptId !== message.attemptId)
|
|
1327
1307
|
return;
|
|
1328
|
-
const { entry } = activeDispatch;
|
|
1308
|
+
const { entry } = leading.activeDispatch;
|
|
1329
1309
|
const { response } = message;
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
sessionClock = response.message.clock;
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1310
|
+
// Replays report their computed clock, which a replacement leader
|
|
1311
|
+
// may have passed at startup. Keep the greater clock.
|
|
1312
|
+
if ((response.message.type === "Mutate" ||
|
|
1313
|
+
response.message.type === "ApplySyncMessage") &&
|
|
1314
|
+
orderTimestamp(leading.clock, response.message.clock) === -1)
|
|
1315
|
+
leading.clock = response.message.clock;
|
|
1339
1316
|
switch (entry.type) {
|
|
1340
1317
|
case "Read":
|
|
1341
1318
|
case "Write":
|
|
@@ -1353,7 +1330,7 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1353
1330
|
const head = queue.shift();
|
|
1354
1331
|
assertNonNullable(head);
|
|
1355
1332
|
updatePendingWork(head, -1);
|
|
1356
|
-
activeDispatch = null;
|
|
1333
|
+
leading.activeDispatch = null;
|
|
1357
1334
|
// Follow-up uploads and retries are already queued or sent. Only
|
|
1358
1335
|
// now can removing the completed head establish convergence.
|
|
1359
1336
|
refreshSyncRoutes();
|
|
@@ -1362,6 +1339,8 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1362
1339
|
runQueue();
|
|
1363
1340
|
break;
|
|
1364
1341
|
}
|
|
1342
|
+
default:
|
|
1343
|
+
exhaustiveCheck(message);
|
|
1365
1344
|
}
|
|
1366
1345
|
};
|
|
1367
1346
|
tabLeaderPort.postMessage({
|
|
@@ -1370,7 +1349,7 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1370
1349
|
consoleLevel,
|
|
1371
1350
|
sqliteSchema,
|
|
1372
1351
|
encryptionKey,
|
|
1373
|
-
memoryOnly,
|
|
1352
|
+
memoryOnly: devicePersistence === "NotPersisted",
|
|
1374
1353
|
sharedWorkerId: workerId,
|
|
1375
1354
|
port: dbWorkerChannel.port1.native,
|
|
1376
1355
|
}, [dbWorkerChannel.port1.native]);
|
|
@@ -1382,11 +1361,6 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1382
1361
|
instance.port.postMessage({ type: "RefreshQueries" });
|
|
1383
1362
|
}
|
|
1384
1363
|
};
|
|
1385
|
-
const queue = [];
|
|
1386
|
-
const pendingWriteCountByOwnerId = new Map();
|
|
1387
|
-
const pendingApplyCountForAllRoutesByOwnerId = new Map();
|
|
1388
|
-
// Queued applies of relay frames, counted per owner and source transport.
|
|
1389
|
-
const ownerTransportApplyRelation = createRefCountedRelation();
|
|
1390
1364
|
const updatePendingCount = (counts, key, delta) => {
|
|
1391
1365
|
const count = (counts.get(key) ?? 0) + delta;
|
|
1392
1366
|
assert(count >= 0, "Pending queue count must not become negative");
|
|
@@ -1430,59 +1404,42 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1430
1404
|
updatePendingWork(entry, 1);
|
|
1431
1405
|
queue.push(entry);
|
|
1432
1406
|
};
|
|
1433
|
-
let sessionClock = null;
|
|
1434
|
-
let startupError = null;
|
|
1435
|
-
let isDisposing = false;
|
|
1436
|
-
// Each tab is told once during this tenant's lifetime, through its own
|
|
1437
|
-
// connection. Recreating the tenant after idle disposal retries startup
|
|
1438
|
-
// and may report the refusal again.
|
|
1439
|
-
const refusedTabPorts = new WeakSet();
|
|
1440
1407
|
const reportRefusal = (tabPort, error) => {
|
|
1441
1408
|
if (refusedTabPorts.has(tabPort))
|
|
1442
1409
|
return;
|
|
1443
1410
|
refusedTabPorts.add(tabPort);
|
|
1444
1411
|
tabPort.postMessage({ type: "Error", error });
|
|
1445
1412
|
};
|
|
1446
|
-
let activeDispatch = null;
|
|
1447
1413
|
const runQueue = () => {
|
|
1448
|
-
if (
|
|
1414
|
+
if (dbWorker.type !== "Leading" ||
|
|
1415
|
+
dbWorker.activeDispatch ||
|
|
1416
|
+
!isNonEmptyArray(queue))
|
|
1449
1417
|
return;
|
|
1450
|
-
assertNonNullable(sessionClock);
|
|
1451
1418
|
const entry = firstInArray(queue);
|
|
1452
1419
|
const attemptId = createId(run.deps);
|
|
1453
|
-
activeDispatch = { entry, attemptId };
|
|
1420
|
+
dbWorker.activeDispatch = { entry, attemptId };
|
|
1454
1421
|
if (entry.type === "Write" || entry.type === "ApplySyncMessage") {
|
|
1455
1422
|
// A write captures its inputs on first dispatch, so a retry after
|
|
1456
1423
|
// leader replacement reproduces the same timestamps.
|
|
1457
|
-
entry.
|
|
1458
|
-
|
|
1459
|
-
|
|
1424
|
+
entry.capturedInputs ??= {
|
|
1425
|
+
clock: dbWorker.clock,
|
|
1426
|
+
now: run.deps.time.now(),
|
|
1427
|
+
};
|
|
1428
|
+
dbWorker.port.postMessage({
|
|
1460
1429
|
type: "Request",
|
|
1461
1430
|
attemptId,
|
|
1462
1431
|
request: entry.request,
|
|
1463
|
-
|
|
1464
|
-
now: entry.now,
|
|
1432
|
+
...entry.capturedInputs,
|
|
1465
1433
|
});
|
|
1466
1434
|
}
|
|
1467
1435
|
else {
|
|
1468
|
-
|
|
1436
|
+
dbWorker.port.postMessage({
|
|
1469
1437
|
type: "Request",
|
|
1470
1438
|
attemptId,
|
|
1471
1439
|
request: entry.request,
|
|
1472
1440
|
});
|
|
1473
1441
|
}
|
|
1474
1442
|
};
|
|
1475
|
-
// Disposal does not wait for the DbWorker. It holds the database lock
|
|
1476
|
-
// until Dispose arrives, its tab closes, or this worker ends, and the next
|
|
1477
|
-
// DbWorker for this database, of this worker or another build, waits for
|
|
1478
|
-
// that lock before it reads the clock. A requested DbWorker that reports
|
|
1479
|
-
// in later gets Dispose from the isDisposing check.
|
|
1480
|
-
disposer.defer(() => {
|
|
1481
|
-
isDisposing = true;
|
|
1482
|
-
dbWorkerPort?.postMessage({ type: "Dispose" });
|
|
1483
|
-
dbWorkerPort = null;
|
|
1484
|
-
activeDispatch = null;
|
|
1485
|
-
});
|
|
1486
1443
|
const handleResponseForEvolu = (response, first) => {
|
|
1487
1444
|
// A disposed instance gets no patches, but its committed write still
|
|
1488
1445
|
// synchronizes.
|
|
@@ -1491,13 +1448,11 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1491
1448
|
case "Mutate":
|
|
1492
1449
|
case "Query": {
|
|
1493
1450
|
if (instance) {
|
|
1494
|
-
const nextRowsByQuery = new Map(instance.rowsByQuery);
|
|
1495
1451
|
const patchesByQuery = new Map();
|
|
1496
1452
|
for (const [query, rows] of response.message.rowsByQuery) {
|
|
1497
|
-
nextRowsByQuery.set(query, rows);
|
|
1498
1453
|
patchesByQuery.set(query, makePatches(instance.rowsByQuery.get(query), rows));
|
|
1454
|
+
instance.rowsByQuery.set(query, rows);
|
|
1499
1455
|
}
|
|
1500
|
-
instance.rowsByQuery = nextRowsByQuery;
|
|
1501
1456
|
instance.port.postMessage({
|
|
1502
1457
|
type: "OnPatchesByQuery",
|
|
1503
1458
|
patchesByQuery,
|
|
@@ -1561,30 +1516,19 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1561
1516
|
exhaustiveCheck(response.message);
|
|
1562
1517
|
}
|
|
1563
1518
|
};
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
failure: null,
|
|
1578
|
-
skip: null,
|
|
1579
|
-
completeAt: null,
|
|
1580
|
-
},
|
|
1581
|
-
lastSentAt: null,
|
|
1582
|
-
lastReceivedAt: null,
|
|
1583
|
-
};
|
|
1584
|
-
routesByOwnerId.set(ownerId, route);
|
|
1585
|
-
}
|
|
1586
|
-
return route;
|
|
1587
|
-
};
|
|
1519
|
+
const getRoute = (ownerId, key) => routesByOwnerIdByKey
|
|
1520
|
+
.getOrInsertComputed(key, () => new Map())
|
|
1521
|
+
.getOrInsertComputed(ownerId, () => ({
|
|
1522
|
+
progress: {
|
|
1523
|
+
type: "Pending",
|
|
1524
|
+
roundRequired: true,
|
|
1525
|
+
failure: null,
|
|
1526
|
+
skip: null,
|
|
1527
|
+
completeAt: null,
|
|
1528
|
+
},
|
|
1529
|
+
lastSentAt: null,
|
|
1530
|
+
lastReceivedAt: null,
|
|
1531
|
+
}));
|
|
1588
1532
|
const getSyncOwners = () => {
|
|
1589
1533
|
const ownersById = new Map();
|
|
1590
1534
|
for (const instance of instancesById.values()) {
|
|
@@ -1653,7 +1597,7 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1653
1597
|
: pending.skip && !pending.skip.isRechecking
|
|
1654
1598
|
? {
|
|
1655
1599
|
type: "Settled",
|
|
1656
|
-
|
|
1600
|
+
skippedError: pending.skip.error,
|
|
1657
1601
|
completeAt: pending.completeAt,
|
|
1658
1602
|
}
|
|
1659
1603
|
: route.progress.type === "Complete"
|
|
@@ -1670,8 +1614,9 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1670
1614
|
const { failedOwnerIds } = response.message;
|
|
1671
1615
|
if (failedOwnerIds.size === 0)
|
|
1672
1616
|
break;
|
|
1673
|
-
// A retry would likely fail the same way, so the routes wait for
|
|
1674
|
-
// explicit request or
|
|
1617
|
+
// A retry would likely fail the same way, so the routes wait for the
|
|
1618
|
+
// next round another event requests, such as an explicit request or
|
|
1619
|
+
// a reopen.
|
|
1675
1620
|
const now = deps.time.now();
|
|
1676
1621
|
for (const ownerId of failedOwnerIds) {
|
|
1677
1622
|
deps.transports.forEachResourceForClaim(ownerId, (_, transport) => {
|
|
@@ -1754,9 +1699,9 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1754
1699
|
};
|
|
1755
1700
|
if (failure !== null) {
|
|
1756
1701
|
// The first failure since the route settled requests one
|
|
1757
|
-
// round. Further failures wait for
|
|
1758
|
-
//
|
|
1759
|
-
// another request.
|
|
1702
|
+
// round. Further failures wait for the next round another event
|
|
1703
|
+
// requests, such as an explicit request or a reopen, even after
|
|
1704
|
+
// a converged reply, which may answer another request.
|
|
1760
1705
|
const requestsRetry = route.progress.type !== "Pending" ||
|
|
1761
1706
|
route.progress.failure === null;
|
|
1762
1707
|
route.progress = failRoute(route.progress, errorToSyncRouteError(failure, now));
|
|
@@ -1903,7 +1848,7 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1903
1848
|
return ownersById;
|
|
1904
1849
|
};
|
|
1905
1850
|
const requestCreateSyncMessages = (ownerIds, target, { afterQueuedWrites = true, } = {}) => {
|
|
1906
|
-
if (
|
|
1851
|
+
if (dbWorker.type === "Refused")
|
|
1907
1852
|
return;
|
|
1908
1853
|
const usedOwnersById = getUsedOwnersById(ownerIds);
|
|
1909
1854
|
// Opening, storing messages from another transport, a failure, and an
|
|
@@ -1941,8 +1886,11 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1941
1886
|
const lastWriteIndex = afterQueuedWrites
|
|
1942
1887
|
? queue.findLastIndex(({ type }) => type === "Write" || type === "ApplySyncMessage")
|
|
1943
1888
|
: -1;
|
|
1889
|
+
const dispatchedEntry = dbWorker.type === "Leading"
|
|
1890
|
+
? dbWorker.activeDispatch?.entry
|
|
1891
|
+
: undefined;
|
|
1944
1892
|
const isQueued = queue.some((entry, index) => index > lastWriteIndex &&
|
|
1945
|
-
entry !==
|
|
1893
|
+
entry !== dispatchedEntry &&
|
|
1946
1894
|
entry.type === "CreateSyncMessages" &&
|
|
1947
1895
|
(entry.target.type === "AllTransports" ||
|
|
1948
1896
|
(target.type === "Transport" && entry.target.key === target.key)) &&
|
|
@@ -1971,23 +1919,12 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
1971
1919
|
const isFirstWritableUse = "writeKey" in syncOwner.owner &&
|
|
1972
1920
|
!getUsedOwnersById(new Set([ownerId])).has(ownerId);
|
|
1973
1921
|
// A transport first claimed for the owner starts every tenant's
|
|
1974
|
-
// round through onFirstClaimAdded. A
|
|
1975
|
-
// its existing history through the
|
|
1976
|
-
//
|
|
1977
|
-
//
|
|
1978
|
-
|
|
1979
|
-
const
|
|
1980
|
-
for (const { ownerRegistrations } of instancesById.values()) {
|
|
1981
|
-
for (const [usedSyncOwner, leases] of ownerRegistrations) {
|
|
1982
|
-
if (usedSyncOwner.owner.id !== ownerId)
|
|
1983
|
-
continue;
|
|
1984
|
-
if (!leases.some((lease) => lease !== null))
|
|
1985
|
-
continue;
|
|
1986
|
-
for (const transport of usedSyncOwner.transports) {
|
|
1987
|
-
usedKeys.add(structuralLookup(transport));
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1922
|
+
// round through onFirstClaimAdded. A tenant's first writable
|
|
1923
|
+
// registration reconciles its existing history through the
|
|
1924
|
+
// transports already claimed for the owner. Later writes upload
|
|
1925
|
+
// through every claimed transport, whichever instance made them, so
|
|
1926
|
+
// later registrations need no round.
|
|
1927
|
+
const claimedKeys = getClaimedKeys(ownerId);
|
|
1991
1928
|
const leases = instance.ownerRegistrations.getOrInsertComputed(syncOwner, () => []);
|
|
1992
1929
|
// First-claim callbacks must see the owner before acquisition finishes.
|
|
1993
1930
|
const index = leases.push(null) - 1;
|
|
@@ -2003,17 +1940,12 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
2003
1940
|
}
|
|
2004
1941
|
deps.refreshAllSyncRoutes();
|
|
2005
1942
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
requestCreateSyncMessages(new Set([ownerId]), {
|
|
2013
|
-
type: "Transport",
|
|
2014
|
-
key,
|
|
2015
|
-
});
|
|
2016
|
-
}
|
|
1943
|
+
if (isFirstWritableUse)
|
|
1944
|
+
for (const key of claimedKeys)
|
|
1945
|
+
requestCreateSyncMessages(new Set([ownerId]), {
|
|
1946
|
+
type: "Transport",
|
|
1947
|
+
key,
|
|
1948
|
+
});
|
|
2017
1949
|
}
|
|
2018
1950
|
else {
|
|
2019
1951
|
const claimLeases = instance.ownerRegistrations.get(syncOwner);
|
|
@@ -2045,8 +1977,8 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
2045
1977
|
deps.publishSyncState();
|
|
2046
1978
|
});
|
|
2047
1979
|
const tenant = disposable({
|
|
2048
|
-
getSyncTenant: () =>
|
|
2049
|
-
? { type: "Refused", name, error:
|
|
1980
|
+
getSyncTenant: () => dbWorker.type === "Refused"
|
|
1981
|
+
? { type: "Refused", name, error: dbWorker.error }
|
|
2050
1982
|
: {
|
|
2051
1983
|
type: "Active",
|
|
2052
1984
|
name,
|
|
@@ -2062,10 +1994,46 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
2062
1994
|
// before yielding. Snapshot reads must not create
|
|
2063
1995
|
// missing routes.
|
|
2064
1996
|
assertNotUndefined(route);
|
|
2065
|
-
|
|
1997
|
+
const transportId = deps.getSyncTransportId(key);
|
|
1998
|
+
const { progress, lastSentAt, lastReceivedAt } = route;
|
|
1999
|
+
if (progress.type === "Pending")
|
|
2000
|
+
return {
|
|
2001
|
+
type: "Pending",
|
|
2002
|
+
transportId,
|
|
2003
|
+
failure: progress.failure,
|
|
2004
|
+
skippedError: progress.skip?.error ?? null,
|
|
2005
|
+
completeAt: progress.completeAt,
|
|
2006
|
+
lastSentAt,
|
|
2007
|
+
lastReceivedAt,
|
|
2008
|
+
};
|
|
2009
|
+
// Only a sent round settles a route or completes
|
|
2010
|
+
// it.
|
|
2011
|
+
assertNonNullable(lastSentAt);
|
|
2012
|
+
if (progress.type === "Complete")
|
|
2013
|
+
return {
|
|
2014
|
+
type: "Complete",
|
|
2015
|
+
transportId,
|
|
2016
|
+
completeAt: progress.completeAt,
|
|
2017
|
+
lastSentAt,
|
|
2018
|
+
lastReceivedAt,
|
|
2019
|
+
};
|
|
2020
|
+
// Only a received reply records a skip.
|
|
2021
|
+
assertNonNullable(lastReceivedAt);
|
|
2022
|
+
return {
|
|
2023
|
+
type: "Settled",
|
|
2024
|
+
transportId,
|
|
2025
|
+
skippedError: progress.skippedError,
|
|
2026
|
+
completeAt: progress.completeAt,
|
|
2027
|
+
lastSentAt,
|
|
2028
|
+
lastReceivedAt,
|
|
2029
|
+
};
|
|
2066
2030
|
}),
|
|
2067
2031
|
}
|
|
2068
|
-
: {
|
|
2032
|
+
: {
|
|
2033
|
+
type: "Readonly",
|
|
2034
|
+
ownerId,
|
|
2035
|
+
transportIds: transportKeys.map(deps.getSyncTransportId),
|
|
2036
|
+
}),
|
|
2069
2037
|
},
|
|
2070
2038
|
refreshSyncRoutes,
|
|
2071
2039
|
addInstance: (message, tabPort, onDisposed) => {
|
|
@@ -2082,13 +2050,11 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
2082
2050
|
}),
|
|
2083
2051
|
port: deps.createMessagePort(message.evoluPort),
|
|
2084
2052
|
tabPort,
|
|
2085
|
-
onDisposed,
|
|
2086
2053
|
rowsByQuery: new Map(),
|
|
2087
|
-
useOwnerMutex: createMutex(),
|
|
2088
2054
|
[Symbol.asyncDispose]: () => disposer.disposeAsync(),
|
|
2089
2055
|
};
|
|
2090
2056
|
instancesById.set(instance.id, instance);
|
|
2091
|
-
disposer.defer(
|
|
2057
|
+
disposer.defer(onDisposed);
|
|
2092
2058
|
disposer.defer(() => {
|
|
2093
2059
|
for (const leases of instance.ownerRegistrations.values()) {
|
|
2094
2060
|
for (const lease of leases)
|
|
@@ -2103,6 +2069,7 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
2103
2069
|
// aborts queued UseOwner batches and waits for the running one, whose
|
|
2104
2070
|
// transport claim cannot be aborted, so the release sees every lease.
|
|
2105
2071
|
const instanceRun = disposer.use(tenantRun.create());
|
|
2072
|
+
const useOwnerMutex = createMutex();
|
|
2106
2073
|
disposer.defer(() => {
|
|
2107
2074
|
instancesById.delete(instance.id);
|
|
2108
2075
|
refreshSyncRoutes();
|
|
@@ -2131,7 +2098,7 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
2131
2098
|
return instance[Symbol.asyncDispose]();
|
|
2132
2099
|
});
|
|
2133
2100
|
instance.port.onMessage = (message) => {
|
|
2134
|
-
if (
|
|
2101
|
+
if (dbWorker.type === "Refused")
|
|
2135
2102
|
return;
|
|
2136
2103
|
switch (message.type) {
|
|
2137
2104
|
case "Query":
|
|
@@ -2163,7 +2130,7 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
2163
2130
|
break;
|
|
2164
2131
|
}
|
|
2165
2132
|
case "UseOwner": {
|
|
2166
|
-
void instanceRun(
|
|
2133
|
+
void instanceRun(useOwnerMutex.withLock(async (run) => {
|
|
2167
2134
|
for (const action of message.actions) {
|
|
2168
2135
|
switch (action.action) {
|
|
2169
2136
|
case "sync":
|
|
@@ -2191,14 +2158,16 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
2191
2158
|
}));
|
|
2192
2159
|
break;
|
|
2193
2160
|
}
|
|
2161
|
+
default:
|
|
2162
|
+
exhaustiveCheck(message);
|
|
2194
2163
|
}
|
|
2195
2164
|
};
|
|
2196
|
-
if (
|
|
2197
|
-
reportRefusal(instance.tabPort,
|
|
2165
|
+
if (dbWorker.type === "Refused")
|
|
2166
|
+
reportRefusal(instance.tabPort, dbWorker.error);
|
|
2198
2167
|
},
|
|
2199
2168
|
requestCreateSyncMessages,
|
|
2200
2169
|
requestApplySyncMessage: (ownerId, inputMessage, source) => {
|
|
2201
|
-
if (
|
|
2170
|
+
if (dbWorker.type === "Refused")
|
|
2202
2171
|
return;
|
|
2203
2172
|
const owner = getUsedOwnersById(new Set([ownerId])).get(ownerId);
|
|
2204
2173
|
if (!owner)
|
|
@@ -2236,41 +2205,16 @@ const createEvoluTenant = ({ name, consoleLevel, sqliteSchema, encryptionKey, me
|
|
|
2236
2205
|
await result_3;
|
|
2237
2206
|
}
|
|
2238
2207
|
};
|
|
2239
|
-
// | (Typed<"reset"> & {
|
|
2240
|
-
// readonly onCompleteId: CallbackId;
|
|
2241
|
-
// readonly reload: boolean;
|
|
2242
|
-
// readonly restore?: {
|
|
2243
|
-
// readonly sqliteSchema: SqliteSchema;
|
|
2244
|
-
// readonly mnemonic: Mnemonic;
|
|
2245
|
-
// };
|
|
2246
|
-
// })
|
|
2247
|
-
// | (Typed<"ensureSqliteSchema"> & {
|
|
2248
|
-
// readonly sqliteSchema: SqliteSchema;
|
|
2249
|
-
// })
|
|
2250
|
-
// | (Typed<"export"> & {
|
|
2251
|
-
// readonly onCompleteId: CallbackId;
|
|
2252
|
-
// })
|
|
2253
|
-
// | (Typed<"useOwner"> & {
|
|
2254
|
-
// readonly use: boolean;
|
|
2255
|
-
// readonly owner: SyncOwner;
|
|
2256
|
-
// });
|
|
2257
|
-
// | (Typed<"onReset"> & {
|
|
2258
|
-
// readonly onCompleteId: CallbackId;
|
|
2259
|
-
// readonly reload: boolean;
|
|
2260
|
-
// })
|
|
2261
2208
|
// TODO: SharedWorker follow-ups.
|
|
2262
2209
|
// - Rotate the node ID when a copied database is detected; see the Duplicate
|
|
2263
2210
|
// node IDs section in the Timestamp module.
|
|
2264
2211
|
// - Detect DbWorker and port liveness so a worker-only crash resumes the queue.
|
|
2265
2212
|
// Defer panicked-worker restart until failure detection and recovery are
|
|
2266
|
-
// defined, accounting for SQLite WASM's detection limits.
|
|
2267
|
-
//
|
|
2268
|
-
//
|
|
2269
|
-
// planned to be forbidden.
|
|
2213
|
+
// defined, accounting for SQLite WASM's detection limits. Mutations, round
|
|
2214
|
+
// creation, and frame application are answered when SQLite throws; a query
|
|
2215
|
+
// or an export that throws is not, and stops the database's queue.
|
|
2270
2216
|
// - Split worker protocol types and the EvoluTenant implementation into focused
|
|
2271
2217
|
// modules.
|
|
2272
|
-
// - Remove the obsolete commented protocol block above.
|
|
2273
|
-
// - Propagate invalid protocol messages to sync state.
|
|
2274
2218
|
// - Bound sync state publishing during a bulk catch-up: every sent and applied
|
|
2275
2219
|
// frame changes a route timestamp, so each frame broadcasts a full snapshot
|
|
2276
2220
|
// to every tab. Throttling needs a wall-clock policy and a deterministic way
|