@evolu/common 8.0.0-next.4 → 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.
package/src/Task.ts CHANGED
@@ -61,7 +61,6 @@ import {
61
61
  type CallbackWithTeardown,
62
62
  type Int1To100,
63
63
  type Mutable,
64
- type NewKeys,
65
64
  type Predicate,
66
65
  } from "./Types.js";
67
66
 
@@ -270,12 +269,22 @@ import {
270
269
  * await run(fetchUser(123));
271
270
  * ```
272
271
  *
273
- * For runtime-created dependencies, use {@link Run#addDeps}.
272
+ * Provide runtime-created dependencies to a single Task with `run(task, deps)`:
274
273
  *
275
- * ### 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
+ * ```
280
+ *
281
+ * For reusable resources that own async work, use `run.create(deps)` and
282
+ * dispose the created Run with the resource.
276
283
  *
277
- * {@link createRun} provides default {@link RunDeps} available to all Tasks
278
- * without declaring `D`:
284
+ * ### Default dependencies
285
+ *
286
+ * {@link createRun} provides default {@link RunDefaultDeps} available to all
287
+ * Tasks without declaring `D`:
279
288
  *
280
289
  * - {@link Console} — logging with hierarchical context via `child()`
281
290
  * - {@link Time} — current time
@@ -312,7 +321,7 @@ import {
312
321
  * ```
313
322
  *
314
323
  * For testing, use {@link testCreateRun} to get deterministic, controllable
315
- * implementations of all RunDeps.
324
+ * implementations of all RunDefaultDeps.
316
325
  *
317
326
  * ## Resource management
318
327
  *
@@ -416,6 +425,35 @@ import {
416
425
  * use {@link assertNotAborted} to crash immediately instead of threading the
417
426
  * impossible case through domain logic.
418
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
+ *
419
457
  * ### How do I type an anonymous Task callback?
420
458
  *
421
459
  * For one-off inline Tasks, put the type arguments on the {@link Run} call:
@@ -578,6 +616,17 @@ export interface Run<D = unknown> extends AsyncDisposable {
578
616
  /** Runs a {@link Task} and returns a {@link Fiber} handle. */
579
617
  <T, E>(task: Task<T, E, D>): Fiber<T, E, D>;
580
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
+
581
630
  /**
582
631
  * Runs a {@link Task} and throws if the returned {@link Result} is an error.
583
632
  *
@@ -601,13 +650,16 @@ export interface Run<D = unknown> extends AsyncDisposable {
601
650
  *
602
651
  * Throws: `Error` with the original Task error attached as `cause`.
603
652
  */
604
- 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
+ };
605
657
 
606
658
  /** Unique {@link Id} for this Run. */
607
659
  readonly id: Id;
608
660
 
609
661
  /** The parent {@link Run}, if this Run was created as a child. */
610
- readonly parent: Run<D> | null;
662
+ readonly parent: Run | null;
611
663
 
612
664
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal */
613
665
  readonly signal: AbortSignal;
@@ -641,7 +693,7 @@ export interface Run<D = unknown> extends AsyncDisposable {
641
693
  readonly getState: () => RunState;
642
694
 
643
695
  /** Returns the current child {@link Fiber}s. */
644
- readonly getChildren: () => ReadonlySet<Fiber<any, any, D>>;
696
+ readonly getChildren: () => ReadonlySet<AnyFiber>;
645
697
 
646
698
  /**
647
699
  * Creates a memoized {@link RunSnapshot} of this Run.
@@ -708,13 +760,15 @@ export interface Run<D = unknown> extends AsyncDisposable {
708
760
  *
709
761
  * For a long-lived reusable {@link Run}, use {@link Run.create}.
710
762
  */
711
- readonly daemon: Run<D>;
763
+ readonly daemon: Run;
712
764
 
713
765
  /**
714
766
  * Creates a {@link Run} from this Run.
715
767
  *
716
768
  * Like {@link createRun}, the returned Run is daemon: it stays running until
717
- * 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}.
718
772
  *
719
773
  * Use this for long-lived disposable resources that need to own async work.
720
774
  * The resource creates one internal Run with `run.create()` and uses that Run
@@ -727,77 +781,19 @@ export interface Run<D = unknown> extends AsyncDisposable {
727
781
  *
728
782
  * To run a single Task as daemon, use {@link Run.daemon}.
729
783
  */
730
- readonly create: () => Run<D>;
784
+ readonly create: {
785
+ (): Run<D>;
786
+ <Deps>(deps: Deps): Run<RunDefaultDeps & Deps>;
787
+ };
731
788
 
732
- /** Returns the dependencies passed to {@link createRun}. */
733
- readonly deps: RunDeps & D;
789
+ /** Returns this Run's dependencies. */
790
+ readonly deps: RunDefaultDeps & D;
734
791
 
735
792
  /**
736
793
  * @see {@link Concurrency}
737
794
  * @see {@link concurrently}
738
795
  */
739
796
  readonly concurrency: Concurrency;
740
-
741
- /**
742
- * Adds additional dependencies to this Run and returns it.
743
- *
744
- * Use for runtime-created dependencies — dependencies that cannot be created
745
- * in the composition root (e.g., app start).
746
- *
747
- * ### Example
748
- *
749
- * ```ts
750
- * // One-shot
751
- * await run.addDeps({ db })(getUser(123));
752
- *
753
- * // Multiple deps at once
754
- * await run.addDeps({ db, cache })(task);
755
- *
756
- * // Reusable — config comes from outside (message, file, etc.)
757
- * type DbWorkerDeps = DbDep; // or DbDep & CacheDep & ...
758
- *
759
- * const init =
760
- * (config: Config): Task<void, InitError, CreateDbDep> =>
761
- * async (run) => {
762
- * const { createDb } = run.deps;
763
- * await using disposer = new AsyncDisposableStack();
764
- *
765
- * const db = disposer.use(await run.orThrow(startApp()));
766
- * if (!db.ok) return db;
767
- *
768
- * const runWithDb = run.addDeps({ db: db.value });
769
- *
770
- * await runWithDb(getUser(123));
771
- * await runWithDb(insertUser(user));
772
- * return ok();
773
- * };
774
- * ```
775
- *
776
- * ## FAQ
777
- *
778
- * ### How does it work?
779
- *
780
- * This is the whole implementation:
781
- *
782
- * ```ts
783
- * run.addDeps = <E extends NewKeys<E, D>>(newDeps: E): Run<D & E> => {
784
- * depsRef.modify((currentDeps) => {
785
- * const duplicate = Object.keys(newDeps).find(
786
- * (k) => k in currentDeps,
787
- * );
788
- * assert(!duplicate, `Dependency '${duplicate}' already added.`);
789
- * return [undefined, { ...currentDeps, ...newDeps }];
790
- * });
791
- * return self as unknown as Run<D & E>;
792
- * };
793
- * ```
794
- *
795
- * Dependencies are stored in a shared {@link Ref}, so `addDeps` propagates to
796
- * all runs. The runtime assertion ensures dependencies are created once —
797
- * automatic deduplication would mask poor design (dependencies should have a
798
- * single, clear point of creation).
799
- */
800
- readonly addDeps: <E extends NewKeys<E, D>>(extraDeps: E) => Run<D & E>;
801
797
  }
802
798
 
803
799
  /**
@@ -896,12 +892,19 @@ export interface Fiber<T = unknown, E = unknown, D = unknown>
896
892
  getState(): RunState<T, E>;
897
893
  }
898
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
+
899
902
  /**
900
903
  * Extracts the value type from a {@link Fiber}.
901
904
  *
902
905
  * @group Type utilities
903
906
  */
904
- export type InferFiberOk<F extends Fiber<any, any, any>> =
907
+ export type InferFiberOk<F extends AnyFiber> =
905
908
  F extends Fiber<infer T, any, any> ? T : never;
906
909
 
907
910
  /**
@@ -909,7 +912,7 @@ export type InferFiberOk<F extends Fiber<any, any, any>> =
909
912
  *
910
913
  * @group Type utilities
911
914
  */
912
- export type InferFiberErr<F extends Fiber<any, any, any>> =
915
+ export type InferFiberErr<F extends AnyFiber> =
913
916
  F extends Fiber<any, infer E, any> ? E : never;
914
917
 
915
918
  /**
@@ -917,7 +920,7 @@ export type InferFiberErr<F extends Fiber<any, any, any>> =
917
920
  *
918
921
  * @group Type utilities
919
922
  */
920
- export type InferFiberDeps<F extends Fiber<any, any, any>> =
923
+ export type InferFiberDeps<F extends AnyFiber> =
921
924
  F extends Fiber<any, any, infer D> ? D : never;
922
925
 
923
926
  /**
@@ -1023,7 +1026,7 @@ export type RunSnapshotState = typeof RunSnapshotState.Type;
1023
1026
  * @see {@link Run.snapshot}
1024
1027
  */
1025
1028
  export interface RunSnapshot {
1026
- /** The {@link Run.id} this snapshot represents. */
1029
+ /** The Run id this snapshot represents. */
1027
1030
  readonly id: Id;
1028
1031
 
1029
1032
  /** The current lifecycle state. */
@@ -1083,7 +1086,7 @@ export interface RunConfigDep {
1083
1086
  }
1084
1087
 
1085
1088
  /** Default deps provided by {@link createRun}. */
1086
- export type RunDeps = ConsoleDep &
1089
+ export type RunDefaultDeps = ConsoleDep &
1087
1090
  RandomBytesDep &
1088
1091
  RandomDep &
1089
1092
  TimeDep &
@@ -1093,7 +1096,7 @@ export type RunDeps = ConsoleDep &
1093
1096
  // Partial<TracerConfigDep> & // TODO:
1094
1097
  // Partial<TracerDep>; // TODO:
1095
1098
 
1096
- const runDeps: RunDeps = {
1099
+ const runDefaultDeps: RunDefaultDeps = {
1097
1100
  console: createConsole(),
1098
1101
  randomBytes: createRandomBytes(),
1099
1102
  random: createRandom(),
@@ -1125,7 +1128,7 @@ export interface CreateRun<BaseDeps> {
1125
1128
  * Node.js `uncaughtException`, `unhandledRejection`, and graceful shutdown
1126
1129
  * handling, and `@evolu/react-native` adds React Native global error handling.
1127
1130
  *
1128
- * {@link RunDeps} provides default dependencies:
1131
+ * {@link RunDefaultDeps} provides default dependencies:
1129
1132
  *
1130
1133
  * - {@link Time}
1131
1134
  * - {@link Console}
@@ -1163,7 +1166,7 @@ export interface CreateRun<BaseDeps> {
1163
1166
  * };
1164
1167
  *
1165
1168
  * // Composition root: create a Run with custom deps
1166
- * type AppDeps = RunDeps & ConfigDep;
1169
+ * type AppDeps = RunDefaultDeps & ConfigDep;
1167
1170
  *
1168
1171
  * const appDeps: AppDeps = {
1169
1172
  * ...testCreateDeps(), // or spread individual deps
@@ -1176,24 +1179,24 @@ export interface CreateRun<BaseDeps> {
1176
1179
  * const result = await run(fetchUser("123"));
1177
1180
  *
1178
1181
  * // TypeScript catches missing deps at compile time:
1179
- * // await using run2 = createRun(); // Run<RunDeps>
1182
+ * // await using run2 = createRun(); // Run<RunDefaultDeps>
1180
1183
  * // run2(fetchUser("123")); // Error: Property 'config' is missing
1181
1184
  * ```
1182
1185
  *
1183
1186
  * @group Creating Run
1184
1187
  */
1185
- export const createRun: CreateRun<RunDeps> = <D>(
1188
+ export const createRun: CreateRun<RunDefaultDeps> = <D>(
1186
1189
  deps?: D,
1187
- ): Run<RunDeps & D> => {
1188
- const mergedDeps = { ...runDeps, ...deps } as RunDeps & D;
1189
- return createRunInternal(createRef(mergedDeps))();
1190
- };
1190
+ ): Run<RunDefaultDeps & D> =>
1191
+ createRunInternal({ ...runDefaultDeps, ...deps } as RunDefaultDeps & D);
1191
1192
 
1192
1193
  /** Internal Run properties, hidden from public API via TypeScript types. */
1193
- interface RunInternal<D extends RunDeps = RunDeps> extends Run<D> {
1194
+ interface RunInternal<
1195
+ D extends RunDefaultDeps = RunDefaultDeps,
1196
+ > extends Run<D> {
1194
1197
  ownTaskSettled: PromiseWithResolvers<void> | null;
1195
1198
 
1196
- readonly requestAbort: (reason: unknown) => void;
1199
+ readonly requestAbort: (abortError: AbortError) => void;
1197
1200
  readonly requestSignal: AbortSignal;
1198
1201
 
1199
1202
  /**
@@ -1208,227 +1211,233 @@ interface RunInternal<D extends RunDeps = RunDeps> extends Run<D> {
1208
1211
  readonly handleTaskSettled: () => void;
1209
1212
  }
1210
1213
 
1211
- const createRunInternal =
1212
- <D extends RunDeps>(depsRef: Ref<D>) =>
1213
- (
1214
- parent?: RunInternal<D>,
1215
- daemon?: RunInternal<D>,
1216
- abortBehavior?: AbortBehavior,
1217
- concurrencyBehavior?: Concurrency,
1218
- ): RunInternal<D> => {
1219
- const parentMask = parent?.abortMask ?? isAbortable;
1220
-
1221
- let abortMask: AbortMask;
1222
- switch (abortBehavior) {
1223
- case undefined:
1224
- abortMask = parentMask;
1225
- break;
1226
- case "unabortable":
1227
- abortMask = increment(parentMask) as AbortMask;
1228
- break;
1229
- default:
1230
- assert(
1231
- abortBehavior <= parentMask,
1232
- "restore used outside its unabortableMask",
1233
- );
1234
- abortMask = abortBehavior;
1235
- }
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
+ }
1236
1238
 
1237
- const requestController = new AbortController();
1238
- const signalController = new AbortController();
1239
+ const requestController = new AbortController();
1240
+ const signalController = new AbortController();
1239
1241
 
1240
- let state: RunState = running;
1241
- let result: UnknownResult | undefined;
1242
- let outcome: UnknownResult | undefined;
1243
- 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;
1244
1246
 
1245
- const requestAbort = (reason: unknown) => {
1246
- const abortError = reason as AbortError;
1247
- if (abortMask === isAbortable) signalController.abort(abortError);
1248
- requestController.abort(abortError);
1249
- };
1247
+ const requestAbort = (abortError: AbortError) => {
1248
+ if (abortMask === isAbortable) signalController.abort(abortError);
1249
+ requestController.abort(abortError);
1250
+ };
1250
1251
 
1251
- if (parent) {
1252
- subscribeToAbort(
1253
- parent.requestSignal,
1254
- () => requestAbort(parent.requestSignal.reason),
1255
- { signal: requestController.signal },
1256
- );
1257
- }
1252
+ if (parent) {
1253
+ subscribeToAbort(
1254
+ parent.requestSignal,
1255
+ () => requestAbort(parent.requestSignal.reason as AbortError),
1256
+ { signal: requestController.signal },
1257
+ );
1258
+ }
1258
1259
 
1259
- const emitEvent = (data: RunEventData) => {
1260
- const deps = depsRef.get();
1261
- if (!deps.runConfig?.eventsEnabled.get()) return;
1262
- const e: RunEvent = { id: self.id, timestamp: deps.time.now(), data };
1263
- for (let node: Run<D> | null = self; node; node = node.parent) {
1264
- node.onEvent?.(e);
1265
- }
1266
- };
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
+ };
1267
1267
 
1268
- const run = <T, E>(task: Task<T, E, D>): Fiber<T, E, D> => {
1269
- const childRun = createRunInternal(depsRef)(
1270
- self,
1271
- daemon ?? self,
1272
- getAbortBehavior(task),
1273
- getConcurrencyBehavior(task),
1274
- );
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
+ );
1275
1287
 
1276
- if (state !== running) {
1277
- childRun.requestAbort(runStoppedAbortError);
1278
- task = () => err(runStoppedAbortError);
1279
- } else if (
1280
- signalController.signal.aborted &&
1281
- childRun.abortMask === isAbortable
1282
- ) {
1283
- childRun.requestAbort(signalController.signal.reason);
1284
- task = () => err(signalController.signal.reason);
1285
- }
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
+ }
1286
1299
 
1287
- const childFiber: Fiber<T, E, D> = Object.assign(
1288
- Promise.try(task, childRun)
1289
- .then(childRun.handleTaskFulfilled)
1290
- .finally(childRun.handleTaskSettled)
1291
- .finally(childRun[Symbol.asyncDispose])
1292
- .finally(() => {
1293
- children = deleteFromSet(children, childFiber);
1294
- emitEvent({ type: "ChildRemoved", childId: childRun.id });
1295
- }),
1296
- {
1297
- run: childRun,
1298
- abort: (reason?: unknown): void => {
1299
- childRun.requestAbort(createAbortError(reason));
1300
- },
1301
- getState: () => childRun.getState() as RunState<T, E>,
1302
- [Symbol.dispose]: () => {
1303
- childFiber.abort();
1304
- },
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));
1305
1313
  },
1306
- );
1307
-
1308
- children = addToSet(children, childFiber);
1309
- emitEvent({ type: "ChildAdded", childId: childRun.id });
1314
+ getState: () => childRun.getState(),
1315
+ [Symbol.dispose]: () => {
1316
+ childFiber.abort();
1317
+ },
1318
+ },
1319
+ );
1310
1320
 
1311
- return childFiber;
1312
- };
1321
+ children = addToSet(children, childFiber);
1322
+ emitEvent({ type: "ChildAdded", childId: childRun.id });
1313
1323
 
1314
- const self = run as RunInternal<D>;
1324
+ return childFiber;
1325
+ };
1315
1326
 
1316
- {
1317
- const run = self as Mutable<RunInternal<D>>;
1318
- const id = createId(depsRef.get());
1319
-
1320
- let snapshot: RunSnapshot | null = null;
1321
- let disposingPromise: Promise<void> | null = null;
1322
-
1323
- run.orThrow = async (task) => getOrThrow(await self(task));
1324
- run.id = id;
1325
- run.parent = parent ?? null;
1326
-
1327
- run.signal = signalController.signal;
1328
- run.abortMask = abortMask;
1329
- run.onAbort = (callback) => {
1330
- if (abortMask !== isAbortable) return;
1331
- subscribeToAbort(
1332
- signalController.signal,
1333
- () => callback((signalController.signal.reason as AbortError).reason),
1334
- { once: true, signal: requestController.signal },
1335
- );
1336
- };
1337
- run.getState = () => state;
1338
- run.getChildren = () => children;
1327
+ let snapshot: RunSnapshot | null = null;
1328
+ let disposingPromise: Promise<void> | null = null;
1339
1329
 
1340
- run.snapshot = () => {
1341
- const childSnapshots = Array.from(children).map((fiber) =>
1342
- fiber.run.snapshot(),
1343
- );
1344
- if (
1345
- snapshot?.state !== state ||
1346
- !eqArrayStrict(snapshot.children, childSnapshots)
1347
- ) {
1348
- snapshot = {
1349
- id,
1350
- state: state as RunSnapshotState,
1351
- children: childSnapshots,
1352
- abortMask,
1353
- };
1354
- }
1355
- return snapshot;
1356
- };
1330
+ const self = run as RunInternal<D>;
1357
1331
 
1358
- 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
+ }
1359
1349
 
1360
- run.create = () => run.daemon(createDeferred().task).run;
1350
+ run.orThrow = orThrow;
1351
+ run.id = id;
1352
+ run.parent = parent ?? null;
1361
1353
 
1362
- 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;
1363
1366
 
1364
- run.concurrency =
1365
- 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
+ };
1366
1384
 
1367
- run.addDeps = <E extends NewKeys<E, D>>(newDeps: E): Run<D & E> => {
1368
- depsRef.modify((currentDeps) => {
1369
- const duplicate = Object.keys(newDeps).find((k) => k in currentDeps);
1370
- assert(
1371
- !duplicate,
1372
- `Dependency '${duplicate}' already added. ` +
1373
- `This assert ensures dependencies are created once. ` +
1374
- `Automatic deduplication would mask bugs.`,
1375
- );
1376
- 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 });
1377
1417
  });
1378
- return self as unknown as Run<D & E>;
1379
- };
1380
-
1381
- run[Symbol.asyncDispose] = () => {
1382
- if (disposingPromise) return disposingPromise;
1383
-
1384
- state = { type: "Disposing" };
1385
- emitEvent({ type: "StateChanged", state });
1386
- requestAbort(runStoppedAbortError);
1387
-
1388
- disposingPromise = Promise.allSettled(
1389
- (run.ownTaskSettled
1390
- ? [run.ownTaskSettled.promise, ...children]
1391
- : children) as Iterable<PromiseLike<unknown>>,
1392
- )
1393
- .then(lazyVoid)
1394
- .finally(() => {
1395
- /**
1396
- * Root and daemon Runs have no own Task, so
1397
- * `run.handleTaskFulfilled` never populates their terminal values.
1398
- * In that case disposal publishes `ok()` for both `result` and
1399
- * `outcome`. Task-backed Runs normally reach this point with both
1400
- * values already set.
1401
- */
1402
- [result, outcome] = [result ?? ok(), outcome ?? ok()];
1403
- state = { type: "Settled", result, outcome };
1404
- emitEvent({ type: "StateChanged", state });
1405
- });
1406
1418
 
1407
- return disposingPromise;
1408
- };
1409
-
1410
- // Internal
1411
- run.ownTaskSettled = parent ? Promise.withResolvers<void>() : null;
1412
-
1413
- run.requestAbort = requestAbort;
1414
- run.requestSignal = requestController.signal;
1415
-
1416
- run.handleTaskFulfilled = (taskOutcome) => {
1417
- const taskResult = run.signal.aborted
1418
- ? (err(run.signal.reason as AbortError) as typeof taskOutcome)
1419
- : taskOutcome;
1420
- result = taskResult;
1421
- outcome = taskOutcome;
1422
- return taskResult;
1423
- };
1419
+ return disposingPromise;
1420
+ };
1424
1421
 
