@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.
Files changed (41) hide show
  1. package/dist/src/Console.d.ts +24 -46
  2. package/dist/src/Console.d.ts.map +1 -1
  3. package/dist/src/Console.js +34 -29
  4. package/dist/src/Crypto.d.ts +21 -3
  5. package/dist/src/Crypto.d.ts.map +1 -1
  6. package/dist/src/Crypto.js +18 -2
  7. package/dist/src/Error.d.ts +10 -6
  8. package/dist/src/Error.d.ts.map +1 -1
  9. package/dist/src/Error.js +52 -20
  10. package/dist/src/Sqlite.d.ts.map +1 -1
  11. package/dist/src/Sqlite.js +16 -12
  12. package/dist/src/Task.d.ts +3 -1
  13. package/dist/src/Task.d.ts.map +1 -1
  14. package/dist/src/Task.js +15 -13
  15. package/dist/src/Worker.d.ts +3 -0
  16. package/dist/src/Worker.d.ts.map +1 -1
  17. package/dist/src/Worker.js +6 -1
  18. package/dist/src/local-first/Db.d.ts.map +1 -1
  19. package/dist/src/local-first/Db.js +7 -7
  20. package/dist/src/local-first/Owner.d.ts +20 -8
  21. package/dist/src/local-first/Owner.d.ts.map +1 -1
  22. package/dist/src/local-first/Owner.js +23 -10
  23. package/dist/src/local-first/Schema.d.ts +1 -1
  24. package/dist/src/local-first/Shared.d.ts +55 -39
  25. package/dist/src/local-first/Shared.d.ts.map +1 -1
  26. package/dist/src/local-first/Shared.js +264 -318
  27. package/package.json +1 -1
  28. package/src/Console.test.ts +75 -247
  29. package/src/Console.ts +68 -110
  30. package/src/Crypto.ts +23 -3
  31. package/src/Error.test.ts +85 -0
  32. package/src/Error.ts +48 -22
  33. package/src/Sqlite.ts +19 -11
  34. package/src/Task.test.ts +46 -0
  35. package/src/Task.ts +17 -13
  36. package/src/Worker.ts +6 -1
  37. package/src/local-first/Db.ts +12 -8
  38. package/src/local-first/Owner.test.ts +52 -1
  39. package/src/local-first/Owner.ts +26 -7
  40. package/src/local-first/Shared.test.ts +414 -39
  41. 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: "debug",
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", name: testName, error });
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: null,
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: null,
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, 7);
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("reconciles a tenant's later writes when it first uses an already claimed transport", async () => {
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. Its first local use of B needs a new round.
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: [{ action: "add", owner: syncOwnerB }],
8366
- });
8367
- await testWaitForWorkerMessage();
8368
- assertSame(first.dbInputs.length, 1);
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();