@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/Task.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  type NonEmptyReadonlyArray,
13
13
  } from "./Array.js";
14
14
  import type { assertNotAborted } from "./Assert.js";
15
- import { assert } from "./Assert.js";
15
+ import { assert, assertNotDisposed } from "./Assert.js";
16
16
  import { type Console, type ConsoleDep, createConsole } from "./Console.js";
17
17
  import type { RandomBytes, RandomBytesDep } from "./Crypto.js";
18
18
  import { createRandomBytes } from "./Crypto.js";
@@ -43,7 +43,6 @@ import {
43
43
  Id,
44
44
  type InferType,
45
45
  maxPositiveInt,
46
- type Name,
47
46
  NonNegativeInt,
48
47
  object,
49
48
  onePositiveInt,
@@ -62,7 +61,6 @@ import {
62
61
  type CallbackWithTeardown,
63
62
  type Int1To100,
64
63
  type Mutable,
65
- type NewKeys,
66
64
  type Predicate,
67
65
  } from "./Types.js";
68
66
 
@@ -271,12 +269,22 @@ import {
271
269
  * await run(fetchUser(123));
272
270
  * ```
273
271
  *
274
- * For runtime-created dependencies, use {@link Run#addDeps}.
272
+ * Provide runtime-created dependencies to a single Task with `run(task, deps)`:
275
273
  *
276
- * ### Built-in dependencies
274
+ * ```ts
275
+ * const dbResult = await run(createDb(config));
276
+ * if (!dbResult.ok) return dbResult;
277
+ *
278
+ * const userResult = await run(fetchUser(123), { db: dbResult.value });
279
+ * ```
277
280
  *
278
- * {@link createRun} provides default {@link RunDeps} available to all Tasks
279
- * without declaring `D`:
281
+ * For reusable resources that own async work, use `run.create(deps)` and
282
+ * dispose the created Run with the resource.
283
+ *
284
+ * ### Default dependencies
285
+ *
286
+ * {@link createRun} provides default {@link RunDefaultDeps} available to all
287
+ * Tasks without declaring `D`:
280
288
  *
281
289
  * - {@link Console} — logging with hierarchical context via `child()`
282
290
  * - {@link Time} — current time
@@ -313,7 +321,7 @@ import {
313
321
  * ```
314
322
  *
315
323
  * For testing, use {@link testCreateRun} to get deterministic, controllable
316
- * implementations of all RunDeps.
324
+ * implementations of all RunDefaultDeps.
317
325
  *
318
326
  * ## Resource management
319
327
  *
@@ -327,13 +335,13 @@ import {
327
335
  * ### Example
328
336
  *
329
337
  * ```ts
330
- * await using stack = new AsyncDisposableStack();
338
+ * await using disposer = new AsyncDisposableStack();
331
339
  *
332
340
  * const fooResult = await run(createFoo());
333
341
  * if (!fooResult.ok) return fooResult;
334
- * const foo = stack.use(fooResult.value);
342
+ * const foo = disposer.use(fooResult.value);
335
343
  *
336
- * stack.defer(async () => {
344
+ * disposer.defer(async () => {
337
345
  * await foo.close();
338
346
  * });
339
347
  * stack.adopt(session, async (session) => {
@@ -417,6 +425,35 @@ import {
417
425
  * use {@link assertNotAborted} to crash immediately instead of threading the
418
426
  * impossible case through domain logic.
419
427
  *
428
+ * ### What should code do with `AbortError`?
429
+ *
430
+ * Treat `AbortError` as structured-concurrency control flow. It usually means
431
+ * the current work should stop because its owning {@link Run} or {@link Fiber} is
432
+ * stopping.
433
+ *
434
+ * In ordinary Task code, return it unchanged:
435
+ *
436
+ * ```ts
437
+ * const result = await run(loadUser(id));
438
+ * if (!result.ok) return result;
439
+ * ```
440
+ *
441
+ * In fire-and-forget or cleanup code where nobody observes the result and the
442
+ * runtime already owns cleanup, returning early is enough:
443
+ *
444
+ * ```ts
445
+ * const result = await run(waitUntilClosed());
446
+ * if (!result.ok) return;
447
+ * ```
448
+ *
449
+ * Do not use {@link Run#orThrow} just to avoid thinking about abort. It turns
450
+ * normal cancellation into an exception. Use `orThrow` at composition
451
+ * boundaries where any error should fail the whole flow.
452
+ *
453
+ * If abort would violate a lifecycle invariant, make that invariant explicit:
454
+ * run the must-finish part with {@link unabortable}, then use
455
+ * {@link assertNotAborted} to fail fast if the Task could not even start.
456
+ *
420
457
  * ### How do I type an anonymous Task callback?
421
458
  *
422
459
  * For one-off inline Tasks, put the type arguments on the {@link Run} call:
@@ -579,6 +616,17 @@ export interface Run<D = unknown> extends AsyncDisposable {
579
616
  /** Runs a {@link Task} and returns a {@link Fiber} handle. */
580
617
  <T, E>(task: Task<T, E, D>): Fiber<T, E, D>;
581
618
 
619
+ /**
620
+ * Runs a {@link Task} with custom dependencies.
621
+ *
622
+ * The provided dependencies replace the current custom dependency set for
623
+ * this Task. Default {@link RunDefaultDeps} are always available.
624
+ */
625
+ <T, E, Deps>(
626
+ task: Task<T, E, Deps>,
627
+ deps: Deps,
628
+ ): Fiber<T, E, RunDefaultDeps & Deps>;
629
+
582
630
  /**
583
631
  * Runs a {@link Task} and throws if the returned {@link Result} is an error.
584
632
  *
@@ -602,13 +650,16 @@ export interface Run<D = unknown> extends AsyncDisposable {
602
650
  *
603
651
  * Throws: `Error` with the original Task error attached as `cause`.
604
652
  */
605
- readonly orThrow: <T, E>(task: Task<T, E, D>) => Promise<T>;
653
+ readonly orThrow: {
654
+ <T, E>(task: Task<T, E, D>): Promise<T>;
655
+ <T, E, Deps>(task: Task<T, E, Deps>, deps: Deps): Promise<T>;
656
+ };
606
657
 
607
658
  /** Unique {@link Id} for this Run. */
608
659
  readonly id: Id;
609
660
 
610
661
  /** The parent {@link Run}, if this Run was created as a child. */
611
- readonly parent: Run<D> | null;
662
+ readonly parent: Run | null;
612
663
 
613
664
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal */
614
665
  readonly signal: AbortSignal;
@@ -642,7 +693,7 @@ export interface Run<D = unknown> extends AsyncDisposable {
642
693
  readonly getState: () => RunState;
643
694
 
644
695
  /** Returns the current child {@link Fiber}s. */
645
- readonly getChildren: () => ReadonlySet<Fiber<any, any, D>>;
696
+ readonly getChildren: () => ReadonlySet<AnyFiber>;
646
697
 
647
698
  /**
648
699
  * Creates a memoized {@link RunSnapshot} of this Run.
@@ -709,13 +760,15 @@ export interface Run<D = unknown> extends AsyncDisposable {
709
760
  *
710
761
  * For a long-lived reusable {@link Run}, use {@link Run.create}.
711
762
  */
712
- readonly daemon: Run<D>;
763
+ readonly daemon: Run;
713
764
 
714
765
  /**
715
766
  * Creates a {@link Run} from this Run.
716
767
  *
717
768
  * Like {@link createRun}, the returned Run is daemon: it stays running until
718
- * disposed. Unlike {@link createRun}, it shares the same Deps as this Run.
769
+ * disposed. Without arguments, it shares this Run's dependencies. With deps,
770
+ * it uses those custom dependencies plus preserved default
771
+ * {@link RunDefaultDeps}.
719
772
  *
720
773
  * Use this for long-lived disposable resources that need to own async work.
721
774
  * The resource creates one internal Run with `run.create()` and uses that Run
@@ -728,77 +781,19 @@ export interface Run<D = unknown> extends AsyncDisposable {
728
781
  *
729
782
  * To run a single Task as daemon, use {@link Run.daemon}.
730
783
  */
731
- readonly create: () => Run<D>;
784
+ readonly create: {
785
+ (): Run<D>;
786
+ <Deps>(deps: Deps): Run<RunDefaultDeps & Deps>;
787
+ };
732
788
 
733
- /** Returns the dependencies passed to {@link createRun}. */
734
- readonly deps: RunDeps & D;
789
+ /** Returns this Run's dependencies. */
790
+ readonly deps: RunDefaultDeps & D;
735
791
 
736
792
  /**
737
793
  * @see {@link Concurrency}
738
794
  * @see {@link concurrently}
739
795
  */
740
796
  readonly concurrency: Concurrency;
741
-
742
- /**
743
- * Adds additional dependencies to this Run and returns it.
744
- *
745
- * Use for runtime-created dependencies — dependencies that cannot be created
746
- * in the composition root (e.g., app start).
747
- *
748
- * ### Example
749
- *
750
- * ```ts
751
- * // One-shot
752
- * await run.addDeps({ db })(getUser(123));
753
- *
754
- * // Multiple deps at once
755
- * await run.addDeps({ db, cache })(task);
756
- *
757
- * // Reusable — config comes from outside (message, file, etc.)
758
- * type DbWorkerDeps = DbDep; // or DbDep & CacheDep & ...
759
- *
760
- * const init =
761
- * (config: Config): Task<void, InitError, CreateDbDep> =>
762
- * async (run) => {
763
- * const { createDb } = run.deps;
764
- * await using stack = new AsyncDisposableStack();
765
- *
766
- * const db = stack.use(await run.orThrow(startApp()));
767
- * if (!db.ok) return db;
768
- *
769
- * const runWithDb = run.addDeps({ db: db.value });
770
- *
771
- * await runWithDb(getUser(123));
772
- * await runWithDb(insertUser(user));
773
- * return ok();
774
- * };
775
- * ```
776
- *
777
- * ## FAQ
778
- *
779
- * ### How does it work?
780
- *
781
- * This is the whole implementation:
782
- *
783
- * ```ts
784
- * run.addDeps = <E extends NewKeys<E, D>>(newDeps: E): Run<D & E> => {
785
- * depsRef.modify((currentDeps) => {
786
- * const duplicate = Object.keys(newDeps).find(
787
- * (k) => k in currentDeps,
788
- * );
789
- * assert(!duplicate, `Dependency '${duplicate}' already added.`);
790
- * return [undefined, { ...currentDeps, ...newDeps }];
791
- * });
792
- * return self as unknown as Run<D & E>;
793
- * };
794
- * ```
795
- *
796
- * Dependencies are stored in a shared {@link Ref}, so `addDeps` propagates to
797
- * all runs. The runtime assertion ensures dependencies are created once —
798
- * automatic deduplication would mask poor design (dependencies should have a
799
- * single, clear point of creation).
800
- */
801
- readonly addDeps: <E extends NewKeys<E, D>>(extraDeps: E) => Run<D & E>;
802
797
  }
803
798
 
804
799
  /**
@@ -897,12 +892,19 @@ export interface Fiber<T = unknown, E = unknown, D = unknown>
897
892
  getState(): RunState<T, E>;
898
893
  }
899
894
 
895
+ /**
896
+ * Shorthand for a {@link Fiber} with `any` type parameters.
897
+ *
898
+ * @group Type utilities
899
+ */
900
+ export type AnyFiber = Fiber<any, any, any>;
901
+
900
902
  /**
901
903
  * Extracts the value type from a {@link Fiber}.
902
904
  *
903
905
  * @group Type utilities
904
906
  */
905
- export type InferFiberOk<F extends Fiber<any, any, any>> =
907
+ export type InferFiberOk<F extends AnyFiber> =
906
908
  F extends Fiber<infer T, any, any> ? T : never;
907
909
 
908
910
  /**
@@ -910,7 +912,7 @@ export type InferFiberOk<F extends Fiber<any, any, any>> =
910
912
  *
911
913
  * @group Type utilities
912
914
  */
913
- export type InferFiberErr<F extends Fiber<any, any, any>> =
915
+ export type InferFiberErr<F extends AnyFiber> =
914
916
  F extends Fiber<any, infer E, any> ? E : never;
915
917
 
916
918
  /**
@@ -918,7 +920,7 @@ export type InferFiberErr<F extends Fiber<any, any, any>> =
918
920
  *
919
921
  * @group Type utilities
920
922
  */
921
- export type InferFiberDeps<F extends Fiber<any, any, any>> =
923
+ export type InferFiberDeps<F extends AnyFiber> =
922
924
  F extends Fiber<any, any, infer D> ? D : never;
923
925
 
924
926
  /**
@@ -1024,7 +1026,7 @@ export type RunSnapshotState = typeof RunSnapshotState.Type;
1024
1026
  * @see {@link Run.snapshot}
1025
1027
  */
1026
1028
  export interface RunSnapshot {
1027
- /** The {@link Run.id} this snapshot represents. */
1029
+ /** The Run id this snapshot represents. */
1028
1030
  readonly id: Id;
1029
1031
 
1030
1032
  /** The current lifecycle state. */
@@ -1084,7 +1086,7 @@ export interface RunConfigDep {
1084
1086
  }
1085
1087
 
1086
1088
  /** Default deps provided by {@link createRun}. */
1087
- export type RunDeps = ConsoleDep &
1089
+ export type RunDefaultDeps = ConsoleDep &
1088
1090
  RandomBytesDep &
1089
1091
  RandomDep &
1090
1092
  TimeDep &
@@ -1094,7 +1096,7 @@ export type RunDeps = ConsoleDep &
1094
1096
  // Partial<TracerConfigDep> & // TODO:
1095
1097
  // Partial<TracerDep>; // TODO:
1096
1098
 
1097
- const runDeps: RunDeps = {
1099
+ const runDefaultDeps: RunDefaultDeps = {
1098
1100
  console: createConsole(),
1099
1101
  randomBytes: createRandomBytes(),
1100
1102
  random: createRandom(),
@@ -1126,7 +1128,7 @@ export interface CreateRun<BaseDeps> {
1126
1128
  * Node.js `uncaughtException`, `unhandledRejection`, and graceful shutdown
1127
1129
  * handling, and `@evolu/react-native` adds React Native global error handling.
1128
1130
  *
1129
- * {@link RunDeps} provides default dependencies:
1131
+ * {@link RunDefaultDeps} provides default dependencies:
1130
1132
  *
1131
1133
  * - {@link Time}
1132
1134
  * - {@link Console}
@@ -1164,7 +1166,7 @@ export interface CreateRun<BaseDeps> {
1164
1166
  * };
1165
1167
  *
1166
1168
  * // Composition root: create a Run with custom deps
1167
- * type AppDeps = RunDeps & ConfigDep;
1169
+ * type AppDeps = RunDefaultDeps & ConfigDep;
1168
1170
  *
1169
1171
  * const appDeps: AppDeps = {
1170
1172
  * ...testCreateDeps(), // or spread individual deps
@@ -1177,24 +1179,24 @@ export interface CreateRun<BaseDeps> {
1177
1179
  * const result = await run(fetchUser("123"));
1178
1180
  *
1179
1181
  * // TypeScript catches missing deps at compile time:
1180
- * // await using run2 = createRun(); // Run<RunDeps>
1182
+ * // await using run2 = createRun(); // Run<RunDefaultDeps>
1181
1183
  * // run2(fetchUser("123")); // Error: Property 'config' is missing
1182
1184
  * ```
1183
1185
  *
1184
1186
  * @group Creating Run
1185
1187
  */
1186
- export const createRun: CreateRun<RunDeps> = <D>(
1188
+ export const createRun: CreateRun<RunDefaultDeps> = <D>(
1187
1189
  deps?: D,
1188
- ): Run<RunDeps & D> => {
1189
- const mergedDeps = { ...runDeps, ...deps } as RunDeps & D;
1190
- return createRunInternal(createRef(mergedDeps))();
1191
- };
1190
+ ): Run<RunDefaultDeps & D> =>
1191
+ createRunInternal({ ...runDefaultDeps, ...deps } as RunDefaultDeps & D);
1192
1192
 
1193
1193
  /** Internal Run properties, hidden from public API via TypeScript types. */
1194
- interface RunInternal<D extends RunDeps = RunDeps> extends Run<D> {
1194
+ interface RunInternal<
1195
+ D extends RunDefaultDeps = RunDefaultDeps,
1196
+ > extends Run<D> {
1195
1197
  ownTaskSettled: PromiseWithResolvers<void> | null;
1196
1198
 
1197
- readonly requestAbort: (reason: unknown) => void;
1199
+ readonly requestAbort: (abortError: AbortError) => void;
1198
1200
  readonly requestSignal: AbortSignal;
1199
1201
 
1200
1202
  /**
@@ -1209,227 +1211,233 @@ interface RunInternal<D extends RunDeps = RunDeps> extends Run<D> {
1209
1211
  readonly handleTaskSettled: () => void;
1210
1212
  }
1211
1213
 
1212
- const createRunInternal =
1213
- <D extends RunDeps>(depsRef: Ref<D>) =>
1214
- (
1215
- parent?: RunInternal<D>,
1216
- daemon?: RunInternal<D>,
1217
- abortBehavior?: AbortBehavior,
1218
- concurrencyBehavior?: Concurrency,
1219
- ): RunInternal<D> => {
1220
- const parentMask = parent?.abortMask ?? isAbortable;
1221
-
1222
- let abortMask: AbortMask;
1223
- switch (abortBehavior) {
1224
- case undefined:
1225
- abortMask = parentMask;
1226
- break;
1227
- case "unabortable":
1228
- abortMask = increment(parentMask) as AbortMask;
1229
- break;
1230
- default:
1231
- assert(
1232
- abortBehavior <= parentMask,
1233
- "restore used outside its unabortableMask",
1234
- );
1235
- abortMask = abortBehavior;
1236
- }
1214
+ const createRunInternal = <D extends RunDefaultDeps>(
1215
+ deps: D,
1216
+ parent?: RunInternal,
1217
+ daemon?: RunInternal,
1218
+ abortBehavior?: AbortBehavior,
1219
+ concurrencyBehavior?: Concurrency,
1220
+ ): RunInternal<D> => {
1221
+ const parentMask = parent?.abortMask ?? isAbortable;
1222
+
1223
+ let abortMask: AbortMask;
1224
+ switch (abortBehavior) {
1225
+ case undefined:
1226
+ abortMask = parentMask;
1227
+ break;
1228
+ case "unabortable":
1229
+ abortMask = increment(parentMask) as AbortMask;
1230
+ break;
1231
+ default:
1232
+ assert(
1233
+ abortBehavior <= parentMask,
1234
+ "restore used outside its unabortableMask",
1235
+ );
1236
+ abortMask = abortBehavior;
1237
+ }
1237
1238
 
1238
- const requestController = new AbortController();
1239
- const signalController = new AbortController();
1239
+ const requestController = new AbortController();
1240
+ const signalController = new AbortController();
1240
1241
 
1241
- let state: RunState = running;
1242
- let result: UnknownResult | undefined;
1243
- let outcome: UnknownResult | undefined;
1244
- let children: ReadonlySet<Fiber<any, any, D>> = emptySet;
1242
+ let state: RunState = running;
1243
+ let result: UnknownResult | undefined;
1244
+ let outcome: UnknownResult | undefined;
1245
+ let children: ReadonlySet<AnyFiber> = emptySet;
1245
1246
 
1246
- const requestAbort = (reason: unknown) => {
1247
- const abortError = reason as AbortError;
1248
- if (abortMask === isAbortable) signalController.abort(abortError);
1249
- requestController.abort(abortError);
1250
- };
1247
+ const requestAbort = (abortError: AbortError) => {
1248
+ if (abortMask === isAbortable) signalController.abort(abortError);
1249
+ requestController.abort(abortError);
1250
+ };
1251
1251
 
1252
- if (parent) {
1253
- subscribeToAbort(
1254
- parent.requestSignal,
1255
- () => requestAbort(parent.requestSignal.reason),
1256
- { signal: requestController.signal },
1257
- );
1258
- }
1252
+ if (parent) {
1253
+ subscribeToAbort(
1254
+ parent.requestSignal,
1255
+ () => requestAbort(parent.requestSignal.reason as AbortError),
1256
+ { signal: requestController.signal },
1257
+ );
1258
+ }
1259
1259
 
1260
- const emitEvent = (data: RunEventData) => {
1261
- const deps = depsRef.get();
1262
- if (!deps.runConfig?.eventsEnabled.get()) return;
1263
- const e: RunEvent = { id: self.id, timestamp: deps.time.now(), data };
1264
- for (let node: Run<D> | null = self; node; node = node.parent) {
1265
- node.onEvent?.(e);
1266
- }
1267
- };
1260
+ const emitEvent = (data: RunEventData) => {
1261
+ if (!deps.runConfig?.eventsEnabled.get()) return;
1262
+ const e: RunEvent = { id: self.id, timestamp: deps.time.now(), data };
1263
+ for (let node: Run | null = self; node; node = node.parent) {
1264
+ node.onEvent?.(e);
1265
+ }
1266
+ };
1268
1267
 
1269
- const run = <T, E>(task: Task<T, E, D>): Fiber<T, E, D> => {
1270
- const childRun = createRunInternal(depsRef)(
1271
- self,
1272
- daemon ?? self,
1273
- getAbortBehavior(task),
1274
- getConcurrencyBehavior(task),
1275
- );
1268
+ const run = (task: AnyTask, taskDeps?: unknown): AnyFiber => {
1269
+ const childRun = createRunInternal(
1270
+ taskDeps === undefined
1271
+ ? deps
1272
+ : {
1273
+ console: deps.console,
1274
+ randomBytes: deps.randomBytes,
1275
+ random: deps.random,
1276
+ time: deps.time,
1277
+ ...(deps.runConfig && {
1278
+ runConfig: deps.runConfig,
1279
+ }),
1280
+ ...taskDeps,
1281
+ },
1282
+ self,
1283
+ daemon ?? self,
1284
+ getAbortBehavior(task),
1285
+ getConcurrencyBehavior(task),
1286
+ );
1276
1287
 
1277
- if (state !== running) {
1278
- childRun.requestAbort(runStoppedAbortError);
1279
- task = () => err(runStoppedAbortError);
1280
- } else if (
1281
- signalController.signal.aborted &&
1282
- childRun.abortMask === isAbortable
1283
- ) {
1284
- childRun.requestAbort(signalController.signal.reason);
1285
- task = () => err(signalController.signal.reason);
1286
- }
1288
+ if (state !== running) {
1289
+ childRun.requestAbort(runStoppedAbortError);
1290
+ task = () => err(runStoppedAbortError);
1291
+ } else if (
1292
+ signalController.signal.aborted &&
1293
+ childRun.abortMask === isAbortable
1294
+ ) {
1295
+ const abortError = signalController.signal.reason as AbortError;
1296
+ childRun.requestAbort(abortError);
1297
+ task = () => err(abortError);
1298
+ }
1287
1299
 
1288
- const childFiber: Fiber<T, E, D> = Object.assign(
1289
- Promise.try(task, childRun)
1290
- .then(childRun.handleTaskFulfilled)
1291
- .finally(childRun.handleTaskSettled)
1292
- .finally(childRun[Symbol.asyncDispose])
1293
- .finally(() => {
1294
- children = deleteFromSet(children, childFiber);
1295
- emitEvent({ type: "ChildRemoved", childId: childRun.id });
1296
- }),
1297
- {
1298
- run: childRun,
1299
- abort: (reason?: unknown): void => {
1300
- childRun.requestAbort(createAbortError(reason));
1301
- },
1302
- getState: () => childRun.getState() as RunState<T, E>,
1303
- [Symbol.dispose]: () => {
1304
- childFiber.abort();
1305
- },
1300
+ const childFiber: Fiber = Object.assign(
1301
+ Promise.try(task, childRun)
1302
+ .then(childRun.handleTaskFulfilled)
1303
+ .finally(childRun.handleTaskSettled)
1304
+ .finally(childRun[Symbol.asyncDispose])
1305
+ .finally(() => {
1306
+ children = deleteFromSet(children, childFiber);
1307
+ emitEvent({ type: "ChildRemoved", childId: childRun.id });
1308
+ }),
1309
+ {
1310
+ run: childRun,
1311
+ abort: (reason?: unknown): void => {
1312
+ childRun.requestAbort(createAbortError(reason));
1306
1313
  },
1307
- );
1308
-
1309
- children = addToSet(children, childFiber);
1310
- emitEvent({ type: "ChildAdded", childId: childRun.id });
1314
+ getState: () => childRun.getState(),
1315
+ [Symbol.dispose]: () => {
1316
+ childFiber.abort();
1317
+ },
1318
+ },
1319
+ );
1311
1320
 
1312
- return childFiber;
1313
- };
1321
+ children = addToSet(children, childFiber);
1322
+ emitEvent({ type: "ChildAdded", childId: childRun.id });
1314
1323
 
1315
- const self = run as RunInternal<D>;
1324
+ return childFiber;
1325
+ };
1316
1326
 
1317
- {
1318
- const run = self as Mutable<RunInternal<D>>;
1319
- const id = createId(depsRef.get());
1320
-
1321
- let snapshot: RunSnapshot | null = null;
1322
- let disposingPromise: Promise<void> | null = null;
1323
-
1324
- run.orThrow = async (task) => getOrThrow(await self(task));
1325
- run.id = id;
1326
- run.parent = parent ?? null;
1327
-
1328
- run.signal = signalController.signal;
1329
- run.abortMask = abortMask;
1330
- run.onAbort = (callback) => {
1331
- if (abortMask !== isAbortable) return;
1332
- subscribeToAbort(
1333
- signalController.signal,
1334
- () => callback((signalController.signal.reason as AbortError).reason),
1335
- { once: true, signal: requestController.signal },
1336
- );
1337
- };
1338
- run.getState = () => state;
1339
- run.getChildren = () => children;
1327
+ let snapshot: RunSnapshot | null = null;
1328
+ let disposingPromise: Promise<void> | null = null;
1340
1329
 
1341
- run.snapshot = () => {
1342
- const childSnapshots = Array.from(children).map((fiber) =>
1343
- fiber.run.snapshot(),
1344
- );
1345
- if (
1346
- snapshot?.state !== state ||
1347
- !eqArrayStrict(snapshot.children, childSnapshots)
1348
- ) {
1349
- snapshot = {
1350
- id,
1351
- state: state as RunSnapshotState,
1352
- children: childSnapshots,
1353
- abortMask,
1354
- };
1355
- }
1356
- return snapshot;
1357
- };
1330
+ const self = run as RunInternal<D>;
1358
1331
 
1359
- run.daemon = daemon ?? self;
1332
+ {
1333
+ const run: Mutable<RunInternal<D>> = self;
1334
+ const id = createId(deps);
1335
+
1336
+ function orThrow<T, E>(task: Task<T, E, D>): Promise<T>;
1337
+ function orThrow<T, E, Deps>(
1338
+ task: Task<T, E, Deps>,
1339
+ taskDeps: Deps,
1340
+ ): Promise<T>;
1341
+ async function orThrow<T, E, Deps>(
1342
+ task: Task<T, E, D | Deps>,
1343
+ taskDeps?: Deps,
1344
+ ): Promise<T> {
1345
+ const result =
1346
+ taskDeps === undefined ? await self(task) : await self(task, taskDeps);
1347
+ return getOrThrow(result);
1348
+ }
1360
1349
 
1361
- run.create = () => run.daemon(createDeferred().task).run;
1350
+ run.orThrow = orThrow;
1351
+ run.id = id;
1352
+ run.parent = parent ?? null;
1362
1353
 
1363
- Object.defineProperty(run, "deps", { get: depsRef.get });
1354
+ run.signal = signalController.signal;
1355
+ run.abortMask = abortMask;
1356
+ run.onAbort = (callback) => {
1357
+ if (abortMask !== isAbortable) return;
1358
+ subscribeToAbort(
1359
+ signalController.signal,
1360
+ () => callback((signalController.signal.reason as AbortError).reason),
1361
+ { once: true, signal: requestController.signal },
1362
+ );
1363
+ };
1364
+ run.getState = () => state;
1365
+ run.getChildren = () => children;
1364
1366
 
1365
- run.concurrency =
1366
- concurrencyBehavior ?? parent?.concurrency ?? defaultConcurrency;
1367
+ run.snapshot = () => {
1368
+ const childSnapshots = Array.from(children).map((fiber) =>
1369
+ fiber.run.snapshot(),
1370
+ );
1371
+ if (
1372
+ snapshot?.state !== state ||
1373
+ !eqArrayStrict(snapshot.children, childSnapshots)
1374
+ ) {
1375
+ snapshot = {
1376
+ id,
1377
+ state,
1378
+ children: childSnapshots,
1379
+ abortMask,
1380
+ };
1381
+ }
1382
+ return snapshot;
1383
+ };
1367
1384
 
1368
- run.addDeps = <E extends NewKeys<E, D>>(newDeps: E): Run<D & E> => {
1369
- depsRef.modify((currentDeps) => {
1370
- const duplicate = Object.keys(newDeps).find((k) => k in currentDeps);
1371
- assert(
1372
- !duplicate,
1373
- `Dependency '${duplicate}' already added. ` +
1374
- `This assert ensures dependencies are created once. ` +
1375
- `Automatic deduplication would mask bugs.`,
1376
- );
1377
- return [undefined, { ...currentDeps, ...newDeps }];
1385
+ run.onEvent = undefined;
1386
+ run.daemon = daemon ?? self;
1387
+ run.create = (runDeps: {} = deps): Run<any> => {
1388
+ const task = createDeferred().task;
1389
+ return run.daemon(task, runDeps).run;
1390
+ };
1391
+ run.deps = deps;
1392
+ run.concurrency =
1393
+ concurrencyBehavior ?? parent?.concurrency ?? defaultConcurrency;
1394
+ run[Symbol.asyncDispose] = () => {
1395
+ if (disposingPromise) return disposingPromise;
1396
+
1397
+ state = { type: "Disposing" };
1398
+ emitEvent({ type: "StateChanged", state });
1399
+ requestAbort(runStoppedAbortError);
1400
+
1401
+ disposingPromise = Promise.allSettled(
1402
+ (self.ownTaskSettled
1403
+ ? [self.ownTaskSettled.promise, ...children]
1404
+ : children) as Iterable<PromiseLike<unknown>>,
1405
+ )
1406
+ .then(lazyVoid)
1407
+ .finally(() => {
1408
+ /**
1409
+ * Root and daemon Runs have no own Task, so `run.handleTaskFulfilled`
1410
+ * never populates their terminal values. In that case disposal
1411
+ * publishes `ok()` for both `result` and `outcome`. Task-backed Runs
1412
+ * normally reach this point with both values already set.
1413
+ */
1414
+ [result, outcome] = [result ?? ok(), outcome ?? ok()];
1415
+ state = { type: "Settled", result, outcome };
1416
+ emitEvent({ type: "StateChanged", state });
1378
1417
  });
1379
- return self as unknown as Run<D & E>;
1380
- };
1381
-
1382
- run[Symbol.asyncDispose] = () => {
1383
- if (disposingPromise) return disposingPromise;
1384
-
1385
- state = { type: "Disposing" };
1386
- emitEvent({ type: "StateChanged", state });
1387
- requestAbort(runStoppedAbortError);
1388
-
1389
- disposingPromise = Promise.allSettled(
1390
- (run.ownTaskSettled
1391
- ? [run.ownTaskSettled.promise, ...children]
1392
- : children) as Iterable<PromiseLike<unknown>>,
1393
- )
1394
- .then(lazyVoid)
1395
- .finally(() => {
1396
- /**
1397
- * Root and daemon Runs have no own Task, so
1398
- * `run.handleTaskFulfilled` never populates their terminal values.
1399
- * In that case disposal publishes `ok()` for both `result` and
1400
- * `outcome`. Task-backed Runs normally reach this point with both
1401
- * values already set.
1402
- */
1403
- [result, outcome] = [result ?? ok(), outcome ?? ok()];
1404
- state = { type: "Settled", result, outcome };
1405
- emitEvent({ type: "StateChanged", state });
1406
- });
1407
-
1408
- return disposingPromise;
1409
- };
1410
-
1411
- // Internal
1412
- run.ownTaskSettled = parent ? Promise.withResolvers<void>() : null;
1413
1418
 
1414
- run.requestAbort = requestAbort;
1415
- run.requestSignal = requestController.signal;
1416
-
1417
- run.handleTaskFulfilled = (taskOutcome) => {
1418
- const taskResult = run.signal.aborted
1419
- ? (err(run.signal.reason as AbortError) as typeof taskOutcome)
1420
- : taskOutcome;
1421
- result = taskResult;
1422
- outcome = taskOutcome;
1423
- return taskResult;
1424
- };
1419
+ return disposingPromise;
1420
+ };
1425
1421
 
1426
- run.handleTaskSettled = () => {
1427
- run.ownTaskSettled?.resolve();
1428
- };
1429
- }
1422
+ // Internal
1423
+ run.ownTaskSettled = parent ? Promise.withResolvers<void>() : null;
1424
+ run.requestAbort = requestAbort;
1425
+ run.requestSignal = requestController.signal;
1426
+ run.handleTaskFulfilled = (taskOutcome) => {
1427
+ const taskResult = self.signal.aborted
1428
+ ? err(self.signal.reason as AbortError)
1429
+ : taskOutcome;
1430
+ result = taskResult;
1431
+ outcome = taskOutcome;
1432
+ return taskResult;
1433
+ };
1434
+ run.handleTaskSettled = () => {
1435
+ self.ownTaskSettled?.resolve();
1436
+ };
1437
+ }
1430
1438
 