1425
- run.handleTaskSettled = () => {
1426
- run.ownTaskSettled?.resolve();
1427
- };
1428
- }
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
+ }
1429
1438
 
1430
- return self;
1431
- };
1439
+ return self;
1440
+ };
1432
1441
 
1433
1442
  const running: RunState = { type: "Running" };
1434
1443
 
@@ -1485,16 +1494,19 @@ const abortBehavior =
1485
1494
  /**
1486
1495
  * Makes a {@link Task} unabortable.
1487
1496
  *
1488
- * Once started, an unabortable Task always completes abort requests are
1489
- * 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.
1490
1499
  *
1491
- * 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)`
1492
1502
  * short-circuits before task execution and returns `err(AbortError)` with
1493
1503
  * {@link runStoppedError} as reason. So `unabortable` means “do not interrupt
1494
1504
  * this Task once it has started”, not “remove AbortError from its type”.
1495
1505
  *
1496
- * When that pre-start abort would be a programmer error, assert it explicitly
1497
- * 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.
1498
1510
  *
1499
1511
  * ### Example
1500
1512
  *
@@ -1753,7 +1765,7 @@ export const callback =
1753
1765
  readonly ok: Callback<T>;
1754
1766
  readonly err: Callback<E>;
1755
1767
  readonly signal: AbortSignal;
1756
- readonly deps: RunDeps;
1768
+ readonly deps: RunDefaultDeps;
1757
1769
  }>,
1758
1770
  ): Task<T, E> =>
1759
1771
  (run) =>
@@ -2023,10 +2035,7 @@ export const retry =
2023
2035
  <T, E, D = unknown, Output = unknown>(
2024
2036
  task: Task<T, E, D>,
2025
2037
  schedule: Schedule<Output, E>,
2026
- {
2027
- retryable = lazyTrue,
2028
- onRetry,
2029
- }: RetryOptions<E, Output> = {},
2038
+ { retryable = lazyTrue, onRetry }: RetryOptions<E, Output> = {},
2030
2039
  ): Task<T, RetryError<E>, D> =>
2031
2040
  async (run) => {
2032
2041
  const step = schedule(run.deps);
@@ -2143,10 +2152,7 @@ export const repeat =
2143
2152
  <T, E, D = unknown, Output = unknown>(
2144
2153
  task: Task<T, E, D>,
2145
2154
  schedule: Schedule<Output, T>,
2146
- {
2147
- repeatable = lazyTrue,
2148
- onRepeat,
2149
- }: RepeatOptions<T, Output> = {},
2155
+ { repeatable = lazyTrue, onRepeat }: RepeatOptions<T, Output> = {},
2150
2156
  ): Task<T, E, D> =>
2151
2157
  async (run) => {
2152
2158
  const step = schedule(run.deps);
@@ -2185,7 +2191,7 @@ export const repeat =
2185
2191
  *
2186
2192
  * Similar to
2187
2193
  * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers | Promise.withResolvers},
2188
- * but integrated with {@link Task} and {@link Run} for cancellation support.
2194
+ * but integrated with {@link Task} and {@link Run}.
2189
2195
  *
2190
2196
  * Use for bridging callback-based APIs or coordinating between Tasks.
2191
2197
  *
@@ -3201,7 +3207,7 @@ export function allSettled(
3201
3207
  input: Iterable<AnyTask> | Readonly<Record<string, AnyTask>>,
3202
3208
  options?: CollectOptions<boolean>,
3203
3209
  ): Task<unknown> {
3204
- return collect("allSettled", input, options) as Task<unknown>;
3210
+ return collect("allSettled", input, options);
3205
3211
  }
3206
3212
 
3207
3213
  /**
@@ -3503,9 +3509,7 @@ export interface AnyAbortError extends InferType<typeof AnyAbortError> {}
3503
3509
  */
