@evolu/common 8.0.0-next.3 → 8.0.0-next.4

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 (73) hide show
  1. package/dist/src/Assert.d.ts +5 -5
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +4 -4
  4. package/dist/src/Identicon.js +4 -4
  5. package/dist/src/LockManager.d.ts +74 -0
  6. package/dist/src/LockManager.d.ts.map +1 -0
  7. package/dist/src/LockManager.js +103 -0
  8. package/dist/src/Microtask.d.ts.map +1 -1
  9. package/dist/src/Microtask.js +90 -26
  10. package/dist/src/Object.d.ts.map +1 -1
  11. package/dist/src/RefCount.d.ts.map +1 -1
  12. package/dist/src/RefCount.js +153 -68
  13. package/dist/src/Resource.d.ts +6 -6
  14. package/dist/src/Resource.d.ts.map +1 -1
  15. package/dist/src/Resource.js +211 -162
  16. package/dist/src/Schedule.d.ts +5 -5
  17. package/dist/src/Schedule.d.ts.map +1 -1
  18. package/dist/src/Schedule.js +53 -44
  19. package/dist/src/Sqlite.d.ts +2 -0
  20. package/dist/src/Sqlite.d.ts.map +1 -1
  21. package/dist/src/Sqlite.js +64 -40
  22. package/dist/src/Task.d.ts +6 -33
  23. package/dist/src/Task.d.ts.map +1 -1
  24. package/dist/src/Task.js +213 -204
  25. package/dist/src/Test.d.ts +4 -4
  26. package/dist/src/Time.d.ts +5 -0
  27. package/dist/src/Time.d.ts.map +1 -1
  28. package/dist/src/Time.js +5 -0
  29. package/dist/src/Type.d.ts +7 -0
  30. package/dist/src/Type.d.ts.map +1 -1
  31. package/dist/src/Type.js +7 -0
  32. package/dist/src/WebSocket.d.ts.map +1 -1
  33. package/dist/src/WebSocket.js +7 -6
  34. package/dist/src/Worker.d.ts +42 -8
  35. package/dist/src/Worker.d.ts.map +1 -1
  36. package/dist/src/Worker.js +292 -112
  37. package/dist/src/index.d.ts +1 -0
  38. package/dist/src/index.d.ts.map +1 -1
  39. package/dist/src/index.js +1 -0
  40. package/dist/src/local-first/Db.d.ts +5 -4
  41. package/dist/src/local-first/Db.d.ts.map +1 -1
  42. package/dist/src/local-first/Db.js +44 -38
  43. package/dist/src/local-first/Evolu.d.ts +3 -2
  44. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  45. package/dist/src/local-first/Evolu.js +90 -80
  46. package/dist/src/local-first/Owner.d.ts.map +1 -1
  47. package/dist/src/local-first/Shared.d.ts +65 -54
  48. package/dist/src/local-first/Shared.d.ts.map +1 -1
  49. package/dist/src/local-first/Shared.js +387 -373
  50. package/package.json +1 -1
  51. package/src/Array.ts +8 -8
  52. package/src/Assert.ts +5 -5
  53. package/src/Identicon.ts +4 -4
  54. package/src/LockManager.ts +181 -0
  55. package/src/Microtask.ts +17 -11
  56. package/src/Object.ts +3 -3
  57. package/src/RefCount.ts +25 -21
  58. package/src/Resource.ts +63 -56
  59. package/src/Schedule.ts +70 -48
  60. package/src/Set.ts +4 -4
  61. package/src/Sqlite.ts +41 -30
  62. package/src/Task.ts +77 -120
  63. package/src/Test.ts +4 -4
  64. package/src/Time.ts +7 -0
  65. package/src/Type.ts +9 -0
  66. package/src/WebSocket.ts +7 -6
  67. package/src/Worker.ts +195 -53
  68. package/src/index.ts +1 -0
  69. package/src/local-first/Db.ts +69 -46
  70. package/src/local-first/Evolu.ts +74 -68
  71. package/src/local-first/Owner.ts +1 -1
  72. package/src/local-first/Query.ts +1 -1
  73. package/src/local-first/Shared.ts +472 -464
