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

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 (81) hide show
  1. package/dist/src/Assert.d.ts +10 -5
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +10 -5
  4. package/dist/src/Error.d.ts +5 -3
  5. package/dist/src/Error.d.ts.map +1 -1
  6. package/dist/src/Error.js +16 -0
  7. package/dist/src/Identicon.js +4 -4
  8. package/dist/src/LockManager.d.ts +74 -0
  9. package/dist/src/LockManager.d.ts.map +1 -0
  10. package/dist/src/LockManager.js +103 -0
  11. package/dist/src/Microtask.d.ts.map +1 -1
  12. package/dist/src/Microtask.js +90 -26
  13. package/dist/src/Object.d.ts.map +1 -1
  14. package/dist/src/RefCount.d.ts.map +1 -1
  15. package/dist/src/RefCount.js +153 -68
  16. package/dist/src/Resource.d.ts +6 -6
  17. package/dist/src/Resource.d.ts.map +1 -1
  18. package/dist/src/Resource.js +211 -162
  19. package/dist/src/Schedule.d.ts +5 -5
  20. package/dist/src/Schedule.d.ts.map +1 -1
  21. package/dist/src/Schedule.js +53 -44
  22. package/dist/src/Sqlite.d.ts +2 -0
  23. package/dist/src/Sqlite.d.ts.map +1 -1
  24. package/dist/src/Sqlite.js +66 -41
  25. package/dist/src/Task.d.ts +99 -123
  26. package/dist/src/Task.d.ts.map +1 -1
  27. package/dist/src/Task.js +273 -256
  28. package/dist/src/Test.d.ts +14 -15
  29. package/dist/src/Test.d.ts.map +1 -1
  30. package/dist/src/Time.d.ts +5 -0
  31. package/dist/src/Time.d.ts.map +1 -1
  32. package/dist/src/Time.js +5 -0
  33. package/dist/src/Type.d.ts +7 -0
  34. package/dist/src/Type.d.ts.map +1 -1
  35. package/dist/src/Type.js +7 -0
  36. package/dist/src/Types.d.ts +0 -8
  37. package/dist/src/Types.d.ts.map +1 -1
  38. package/dist/src/WebSocket.d.ts.map +1 -1
  39. package/dist/src/WebSocket.js +7 -6
  40. package/dist/src/Worker.d.ts +42 -8
  41. package/dist/src/Worker.d.ts.map +1 -1
  42. package/dist/src/Worker.js +292 -112
  43. package/dist/src/index.d.ts +1 -0
  44. package/dist/src/index.d.ts.map +1 -1
  45. package/dist/src/index.js +1 -0
  46. package/dist/src/local-first/Db.d.ts +5 -4
  47. package/dist/src/local-first/Db.d.ts.map +1 -1
  48. package/dist/src/local-first/Db.js +43 -37
  49. package/dist/src/local-first/Evolu.d.ts +3 -2
  50. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  51. package/dist/src/local-first/Evolu.js +90 -80
  52. package/dist/src/local-first/Owner.d.ts.map +1 -1
  53. package/dist/src/local-first/Shared.d.ts +65 -54
  54. package/dist/src/local-first/Shared.d.ts.map +1 -1
  55. package/dist/src/local-first/Shared.js +390 -376
  56. package/package.json +1 -1
  57. package/src/Array.ts +8 -8
  58. package/src/Assert.ts +11 -6
  59. package/src/Error.ts +5 -4
  60. package/src/Identicon.ts +4 -4
  61. package/src/LockManager.ts +181 -0
  62. package/src/Microtask.ts +17 -11
  63. package/src/Object.ts +3 -3
  64. package/src/RefCount.ts +25 -21
  65. package/src/Resource.ts +63 -56
  66. package/src/Schedule.ts +70 -48
  67. package/src/Set.ts +4 -4
  68. package/src/Sqlite.ts +43 -31
  69. package/src/Task.ts +392 -430
  70. package/src/Test.ts +14 -15
  71. package/src/Time.ts +7 -0
  72. package/src/Type.ts +9 -0
  73. package/src/Types.ts +0 -9
  74. package/src/WebSocket.ts +7 -6
  75. package/src/Worker.ts +195 -53
  76. package/src/index.ts +1 -0
  77. package/src/local-first/Db.ts +86 -61
  78. package/src/local-first/Evolu.ts +74 -68
  79. package/src/local-first/Owner.ts +1 -1
  80. package/src/local-first/Query.ts +1 -1
  81. package/src/local-first/Shared.ts +485 -476