3504
3510
  export const anyAbortError: AnyAbortError = { type: "AnyAbortError" };
3505
3511
 
3506
- type CollectInput =
3507
- | Iterable<Task<unknown, unknown>>
3508
- | Readonly<Record<string, AnyTask>>;
3512
+ type CollectInput = Iterable<AnyTask> | Readonly<Record<string, AnyTask>>;
3509
3513
 
3510
3514
  /** Shared implementation for {@link all} and {@link allSettled}. */
3511
3515
  const collect = (
@@ -3515,15 +3519,16 @@ const collect = (
3515
3519
  collect = true,
3516
3520
  abortReason = type === "all" ? allAbortError : allSettledAbortError,
3517
3521
  }: CollectOptions<boolean> = {},
3518
- ): Task<unknown, unknown> => {
3522
+ ): AnyTask => {
3519
3523
  const stopOn = type === "all" ? ("error" as const) : null;
3520
3524
 
3521
3525
  if (isIterable(input)) {
3522
- const array = arrayFrom(input as Iterable<unknown>);
3526
+ const tasks: Iterable<AnyTask> = input;
3527
+ const array = arrayFrom(tasks);
3523
3528
  if (!isNonEmptyArray(array))
3524
3529
  return () => ok(collect ? emptyArray : undefined);
3525
3530
 
3526
- return pool(array as ReadonlyArray<Task<unknown, unknown>>, {
3531
+ return pool(array, {
3527
3532
  stopOn,
3528
3533
  collect,
3529
3534
  abortReason,
@@ -3534,7 +3539,7 @@ const collect = (
3534
3539
  const taskArray: Array<AnyTask> = [];
3535
3540
  for (const key in input) {
3536
3541
  keys.push(key);
3537
- taskArray.push((input as Record<string, AnyTask>)[key]);
3542
+ taskArray.push(input[key]);
3538
3543
  }
3539
3544
  if (keys.length === 0) return () => ok(collect ? emptyRecord : undefined);
3540
3545
 
@@ -3706,7 +3711,7 @@ function pool<T, E>(
3706
3711
  };
3707
3712
 
3708
3713
  const workerCount = Math.min(run.concurrency, length);
3709
- const workers = arrayFrom(workerCount, () => run.daemon(worker));
3714
+ const workers = arrayFrom(workerCount, () => run.daemon(worker, run.deps));
3710
3715
 
3711
3716
  using _ = new DisposableStack();
3712
3717
  _.defer(() => {