API reference@evolu/common › Task

JavaScript-native structured concurrency.

Structured concurrency organizes running tasks into a tree. Every child belongs to a parent, a parent waits for its children before it completes, and abort propagates from parents to descendants. Races and fail-fast control flow abort siblings that are no longer needed.

With plain AbortController code, these guarantees depend on call-site discipline: someone must remember the finally that aborts started tasks and the await that waits for cleanup. Evolu makes both structural: run(task) registers every child before it starts, and the parent Run settles only after child cleanup finishes.

Evolu implements structured concurrency with:

  • A Task is a function passed to Run that returns an Awaitable Result and declares its dependencies.
  • A Run starts Tasks and owns their lifetimes.
  • A Fiber is the Promise-backed handle returned when a Run starts a Task.
  • An AbortableFiber adds explicit abort and async disposal.

Together, these APIs provide abort, cleanup, defect handling, dependency injection, monitoring, and resource management.

Tasks return a Result containing either success or a domain error. Abort is control flow represented by AbortError. If a Task throws or rejects with anything else, that is a defect: the root Run reports it and shuts down its tree so code does not continue in a potentially invalid state.

import {
  assertOk,
  assertType,
  createRun,
  err,
  ok,
  type Result,
  type Task,
  type Typed,
} from "@evolu/common";

interface User {
  readonly id: string;
  readonly name: string;
}

interface Db {
  readonly usersById: ReadonlyMap<string, User>;
}

interface DbDep {
  readonly db: Db;
}

const getUser =
  (id: string): Task<User, UserNotFoundError, DbDep> =>
  (run) => {
    const user = run.deps.db.usersById.get(id);
    return user ? ok(user) : err({ type: "UserNotFound", id });
  };

// Typed declares the `type` discriminant without repeating the property.
interface UserNotFoundError extends Typed<"UserNotFound"> {
  readonly id: string;
}

const user: User = { id: "user-1", name: "Ada" };

// Provide dependencies at the composition root. `await using` disposes the
// Run and waits for its child Tasks before leaving this scope.
await using run = createRun({
  db: { usersById: new Map([[user.id, user]]) },
});

const result = await run(getUser(user.id));
assertType<Result<User, UserNotFoundError>, typeof result>();
assertOk(result, user);

In composition roots, prefer the lifecycle API from the matching Evolu platform package:

  • Node.js: @evolu/nodejs
  • Web: @evolu/web
  • React Native: @evolu/react-native

Composition

CategoryHelperDescription
CollectionallReturn Ok values or stop on first Err
allSettledReturn every Task Result
eachHandle each Task Result
InteropcallbackWrap callback APIs
fetchNative fetch with bounded Response use
TimingsleepPause execution
timeoutTime-bounded execution
ResilienceretryRetry domain errors with a schedule
RepetitionrepeatRepeat successes with a schedule
RacinganyFirst Ok wins
raceFirst settled Result wins
firstNFirst n Ok values win
firstNSettledFirst n Results win
SchedulingprioritizedAssign scheduler priority
yieldNowYield to the host scheduler
LifetimedaemonRun under root ownership
acquireUseReleaseBracket acquire, use, and release
AbortabilityunabortableMask abort after a Task starts
unabortableMaskMask abort and selectively restore it

Helpers that process multiple Tasks run sequentially by default. Use a concurrency option to run more than one Task at a time.

Sequential composition

For ordinary sequential composition, use imperative code:

import {
  assertOk,
  assertType,
  createRun,
  err,
  ok,
  type Result,
  type Task,
  type Typed,
} from "@evolu/common";

interface User {
  readonly id: string;
  readonly profileId: string;
}

interface Profile {
  readonly id: string;
}

const getUser =
  (id: string): Task<User, UserNotFoundError> =>
  () =>
    id === "user-1"
      ? ok({ id, profileId: "profile-1" })
      : err({ type: "UserNotFound", id });

interface UserNotFoundError extends Typed<"UserNotFound"> {
  readonly id: string;
}

const getProfile =
  (id: string): Task<Profile, ProfileNotFoundError> =>
  () =>
    id === "profile-1" ? ok({ id }) : err({ type: "ProfileNotFound", id });

interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {
  readonly id: string;
}

