@evolu/common 8.0.0-next.2 → 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 +298 -75
  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 +240 -49
  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,29 +275,46 @@ export const createMessageChannel: CreateMessageChannel = <
250
275
  handler: null,
251
276
  queue: [],
252
277
  flushScheduled: false,
278
+ flushTimeoutId: null,
279
+ ownerCount: 0,
280
+ pendingTransferCount: 0,
253
281
  };
254
282
  const state2: PortState<Input> = {
255
283
  handler: null,
256
284
  queue: [],
257
285
  flushScheduled: false,
286
+ flushTimeoutId: null,
287
+ ownerCount: 0,
288
+ pendingTransferCount: 0,
258
289
  };
259
290
 
260
291
  const native1 = createNativeMessagePortToken<Input, Output>();
261
292
  const native2 = createNativeMessagePortToken<Output, Input>();
262
293
 
263
- const port1 = createTestPort<Input, Output>(state1, state2, native1);
264
- const port2 = createTestPort<Output, Input>(state2, state1, native2);
294
+ const port1Registration: PortRegistration<Input, Output> = {
295
+ receive: state1,
296
+ peerReceive: state2,
297
+ native: native1,
298
+ };
299
+ const port2Registration: PortRegistration<Output, Input> = {
300
+ receive: state2,
301
+ peerReceive: state1,
302
+ native: native2,
303
+ };
304
+
305
+ using disposer = new DisposableStack();
265
306
 
266
- nativePortRegistry.set(native1, port1);
267
- nativePortRegistry.set(native2, port2);
307
+ const port1 = disposer.use(createTestPort(port1Registration));
308
+ const port2 = disposer.use(createTestPort(port2Registration));
309
+ const disposables = disposer.move();
310
+
311
+ nativePortRegistry.set(native1, port1Registration);
312
+ nativePortRegistry.set(native2, port2Registration);
268
313
 
269
314
  return {
270
315
  port1,
271
316
  port2,
272
- [Symbol.dispose]: () => {
273
- port1[Symbol.dispose]();
274
- port2[Symbol.dispose]();
275
- },
317
+ [Symbol.dispose]: () => disposables.dispose(),
276
318
  };
277
319
  };
278
320
 
@@ -285,11 +327,80 @@ export const createMessageChannel: CreateMessageChannel = <
285
327
  export const createMessagePort: CreateMessagePort = <Input, Output = never>(
286
328
  nativePort: NativeMessagePort<Input, Output>,
287
329
  ): MessagePort<Input, Output> => {
288
- const pair = nativePortRegistry.get(nativePort);
289
- assert(pair, "Unknown native port — did you transfer it?");
290
- return pair as MessagePort<Input, Output>;
330
+ const registration = nativePortRegistry.get(nativePort);
331
+ assert(registration, "Unknown native port — did you transfer it?");
332
+ return createTestPort(registration as PortRegistration<Input, Output>);
333
+ };
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
+ };
291
395
  };
292
396
 
397
+ type BroadcastChannelDispatch<T> = (message: T) => void;
398
+
399
+ const broadcastChannelDispatchesByName = new Map<
400
+ string,
401
+ Set<BroadcastChannelDispatch<any>>
402
+ >();
403
+
293
404
  /**
294
405
  * Test {@link Worker} with access to its paired worker-side `self`.
295
406
  *
@@ -325,6 +436,14 @@ export interface TestMessageChannel<
325
436
  readonly isDisposed: () => boolean;
326
437
  }
327
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
+
328
447
  /**
329
448
  * Creates a connected {@link TestWorker} for testing.
330
449
  *
@@ -341,7 +460,7 @@ export const testCreateWorker = <Input, Output = never>(): TestWorker<
341
460
  self = nextSelf;
342
461
  });
343
462
 
344
- return Object.assign(worker, { self }) as TestWorker<Input, Output>;
463
+ return Object.assign(worker, { self });
345
464
  };
346
465
 
347
466
  /**
@@ -361,7 +480,7 @@ export const testCreateSharedWorker = <
361
480
  return Object.assign(worker, {
362
481
  self,
363
482
  connect,
364
- }) as TestSharedWorker<Input, Output>;
483
+ });
365
484
  };
366
485
 
367
486
  /**
@@ -377,18 +496,16 @@ export const testCreateMessageChannel = <
377
496
  Input,
378
497
  Output = never,
379
498
  >(): TestMessageChannel<Input, Output> => {
380
- const channel = createMessageChannel<Input, Output>();
499
+ using disposer = new DisposableStack();
381
500
 
382
- let disposed = false;
501
+ const channel = disposer.use(createMessageChannel<Input, Output>());
502
+ const disposables = disposer.move();
383
503
 
384
504
  return {
385
505
  port1: channel.port1,
386
506
  port2: channel.port2,
387
- isDisposed: () => disposed,
388
- [Symbol.dispose]: () => {
389
- disposed = true;
390
- channel[Symbol.dispose]();
391
- },
507
+ isDisposed: () => disposables.disposed,
508
+ [Symbol.dispose]: () => disposables.dispose(),
392
509
  };
393
510
  };
394
511
 
@@ -397,11 +514,35 @@ export const testCreateMessagePort: CreateMessagePort = <Input, Output = never>(
397
514
  nativePort: NativeMessagePort<Input, Output>,
398
515
  ): MessagePort<Input, Output> => createMessagePort(nativePort);
399
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
+
400
539
  /**
401
- * 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.
402
542
  *
403
- * Some flows require several queued macrotasks across worker/message-port
404
- * 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.
405
546
  */