1431
- return self;
1432
- };
1439
+ return self;
1440
+ };
1433
1441
 
1434
1442
  const running: RunState = { type: "Running" };
1435
1443
 
@@ -1486,16 +1494,19 @@ const abortBehavior =
1486
1494
  /**
1487
1495
  * Makes a {@link Task} unabortable.
1488
1496
  *
1489
- * Once started, an unabortable Task always completes abort requests are
1490
- * ignored and `signal.aborted` remains `false`.
1497
+ * Once started, an unabortable Task always completes. Abort requests are masked
1498
+ * while it runs, and `signal.aborted` remains `false` inside the Task.
1491
1499
  *
1492
- * If the parent {@link Run} is already disposing or settled, `run(task)`
1500
+ * `unabortable` controls abort signal propagation; it does not force work to
1501
+ * start. If the parent {@link Run} is already disposing or settled, `run(task)`
1493
1502
  * short-circuits before task execution and returns `err(AbortError)` with
1494
1503
  * {@link runStoppedError} as reason. So `unabortable` means “do not interrupt
1495
1504
  * this Task once it has started”, not “remove AbortError from its type”.
1496
1505
  *
1497
- * When that pre-start abort would be a programmer error, assert it explicitly
1498
- * with `assertNotAborted` after awaiting the result.
1506
+ * Most callers should still propagate or ignore {@link AbortError} according to
1507
+ * ordinary structured-concurrency ownership. When abort would violate a
1508
+ * lifecycle invariant, await the unabortable Task and use
1509
+ * {@link assertNotAborted} to fail fast if it could not even start.
1499
1510
  *
1500
1511
  * ### Example
1501
1512
  *
@@ -1754,7 +1765,7 @@ export const callback =
1754
1765
  readonly ok: Callback<T>;
1755
1766
  readonly err: Callback<E>;
1756
1767
  readonly signal: AbortSignal;
1757
- readonly deps: RunDeps;
1768
+ readonly deps: RunDefaultDeps;
1758
1769
  }>,
1759
1770
  ): Task<T, E> =>
1760
1771
  (run) =>
@@ -2024,10 +2035,7 @@ export const retry =
2024
2035
  <T, E, D = unknown, Output = unknown>(
2025
2036
  task: Task<T, E, D>,
2026
2037
  schedule: Schedule<Output, E>,
2027
- {
2028
- retryable = lazyTrue as Predicate<E>,
2029
- onRetry,
2030
- }: RetryOptions<E, Output> = {},
2038
+ { retryable = lazyTrue, onRetry }: RetryOptions<E, Output> = {},
2031
2039
  ): Task<T, RetryError<E>, D> =>
2032
2040
  async (run) => {
2033
2041
  const step = schedule(run.deps);
@@ -2144,10 +2152,7 @@ export const repeat =
2144
2152
  <T, E, D = unknown, Output = unknown>(
2145
2153
  task: Task<T, E, D>,
2146
2154
  schedule: Schedule<Output, T>,
2147
- {
2148
- repeatable = lazyTrue as Predicate<T>,
2149
- onRepeat,
2150
- }: RepeatOptions<T, Output> = {},
2155
+ { repeatable = lazyTrue, onRepeat }: RepeatOptions<T, Output> = {},
2151
2156
  ): Task<T, E, D> =>
2152
2157
  async (run) => {
2153
2158
  const step = schedule(run.deps);
@@ -2186,7 +2191,7 @@ export const repeat =
2186
2191
  *
2187
2192
  * Similar to
2188
2193
  * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers | Promise.withResolvers},
2189
- * but integrated with {@link Task} and {@link Run} for cancellation support.
2194
+ * but integrated with {@link Task} and {@link Run}.
2190
2195
  *
2191
2196
  * Use for bridging callback-based APIs or coordinating between Tasks.
2192
2197
  *
@@ -2340,36 +2345,43 @@ export interface Gate<D = unknown> extends Disposable {
2340
2345
  * @group Concurrency primitives
2341
2346
  */