const getUserWithProfile =
  (
    id: string,
  ): Task<
    { readonly user: User; readonly profile: Profile },
    UserNotFoundError | ProfileNotFoundError
  > =>
  async (run) => {
    const user = await run(getUser(id));
    if (!user.ok) return user;

    const profile = await run(getProfile(user.value.profileId));
    if (!profile.ok) return profile;

    return ok({ user: user.value, profile: profile.value });
  };

await using run = createRun();
const result = await run(getUserWithProfile("user-1"));
assertType<
  Result<
    { readonly user: User; readonly profile: Profile },
    UserNotFoundError | ProfileNotFoundError
  >,
  typeof result
>();
assertOk(result, {
  user: { id: "user-1", profileId: "profile-1" },
  profile: { id: "profile-1" },
});

Evolu intentionally avoids pipe APIs, chainable methods, and generator-based effect DSLs. Plain async/await with early returns is easier to read, review, and debug, and it lets TypeScript narrow Result values through ordinary control flow.

Resilient fetch

fetch with a body mode already returns a plain value, so resilience is ordinary Task composition. Combine timeout and retry to bound each attempt and retry recoverable domain errors:

import {
  assertType,
  exponential,
  fetch,
  jitter,
  maxDelay,
  retry,
  take,
  timeout,
  type FetchError,
  type RetryTaskError,
  type Task,
  type TimeoutError,
} from "@evolu/common";

const fetchWithRetry = (url: string) =>
  retry(
    timeout(fetch(url, "text"), "30s"),
    // A jittered, capped, limited exponential backoff.
    jitter("100%")(maxDelay("20s")(take(2)(exponential("100ms")))),
  );

assertType<
  Task<string, RetryTaskError<FetchError | TimeoutError>>,
  ReturnType<typeof fetchWithRetry>
>();

Concurrent composition

Run composed Tasks with a concurrency option and all:

import {
  assertEqual,
  assertOk,
  all,
  createRun,
  ok,
  sleep,
  type Task,
} from "@evolu/common";

await using run = createRun();

const urls = [
  "https://api.example.com/users",
  "https://api.example.com/posts",
  "https://api.example.com/comments",
];
let activeRequests = 0;
let maxActiveRequests = 0;
const fetchUrl =
  (url: string): Task<string> =>
  async (run) => {
    activeRequests += 1;
    maxActiveRequests = Math.max(maxActiveRequests, activeRequests);
    await run.ok(sleep("1ms"));
    activeRequests -= 1;
    return ok(url);
  };

// At most 2 concurrent requests.
const result = await run(all(urls, fetchUrl, { concurrency: 2 }));
assertOk(result, urls);
assertEqual(maxActiveRequests, 2);

Task helpers compose Tasks; concurrency primitives are stateful objects that coordinate Tasks across call sites. Create them with their createX factories and share them where coordination is needed.

PrimitiveDescription
DeferredOne-shot value resolved from outside
GateBlock and release Tasks repeatedly
SemaphoreLimit concurrent Tasks with permits
MutexRun Tasks one at a time
SemaphoreByKeyPer-key permits with automatic cleanup
MutexByKeyPer-key one-at-a-time execution
MutexRefRef with serialized Task transitions

Dependency injection

Task DI is Evolu Pure DI applied to Run. A Task declares required capabilities with its D type parameter and reads them from Run.deps.

createRun supplies dependencies to the root Run and its children. A Run can also start one child Task with runtime-created dependencies by calling run(task, deps), where deps is checked as RunCustomDeps.

Use normal Task arguments for per-call values and D for capabilities, resources, or services shared by all code running inside a Run.

import { assertOk, createRun, ok, type Task } from "@evolu/common";

interface GreetingFormatter {
  readonly format: (name: string) => string;
}

interface GreetingFormatterDep {
  readonly greetingFormatter: GreetingFormatter;
}

const greet =
  (name: string): Task<string, never, GreetingFormatterDep> =>
  (run) =>
    ok(run.deps.greetingFormatter.format(name));

const formal: GreetingFormatter = {
  format: (name) => `Hello, ${name}`,
};
const casual: GreetingFormatter = {
  format: (name) => `Hi, ${name}`,
};

await using run = createRun({ greetingFormatter: formal });

// Root dependencies are inherited.
assertOk(await run(greet("Ada")), "Hello, Ada");

// Child-specific dependencies replace the root's custom dependencies.
assertOk(await run(greet("Ada"), { greetingFormatter: casual }), "Hi, Ada");

Default dependencies

createRun provides default RunDefaultDeps available to all Tasks without declaring D:

For testing, use testCreateRun to get deterministic, controllable implementations of all RunDefaultDeps.

Resource management

JavaScript provides standard resource management. Evolu adds DisposableRun.defer for closure-held state owned by a reusable Run.

Choose the ownership primitive by where the resource is reachable:

Returning resources from Tasks

A Task that successfully returns a disposable resource transfers ownership of a live resource to its caller. The resource must remain live after the Task settles. Do not register its disposal with the creating Task's DisposableRun.defer, because that child Run is disposed when the Task settles.

Use AsyncDisposableStack while creating a resource. On a Result error, abort, or defect, stack unwinding disposes partially created resources. On success, AsyncDisposableStack.move transfers ownership to the returned resource. A recoverable creation failure should be a typed Result error; undefined should represent valid absence, not failure.

import {
  assertEqual,
  assertFalse,
  assertTrue,
  createRun,
  ok,
  type Task,
  type Typed,
} from "@evolu/common";

interface Socket extends AsyncDisposable {
  readonly send: (message: string) => string;
}

interface Connection extends AsyncDisposable {
  readonly send: (message: string) => string;
}

let socketDisposed = false;
const openSocket: Task<Socket, ConnectionFailedError> = () =>
  ok({
    send: (message) => message,
    [Symbol.asyncDispose]: () => {
      socketDisposed = true;
      return Promise.resolve();
    },
  });

interface ConnectionFailedError extends Typed<"ConnectionFailed"> {}

const handshake =
  (_socket: Socket): Task<void, ConnectionFailedError> =>
  () =>
    ok();

const createConnection: Task<Connection, ConnectionFailedError> = async (
  run,
) => {
  await using disposer = new AsyncDisposableStack();

  const socketResult = await run(openSocket);
  if (!socketResult.ok) return socketResult;
  const socket = disposer.use(socketResult.value);

  const handshakeResult = await run(handshake(socket));
  if (!handshakeResult.ok) return handshakeResult;

  const disposables = disposer.move();
  return ok({
    send: (message) => socket.send(message),
    [Symbol.asyncDispose]: () => disposables.disposeAsync(),
  });
};

await using run = createRun();
const result = await run(createConnection);
assertTrue(result.ok);
assertFalse(socketDisposed);
assertEqual(result.value.send("hello"), "hello");
await result.value[Symbol.asyncDispose]();
assertTrue(socketDisposed);

Use Run.ok with await using when a Task whose error type is never returns a disposable value. Use acquireUseRelease when acquisition and release are separate steps rather than a disposable value.

Awaitable

A Task returns Awaitable, so its body may produce a Result immediately or asynchronously. Run is always async and returns a Fiber; callers use the same ownership model either way.

  • SyncResult, native using / DisposableStack
  • Async → Task, Run, Fiber, await using / AsyncDisposableStack

A Task is an async ownership boundary, not a general unit of program decomposition. Calling run(task) always creates a child Run by design. Use a plain async function when it does not need its own Run.

A unified sync/async effect API is technically possible. It can detect Promise-like values with isPromiseLike, dispose synchronous resources first, continue with asynchronous disposal when necessary, and track whether callers must await the result. Evolu deliberately keeps the two models separate instead: plain functions and Result for synchronous code, Task and Run for asynchronous ownership. Most effects involve inherently asynchronous I/O, while synchronous code benefits from a simpler API and no Task overhead.

Keep synchronous computation as plain functions returning Result. Prefer passing values rather than dependencies, following the impure/pure/impure sandwich pattern where impure code gathers data, pure functions process it, and impure code performs effects with the result. For example, a pure function can accept a RandomNumber value instead of depending on Random.

Large CPU-bound computations, such as parsing large JSON, sorting millions of items, or complex cryptography, belong in a worker. Model the asynchronous call to that worker as a Task so Run can provide timeout, abort, cleanup, and monitoring.

Glossary

  • Defect — a thrown or rejected value other than AbortError, rather than a declared Result error.
  • Outcome — a Fiber's settlement: resolution with the Task Result, or rejection with AbortError. The original defect is reported through ReportDefectDep whether or not the Fiber is observed; the Fiber boundary represents the panic with AbortError whose reason is PanicAbortReason.
  • Create — construct a new value or a resource.
  • Acquire — obtain a usable resource. Acquisition may create a new resource, borrow one, open one, or take a lease/lock.
  • Release — relinquish a previously acquired resource or lease. Release pairs with acquire and need not mean disposal; examples include unlock, logout, or returning a pooled resource.
  • Dispose / disposal — owner-driven resource finalization via JavaScript resource management (Symbol.dispose, Symbol.asyncDispose, using, AsyncDisposableStack).

