@evolu/common 8.15.1 → 8.17.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/Crypto.d.ts +21 -3
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +18 -2
- package/dist/src/Error.d.ts +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/Owner.d.ts +20 -8
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +23 -10
- package/dist/src/local-first/Schema.d.ts +1 -1
- 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 -318
- package/package.json +1 -1
- package/src/Console.test.ts +75 -247
- package/src/Console.ts +68 -110
- package/src/Crypto.ts +23 -3
- 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/Owner.test.ts +52 -1
- package/src/local-first/Owner.ts +26 -7
- package/src/local-first/Shared.test.ts +414 -39
- package/src/local-first/Shared.ts +386 -438
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
} from "./Schema.ts";
|
|
51
51
|
import {
|
|
52
52
|
consoleEntryOrErrorBroadcastChannelName,
|
|
53
|
+
postConsoleEntry,
|
|
53
54
|
relaySyncStateToStatus,
|
|
54
55
|
syncStateToOwnerSyncStatus,
|
|
55
56
|
syncStateToRelaySyncStates,
|
|
@@ -78,7 +79,7 @@ import {
|
|
|
78
79
|
type SyncTenantOwner,
|
|
79
80
|
type SyncTransport,
|
|
80
81
|
} from "./Shared.ts";
|
|
81
|
-
import type { NativeMessagePort } from "../Worker.ts";
|
|
82
|
+
import type { BroadcastChannel, NativeMessagePort } from "../Worker.ts";
|
|
82
83
|
import {
|
|
83
84
|
DbChange,
|
|
84
85
|
testCreateCrdtMessage,
|
|
@@ -241,8 +242,10 @@ const setupSharedWorker = async ({
|
|
|
241
242
|
autoDispose = true,
|
|
242
243
|
initialClock = createTimestamp(),
|
|
243
244
|
memoryOnly = false,
|
|
245
|
+
consoleLevel = "debug",
|
|
244
246
|
}: {
|
|
245
247
|
tenantName?: Name;
|
|
248
|
+
consoleLevel?: ConsoleLevel;
|
|
246
249
|
memoryOnly?: boolean;
|
|
247
250
|
evoluChannel?: TestMessageChannel<EvoluOutput, EvoluInput>;
|
|
248
251
|
releaseDbWorkerLeaderOnDispose?: boolean;
|
|
@@ -270,7 +273,6 @@ const setupSharedWorker = async ({
|
|
|
270
273
|
dbWorkerPort.postMessage({
|
|
271
274
|
clock: initialClock,
|
|
272
275
|
type: "LeaderAcquired",
|
|
273
|
-
name: tenantName,
|
|
274
276
|
});
|
|
275
277
|
await testWaitForWorkerMessage();
|
|
276
278
|
};
|
|
@@ -289,7 +291,7 @@ const setupSharedWorker = async ({
|
|
|
289
291
|
type: "CreateEvolu",
|
|
290
292
|
name: tenantName,
|
|
291
293
|
id,
|
|
292
|
-
consoleLevel
|
|
294
|
+
consoleLevel,
|
|
293
295
|
sqliteSchema: testSqliteSchema,
|
|
294
296
|
encryptionKey: testAppOwner.encryptionKey,
|
|
295
297
|
memoryOnly,
|
|
@@ -588,6 +590,27 @@ describe("AnnounceTabLeader", () => {
|
|
|
588
590
|
assertEqual(outputs, [{ type: "ConsoleEntry", entry }]);
|
|
589
591
|
});
|
|
590
592
|
|
|
593
|
+
it("delivers a console entry argument that cannot be cloned as a string", async () => {
|
|
594
|
+
await using setup = await setupSharedWorker();
|
|
595
|
+
const { consoleStoreOutputEntry, announceTabLeader } = setup;
|
|
596
|
+
const { outputs } = await announceTabLeader();
|
|
597
|
+
|
|
598
|
+
consoleStoreOutputEntry.set({
|
|
599
|
+
method: "info",
|
|
600
|
+
path: ["test"],
|
|
601
|
+
args: [{ attempt: 2, retry: () => undefined }],
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
await testWaitForWorkerMessage();
|
|
605
|
+
|
|
606
|
+
assertEqual(outputs, [
|
|
607
|
+
{
|
|
608
|
+
type: "ConsoleEntry",
|
|
609
|
+
entry: { method: "info", path: ["test"], args: ['{"attempt":2}'] },
|
|
610
|
+
},
|
|
611
|
+
]);
|
|
612
|
+
});
|
|
613
|
+
|
|
591
614
|
it("ignores null console store updates", async () => {
|
|
592
615
|
await using setup = await setupSharedWorker();
|
|
593
616
|
const { consoleStoreOutputEntry, announceTabLeader } = setup;
|
|
@@ -622,6 +645,79 @@ describe("AnnounceTabLeader", () => {
|
|
|
622
645
|
args: ["Unknown shared worker input", { type: "UnknownInput" }],
|
|
623
646
|
});
|
|
624
647
|
});
|
|
648
|
+
|
|
649
|
+
it("applies the tab leader's console level to the worker's console", async () => {
|
|
650
|
+
await using setup = await setupSharedWorker();
|
|
651
|
+
const { run, worker } = setup;
|
|
652
|
+
|
|
653
|
+
worker.port.postMessage({
|
|
654
|
+
type: "AnnounceTabLeader",
|
|
655
|
+
consoleLevel: "warn",
|
|
656
|
+
});
|
|
657
|
+
await testWaitForWorkerMessage();
|
|
658
|
+
|
|
659
|
+
assertEqual(run.deps.console.getLevel(), "warn");
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("logs each tenant at the console level of the Evolu that created it", async () => {
|
|
663
|
+
await using setup = await setupSharedWorker();
|
|
664
|
+
const { console } = setup.run.deps;
|
|
665
|
+
const quietName = Name.orThrow("quiet");
|
|
666
|
+
|
|
667
|
+
await setup.createEvolu({ consoleLevel: "info" });
|
|
668
|
+
await setup.createEvolu({ tenantName: quietName, consoleLevel: "warn" });
|
|
669
|
+
|
|
670
|
+
const infoTenantNames = new Set(
|
|
671
|
+
console
|
|
672
|
+
.getEntriesSnapshot()
|
|
673
|
+
.filter(({ method }) => method === "info")
|
|
674
|
+
.map(({ path }) => path[0]),
|
|
675
|
+
);
|
|
676
|
+
assertTrue(infoTenantNames.has(testName));
|
|
677
|
+
assertFalse(infoTenantNames.has(quietName));
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("posts console entry arguments that cannot be cloned as strings", () => {
|
|
681
|
+
const messages: Array<ConsoleEntryOrError> = [];
|
|
682
|
+
// Like a native BroadcastChannel, this one clones each message.
|
|
683
|
+
const channel: BroadcastChannel<ConsoleEntryOrError> = {
|
|
684
|
+
postMessage: (message) => {
|
|
685
|
+
messages.push(structuredClone(message));
|
|
686
|
+
},
|
|
687
|
+
onMessage: null,
|
|
688
|
+
[Symbol.dispose]: () => undefined,
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const error = new Error("boom", {
|
|
692
|
+
cause: { attempt: 2, retry: () => undefined },
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
postConsoleEntry(channel, {
|
|
696
|
+
method: "error",
|
|
697
|
+
path: ["sql"],
|
|
698
|
+
args: ["failed", { attempt: 2, retry: () => undefined }, error],
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
assertEqual(messages, [
|
|
702
|
+
{
|
|
703
|
+
type: "ConsoleEntry",
|
|
704
|
+
entry: {
|
|
705
|
+
method: "error",
|
|
706
|
+
path: ["sql"],
|
|
707
|
+
args: [
|
|
708
|
+
"failed",
|
|
709
|
+
'{"attempt":2}',
|
|
710
|
+
{
|
|
711
|
+
name: "Error",
|
|
712
|
+
message: "boom",
|
|
713
|
+
stack: error.stack,
|
|
714
|
+
cause: '{"attempt":2}',
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
]);
|
|
720
|
+
});
|
|
625
721
|
});
|
|
626
722
|
|
|
627
723
|
describe("storage", () => {
|
|
@@ -666,7 +762,6 @@ describe("storage", () => {
|
|
|
666
762
|
);
|
|
667
763
|
leaderPort.postMessage({
|
|
668
764
|
type: "LeaderAcquired",
|
|
669
|
-
name: testName,
|
|
670
765
|
clock: createTimestamp(),
|
|
671
766
|
});
|
|
672
767
|
await testWaitForWorkerMessage();
|
|
@@ -787,7 +882,6 @@ describe("builds", () => {
|
|
|
787
882
|
);
|
|
788
883
|
port.postMessage({
|
|
789
884
|
type: "LeaderAcquired",
|
|
790
|
-
name: init.name,
|
|
791
885
|
clock: createTimestamp(),
|
|
792
886
|
});
|
|
793
887
|
await testWaitForWorkerMessage();
|
|
@@ -1847,6 +1941,32 @@ describe("sync state", () => {
|
|
|
1847
1941
|
assertSame(route.skippedError, null);
|
|
1848
1942
|
});
|
|
1849
1943
|
|
|
1944
|
+
it("broadcasts nothing when a publication runs after the worker is disposed", async (t) => {
|
|
1945
|
+
const createWebSocket = testCreateWebSocket();
|
|
1946
|
+
await using setup = await setupSharedWorker({ createWebSocket });
|
|
1947
|
+
using disposer = new DisposableStack();
|
|
1948
|
+
const { states } = setupSyncStates(setup, disposer);
|
|
1949
|
+
const instance = await setup.createEvolu();
|
|
1950
|
+
const transport = await setupAppOwnerTransport(
|
|
1951
|
+
instance,
|
|
1952
|
+
"wss://disposed-worker.example",
|
|
1953
|
+
);
|
|
1954
|
+
|
|
1955
|
+
const publish = setupDeferredSyncStatePublication(
|
|
1956
|
+
t,
|
|
1957
|
+
createWebSocket,
|
|
1958
|
+
transport.url,
|
|
1959
|
+
);
|
|
1960
|
+
await setup[Symbol.asyncDispose]();
|
|
1961
|
+
await testWaitForWorkerMessage();
|
|
1962
|
+
const publishedCount = states.length;
|
|
1963
|
+
|
|
1964
|
+
// The publication was scheduled before disposal and runs after it.
|
|
1965
|
+
publish();
|
|
1966
|
+
await testWaitForWorkerMessage();
|
|
1967
|
+
assertLength(states, publishedCount);
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1850
1970
|
for (const removed of [
|
|
1851
1971
|
"writable registration",
|
|
1852
1972
|
"source transport",
|
|
@@ -2231,6 +2351,120 @@ describe("sync state", () => {
|
|
|
2231
2351
|
assertSame(routeOf(latest(), testName, 1).type, "Complete");
|
|
2232
2352
|
});
|
|
2233
2353
|
|
|
2354
|
+
it("reports SyncFailed only for failed owners that still have a route", async () => {
|
|
2355
|
+
const createWebSocket = testCreateWebSocket({ isOpen: false });
|
|
2356
|
+
await using setup = await setupSharedWorker({ createWebSocket });
|
|
2357
|
+
using disposer = new DisposableStack();
|
|
2358
|
+
const { latest } = setupSyncStates(setup, disposer);
|
|
2359
|
+
const instance = await setup.createEvolu();
|
|
2360
|
+
// Both owners share one socket.
|
|
2361
|
+
const transport = {
|
|
2362
|
+
type: "WebSocket",
|
|
2363
|
+
url: "wss://downgraded-owner.example",
|
|
2364
|
+
} as const;
|
|
2365
|
+
instance.evoluChannel.port2.postMessage({
|
|
2366
|
+
type: "UseOwner",
|
|
2367
|
+
actions: [testAppOwner, testAppOwner2].map((owner) => ({
|
|
2368
|
+
owner: { owner, transports: [transport] },
|
|
2369
|
+
action: "add",
|
|
2370
|
+
})),
|
|
2371
|
+
});
|
|
2372
|
+
await testWaitForWorkerMessage();
|
|
2373
|
+
createWebSocket.open(transport.url);
|
|
2374
|
+
await testWaitForWorkerMessage();
|
|
2375
|
+
const round = instance.dbInputs.at(-1);
|
|
2376
|
+
assertNotUndefined(round);
|
|
2377
|
+
assertEqual(round.request, {
|
|
2378
|
+
type: "ForSharedWorker",
|
|
2379
|
+
message: {
|
|
2380
|
+
type: "CreateSyncMessages",
|
|
2381
|
+
owners: [testAppOwner, testAppOwner2],
|
|
2382
|
+
},
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
// While the round is dispatched, the other owner becomes readonly. It
|
|
2386
|
+
// keeps its claim on the transport but no longer has a route.
|
|
2387
|
+
instance.evoluChannel.port2.postMessage({
|
|
2388
|
+
type: "UseOwner",
|
|
2389
|
+
actions: [
|
|
2390
|
+
{
|
|
2391
|
+
owner: {
|
|
2392
|
+
owner: {
|
|
2393
|
+
id: testAppOwner2.id,
|
|
2394
|
+
encryptionKey: testAppOwner2.encryptionKey,
|
|
2395
|
+
},
|
|
2396
|
+
transports: [transport],
|
|
2397
|
+
},
|
|
2398
|
+
action: "add",
|
|
2399
|
+
},
|
|
2400
|
+
{
|
|
2401
|
+
owner: { owner: testAppOwner2, transports: [transport] },
|
|
2402
|
+
action: "remove",
|
|
2403
|
+
},
|
|
2404
|
+
],
|
|
2405
|
+
});
|
|
2406
|
+
await testWaitForWorkerMessage();
|
|
2407
|
+
const transportId = latest().transports[0]?.id;
|
|
2408
|
+
assertNotUndefined(transportId);
|
|
2409
|
+
assertEqual(
|
|
2410
|
+
ownersOf(latest()).find(({ ownerId }) => ownerId === testAppOwner2.id),
|
|
2411
|
+
{
|
|
2412
|
+
type: "Readonly",
|
|
2413
|
+
ownerId: testAppOwner2.id,
|
|
2414
|
+
transportIds: [transportId],
|
|
2415
|
+
},
|
|
2416
|
+
);
|
|
2417
|
+
|
|
2418
|
+
instance.dbWorkerPort.postMessage({
|
|
2419
|
+
type: "OnQueuedResponse",
|
|
2420
|
+
attemptId: round.attemptId,
|
|
2421
|
+
response: {
|
|
2422
|
+
type: "ForSharedWorker",
|
|
2423
|
+
message: {
|
|
2424
|
+
type: "CreateSyncMessages",
|
|
2425
|
+
protocolMessagesByOwnerId: new Map(),
|
|
2426
|
+
failedOwnerIds: new Set([testAppOwner2.id, testAppOwner.id]),
|
|
2427
|
+
},
|
|
2428
|
+
},
|
|
2429
|
+
});
|
|
2430
|
+
await testWaitForWorkerMessage();
|
|
2431
|
+
|
|
2432
|
+
// The writable owner's route fails, and the readonly owner gets no
|
|
2433
|
+
// route.
|
|
2434
|
+
assertEqual(ownersOf(latest()), [
|
|
2435
|
+
{
|
|
2436
|
+
type: "Writable",
|
|
2437
|
+
ownerId: testAppOwner.id,
|
|
2438
|
+
routes: [
|
|
2439
|
+
{
|
|
2440
|
+
type: "Pending",
|
|
2441
|
+
transportId,
|
|
2442
|
+
failure: { type: "SyncFailed", at: setup.run.deps.time.now() },
|
|
2443
|
+
skippedError: null,
|
|
2444
|
+
completeAt: null,
|
|
2445
|
+
lastSentAt: null,
|
|
2446
|
+
lastReceivedAt: null,
|
|
2447
|
+
},
|
|
2448
|
+
],
|
|
2449
|
+
},
|
|
2450
|
+
{
|
|
2451
|
+
type: "Readonly",
|
|
2452
|
+
ownerId: testAppOwner2.id,
|
|
2453
|
+
transportIds: [transportId],
|
|
2454
|
+
},
|
|
2455
|
+
]);
|
|
2456
|
+
|
|
2457
|
+
// The response was handled, so the queue dispatches the next round.
|
|
2458
|
+
instance.evoluChannel.port2.postMessage({
|
|
2459
|
+
type: "UseOwner",
|
|
2460
|
+
actions: [{ ownerId: testAppOwner.id, action: "sync" }],
|
|
2461
|
+
});
|
|
2462
|
+
await testWaitForWorkerMessage();
|
|
2463
|
+
assertEqual(await respondToSyncRound(instance, createWebSocket), [
|
|
2464
|
+
transport.url,
|
|
2465
|
+
]);
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2234
2468
|
it("keeps SyncFailed when a round fails after an earlier round was sent", async () => {
|
|
2235
2469
|
const createWebSocket = testCreateWebSocket();
|
|
2236
2470
|
await using setup = await setupSharedWorker({ createWebSocket });
|
|
@@ -3042,7 +3276,6 @@ describe("sync state", () => {
|
|
|
3042
3276
|
};
|
|
3043
3277
|
dbWorkerPort.postMessage({
|
|
3044
3278
|
type: "LeaderAcquired",
|
|
3045
|
-
name: testName,
|
|
3046
3279
|
clock: createTimestamp(),
|
|
3047
3280
|
});
|
|
3048
3281
|
await testWaitForWorkerMessage();
|
|
@@ -3690,6 +3923,78 @@ describe("sync state", () => {
|
|
|
3690
3923
|
assertSame(latest().transports[0].connection.type, "Disconnected");
|
|
3691
3924
|
});
|
|
3692
3925
|
|
|
3926
|
+
it("reconnects a shared socket when the owner silent longest reaches the timeout", async () => {
|
|
3927
|
+
const createWebSocket = testCreateWebSocket({ isOpen: false });
|
|
3928
|
+
await using setup = await setupSharedWorker({ createWebSocket });
|
|
3929
|
+
const instance = await setup.createEvolu();
|
|
3930
|
+
const { time } = setup.run.deps;
|
|
3931
|
+
// Both owners share one socket.
|
|
3932
|
+
const transport = {
|
|
3933
|
+
type: "WebSocket",
|
|
3934
|
+
url: "wss://shared-silent.example",
|
|
3935
|
+
} as const;
|
|
3936
|
+
|
|
3937
|
+
instance.evoluChannel.port2.postMessage({
|
|
3938
|
+
type: "UseOwner",
|
|
3939
|
+
actions: [
|
|
3940
|
+
{
|
|
3941
|
+
owner: { owner: testAppOwner, transports: [transport] },
|
|
3942
|
+
action: "add",
|
|
3943
|
+
},
|
|
3944
|
+
],
|
|
3945
|
+
});
|
|
3946
|
+
await testWaitForWorkerMessage();
|
|
3947
|
+
createWebSocket.open(transport.url);
|
|
3948
|
+
await testWaitForWorkerMessage();
|
|
3949
|
+
assertEqual(await respondToSyncRound(instance, createWebSocket), [
|
|
3950
|
+
transport.url,
|
|
3951
|
+
]);
|
|
3952
|
+
await postMutation(instance, setup.run);
|
|
3953
|
+
assertEqual(
|
|
3954
|
+
createWebSocket.sentMessages.splice(0).map(({ url }) => url),
|
|
3955
|
+
[transport.url],
|
|
3956
|
+
);
|
|
3957
|
+
|
|
3958
|
+
// One of the app owner's two requests is answered five seconds in, so
|
|
3959
|
+
// its silence restarts there.
|
|
3960
|
+
time.advance("5s");
|
|
3961
|
+
createWebSocket.message(transport.url, relayResponse());
|
|
3962
|
+
await testWaitForWorkerMessage();
|
|
3963
|
+
await respondToApplySync(instance, false, {
|
|
3964
|
+
ok: true,
|
|
3965
|
+
value: { type: "Converged" },
|
|
3966
|
+
});
|
|
3967
|
+
|
|
3968
|
+
// The second owner's request follows five seconds later.
|
|
3969
|
+
time.advance("5s");
|
|
3970
|
+
instance.evoluChannel.port2.postMessage({
|
|
3971
|
+
type: "UseOwner",
|
|
3972
|
+
actions: [
|
|
3973
|
+
{
|
|
3974
|
+
owner: { owner: testAppOwner2, transports: [transport] },
|
|
3975
|
+
action: "add",
|
|
3976
|
+
},
|
|
3977
|
+
],
|
|
3978
|
+
});
|
|
3979
|
+
await testWaitForWorkerMessage();
|
|
3980
|
+
assertEqual(
|
|
3981
|
+
await respondToSyncRound(instance, createWebSocket, testAppOwner2),
|
|
3982
|
+
[transport.url],
|
|
3983
|
+
);
|
|
3984
|
+
|
|
3985
|
+
// When the first timer fires, neither owner has been silent for the
|
|
3986
|
+
// whole timeout, so it waits for the app owner, silent the longest.
|
|
3987
|
+
time.advance(Millis.orThrow(80_000));
|
|
3988
|
+
await testWaitForWorkerMessage();
|
|
3989
|
+
assertEqual(createWebSocket.reconnectedUrls, []);
|
|
3990
|
+
time.advance(Millis.orThrow(4_999));
|
|
3991
|
+
await testWaitForWorkerMessage();
|
|
3992
|
+
assertEqual(createWebSocket.reconnectedUrls, []);
|
|
3993
|
+
time.advance("1ms");
|
|
3994
|
+
await testWaitForWorkerMessage();
|
|
3995
|
+
assertEqual(createWebSocket.reconnectedUrls, [transport.url]);
|
|
3996
|
+
});
|
|
3997
|
+
|
|
3693
3998
|
it("stops timing a request when its socket closes", async () => {
|
|
3694
3999
|
const createWebSocket = testCreateWebSocket({ isOpen: false });
|
|
3695
4000
|
await using setup = await setupSharedWorker({ createWebSocket });
|
|
@@ -4456,7 +4761,6 @@ describe("sync state", () => {
|
|
|
4456
4761
|
};
|
|
4457
4762
|
leaderPort.postMessage({
|
|
4458
4763
|
type: "LeaderRefused",
|
|
4459
|
-
name: refusedName,
|
|
4460
4764
|
error,
|
|
4461
4765
|
});
|
|
4462
4766
|
await testWaitForWorkerMessage();
|
|
@@ -6263,7 +6567,7 @@ describe("sync state", () => {
|
|
|
6263
6567
|
storedVersion: PositiveInt.orThrow(2),
|
|
6264
6568
|
supportedVersion: PositiveInt.orThrow(1),
|
|
6265
6569
|
};
|
|
6266
|
-
leaderPort.postMessage({ type: "LeaderRefused",
|
|
6570
|
+
leaderPort.postMessage({ type: "LeaderRefused", error });
|
|
6267
6571
|
await testWaitForWorkerMessage();
|
|
6268
6572
|
|
|
6269
6573
|
// Nothing the refused database holds syncs, so it publishes its error
|
|
@@ -6364,7 +6668,7 @@ const quotaFailure = (at: number): SyncRouteError => ({
|
|
|
6364
6668
|
|
|
6365
6669
|
const skippedChange = (at: number): SyncRouteError => ({
|
|
6366
6670
|
type: "DecryptWithXChaCha20Poly1305Error",
|
|
6367
|
-
error: "wrong encryption key",
|
|
6671
|
+
error: createUnknownError("wrong encryption key"),
|
|
6368
6672
|
at: Millis.orThrow(at),
|
|
6369
6673
|
});
|
|
6370
6674
|
|
|
@@ -6449,7 +6753,7 @@ describe("SyncState", () => {
|
|
|
6449
6753
|
skippedError: skippedChange(1000),
|
|
6450
6754
|
completeAt: null,
|
|
6451
6755
|
lastSentAt: Millis.orThrow(900),
|
|
6452
|
-
lastReceivedAt:
|
|
6756
|
+
lastReceivedAt: Millis.orThrow(1000),
|
|
6453
6757
|
},
|
|
6454
6758
|
// @ts-expect-error A settled route has sent a round, so it has when it last sent.
|
|
6455
6759
|
{
|
|
@@ -6458,6 +6762,15 @@ describe("SyncState", () => {
|
|
|
6458
6762
|
skippedError: skippedChange(1000),
|
|
6459
6763
|
completeAt: null,
|
|
6460
6764
|
lastSentAt: null,
|
|
6765
|
+
lastReceivedAt: Millis.orThrow(1000),
|
|
6766
|
+
},
|
|
6767
|
+
// @ts-expect-error A settled route has received the reply that skipped its change, so it has when it last received.
|
|
6768
|
+
{
|
|
6769
|
+
type: "Settled",
|
|
6770
|
+
transportId,
|
|
6771
|
+
skippedError: skippedChange(1000),
|
|
6772
|
+
completeAt: null,
|
|
6773
|
+
lastSentAt: Millis.orThrow(900),
|
|
6461
6774
|
lastReceivedAt: null,
|
|
6462
6775
|
},
|
|
6463
6776
|
{
|
|
@@ -6467,7 +6780,7 @@ describe("SyncState", () => {
|
|
|
6467
6780
|
skippedError: null,
|
|
6468
6781
|
completeAt: null,
|
|
6469
6782
|
lastSentAt: Millis.orThrow(900),
|
|
6470
|
-
lastReceivedAt:
|
|
6783
|
+
lastReceivedAt: Millis.orThrow(1000),
|
|
6471
6784
|
},
|
|
6472
6785
|
];
|
|
6473
6786
|
|
|
@@ -6493,8 +6806,25 @@ describe("SyncState", () => {
|
|
|
6493
6806
|
},
|
|
6494
6807
|
];
|
|
6495
6808
|
|
|
6809
|
+
const errors: ReadonlyArray<SyncRouteError> = [
|
|
6810
|
+
skippedChange(1000),
|
|
6811
|
+
{
|
|
6812
|
+
type: "DecryptWithXChaCha20Poly1305Error",
|
|
6813
|
+
// @ts-expect-error A caught value in a route error is an UnknownError.
|
|
6814
|
+
error: "wrong encryption key",
|
|
6815
|
+
at: Millis.orThrow(1000),
|
|
6816
|
+
},
|
|
6817
|
+
{
|
|
6818
|
+
type: "ProtocolInvalidDataError",
|
|
6819
|
+
// @ts-expect-error A caught value in a route error is an UnknownError.
|
|
6820
|
+
error: "invalid frame",
|
|
6821
|
+
at: Millis.orThrow(1000),
|
|
6822
|
+
},
|
|
6823
|
+
];
|
|
6824
|
+
|
|
6496
6825
|
assertLength(connections, 4);
|
|
6497
|
-
assertLength(routes,
|
|
6826
|
+
assertLength(routes, 8);
|
|
6827
|
+
assertLength(errors, 3);
|
|
6498
6828
|
assertLength(owners, 2);
|
|
6499
6829
|
assertLength(tenants, 2);
|
|
6500
6830
|
|
|
@@ -6911,6 +7241,70 @@ describe("with one evolu instance", () => {
|
|
|
6911
7241
|
});
|
|
6912
7242
|
});
|
|
6913
7243
|
|
|
7244
|
+
it("coalesces sync rounds requested before a tab leader starts the DbWorker", async () => {
|
|
7245
|
+
const createWebSocket = testCreateWebSocket();
|
|
7246
|
+
await using setup = await setupSharedWorker({ createWebSocket });
|
|
7247
|
+
using disposer = new DisposableStack();
|
|
7248
|
+
const id = testCreateId()<"EvoluInstance">();
|
|
7249
|
+
await using _instanceLock = await setup.run.ok(acquireLeaderLock(id));
|
|
7250
|
+
using evoluChannel = testCreateMessageChannel<EvoluOutput, EvoluInput>();
|
|
7251
|
+
const transport = createOwnerWebSocketTransport({
|
|
7252
|
+
url: "wss://before-leader.example",
|
|
7253
|
+
ownerId: testAppOwner.id,
|
|
7254
|
+
});
|
|
7255
|
+
|
|
7256
|
+
// Without a tab leader, the tenant starts without a DbWorker.
|
|
7257
|
+
setup.worker.port.postMessage({
|
|
7258
|
+
type: "CreateEvolu",
|
|
7259
|
+
name: testName,
|
|
7260
|
+
id,
|
|
7261
|
+
consoleLevel: "debug",
|
|
7262
|
+
sqliteSchema: testSqliteSchema,
|
|
7263
|
+
encryptionKey: testAppOwner.encryptionKey,
|
|
7264
|
+
memoryOnly: false,
|
|
7265
|
+
evoluPort: evoluChannel.port1.native,
|
|
7266
|
+
});
|
|
7267
|
+
await testWaitForWorkerMessage();
|
|
7268
|
+
assertEqual(setup.sharedWorkerOutputs, []);
|
|
7269
|
+
|
|
7270
|
+
// The first claim through an open transport queues a round, and the
|
|
7271
|
+
// transport reopening requests the same round again. Nothing is
|
|
7272
|
+
// dispatched before a leader, so the queued round absorbs it.
|
|
7273
|
+
evoluChannel.port2.postMessage({
|
|
7274
|
+
type: "UseOwner",
|
|
7275
|
+
actions: [
|
|
7276
|
+
{
|
|
7277
|
+
action: "add",
|
|
7278
|
+
owner: { owner: testAppOwner, transports: [transport] },
|
|
7279
|
+
},
|
|
7280
|
+
],
|
|
7281
|
+
});
|
|
7282
|
+
await testWaitForWorkerMessage();
|
|
7283
|
+
createWebSocket.close(transport.url);
|
|
7284
|
+
createWebSocket.open(transport.url);
|
|
7285
|
+
await testWaitForWorkerMessage();
|
|
7286
|
+
|
|
7287
|
+
const init = await setupTabLeader(setup, disposer);
|
|
7288
|
+
const dbWorkerPort = disposer.use(
|
|
7289
|
+
testCreateMessagePort<DbWorkerOutput, DbWorkerInput>(init.port),
|
|
7290
|
+
);
|
|
7291
|
+
const dbInputs: Array<ExtractTyped<DbWorkerInput, "Request">> = [];
|
|
7292
|
+
dbWorkerPort.onMessage = (input) => {
|
|
7293
|
+
if (input.type === "Request") dbInputs.push(input);
|
|
7294
|
+
};
|
|
7295
|
+
dbWorkerPort.postMessage({
|
|
7296
|
+
type: "LeaderAcquired",
|
|
7297
|
+
clock: createTimestamp(),
|
|
7298
|
+
});
|
|
7299
|
+
await testWaitForWorkerMessage();
|
|
7300
|
+
|
|
7301
|
+
assertEqual(
|
|
7302
|
+
await respondToSyncRound({ dbInputs, dbWorkerPort }, createWebSocket),
|
|
7303
|
+
[transport.url],
|
|
7304
|
+
);
|
|
7305
|
+
assertLength(dbInputs, 1);
|
|
7306
|
+
});
|
|
7307
|
+
|
|
6914
7308
|
it("starts the next queued request after the first response arrives", async () => {
|
|
6915
7309
|
await using setup = await setupSharedWorker();
|
|
6916
7310
|
const { createEvolu, run } = setup;
|
|
@@ -7234,7 +7628,6 @@ describe("with one evolu instance", () => {
|
|
|
7234
7628
|
dbWorkerPort.postMessage({
|
|
7235
7629
|
clock: createTimestamp(),
|
|
7236
7630
|
type: "LeaderAcquired",
|
|
7237
|
-
name: testName,
|
|
7238
7631
|
});
|
|
7239
7632
|
await disposePromise;
|
|
7240
7633
|
await testWaitForWorkerMessage();
|
|
@@ -8235,7 +8628,7 @@ describe("with one evolu instance", () => {
|
|
|
8235
8628
|
]);
|
|
8236
8629
|
});
|
|
8237
8630
|
|
|
8238
|
-
it("
|
|
8631
|
+
it("uploads an unregistered sibling's write through every claimed transport, so later registrations start no round", async () => {
|
|
8239
8632
|
const createWebSocket = testCreateWebSocket();
|
|
8240
8633
|
await using setup = await setupSharedWorker({ createWebSocket });
|
|
8241
8634
|
const first = await setup.createEvolu();
|
|
@@ -8359,23 +8752,15 @@ describe("with one evolu instance", () => {
|
|
|
8359
8752
|
second.dbInputs.length = 0;
|
|
8360
8753
|
|
|
8361
8754
|
// The tenant already has a writable registration through A, and B's
|
|
8362
|
-
// global claim remains active.
|
|
8755
|
+
// global claim remains active. The sibling's registrations of B start no
|
|
8756
|
+
// round, because its write already went through B, and construct no
|
|
8757
|
+
// socket.
|
|
8363
8758
|
siblingChannel.port2.postMessage({
|
|
8364
8759
|
type: "UseOwner",
|
|
8365
|
-
actions: [
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
assertEqual(await respondToSyncRound(first, createWebSocket), [
|
|
8370
|
-
transportB.url,
|
|
8371
|
-
]);
|
|
8372
|
-
assertEqual(second.dbInputs, []);
|
|
8373
|
-
first.dbInputs.length = 0;
|
|
8374
|
-
|
|
8375
|
-
// Repeated use does not start another round or construct another socket.
|
|
8376
|
-
siblingChannel.port2.postMessage({
|
|
8377
|
-
type: "UseOwner",
|
|
8378
|
-
actions: [{ action: "add", owner: syncOwnerB }],
|
|
8760
|
+
actions: [
|
|
8761
|
+
{ action: "add", owner: syncOwnerB },
|
|
8762
|
+
{ action: "add", owner: syncOwnerB },
|
|
8763
|
+
],
|
|
8379
8764
|
});
|
|
8380
8765
|
await testWaitForWorkerMessage();
|
|
8381
8766
|
assertEqual(first.dbInputs, []);
|
|
@@ -9764,7 +10149,6 @@ describe("with one evolu instance", () => {
|
|
|
9764
10149
|
};
|
|
9765
10150
|
port.postMessage({
|
|
9766
10151
|
type: "LeaderAcquired",
|
|
9767
|
-
name: testName,
|
|
9768
10152
|
clock: committedClock,
|
|
9769
10153
|
});
|
|
9770
10154
|
await testWaitForWorkerMessage();
|
|
@@ -9915,7 +10299,6 @@ describe("with one evolu instance", () => {
|
|
|
9915
10299
|
};
|
|
9916
10300
|
dbWorkerPort.postMessage({
|
|
9917
10301
|
type: "LeaderAcquired",
|
|
9918
|
-
name: testName,
|
|
9919
10302
|
clock: createTimestamp(),
|
|
9920
10303
|
});
|
|
9921
10304
|
await testWaitForWorkerMessage();
|
|
@@ -9983,7 +10366,6 @@ describe("with one evolu instance", () => {
|
|
|
9983
10366
|
dbWorkerPort.postMessage({
|
|
9984
10367
|
clock: createTimestamp({ nodeId: maxNodeId }),
|
|
9985
10368
|
type: "LeaderAcquired",
|
|
9986
|
-
name: testName,
|
|
9987
10369
|
});
|
|
9988
10370
|
await testWaitForWorkerMessage();
|
|
9989
10371
|
// A replacement leader refreshes every instance's subscribed queries.
|
|
@@ -10116,7 +10498,6 @@ describe("with one evolu instance", () => {
|
|
|
10116
10498
|
const startupClock = createTimestamp({ millis: Millis.orThrow(1000) });
|
|
10117
10499
|
port.postMessage({
|
|
10118
10500
|
type: "LeaderAcquired",
|
|
10119
|
-
name: testName,
|
|
10120
10501
|
clock: startupClock,
|
|
10121
10502
|
});
|
|
10122
10503
|
await testWaitForWorkerMessage();
|
|
@@ -10257,7 +10638,6 @@ describe("with one evolu instance", () => {
|
|
|
10257
10638
|
replacement.use(await replacementLock);
|
|
10258
10639
|
replacementPort.postMessage({
|
|
10259
10640
|
type: "LeaderAcquired",
|
|
10260
|
-
name: testName,
|
|
10261
10641
|
clock: createTimestamp(),
|
|
10262
10642
|
});
|
|
10263
10643
|
await testWaitForWorkerMessage();
|
|
@@ -10846,7 +11226,6 @@ describe("startup refusal", () => {
|
|
|
10846
11226
|
const instance = await setup.createEvoluBeforeDbWorkerLeader();
|
|
10847
11227
|
instance.dbWorkerPort.postMessage({
|
|
10848
11228
|
type: "LeaderRefused",
|
|
10849
|
-
name: testName,
|
|
10850
11229
|
error: refusal,
|
|
10851
11230
|
});
|
|
10852
11231
|
await testWaitForWorkerMessage();
|
|
@@ -10884,7 +11263,6 @@ describe("startup refusal", () => {
|
|
|
10884
11263
|
|
|
10885
11264
|
instance.dbWorkerPort.postMessage({
|
|
10886
11265
|
type: "LeaderRefused",
|
|
10887
|
-
name: testName,
|
|
10888
11266
|
error: refusal,
|
|
10889
11267
|
});
|
|
10890
11268
|
await testWaitForWorkerMessage();
|
|
@@ -11008,7 +11386,6 @@ describe("startup refusal", () => {
|
|
|
11008
11386
|
};
|
|
11009
11387
|
leaderPort.postMessage({
|
|
11010
11388
|
type: "LeaderRefused",
|
|
11011
|
-
name: testName,
|
|
11012
11389
|
error: refusal,
|
|
11013
11390
|
});
|
|
11014
11391
|
await testWaitForWorkerMessage();
|
|
@@ -11055,13 +11432,11 @@ describe("startup refusal", () => {
|
|
|
11055
11432
|
|
|
11056
11433
|
instance.dbWorkerPort.postMessage({
|
|
11057
11434
|
type: "LeaderRefused",
|
|
11058
|
-
name: testName,
|
|
11059
11435
|
error: refusal,
|
|
11060
11436
|
});
|
|
11061
11437
|
await testWaitForWorkerMessage();
|
|
11062
11438
|
leaderPort.postMessage({
|
|
11063
11439
|
type: "LeaderAcquired",
|
|
11064
|
-
name: testName,
|
|
11065
11440
|
clock: createTimestamp(),
|
|
11066
11441
|
});
|
|
11067
11442
|
await testWaitForWorkerMessage();
|