2342
2347
  export const createGate = <D = unknown>(): Gate<D> => {
2348
+ using disposer = new DisposableStack();
2343
2349
  let isOpen = false;
2344
- let disposed = false;
2345
2350
  let deferred = createDeferred<void>();
2346
2351
 
2352
+ disposer.defer(() => {
2353
+ deferred[Symbol.dispose]();
2354
+ });
2355
+
2356
+ const disposables = disposer.move();
2357
+
2347
2358
  return {
2348
2359
  wait: (run) => {
2349
- if (disposed) return err(deferredDisposedError);
2360
+ if (disposables.disposed) return err(deferredDisposedError);
2350
2361
  if (isOpen) return ok();
2351
2362
  return run(deferred.task);
2352
2363
  },
2353
2364
 
2354
2365
  open: () => {
2355
- if (disposed || isOpen) return;
2366
+ assertNotDisposed(disposables);
2367
+ if (isOpen) return;
2356
2368
  isOpen = true;
2357
2369
  deferred.resolve(ok());
2358
2370
  },
2359
2371
 
2360
2372
  close: () => {
2361
- if (disposed || !isOpen) return;
2373
+ assertNotDisposed(disposables);
2374
+ if (!isOpen) return;
2362
2375
  isOpen = false;
2363
2376
  deferred = createDeferred<void>();
2364
2377
  },
2365
2378
 
2366
- isOpen: () => isOpen,
2367
-
2368
- [Symbol.dispose]: () => {
2369
- if (disposed) return;
2370
- disposed = true;
2371
- deferred[Symbol.dispose]();
2379
+ isOpen: () => {
2380
+ assertNotDisposed(disposables);
2381
+ return isOpen;
2372
2382
  },
2383
+
2384
+ [Symbol.dispose]: () => disposables.dispose(),
2373
2385
  };
2374
2386
  };
2375
2387
 
@@ -2483,10 +2495,26 @@ export const createSemaphore = (permits: Concurrency): Semaphore => {
2483
2495
  readonly resolve: Callback<Result<void, AbortError>>;
2484
2496
  }
2485
2497
 
2498
+ using disposer = new DisposableStack();
2486
2499
  const fibers = new Set<Fiber>();
2487
2500
  const waiters: Array<Waiter> = [];
2488
2501
  let taken = zeroNonNegativeInt;
2489
- let disposed = false;
2502
+
2503
+ disposer.defer(() => {
2504
+ using disposer = new DisposableStack();
2505
+ for (const fiber of fibers) {
2506
+ disposer.adopt(fiber, (fiber) => {
2507
+ fiber.abort(semaphoreDisposedError);
2508
+ });
2509
+ }
2510
+
2511
+ for (const waiter of waiters) {
2512
+ waiter.resolve(err(semaphoreDisposedAbortError));
2513
+ }
2514
+ waiters.length = 0;
2515
+ });
2516
+
2517
+ const disposables = disposer.move();
2490
2518
 
2491
2519
  const withPermits =
2492
2520
  <T, E, D>(requestedPermits: Concurrency) =>
@@ -2499,7 +2527,7 @@ export const createSemaphore = (permits: Concurrency): Semaphore => {
2499
2527
  "Requested permits must not exceed semaphore capacity.",
2500
2528
  );
2501
2529
 
2502
- if (disposed) return err(semaphoreDisposedAbortError);
2530
+ if (disposables.disposed) return err(semaphoreDisposedAbortError);
2503
2531
 
2504
2532
  if (waiters.length > 0 || taken + requested > permits) {
2505
2533
  const waiter = Promise.withResolvers<Result<void, AbortError>>();
@@ -2548,31 +2576,19 @@ export const createSemaphore = (permits: Concurrency): Semaphore => {
2548
2576
  withPermit: <T, E, D>(task: Task<T, E, D>): Task<T, E, D> =>
2549
2577
  withPermits<T, E, D>(1)(task),
2550
2578
 
2551
- snapshot: () => ({
2552
- permits,
2553
- taken,
2554
- waiting: NonNegativeInt.orThrow(waiters.length),
2555
- available: NonNegativeInt.orThrow(permits - taken),
2556
- isIdle: taken === 0 && waiters.length === 0,
2557
- disposed,
2558
- }),
2559
-
2560
- [Symbol.dispose]: () => {
2561
- if (disposed) return;
2562
- disposed = true;
2563
-
2564
- using stack = new DisposableStack();
2565
- for (const fiber of fibers) {
2566
- stack.adopt(fiber, (fiber) => {
2567
- fiber.abort(semaphoreDisposedError);
2568
- });
2569
- }
2570
-
2571
- for (const waiter of waiters) {
2572
- waiter.resolve(err(semaphoreDisposedAbortError));
2573
- }
2574
- waiters.length = 0;
2579
+ snapshot: () => {
2580
+ assertNotDisposed(disposables);
2581
+ return {
2582
+ permits,
2583
+ taken,
2584
+ waiting: NonNegativeInt.orThrow(waiters.length),
2585
+ available: NonNegativeInt.orThrow(permits - taken),
2586
+ isIdle: taken === 0 && waiters.length === 0,
2587
+ disposed: disposables.disposed,
2588
+ };
2575
2589
  },
2590
+
2591
+ [Symbol.dispose]: () => disposables.dispose(),
2576
2592
  };
2577
2593
  };
2578
2594
 
@@ -2659,16 +2675,29 @@ export function createSemaphoreByKey<K, L = K>(
2659
2675
  permits: Concurrency,
2660
2676
  { lookup = identity as Lookup<K, L> }: CreateSemaphoreByKeyOptions<K, L> = {},
2661
2677
  ): SemaphoreByKey<K> {
2678
+ using disposer = new DisposableStack();
2662
2679
  const semaphoresByKey = createLookupMap<K, Semaphore, L>({
2663
2680
  lookup,
2664
2681
  });
2665
- let disposed = false;
2682
+
2683
+ disposer.defer(() => {
2684
+ using disposer = new DisposableStack();
2685
+ disposer.defer(() => {
2686
+ semaphoresByKey.clear();
2687
+ });
2688
+
2689
+ for (const semaphore of semaphoresByKey.values()) {
2690
+ disposer.use(semaphore);
2691
+ }
2692
+ });
2693
+
2694
+ const disposables = disposer.move();
2666
2695
 
2667
2696
  const withPermits =
2668
2697
  <T, E, D>(key: K, requestedPermits: Concurrency) =>
2669
2698
  (task: Task<T, E, D>): Task<T, E, D> =>
2670
2699
  async (run: Run<D>) => {
2671
- if (disposed) return err(semaphoreDisposedAbortError);
2700
+ if (disposables.disposed) return err(semaphoreDisposedAbortError);
2672
2701
 
2673
2702
  let semaphore = semaphoresByKey.get(key);
2674
2703
  if (!semaphore) {
@@ -2678,6 +2707,8 @@ export function createSemaphoreByKey<K, L = K>(
2678
2707
 
2679
2708
  using _ = {
2680
2709
  [Symbol.dispose]: () => {
2710
+ if (semaphoresByKey.get(key) !== semaphore) return;
2711
+
2681
2712
  const snapshot = semaphore.snapshot();
2682
2713
  if (snapshot.isIdle) {
2683
2714
  semaphoresByKey.delete(key);
@@ -2695,18 +2726,12 @@ export function createSemaphoreByKey<K, L = K>(
2695
2726
 
2696
2727
  withPermits,
2697
2728
 
2698
- snapshot: (key) => semaphoresByKey.get(key)?.snapshot() ?? null,
2699
-
2700
- [Symbol.dispose]: () => {
2701
- if (disposed) return;
2702
- disposed = true;
2703
-
2704
- using stack = new DisposableStack();
2705
- for (const semaphore of semaphoresByKey.values()) {
2706
- stack.use(semaphore);
2707
- }
2708
- semaphoresByKey.clear();
2729
+ snapshot: (key) => {
2730
+ assertNotDisposed(disposables);
2731
+ return semaphoresByKey.get(key)?.snapshot() ?? null;
2709
2732
  },
2733
+
2734
+ [Symbol.dispose]: () => disposables.dispose(),
2710
2735
  };
2711
2736
  }
2712
2737
 
@@ -2931,68 +2956,6 @@ export const createMutexRef = <T>(initialValue: T): MutexRef<T> => {
2931
2956
  };
2932
2957
  };
2933
2958
 
2934
- /**
2935
- * Cross-platform leader lock abstraction.
2936
- *
2937
- * `lock` returns a {@link Task} that waits until leadership is acquired and
2938
- * yields a lease.
2939
- *
2940
- * Returns {@link AsyncDisposable} lease. Dispose it to release leadership.
2941
- *
2942
- * @group Concurrency primitives
2943
- */
2944
- export interface LeaderLock {
2945
- readonly lock: (name: Name) => Task<AsyncDisposable>;
2946
- }
2947
-
2948
- /** @group Concurrency primitives */
2949
- export interface LeaderLockDep {
2950
- readonly leaderLock: LeaderLock;
2951
- }
2952
-
2953
- /**
2954
- * Creates an in-process {@link LeaderLock}.
2955
- *
2956
- * Uses one {@link Mutex} per {@link Name}. Suitable for runtimes without a
2957
- * cross-process lock manager (for example in-memory worker tests or React
2958
- * Native).
2959
- *
2960
- * @group Concurrency primitives
2961
- */
2962
- export const createInMemoryLeaderLock = (): LeaderLock => {
2963
- const mutexByName = createMutexByKey<Name>();
2964
-
2965
- return {
2966
- lock: (name) => async (run) => {
2967
- const leaseRun = run.create();
2968
- const released = createDeferred<void>();
2969
- const acquired = createDeferred<void>();
2970
-
2971
- void leaseRun(
2972
- mutexByName.withLock(name, async (run) => {
2973
- acquired.resolve(ok());
2974
- await run(released.task);
2975
- return ok();
2976
- }),
2977
- );
2978
-
2979
- const acquiredResult = await run(acquired.task);
2980
- if (!acquiredResult.ok) {
2981
- assert(
2982
- AbortError.is(acquiredResult.error),
2983
- "Leader lock acquisition deferred must not be disposed.",
2984
- );
2985
- void leaseRun[Symbol.asyncDispose]();
2986
- return err(acquiredResult.error);
2987
- }
2988
-
2989
- return ok({
2990
- [Symbol.asyncDispose]: leaseRun[Symbol.asyncDispose],
2991
- });
2992
- },
2993
- };
2994
- };
2995
-
2996
2959
  /**
2997
2960
  * Options for {@link all}, {@link allSettled}, {@link map}, and {@link mapSettled}.
2998
2961
  *
@@ -3244,7 +3207,7 @@ export function allSettled(
3244
3207
  input: Iterable<AnyTask> | Readonly<Record<string, AnyTask>>,
3245
3208
  options?: CollectOptions<boolean>,
3246
3209
  ): Task<unknown> {
3247
- return collect("allSettled", input, options) as Task<unknown>;
3210
+ return collect("allSettled", input, options);
3248
3211
  }
3249
3212
 
3250
3213
  /**
@@ -3546,9 +3509,7 @@ export interface AnyAbortError extends InferType<typeof AnyAbortError> {}
3546
3509
  */
3547
3510
  export const anyAbortError: AnyAbortError = { type: "AnyAbortError" };
3548
3511
 
3549
- type CollectInput =
3550
- | Iterable<Task<unknown, unknown>>
3551
- | Readonly<Record<string, AnyTask>>;
3512
+ type CollectInput = Iterable<AnyTask> | Readonly<Record<string, AnyTask>>;
3552
3513
 
3553
3514
  /** Shared implementation for {@link all} and {@link allSettled}. */
3554
3515
  const collect = (
@@ -3558,15 +3519,16 @@ const collect = (
3558
3519
  collect = true,
3559
3520
  abortReason = type === "all" ? allAbortError : allSettledAbortError,
3560
3521
  }: CollectOptions<boolean> = {},
3561
- ): Task<unknown, unknown> => {
3522
+ ): AnyTask => {
3562
3523
  const stopOn = type === "all" ? ("error" as const) : null;
3563
3524
 
3564
3525
  if (isIterable(input)) {
3565
- const array = arrayFrom(input as Iterable<unknown>);
3526
+ const tasks: Iterable<AnyTask> = input;
3527
+ const array = arrayFrom(tasks);
3566
3528
  if (!isNonEmptyArray(array))
3567
3529
  return () => ok(collect ? emptyArray : undefined);
3568
3530
 
3569
- return pool(array as ReadonlyArray<Task<unknown, unknown>>, {
3531
+ return pool(array, {
3570
3532
  stopOn,
3571
3533
  collect,
3572
3534
  abortReason,
@@ -3577,7 +3539,7 @@ const collect = (
3577
3539
  const taskArray: Array<AnyTask> = [];
3578
3540
  for (const key in input) {
3579
3541
  keys.push(key);
3580
- taskArray.push((input as Record<string, AnyTask>)[key]);
3542
+ taskArray.push(input[key]);
3581
3543
  }
3582
3544
  if (keys.length === 0) return () => ok(collect ? emptyRecord : undefined);
3583
3545
 
@@ -3749,7 +3711,7 @@ function pool<T, E>(
3749
3711
  };
3750
3712
 
3751
3713
  const workerCount = Math.min(run.concurrency, length);
3752
- const workers = arrayFrom(workerCount, () => run.daemon(worker));
3714
+ const workers = arrayFrom(workerCount, () => run.daemon(worker, run.deps));
3753
3715
 
3754
3716
  using _ = new DisposableStack();
3755
3717
  _.defer(() => {