FAQ

Why is AbortError not part of every Task error type?

The E type parameter represents declared domain errors. Abort is structured-concurrency control flow, not a domain error. A direct run(task) rejects with AbortError when the Task observes abort. Use run.abortable(task) when abort should be handled as an ordinary Result error at the Fiber boundary, or daemon(task) when waiting for a Task should stop immediately after abort.

Do I have to await every Fiber?

No. Awaiting a Fiber is join: it makes the child outcome part of the current control flow. When the outcome does not matter — a fire-and-forget side effect — discard the Fiber explicitly with void run(task).

That is safe because the Run tree supervises every Fiber it creates. A discarded Fiber whose Task observes abort (for example during Run disposal) never surfaces as an unhandled rejection, and cleanup is not lost — disposal already aborts and awaits the child. Defects are different: they still panic the root Run and are reported through ReportDefectDep, so discarding a Fiber never hides bugs.

Choose the boundary explicitly:

  • void run(task) — the outcome does not matter. Abort is silent; defects are still reported.
  • await run(task) — the continuation depends on the Result, so abort rejects into the awaiter and the boundary must handle it.
  • run.abortable(task) — abort is an expected outcome handled as a Result error.

What should Task code do with defects?

Nothing. Once a defect reaches the Run, it is too late: the root Run panics, running Tasks are aborted, and the Run tree shuts down. Use trySync or tryAsync to turn recoverable exceptions and Promise rejections into typed Result errors. Let unrecoverable failures propagate as defects.

Why does a defect panic the whole Run tree?

The obvious alternative is partial recovery: only the failing subtree shuts down or restarts while the rest keeps running. Erlang/OTP made this "let it crash" model with supervisors the benchmark for fault-tolerant runtime design.

Erlang can recover partially because of process isolation: each process owns its heap, so a crashed process cannot leave another process's state corrupted. JavaScript Tasks share a heap. A defect may throw after partially updating shared state, and the Run cannot prove which invariants are still valid. A subtree panic would stop the failing Task while leaving any corrupted shared state available to surviving Tasks. Locks make it worse: a defect inside a critical section may leave protected invariants half-updated. In-process restart is not a reliable recovery boundary either, because the restarted code may still share the same module state, closures, caches, or resources.

JavaScript does have a boundary with Erlang-like isolation: workers. A worker has its own heap and structured-clone messaging, so corruption cannot cross the boundary, and respawning a worker starts from clean state. A defect can panic the worker's Run tree, the worker boundary can be torn down, and the supervising side decides whether to respawn — retry with a Schedule around a "spawn worker, run until exit" Task is a one-for-one supervisor. Multiple root Runs that share no mutable state are a lighter alternative, but the share-nothing guarantee is then architectural discipline rather than enforced isolation, so keep it opt-in and rare.

Why imperative code instead of monadic effect composition?

Monads give pure functional languages a way to sequence effects while keeping functions pure. JavaScript already has native effect sequencing: loops, early returns, try/finally, exceptions, and async/await.

A monadic effect wrapper moves that control flow into a library DSL. The wrapper type becomes viral, and ordinary debugging, profiling, stack traces, and TypeScript narrowing have to work through the DSL instead of the language.

Task follows the opposite approach: Tasks are ordinary async functions, Run owns lifetimes and scoped context, Result carries expected domain errors, and defects keep real exceptions with real stacks. Result propagation is explicit at each async boundary, so TypeScript narrows it through ordinary control flow and readers can see where an error is handled or returned.

Are recursive Tasks stack-safe?

Tasks have native JavaScript stack behavior. A deeply recursive Task can exceed the call stack when each step starts the next step synchronously. await alone does not prevent this: JavaScript evaluates its operand before suspending, and run(nextTask) starts the child Task immediately.

Implement deep recursive algorithms with a loop and an explicit worklist so each iteration reuses the same stack frame:

import { assertOk, createRun, ok, type Task } from "@evolu/common";

interface TreeNode {
  readonly value: string;
  readonly children: ReadonlyArray<TreeNode>;
}