406
547
  export const testWaitForWorkerMessage = async (): Promise<void> => {
407
548
  await testWaitForMacrotask();
@@ -481,55 +622,105 @@ interface PortState<T> {
481
622
  handler: ((message: T) => void) | null;
482
623
  readonly queue: Array<T>;
483
624
  flushScheduled: boolean;
625
+ flushTimeoutId: ReturnType<typeof globalThis.setTimeout> | null;
626
+ ownerCount: number;
627
+ pendingTransferCount: number;
484
628
  }
485
629
 
486
- const createPort = <Input, Output>(
487
- receive: PortState<Output>,
488
- peerReceive: PortState<Input>,
489
- native: NativeMessagePort<Input, Output>,
490
- ): MessagePort<Input, Output> => {
630
+ interface PortRegistration<Input, Output> {
631
+ readonly receive: PortState<Output>;
632
+ readonly peerReceive: PortState<Input>;
633
+ readonly native: NativeMessagePort<Input, Output>;
634
+ }
635
+
636
+ const createPort = <Input, Output>({
637
+ receive,
638
+ peerReceive,
639
+ native,
640
+ }: PortRegistration<Input, Output>): MessagePort<Input, Output> => {
641
+ using disposer = new DisposableStack();
642
+
643
+ if (receive.pendingTransferCount > 0) receive.pendingTransferCount -= 1;
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();
663
+
491
664
  const scheduleFlush = (state: PortState<any>): void => {
492
- if (state.flushScheduled) return;
665
+ if (state.flushScheduled || state.ownerCount === 0) return;
493
666
  state.flushScheduled = true;
494
667
 
495
668
  // Native worker messages are task-queued; use macrotask timing.
496
- setTimeout(() => {
669
+ state.flushTimeoutId = globalThis.setTimeout(() => {
670
+ state.flushTimeoutId = null;
497
671
  state.flushScheduled = false;
672
+ if (state.ownerCount === 0) return;
498
673
 
499
- const handler = state.handler;
500
- if (!handler) return;
674
+ const firstHandler = state.handler;
675
+ if (!firstHandler) return;
501
676
 
502
677
  for (const message of state.queue.splice(0)) {
678
+ const handler = state.handler;
679
+ if (!handler) return;
680
+
503
681
  handler(message);
504
682
  }
505
683
  }, 0);
506
684
  };
507
685
 
508
686
  return {
509
- postMessage: (message) => {
687
+ postMessage: (message, transfer) => {
688
+ if (
689
+ disposables.disposed ||
690
+ receive.ownerCount === 0 ||
691
+ peerReceive.ownerCount === 0
692
+ )
693
+ return;
694
+
695
+ for (const transferable of transfer ?? []) {
696
+ if (transferable instanceof globalThis.ArrayBuffer) continue;
697
+
698
+ const registration = nativePortRegistry.get(transferable);
699
+ if (!registration) continue;
700
+
701
+ registration.receive.ownerCount += 1;
702
+ registration.receive.pendingTransferCount += 1;
703
+ }
704
+
510
705
  peerReceive.queue.push(message);
511
706
  scheduleFlush(peerReceive);
512
707
  },
513
708
  get onMessage() {
514
- return receive.handler;
709
+ return disposables.disposed ? null : receive.handler;
515
710
  },
516
711
  set onMessage(fn) {
712
+ if (disposables.disposed || receive.ownerCount === 0) return;
517
713
  receive.handler = fn;
518
714
  if (fn) scheduleFlush(receive);
519
715
  },
520
716
  native,
521
- [Symbol.dispose]: () => {
522
- receive.handler = null;
523
- receive.flushScheduled = false;
524
- },
717
+ [Symbol.dispose]: () => disposables.dispose(),
525
718
  };
526
719
  };
527
720
 
528
721
  const createTestPort = <Input, Output>(
529
- receive: PortState<Output>,
530
- peerReceive: PortState<Input>,
531
- native: NativeMessagePort<Input, Output>,
532
- ): MessagePort<Input, Output> => createPort(receive, peerReceive, native);
722
+ registration: PortRegistration<Input, Output>,
723
+ ): MessagePort<Input, Output> => createPort(registration);
533
724
 
534
725
  const createNativeMessagePortToken = <Input, Output>(): NativeMessagePort<
535
726
  Input,
@@ -548,5 +739,5 @@ const createNativeMessagePortToken = <Input, Output>(): NativeMessagePort<
548
739
  */
549
740
  const nativePortRegistry = new WeakMap<
550
741
  NativeMessagePort<any, any>,
551
- MessagePort<any, any>
742
+ PortRegistration<any, any>
552
743
  >();
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<