package/src/Worker.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Platform-agnostic Worker abstractions.
2
+ * Typed, disposable worker and messaging primitives.
3
3
  *
4
4
  * @module
5
5
  */
6
6
 
7
- import { assert } from "./Assert.js";
7
+ import { assert, assertNotDisposed } from "./Assert.js";
8
8
  import type { Brand } from "./Brand.js";
9
9
  import type { ConsoleDep, ConsoleStoreOutputEntryDep } from "./Console.js";
10
10
  import { testWaitForMacrotask } from "./Test.js";
11
11
 
12
12
  /**
13
- * Platform-agnostic Worker.
13
+ * Typed, disposable Worker.
14
14
  *
15
15
  * @see https://developer.mozilla.org/en-US/docs/Web/API/Worker
16
16
  */
@@ -20,7 +20,7 @@ export interface Worker<Input, Output = never> extends MessagePort<
20
20
  > {}
21
21
 
22
22
  /**
23
- * Platform-agnostic SharedWorker.
23
+ * Typed, disposable SharedWorker.
24
24
  *
25
25
  * A shared worker is shared across multiple clients (tabs, windows, iframes)
26
26
  * and provides a port for bidirectional communication with each client.
@@ -33,7 +33,7 @@ export interface SharedWorker<Input, Output = never> extends Disposable {
33
33
  }
34
34
 
35
35
  /**
36
- * Platform-agnostic MessagePort.
36
+ * Typed, disposable MessagePort.
37
37
  *
38
38
  * Note: There is no reliable way to detect when a port is closed or
39
39
  * disconnected. Calling `postMessage` on a disposed port does not throw — it
@@ -114,7 +114,7 @@ export type WorkerDeps = ConsoleDep &
114
114
  CreateMessagePortDep;
115
115
 
116
116
  /**
117
- * Platform-agnostic MessageChannel.
117
+ * Typed, disposable MessageChannel.
118
118
  *
119
119
  * Creates two entangled ports: keep one and transfer the other (e.g., to a
120
120
  * SharedWorker via `postMessage` with `transfer`). Messages sent to one port
@@ -178,6 +178,31 @@ export interface CreateMessageChannelDep {
178
178
  readonly createMessageChannel: CreateMessageChannel;
179
179
  }
180
180
 
181
+ /**
182
+ * Typed, disposable BroadcastChannel.
183
+ *
184
+ * Broadcast channels deliver messages to other channels opened with the same
185
+ * name. The sending channel does not receive its own messages.
186
+ *
187
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
188
+ */
189
+ export interface BroadcastChannel<Input, Output = Input> extends Disposable {
190
+ /** Posts a message to other channels with the same name. */
191
+ readonly postMessage: (message: Input) => void;
192
+
193
+ /** Handler for incoming broadcast messages. */
194
+ onMessage: ((message: Output) => void) | null;
195
+ }
196
+
197
+ /** Factory function to create a {@link BroadcastChannel}. */
198
+ export type CreateBroadcastChannel = <Input, Output = Input>(
199
+ name: string,
200
+ ) => BroadcastChannel<Input, Output>;
201
+
202
+ export interface CreateBroadcastChannelDep {
203
+ readonly createBroadcastChannel: CreateBroadcastChannel;
204
+ }
205
+
181
206
  /**
182
207
  * Typed `self` for code running inside a dedicated worker.
183
208
  *
@@ -250,17 +275,17 @@ export const createMessageChannel: CreateMessageChannel = <
250
275
  handler: null,
251
276
  queue: [],
252
277
  flushScheduled: false,
253
- refCount: 0,
278
+ flushTimeoutId: null,
279
+ ownerCount: 0,
254
280
  pendingTransferCount: 0,
255
- closed: false,
256
281
  };
257
282
  const state2: PortState<Input> = {
258
283
  handler: null,
259
284
  queue: [],
260
285
  flushScheduled: false,
261
- refCount: 0,
286
+ flushTimeoutId: null,
287
+ ownerCount: 0,
262
288
  pendingTransferCount: 0,
263
- closed: false,
264
289
  };
265
290
 
266
291
  const native1 = createNativeMessagePortToken<Input, Output>();
@@ -277,8 +302,11 @@ export const createMessageChannel: CreateMessageChannel = <
277
302
  native: native2,
278
303
  };
279
304
 
280
- const port1 = createTestPort(port1Registration);
281
- const port2 = createTestPort(port2Registration);
305
+ using disposer = new DisposableStack();
306
+
307
+ const port1 = disposer.use(createTestPort(port1Registration));
308
+ const port2 = disposer.use(createTestPort(port2Registration));
309
+ const disposables = disposer.move();
282
310
 
283
311
  nativePortRegistry.set(native1, port1Registration);
284
312
  nativePortRegistry.set(native2, port2Registration);
@@ -286,10 +314,7 @@ export const createMessageChannel: CreateMessageChannel = <
286
314
  return {
287
315
  port1,
288
316
  port2,
289
- [Symbol.dispose]: () => {
290
- port1[Symbol.dispose]();
291
- port2[Symbol.dispose]();
292
- },
317
+ [Symbol.dispose]: () => disposables.dispose(),
293
318
  };
294
319
  };
295
320
 
@@ -307,6 +332,75 @@ export const createMessagePort: CreateMessagePort = <Input, Output = never>(
307
332
  return createTestPort(registration as PortRegistration<Input, Output>);
308
333
  };
309
334
 
335
+ /**
336
+ * Creates an in-memory {@link BroadcastChannel}.
337
+ *
338
+ * This is a memory-only fallback for platforms without native BroadcastChannel
339
+ * support. Message delivery is asynchronous in-process.
340
+ */
341
+ export const createBroadcastChannel: CreateBroadcastChannel = <
342
+ Input,
343
+ Output = Input,
344
+ >(
345
+ name: string,
346
+ ): BroadcastChannel<Input, Output> => {
347
+ let onMessageHandler: ((message: Output) => void) | null = null;
348
+ const dispatch: BroadcastChannelDispatch<Output> = (message) => {
349
+ onMessageHandler?.(message);
350
+ };
351
+
352
+ let dispatches = broadcastChannelDispatchesByName.get(name);
353
+ if (!dispatches) {
354
+ dispatches = new Set();
355
+ broadcastChannelDispatchesByName.set(name, dispatches);
356
+ }
357
+ dispatches.add(dispatch as BroadcastChannelDispatch<any>);
358
+
359
+ using disposer = new DisposableStack();
360
+
361
+ disposer.defer(() => {
362
+ onMessageHandler = null;
363
+
364
+ dispatches.delete(dispatch as BroadcastChannelDispatch<any>);
365
+ if (dispatches.size === 0) broadcastChannelDispatchesByName.delete(name);
366
+ });
367
+
368
+ const disposables = disposer.move();
369
+
370
+ return {
371
+ postMessage: (message) => {
372
+ assertNotDisposed(disposables);
373
+
374
+ const dispatches = broadcastChannelDispatchesByName.get(name);
375
+ assert(dispatches, "Expected broadcast channel dispatches");
376
+
377
+ for (const otherDispatch of [...dispatches]) {
378
+ if (otherDispatch === dispatch) continue;
379
+
380
+ globalThis.setTimeout(() => {
381
+ if (!dispatches.has(otherDispatch)) return;
382
+ otherDispatch(message);
383
+ }, 0);
384
+ }
385
+ },
386
+ get onMessage() {
387
+ return disposables.disposed ? null : onMessageHandler;
388
+ },
389
+ set onMessage(fn) {
390
+ if (disposables.disposed) return;
391
+ onMessageHandler = fn;
392
+ },
393
+ [Symbol.dispose]: () => disposables.dispose(),
394
+ };
395
+ };
396
+
397
+ type BroadcastChannelDispatch<T> = (message: T) => void;
398
+
399
+ const broadcastChannelDispatchesByName = new Map<
400
+ string,
401
+ Set<BroadcastChannelDispatch<any>>
402
+ >();
403
+
310
404
  /**
311
405
  * Test {@link Worker} with access to its paired worker-side `self`.
312
406
  *
@@ -342,6 +436,14 @@ export interface TestMessageChannel<
342
436
  readonly isDisposed: () => boolean;
343
437
  }
344
438
 
439
+ /** {@link BroadcastChannel} with disposal tracking for testing. */
440
+ export interface TestBroadcastChannel<
441
+ Input,
442
+ Output = Input,
443
+ > extends BroadcastChannel<Input, Output> {
444
+ readonly isDisposed: () => boolean;
445
+ }
446
+
345
447
  /**
346
448
  * Creates a connected {@link TestWorker} for testing.
347
449
  *
@@ -358,7 +460,7 @@ export const testCreateWorker = <Input, Output = never>(): TestWorker<
358
460
  self = nextSelf;
359
461
  });
360
462
 
361
- return Object.assign(worker, { self }) as TestWorker<Input, Output>;
463
+ return Object.assign(worker, { self });
362
464
  };
363
465
 
364
466
  /**
@@ -378,7 +480,7 @@ export const testCreateSharedWorker = <
378
480
  return Object.assign(worker, {
379
481
  self,
380
482
  connect,
381
- }) as TestSharedWorker<Input, Output>;
483
+ });
382
484
  };
383
485
 
384
486
  /**
@@ -394,18 +496,16 @@ export const testCreateMessageChannel = <
394
496
  Input,
395
497
  Output = never,
396
498
  >(): TestMessageChannel<Input, Output> => {
397
- const channel = createMessageChannel<Input, Output>();
499
+ using disposer = new DisposableStack();
398
500
 
399
- let disposed = false;
501
+ const channel = disposer.use(createMessageChannel<Input, Output>());
502
+ const disposables = disposer.move();
400
503
 
401
504
  return {
402
505
  port1: channel.port1,
403
506
  port2: channel.port2,
404
- isDisposed: () => disposed,
405
- [Symbol.dispose]: () => {
406
- disposed = true;
407
- channel[Symbol.dispose]();
408
- },
507
+ isDisposed: () => disposables.disposed,
508
+ [Symbol.dispose]: () => disposables.dispose(),
409
509
  };
410
510
  };
411
511
 
@@ -414,11 +514,35 @@ export const testCreateMessagePort: CreateMessagePort = <Input, Output = never>(
414
514
  nativePort: NativeMessagePort<Input, Output>,
415
515
  ): MessagePort<Input, Output> => createMessagePort(nativePort);
416
516
 
517
+ /** Creates an in-memory {@link BroadcastChannel} for testing. */
518
+ export const testCreateBroadcastChannel = <Input, Output = Input>(
519
+ name: string,
520
+ ): TestBroadcastChannel<Input, Output> => {
521
+ using disposer = new DisposableStack();
522
+
523
+ const channel = disposer.use(createBroadcastChannel<Input, Output>(name));
524
+ const disposables = disposer.move();
525
+
526
+ return {
527
+ postMessage: channel.postMessage,
528
+ get onMessage() {
529
+ return channel.onMessage;
530
+ },
531
+ set onMessage(value) {
532
+ channel.onMessage = value;
533
+ },
534
+ isDisposed: () => disposables.disposed,
535
+ [Symbol.dispose]: () => disposables.dispose(),
536
+ };
537
+ };
538
+
417
539
  /**
418
- * Waits long enough for multi-hop in-memory worker message delivery in tests.
540
+ * Waits the minimal time necessary for in-memory worker message delivery in
541
+ * tests.
419
542
  *
420
- * Some flows require several queued macrotasks across worker/message-port
421
- * boundaries; this helper advances that pipeline deterministically.
543
+ * This is better than `vi.waitFor` when a test only needs to flush the known
544
+ * in-memory worker/message-port pipeline. It encodes that transport boundary
545
+ * once, avoids polling, and keeps worker-focused tests fast and explicit.
422
546
  */