const visitTree =
  (root: TreeNode): Task<ReadonlyArray<string>> =>
  () => {
    const remaining = [root];
    const visited: Array<string> = [];

    while (remaining.length > 0) {
      const node = remaining.pop();
      if (!node) continue;
      visited.push(node.value);
      for (const child of node.children) remaining.push(child);
    }

    return ok(visited);
  };

await using run = createRun();
assertOk(
  await run(
    visitTree({
      value: "root",
      children: [{ value: "child", children: [] }],
    }),
  ),
  ["root", "child"],
);

Task favors direct native execution, async/await, and native tooling over interpreted control flow. The trade-off is no transparent stack safety or automatic scheduling fairness. Use loops or worklists for deep algorithms, periodically await yieldNow for cooperative scheduling, and move CPU-bound work to a worker.

Where are fork and join?

Calling run(task) is fork: it starts a child Task and returns a Fiber. Awaiting or returning that Fiber is join: it makes the child Result or rejection part of the parent Task control flow.

What runtime features does Task require?

Task uses modern JavaScript APIs such as Promise.withResolvers, AbortSignal.throwIfAborted, Symbol.dispose, Symbol.asyncDispose, DisposableStack, and AsyncDisposableStack. Evolu provides polyfills for supported runtimes that need them: call installPolyfills from @evolu/common/polyfills, or from the platform package such as @evolu/react-native/polyfills. The using and await using syntax is emitted by TypeScript; the polyfills provide the runtime resource-management globals.

Core

NameDescription
AbortableFiberA Fiber with explicit abort and async-disposal controls.
AbortErrorStructured-concurrency abort control-flow value.
AbortReasonStructured data explaining why a Run was aborted.
DisposableRunA Run with explicit disposal.
FiberA Promise-backed handle to a Task started by a Run.
PanicAbortReasonAbort reason recorded when a defect panics the root Run.
RunA callable object that starts Tasks and owns their lifetimes.
RunAbortStateRun abort state.
RunStateAbortedThe Run has an abort request and may still be running or disposing.
RunStateRunningThe Run has no abort request, observed abort, or exit.
RunStateSettledThe Run has recorded its final outcome and all descendants have settled.
NextTaskA Task that can return a value, signal done, or return a Result error.
RunCustomDepsCustom deps accepted by Run APIs.
RunExitFinal outcome recorded by a Run.
RunStateRun lifetime states.
TaskA function passed to Run that returns an Awaitable Result and declares its dependencies through D.
AbortErrorRuntime Type for structured-concurrency abort control flow.
AbortReasonRuntime Type for structured data explaining why a Run was aborted.
createAbortErrorCreates an AbortError from an AbortReason.
createPanicAbortReasonCreates a PanicAbortReason from a defect.

Run

NameDescription
CreateRunFactory type for creating root DisposableRun instances.
ReportDefectDepDependency wrapper for ReportDefect.
ReportDefectReports a defect.
RunDefaultDepsDefault dependencies provided by createRun.
createRunCreates a root DisposableRun.
explicitAbortReasonShared abort reason used when callers explicitly request abort without a more specific reason.
reportDefectAfterMicrotaskDefault ReportDefect for platform-independent createRun.
runDisposedAbortReasonShared abort reason used for ordinary Run cleanup.
createRunDefaultDepsCreates RunDefaultDeps.

Collection

NameDescription
AllOptionsOptions for all.
TaskCollectionOptionsOptions shared by Task collection helpers.
EachCallbackHandles one settled Task Result from each.
EachDecisionDecision returned by an each result handler.
allRuns Tasks until all return Ok or one returns Err.
allSettledRuns all Tasks and returns every Task Result.
eachRuns Tasks under a concurrency limit and calls onResult for each Task Result as it settles.

Lifetime

FunctionDescription
acquireUseReleaseRuns acquire, use, and release as one bracketed Task.
daemonStarts a Task with Run.daemon and waits until it settles or the current Run aborts.

Interop

FunctionDescription
callbackCreates a Task from a callback-based API.

Timing

NameDescription
TimeoutErrorError returned by timeout when a Task exceeds its duration.
timeoutErrorThe TimeoutError instance returned by timeout.
TimeoutErrorRuntime Type for the error returned by timeout when a Task exceeds its duration.
sleepPauses execution for a specified PositiveDuration.
timeoutLimits how long a Task may run.

Resilience

NameDescription
RetryAttemptInformation passed to RetryOptions.onRetry.
RetryErrorError returned by retry when retrying stops after a domain error.
RetryOptionsOptions for retry.
RetryTaskErrorError type returned by retry.
retryRetries a Task according to a Schedule.