package/src/Test.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  type Random,
18
18
  type RandomLibDep,
19
19
  } from "./Random.js";
20
- import { createRun, type Run, type RunDeps } from "./Task.js";
20
+ import { createRun, type Run, type RunDefaultDeps } from "./Task.js";
21
21
  import {
22
22
  minMillis,
23
23
  setTimeout,
@@ -35,14 +35,14 @@ export type TestCreateId = <B extends string = never>() => [B] extends [never]
35
35
  /**
36
36
  * Cheap deterministic baseline deps for tests.
37
37
  *
38
- * `TestDeps` includes test-friendly replacements for {@link RunDeps}, such as
39
- * {@link TestConsole} and {@link TestTime}, plus extra helpers commonly useful in
40
- * tests and fixtures, such as `randomLib` (only that for now).
38
+ * `TestDeps` includes test-friendly replacements for {@link RunDefaultDeps},
39
+ * such as {@link TestConsole} and {@link TestTime}, plus extra helpers commonly
40
+ * useful in tests and fixtures, such as `randomLib` (only that for now).
41
41
  *
42
42
  * Use it directly for synchronous test setup, fixtures, and helpers. It is also
43
43
  * intentionally the base deps used by {@link testCreateRun}.
44
44
  */
45
- export type TestDeps = Omit<RunDeps, "console" | "time"> &
45
+ export type TestDeps = Omit<RunDefaultDeps, "console" | "time"> &
46
46
  TestConsoleDep &
47
47
  TestTimeDep &
48
48
  RandomLibDep;
@@ -103,7 +103,7 @@ export const testCreateDeps = (options?: {
103
103
  * ### Example
104
104
  *
105
105
  * ```ts
106
- * // Built-in TestTime
106
+ * // Default TestTime
107
107
  * await using run = testCreateRun();
108
108
  * const fiber = run(sleep("1s"));
109
109
  * run.deps.time.advance("1s");
@@ -112,24 +112,23 @@ export const testCreateDeps = (options?: {
112
112
  * // For a single test, create the dependency using the Run.
113
113
  * await using run = testCreateRun();
114
114
  * await using foo = await run.orThrow(createFoo());
115
- * const runWithFoo = run.addDeps({ foo });
116
- *
117
- * expect(runWithFoo.deps.foo).toBe(foo);
115
+ * await run.orThrow(taskThatNeedsFoo, { foo });
118
116
  *
119
117
  * // If multiple tests need the same setup, create a disposable helper.
120
118
  * // Then `await using setup` disposes everything the helper owns.
121
119
  * const setupFoo = async () => {
122
- * await using stack = new AsyncDisposableStack();
123
- * const run = stack.use(testCreateRun());
124
- * const foo = stack.use(await run.orThrow(createFoo()));
125
- * const moved = stack.move();
120
+ * await using disposer = new AsyncDisposableStack();
121
+ * const run = disposer.use(testCreateRun());
122
+ * const foo = disposer.use(await run.orThrow(createFoo()));
123
+ * const runWithFoo = disposer.use(run.create({ ...run.deps, foo }));
124
+ * const disposables = disposer.move();
126
125
  *
127
126
  * // Return whatever the tests need: a Run with the dependency,
128
127
  * // the dependency itself, or both.
129
128
  * return {
130
- * run: run.addDeps({ foo }),
129
+ * run: runWithFoo,
131
130
  * foo,
132
- * [Symbol.asyncDispose]: () => moved.disposeAsync(),
131
+ * [Symbol.asyncDispose]: () => disposables.disposeAsync(),
133
132
  * };
134
133
  * };
135
134
  *
package/src/Time.ts CHANGED
@@ -177,6 +177,13 @@ export const minMillis = 0 as Millis;
177
177
  /** Maximum {@link Millis} value. */
178
178
  export const maxMillis = (maxMillisWithInfinity - 1) as Millis;
179
179
 
180
+ /**
181
+ * Converts a number to {@link Millis}, rounding to the nearest millisecond and
182
+ * saturating overflow at {@link maxMillis}.
183
+ */
184
+ export const saturateMillis = (value: number): Millis =>
185
+ Millis.orNull(Math.max(0, Math.round(value))) ?? maxMillis;
186
+
180
187
  /**
181
188
  * Converts {@link Millis} to {@link DateIso}.
182
189
  *
package/src/Type.ts CHANGED
@@ -2334,6 +2334,14 @@ export const formatFiniteError =
2334
2334
  export const FiniteNumber = /*#__PURE__*/ finite(Number);
2335
2335
  export type FiniteNumber = typeof FiniteNumber.Type;
2336
2336
 
2337
+ /**
2338
+ * Non-negative finite number.
2339
+ *
2340
+ * @group Number
2341
+ */
2342
+ export const NonNegativeFiniteNumber = /*#__PURE__*/ nonNegative(FiniteNumber);
2343
+ export type NonNegativeFiniteNumber = typeof NonNegativeFiniteNumber.Type;
2344
+
2337
2345
  /**
2338
2346
  * Number that is a multiple of a divisor.
2339
2347
  *
@@ -4651,6 +4659,7 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
4651
4659
 
4652
4660
  if (extraMessage != null) return extraMessage;
4653
4661
 
4662
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
4654
4663
  error = error as TypeErrors;
4655
4664
 
4656
4665
  switch (error.type) {
package/src/Types.ts CHANGED
@@ -359,12 +359,3 @@ export type ExtractType<
359
359
  TUnion extends { readonly type: TypeName },
360
360
  TType extends TUnion["type"],
361
361
  > = Extract<TUnion, { readonly type: TType }>;
362
-
363
- /**
364
- * Constrains `T` to only contain keys not present in `Existing`.
365
- *
366
- * Use as a generic constraint to prevent overriding existing properties.
367
- */
368
- export type NewKeys<T, Existing> = {
369
- [K in keyof T]: K extends keyof Existing ? never : T[K];
370
- };
package/src/WebSocket.ts CHANGED
@@ -200,7 +200,7 @@ export const createWebSocket: CreateWebSocket =
200
200
  } = {},
201
201
  ) =>
202
202
  async (run) => {
203
- await using stack = new AsyncDisposableStack();
203
+ await using disposer = new AsyncDisposableStack();
204
204
 
205
205
  let socket: globalThis.WebSocket | null = null;
206
206
 
@@ -268,7 +268,7 @@ export const createWebSocket: CreateWebSocket =
268
268
  return closeSocket;
269
269
  });
270
270
 
271
- const retryFiber = stack.use(run.daemon(retry(connect, schedule)));
271
+ const retryFiber = disposer.use(run.daemon(retry(connect, schedule)));
272
272
 
273
273
  // Report RetryError (schedule exhausted) via onError callback
274
274
  void retryFiber.then((result) => {
@@ -277,7 +277,7 @@ export const createWebSocket: CreateWebSocket =
277
277
  }
278
278
  });
279
279
 
280
- const moved = stack.move();
280
+ const disposables = disposer.move();
281
281
 
282
282
  return ok<WebSocket>({
283
283
  send: (data) => {
@@ -290,14 +290,15 @@ export const createWebSocket: CreateWebSocket =
290
290
  },
291
291
 
292
292
  getReadyState: () => {
293
- if (moved.disposed) return "closed";
293
+ if (disposables.disposed) return "closed";
294
294
  return socket ? nativeToStringState[socket.readyState] : "connecting";
295
295
  },
296
296
 
297
297
  isOpen: () =>
298
- !moved.disposed && socket?.readyState === globalThis.WebSocket.OPEN,
298
+ !disposables.disposed &&
299
+ socket?.readyState === globalThis.WebSocket.OPEN,
299
300
 
300
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
301
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
301
302
  });
302
303
  };
303
304
 
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";