423
547
  export const testWaitForWorkerMessage = async (): Promise<void> => {
424
548
  await testWaitForMacrotask();
@@ -498,9 +622,9 @@ interface PortState<T> {
498
622
  handler: ((message: T) => void) | null;
499
623
  readonly queue: Array<T>;
500
624
  flushScheduled: boolean;
501
- refCount: number;
625
+ flushTimeoutId: ReturnType<typeof globalThis.setTimeout> | null;
626
+ ownerCount: number;
502
627
  pendingTransferCount: number;
503
- closed: boolean;
504
628
  }
505
629
 
506
630
  interface PortRegistration<Input, Output> {
@@ -514,23 +638,46 @@ const createPort = <Input, Output>({
514
638
  peerReceive,
515
639
  native,
516
640
  }: PortRegistration<Input, Output>): MessagePort<Input, Output> => {
641
+ using disposer = new DisposableStack();
642
+
517
643
  if (receive.pendingTransferCount > 0) receive.pendingTransferCount -= 1;
518
- receive.refCount += 1;
519
- let isDisposed = false;
644
+ else receive.ownerCount += 1;
645
+
646
+ disposer.defer(() => {
647
+ receive.ownerCount -= 1;
648
+ if (receive.ownerCount > 0) return;
649
+
650
+ nativePortRegistry.delete(native);
651
+
652
+ if (receive.flushTimeoutId != null) {
653
+ globalThis.clearTimeout(receive.flushTimeoutId);
654
+ receive.flushTimeoutId = null;
655
+ }
656
+
657
+ receive.handler = null;
658
+ receive.flushScheduled = false;
659
+ receive.queue.length = 0;
660
+ });
661
+
662
+ const disposables = disposer.move();
520
663
 
521
664
  const scheduleFlush = (state: PortState<any>): void => {
522
- if (state.flushScheduled || state.closed) return;
665
+ if (state.flushScheduled || state.ownerCount === 0) return;
523
666
  state.flushScheduled = true;
524
667
 
525
668
  // Native worker messages are task-queued; use macrotask timing.
526
- setTimeout(() => {
669
+ state.flushTimeoutId = globalThis.setTimeout(() => {
670
+ state.flushTimeoutId = null;
527
671
  state.flushScheduled = false;
528
- if (state.closed) return;
672
+ if (state.ownerCount === 0) return;
529
673
 
530
- const handler = state.handler;
531
- if (!handler) return;
674
+ const firstHandler = state.handler;
675
+ if (!firstHandler) return;
532
676
 
533
677
  for (const message of state.queue.splice(0)) {
678
+ const handler = state.handler;
679
+ if (!handler) return;
680
+
534
681
  handler(message);
535
682
  }
536
683
  }, 0);
@@ -538,41 +685,36 @@ const createPort = <Input, Output>({
538
685
 
539
686
  return {
540
687
  postMessage: (message, transfer) => {
688
+ if (
689
+ disposables.disposed ||
690
+ receive.ownerCount === 0 ||
691
+ peerReceive.ownerCount === 0
692
+ )
693
+ return;
694
+
541
695
  for (const transferable of transfer ?? []) {
542
696
  if (transferable instanceof globalThis.ArrayBuffer) continue;
543
697
 
544
698
  const registration = nativePortRegistry.get(transferable);
545
699
  if (!registration) continue;
546
700
 
701
+ registration.receive.ownerCount += 1;
547
702
  registration.receive.pendingTransferCount += 1;
548
- registration.receive.closed = false;
549
703
  }
550
704
 
551
- if (peerReceive.closed) return;
552
705
  peerReceive.queue.push(message);
553
706
  scheduleFlush(peerReceive);
554
707
  },
555
708
  get onMessage() {
556
- return isDisposed ? null : receive.handler;
709
+ return disposables.disposed ? null : receive.handler;
557
710
  },
558
711
  set onMessage(fn) {
559
- if (isDisposed || receive.closed) return;
712
+ if (disposables.disposed || receive.ownerCount === 0) return;
560
713
  receive.handler = fn;
561
714
  if (fn) scheduleFlush(receive);
562
715
  },
563
716
  native,
564
- [Symbol.dispose]: () => {
565
- if (isDisposed) return;
566
- isDisposed = true;
567
-
568
- receive.refCount -= 1;
569
- if (receive.refCount > 0 || receive.pendingTransferCount > 0) return;
570
-
571
- receive.closed = true;
572
- receive.handler = null;
573
- receive.flushScheduled = false;
574
- receive.queue.length = 0;
575
- },
717
+ [Symbol.dispose]: () => disposables.dispose(),
576
718
  };
577
719
  };
578
720
 
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export * from "./Eq.js";
17
17
  export * from "./Error.js";
18
18
  export * from "./Function.js";
19
19
  export * from "./Identicon.js";
20
+ export * from "./LockManager.js";
20
21
  export * from "./Lookup.js";
21
22
  export * from "./Microtask.js";
22
23
  export * from "./Number.js";
@@ -18,6 +18,8 @@ import {
18
18
  type RandomBytesDep,
19
19
  } from "../Crypto.js";
20
20
  import { exhaustiveCheck, lazyFalse, lazyVoid } from "../Function.js";
21
+ import type { LockManagerDep } from "../LockManager.js";
22
+ import { acquireLeaderLock } from "../LockManager.js";
21
23
  import { createRecord, getProperty, objectToEntries } from "../Object.js";
22
24
  import { ok, type Result } from "../Result.js";
23
25
  import type {
@@ -35,7 +37,7 @@ import {
35
37
  sqliteQueryStringToSqliteQuery,
36
38
  SqliteValue,
37
39
  } from "../Sqlite.js";
38
- import { callback, type LeaderLockDep, type Task } from "../Task.js";
40
+ import { callback, type Task } from "../Task.js";
39
41
  import { Millis, millisToDateIso, type TimeDep } from "../Time.js";
40
42
  import type { Name } from "../Type.js";
41
43
  import {
@@ -47,6 +49,7 @@ import {
47
49
  } from "../Type.js";
48
50
  import type { ExtractType } from "../Types.js";
49
51
  import type {
52
+ CreateBroadcastChannelDep,
50
53
  NativeMessagePort,
51
54
  Worker,
52
55
  WorkerDeps,
@@ -72,7 +75,14 @@ import {
72
75
  getEvoluSqliteSchema,
73
76
  systemColumns,
74
77
  } from "./Schema.js";
75
- import type { DbWorkerInput, DbWorkerOutput } from "./Shared.js";
78
+ import type {
79
+ ConsoleEntryOrError,
80
+ DbWorkerInput,
81
+ DbWorkerOutput,
82
+ DbWorkerQueuedResponse,
83
+ DbWorkerRequest,
84
+ } from "./Shared.js";
85
+ import { consoleEntryOrErrorBroadcastChannelName } from "./Shared.js";
76
86
  import {
77
87
  createBaseSqliteStorage,
78
88
  createBaseSqliteStorageTables,
@@ -105,7 +115,7 @@ import {
105
115
  export type DbWorker = Worker<DbWorkerInit>;
106
116
 
107
117
  export interface DbWorkerInit {
108
- readonly type: "Init";
118
+ readonly type: "DbWorkerInit";
109
119
  readonly name: Name;
110
120
  readonly consoleLevel: ConsoleLevel;
111
121
  readonly sqliteSchema: SqliteSchema;
@@ -120,18 +130,22 @@ export interface CreateDbWorkerDep {
120
130
  readonly createDbWorker: CreateDbWorker;
121
131
  }
122
132
 
123
- export type DbWorkerDeps = WorkerDeps & LeaderLockDep & CreateSqliteDriverDep;
133
+ export type DbWorkerDeps = WorkerDeps &
134
+ CreateBroadcastChannelDep &
135
+ LockManagerDep &
136
+ CreateSqliteDriverDep;
124
137
 
125
138
  export const startDbWorker =
126
139
  (self: WorkerSelf<DbWorkerInit>): Task<void, never, DbWorkerDeps> =>
127
- async (_run) => {
128
- await using stack = new AsyncDisposableStack();
129
- const run = stack.use(_run.create());
130
- const { deps } = run;
140
+ async (run) => {
141
+ await using disposer = new AsyncDisposableStack();
142
+ disposer.use(self);
143
+ const dbWorkerRun = disposer.use(run.create());
144
+ const { deps } = dbWorkerRun;
131
145
 
132
146
  let initialized = false;
133
147
 
134
- const initMessage = await run.orThrow(
148
+ const initMessage = await dbWorkerRun.orThrow(
135
149
  callback<DbWorkerInit>(({ ok }) => {
136
150
  self.onMessage = (message) => {
137
151
  assert(!initialized, "DbWorker must be initialized only once");
@@ -143,26 +157,36 @@ export const startDbWorker =
143
157
 
144
158
  const console = deps.console.child(initMessage.name).child("DbWorker");
145
159
  console.setLevel(initMessage.consoleLevel);
146
- console.info("start DbWorker");
147
160
 
148
- const port = stack.use(
161
+ const port = disposer.use(
149
162
  deps.createMessagePort<DbWorkerOutput, DbWorkerInput>(initMessage.port),
150
163
  );
164
+ const consoleEntryOrErrorBroadcastChannel = disposer.use(
165
+ deps.createBroadcastChannel<ConsoleEntryOrError>(
166
+ consoleEntryOrErrorBroadcastChannelName,
167
+ ),
168
+ );
151
169
 
152
- stack.defer(
170
+ disposer.defer(
153
171
  deps.consoleStoreOutputEntry.subscribe(() => {
154
172
  const entry = deps.consoleStoreOutputEntry.get();
155
- if (entry) port.postMessage({ type: "OnConsoleEntry", entry });
173
+ if (entry)
174
+ consoleEntryOrErrorBroadcastChannel.postMessage({
175
+ type: "ConsoleEntry",
176
+ entry,
177
+ });
156
178
  }),
157
179
  );
180
+ console.debug("createDbWorker");
158
181
 
159
- stack.use(await run.orThrow(deps.leaderLock.lock(initMessage.name)));
160
- if (stack.disposed) return ok();
182
+ disposer.use(
183
+ await dbWorkerRun.orThrow(acquireLeaderLock(initMessage.name)),
184
+ );
161
185
 
162
186
  port.postMessage({ type: "LeaderAcquired", name: initMessage.name });
163
187
 
164
- const sqlite = stack.use(
165
- await run.orThrow(
188
+ const sqlite = disposer.use(
189
+ await dbWorkerRun.orThrow(
166
190
  createSqlite(
167
191
  initMessage.name,
168
192
  initMessage.memoryOnly
@@ -173,10 +197,13 @@ export const startDbWorker =
173
197
  );
174
198
  console.debug("SQLite created");
175
199
 
176
- const baseSqliteStorage = createBaseSqliteStorage({ sqlite, ...run.deps });
200
+ const baseSqliteStorage = createBaseSqliteStorage({
201
+ sqlite,
202
+ ...dbWorkerRun.deps,
203
+ });
177
204
 
178
205
  const dbDeps = {
179
- ...run.deps,
206
+ ...dbWorkerRun.deps,
180
207
  sqlite,
181
208
  sqliteSchema: initMessage.sqliteSchema,
182
209
  baseSqliteStorage,
@@ -195,33 +222,26 @@ export const startDbWorker =
195
222
 
196
223
  const storage = createClientStorage({ ...dbDeps, clock })({
197
224
  onError: (error) => {
198
- port.postMessage({ type: "OnError", error });
225
+ consoleEntryOrErrorBroadcastChannel.postMessage({
226
+ type: "Error",
227
+ error,
228
+ });
199
229
  },
200
230
  });
201
231
 
202
- // TODO: Call on dispose message.
203
- const _moved = stack.move();
204
- const runWithStorage = run.addDeps({ storage });
232
+ const disposables = disposer.move();
233
+ const dbWorkerRunWithStorage = dbWorkerRun.addDeps({ storage });
205
234
 
206
- /**
207
- * SharedWorker retries until some leader replies, so this worker must
208
- * ignore callback IDs it already completed.
209
- *
210
- * The Set intentionally lives for one leader's lifetime. If that leader tab
211
- * closes, retries go to a new leader with a fresh Set. Revisit this only if
212
- * memory pressure shows up; even millions of short callback IDs are
213
- * acceptable here, and naive eviction could let a still-retried request run
214
- * again.
215
- */
216
- const processedRequestIds = new Set<Id>();
235
+ port.onMessage = (input) => {
236
+ if (input.type === "Dispose") {
237
+ if (!disposables.disposed) console.debug("disposeDbWorker");
238
+ void disposables.disposeAsync();
239
+ return;
240
+ }
217
241
 
218
- port.onMessage = ({ callbackId, request }) => {
219
- if (processedRequestIds.has(callbackId)) return;
220
- processedRequestIds.add(callbackId);
242
+ const { callbackId, request } = input;
221
243
 
222
- const postQueuedResponse = (
223
- response: ExtractType<DbWorkerOutput, "OnQueuedResponse">["response"],
224
- ): void => {
244
+ const postQueuedResponse = (response: DbWorkerQueuedResponse): void => {
225
245
  port.postMessage(
226
246
  { type: "OnQueuedResponse", callbackId, response },
227
247
  response.type === "ForEvolu" && response.message.type === "Export"
@@ -238,11 +258,14 @@ export const startDbWorker =
238
258
  request.message,
239
259
  );
240
260
  if (!result.ok) {
241
- port.postMessage({ type: "OnError", error: result.error });
261
+ consoleEntryOrErrorBroadcastChannel.postMessage({
262
+ type: "Error",
263
+ error: result.error,
264
+ });
242
265
  } else {
243
266
  postQueuedResponse({
244
267
  type: "ForEvolu",
245
- evoluPortId: request.evoluPortId,
268
+ id: request.id,
246
269
  message: result.value,
247
270
  });
248
271
  }
@@ -252,7 +275,7 @@ export const startDbWorker =
252
275
  case "Query":
253
276
  postQueuedResponse({
254
277
  type: "ForEvolu",
255
- evoluPortId: request.evoluPortId,
278
+ id: request.id,
256
279
  message: {
257
280
  type: "Query",
258
281
  rowsByQuery: loadQueries(dbDeps)(request.message.queries),
@@ -263,7 +286,7 @@ export const startDbWorker =
263
286
  case "Export":
264
287
  postQueuedResponse({
265
288
  type: "ForEvolu",
266
- evoluPortId: request.evoluPortId,
289
+ id: request.id,
267
290
  message: {
268
291
  type: "Export",
269
292
  file: dbDeps.sqlite.export(),
@@ -307,7 +330,7 @@ export const startDbWorker =
307
330
  case "ApplySyncMessage": {
308
331
  const { owner, inputMessage } = request.message;
309
332
 
310
- runWithStorage<void, never>(async (run) => {
333
+ dbWorkerRunWithStorage<void, never>(async (run) => {
311
334
  storage.setRequestContext(owner.encryptionKey);
312
335
 
313
336
  const result = await run(
@@ -777,7 +800,7 @@ const handleMutation =
777
800
  ) =>
778
801
  (
779
802
  message: ExtractType<
780
- ExtractType<DbWorkerInput["request"], "ForEvolu">["message"],
803
+ ExtractType<DbWorkerRequest, "ForEvolu">["message"],
781
804
  "Mutate"
782
805
  >,
783
806
  ): Result<