Repetition

NameDescription
RepeatAttemptInformation passed to RepeatOptions.onRepeat.
RepeatOptionsOptions for repeat.
repeatRepeats a Task according to a Schedule.

Racing

FunctionDescription
anyRuns Tasks until one returns Ok or all return Err.
firstNRuns Tasks until count Tasks return Ok or all Tasks settle.
firstNSettledRuns Tasks until count Tasks settle or all Tasks settle.
raceRuns Tasks until the first Task settles.

Scheduling

NameDescription
TaskPriorityScheduler priority for Tasks started through a native scheduler.
yieldNowYields execution to the host scheduler.
prioritizedAssigns static scheduler priority to a Task.

Abortability

NameDescription
AbortMaskAbort mask depth for a Run.
unabortableMakes a Task unabortable after it starts.
waitForAbortWaits until the current Run aborts, then rejects with its AbortError.
unabortableMaskLike unabortable, but provides restore for child Tasks that should run with the previous abort mask.

Concurrency primitives

NameDescription
CreateMutexByKeyOptionsOptions for createMutexByKey.
CreateSemaphoreByKeyOptionsOptions for createSemaphoreByKey.
DeferredA one-shot value resolved from outside the waiting Task.
GateA reusable gate for blocking and releasing Tasks.
MutexRuns Tasks one at a time.
MutexByKeyRuns Tasks one at a time independently for each key, like Mutex.
MutexRefRef protected by a Mutex.
SemaphoreCoordinates concurrent Tasks by acquiring and releasing permits.
SemaphoreByKeyCoordinates concurrent Tasks independently for each key.
SemaphorePermitAn owned semaphore acquisition returned by Semaphore.take.
SemaphoreSnapshotSnapshot returned by Semaphore.snapshot.
SemaphorePolicyScheduling policy for semaphore acquisition.
createDeferredCreates a Deferred.
createGateCreates a Gate.
createMutexCreates a Mutex.
createMutexByKeyCreates a MutexByKey.
createMutexRefCreates a MutexRef.
createSemaphoreCreates a Semaphore.
createSemaphoreByKeyCreates a SemaphoreByKey.

Monitoring

NameDescription
RunConfigConfiguration for Run monitoring behavior.
RunConfigDepDependency wrapper for RunConfig.
RunEventEvent emitted by a Run for monitoring and debugging.
RunEventDataChildAddedA child Run was added to the emitting Run.
RunEventDataChildRemovedA child Run was removed from the emitting Run.
RunEventDataStateChangedThe emitting Run changed state.
RunSnapshotRecursive snapshot of a Run tree.
RunEventDataEvent-specific payload of a RunEvent.

Testing

NameDescription
TestReportDefectTest ReportDefect that records reported defects.
TestReportDefectDepDependency wrapper for TestReportDefect.
TestRunDepProvides a test Run with deterministic default dependencies.
TestRunDefaultDepsDeterministic test variants of RunDefaultDeps.
testAbortErrorShared AbortError for tests, created from testAbortReason.
testAbortReasonShared abort reason for tests that need a non-production abort reason.
testCreateDepsCreates TestRunDefaultDeps.
testCreateReportDefectCreates TestReportDefect.
testCreateRunCreates a root DisposableRun with TestRunDefaultDeps.

Type utilities

Type AliasDescription
AnyFiberShorthand for a Fiber with any type parameters.
AnyTaskShorthand for a Task with any type parameters.
InferFiberDepsExtracts the dependency type from a Fiber.
InferFiberErrExtracts the Result error type from a Fiber.
InferFiberOkExtracts the Ok value type from a Fiber.
InferTaskDepsExtracts the dependency type from a Task.
InferTaskDoneExtracts the done value type from a NextTask.
InferTaskErrExtracts the Result error type from a Task.
InferTaskOkExtracts the Ok value type from a Task.
InferTaskRecordDepsExtracts the dependency intersection required by a Task record.
InferTasksDepsExtracts the dependency intersection required by a readonly Task array.
InferTasksOkMaps a Task array or record to the Ok values produced by its Tasks.
InferTasksResultExtracts the Result type produced by one Task in a non-empty Task array.
InferTasksSettledMaps a Task array or record to the Result values produced by its Tasks.
TaskRecordA readonly record whose values are Tasks.
TaskWithErrorA Task whose error type is not never.