API reference@evolu/commonTask › DisposableRun

Defined in: packages/common/src/Task.ts:1555

A Run with explicit disposal.

createRun creates a root DisposableRun. Run.create creates one attached to that root, typically to give a reusable resource its own lifetime. A DisposableRun owns its child Tasks and closure-held cleanup registered with DisposableRun.defer; disposing it shuts down both.

Sync disposal starts shutdown without waiting. Async disposal waits for child Tasks and registered cleanup to finish.

Use createRun at composition roots such as app, server, worker, or test entry points. The common factory is platform-agnostic; platform adapters can wrap it to add global error handling or shutdown integration.

Example

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

await using run = createRun();
const loadData: Task<string> = () => ok("data");

expectOk(await run(loadData), "data");

Example with custom dependencies

import { createRun, type DisposableRun } from "@evolu/common";

interface ConfigDep {
  readonly config: { readonly apiUrl: string };
}

await using run = createRun<ConfigDep>({
  config: { apiUrl: "https://api.example.com" },
});

expectTypeOf(run).toEqualTypeOf<DisposableRun<ConfigDep>>();
expect(run.deps.config.apiUrl).toBe("https://api.example.com");

Extends

Call Signature

DisposableRun<T, E>(task: Task<T, E, D>): Fiber<T, E, D>;

Defined in: packages/common/src/Task.ts:1555

Starts a Task, invokes it with a child Run, and returns a Fiber.

The Fiber resolves with the Task Result. Await or return the Fiber to make the child outcome part of the current Task result. Discard it with void when the outcome does not matter; the Run tree supervises the Fiber, so a discarded Fiber's abort never surfaces as an unhandled rejection while defects are still reported. Use Run.daemon for work that should outlive the current Task.

The optional deps argument replaces the custom deps available to the Task. Default deps (RunDefaultDeps) are inherited unless replaced with assignable alternatives.

The Fiber rejects when the Task observes abort by throwing AbortError. It also rejects with AbortError whose reason is PanicAbortReason when the Task defects and panics the Run tree. Use Run.abortable when abort or panic should be returned as an Err; do not catch AbortError from run(task) to model expected cancellation.

Calling a disposed Run is a programmer error and throws synchronously before a Fiber is created.

Example

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

interface Db {
  readonly name: string;
}
interface DbDep {
  readonly db: Db;
}

const db: Db = { name: "main" };
const loadUser: Task<string> = () => ok("Ada");
const saveUser: Task<void, never, DbDep> = ({ deps }) => {
  expect(deps.db).toBe(db);
  return ok();
};

await using run = createRun();
const userResult = await run(loadUser);
const savedResult = await run(saveUser, { db });
expectOk(userResult, "Ada");
expectOk(savedResult, undefined);

Call Signature

DisposableRun<T, E, Deps>(task: Task<T, E, Deps>, deps: RunCustomDeps<Deps>): Fiber<T, E, Deps>;

Defined in: packages/common/src/Task.ts:1555

A Run with explicit disposal.

createRun creates a root DisposableRun. Run.create creates one attached to that root, typically to give a reusable resource its own lifetime. A DisposableRun owns its child Tasks and closure-held cleanup registered with DisposableRun.defer; disposing it shuts down both.

Sync disposal starts shutdown without waiting. Async disposal waits for child Tasks and registered cleanup to finish.

Use createRun at composition roots such as app, server, worker, or test entry points. The common factory is platform-agnostic; platform adapters can wrap it to add global error handling or shutdown integration.

Example

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

await using run = createRun();
const loadData: Task<string> = () => ok("data");

expectOk(await run(loadData), "data");

Example with custom dependencies

import { createRun, type DisposableRun } from "@evolu/common";

interface ConfigDep {
  readonly config: { readonly apiUrl: string };
}

await using run = createRun<ConfigDep>({
  config: { apiUrl: "https://api.example.com" },
});

expectTypeOf(run).toEqualTypeOf<DisposableRun<ConfigDep>>();
expect(run.deps.config.apiUrl).toBe("https://api.example.com");

Methods

[asyncDispose]()

asyncDispose: PromiseLike<void>;

Defined in: node_modules/@typescript/old/lib/lib.esnext.disposable.d.ts:38

Inherited from

AsyncDisposable.[asyncDispose]

[dispose]()

dispose: void;

Defined in: node_modules/@typescript/old/lib/lib.esnext.disposable.d.ts:34

Inherited from

Disposable.[dispose]

Properties

abort

readonly abort: (reason?: AbortReason) => void;

Defined in: packages/common/src/Task.ts:1579

Requests abort with an optional AbortReason and starts sync disposal.

abortable

readonly abortable: {
<T, E>  (task: Task<T, E, D>): AbortableFiber<T, E, D>;
<T, E, Deps>  (task: Task<T, E, Deps>, deps: RunCustomDeps<Deps>): AbortableFiber<T, E, Deps>;
};

Defined in: packages/common/src/Task.ts:1230

Runs a Task and returns an AbortableFiber.

An AbortableFiber is a Fiber that can request abort with .abort() or async disposal. If the Task throws or rejects with AbortError, the Fiber catches it and returns it as a Result error. Use this API instead of catching AbortError from run(task) when abort is an expected outcome. Check AbortError.reason to distinguish explicit abort, normal Run disposal, and panic-driven shutdown.

Use deps to replace the custom deps available to the Task. Default deps (RunDefaultDeps) are inherited unless explicitly replaced with assignable alternatives.

Example

import {
  AbortError,
  createRun,
  ok,
  sleep,
  type AbortableFiber,
  type Task,
} from "@evolu/common";

interface DbDep {
  readonly db: { readonly name: string };
}
const db = { name: "main" };
const loadUser: Task<string, "LoadUserError", DbDep> = async (run) => {
  await run.ok(sleep("1s"));
  return ok(run.deps.db.name);
};

await using run = createRun();
const fiber = run.abortable(loadUser, { db });
expectTypeOf(fiber).toEqualTypeOf<
  AbortableFiber<string, "LoadUserError", DbDep>
>();
fiber.abort();
const userResult = await fiber;
assert(!userResult.ok);
expect(AbortError.is(userResult.error)).toBe(true);

Call Signature

<T, E>(task: Task<T, E, D>): AbortableFiber<T, E, D>;
Type Parameters
Type Parameter
T
E
Parameters
ParameterType
taskTask<T, E, D>
Returns

AbortableFiber<T, E, D>

Call Signature

<T, E, Deps>(task: Task<T, E, Deps>, deps: RunCustomDeps<Deps>): AbortableFiber<T, E, Deps>;
Type Parameters
Type Parameter
T
E
Deps extends object
Parameters
ParameterType
taskTask<T, E, Deps>
depsRunCustomDeps<Deps>
Returns

AbortableFiber<T, E, Deps>

Inherited from

Run.abortable


create

readonly create: {
  (): DisposableRun<D>;
<Deps>  (deps: RunCustomDeps<Deps>): DisposableRun<Deps>;
};

Defined in: packages/common/src/Task.ts:1390

Creates a DisposableRun attached to the root Run with this Run's deps.

Use it when you need a Run that can be reused across multiple operations. For a single long-lived Task, use Run.daemon.

Use deps to replace the created Run's custom deps. Default deps (RunDefaultDeps) are inherited unless explicitly replaced with assignable alternatives.

A recorded abort request prevents creating a Run: run.create throws AbortError even while the caller's abort mask keeps run.signal un-aborted, because a detached Run must not start under a scope that is shutting down.

Example

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

interface DbDep {
  readonly db: { readonly users: Array<string> };
}
const db = { users: ["Ada"] };
const loadUser: Task<string, never, DbDep> = ({ deps }) =>
  ok(deps.db.users[0] ?? "Unknown");
const saveUser: Task<void, never, DbDep> = ({ deps }) => {
  deps.db.users.push("Grace");
  return ok();
};

await using run = createRun();
await using createdRun = run.create({ db });
const userResult = await createdRun(loadUser);
const savedResult = await createdRun(saveUser);
expectOk(userResult, "Ada");
expectOk(savedResult, undefined);
expect(db.users).toEqual(["Ada", "Grace"]);

Call Signature

(): DisposableRun<D>;
Returns

DisposableRun<D>

Call Signature

<Deps>(deps: RunCustomDeps<Deps>): DisposableRun<Deps>;
Type Parameters
Type Parameter
Deps extends object
Parameters
ParameterType
depsRunCustomDeps<Deps>
Returns

DisposableRun<Deps>

Inherited from

Run.create


daemon

readonly daemon: {
<T, E>  (task: Task<T, E, D>): AbortableFiber<T, E, D>;
<T, E, Deps>  (task: Task<T, E, Deps>, deps: RunCustomDeps<Deps>): AbortableFiber<T, E, Deps>;
};

Defined in: packages/common/src/Task.ts:1341

Runs a Task as daemon and returns an AbortableFiber.

Normal child Runs are disposed after their Task settles. Tasks started by run.daemon detach their lifetime from the current Task and attach to the root Run, so they keep running until they settle or the root Run is disposed. Calling .abort() or async-disposing the returned Fiber requests abort. Keep the returned Fiber for lifetime control.

The daemon receives deps derived from the Run that starts it, not from the root Run: deps replace that Run's custom deps for the daemon Task, while lifetime is attached to the root Run. Default deps (RunDefaultDeps) are inherited unless explicitly replaced with assignable alternatives.

The caller's abort mask is not inherited. A daemon detaches to the root, so a mask-inheriting daemon could never observe abort and would hang root disposal. Wrap the daemon Task with unabortable when it must finish once started.

A recorded abort request prevents starting a daemon: run.daemon throws AbortError even while the caller's abort mask keeps run.signal un-aborted, because detached work must not spawn under a scope that is shutting down.

Example

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

const syncUsers: Task<string> = () => ok("synced");
const syncParent = unabortable(async (run) => {
  expect(run.snapshot().abortMask).toBe(1);

  // Plain daemon — the caller's mask does not follow it, so abort
  // requests are observed.
  const fiber = run.daemon(syncUsers);
  expect(fiber.run.snapshot().abortMask).toBe(0);

  // Explicitly masked daemon — finishes once started.
  const maskedFiber = run.daemon(unabortable(syncUsers));
  expect(maskedFiber.run.snapshot().abortMask).toBe(1);
  const firstResult = await fiber;
  const secondResult = await maskedFiber;
  assert(firstResult.ok);
  assert(secondResult.ok);
  return ok([firstResult.value, secondResult.value] as const);
});

await using run = createRun();
expectOk(await run(syncParent), ["synced", "synced"]);

For a long-lived reusable Run, use Run.create.

Example

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

interface DbDep {
  readonly db: { readonly name: string };
}
const db = { name: "main" };
const syncUsers: Task<void, never, DbDep> = async (run) => {
  await run.ok(sleep("1s"));
  return ok();
};

await using run = createRun();
const fiber = run.daemon(syncUsers, { db });
fiber.abort();
const syncResult = await fiber;
assert(!syncResult.ok);
expect(AbortError.is(syncResult.error)).toBe(true);
import { createRun, ok, waitForAbort, type Task } from "@evolu/common";

let syncStopped = false;
const syncUsers: Task<never> = async (run) => {
  using _ = run.onAbort(() => {
    syncStopped = true;
  });
  return await run(waitForAbort);
};
const loadUser: Task<string> = () => ok("Ada");

await using run = createRun();
{
  // Async disposal requests abort and waits for the daemon to stop.
  await using _syncFiber = run.daemon(syncUsers);
  expectOk(await run(loadUser), "Ada");
}
expect(syncStopped).toBe(true);

Call Signature

<T, E>(task: Task<T, E, D>): AbortableFiber<T, E, D>;
Type Parameters
Type Parameter
T
E
Parameters
ParameterType
taskTask<T, E, D>
Returns

AbortableFiber<T, E, D>

Call Signature

<T, E, Deps>(task: Task<T, E, Deps>, deps: RunCustomDeps<Deps>): AbortableFiber<T, E, Deps>;
Type Parameters
Type Parameter
T
E
Deps extends object
Parameters
ParameterType
taskTask<T, E, Deps>
depsRunCustomDeps<Deps>
Returns

AbortableFiber<T, E, Deps>

Inherited from

Run.daemon


defer

readonly defer: (finalizer: () => Awaitable<void>) => void;

Defined in: packages/common/src/Task.ts:1573

Registers closure-held cleanup owned by this Run.

Finalizers run in LIFO order after child Tasks settle and are awaited by async disposal. The Run is in Aborted state while they run and transitions to Settled afterward, so a finalizer cannot start Tasks on it. Use using for resources owned by a Task stack frame; use defer for closure-held state whose lifetime is bounded by a reusable DisposableRun.

Sync disposal starts cleanup without waiting and does not throw finalizer defects synchronously. Async disposal awaits cleanup. If a finalizer defects, the defect is reported once, and every async disposal call rejects with the same already-reported AbortError.

Calling defer after disposal starts is a programmer error.

deps

readonly deps: ConsoleDep & LeakDetectorDep & NativeFetchDep & RandomBytesDep & RandomDep & ReportDefectDep & TimeDep & Partial<RunConfigDep> & D;

Defined in: packages/common/src/Task.ts:1402

Dependencies available to the Task, including RunDefaultDeps.

Inherited from

Run.deps


getState

readonly getState: () => RunState;

Defined in: packages/common/src/Task.ts:1476

Returns the current RunState of this Run.

Inherited from

Run.getState


id

readonly id: string & Brand<"Id">;

Defined in: packages/common/src/Task.ts:1396

Unique Id for this Run.

Inherited from

Run.id


ok

readonly ok: {
<T>  (task: Task<T, never, D>): Promise<T>;
<T, Deps>  (task: Task<T, never, Deps>, deps: RunCustomDeps<Deps>): Promise<T>;
};

Defined in: packages/common/src/Task.ts:1176

Runs a Task whose error type is never and returns its Ok value.

This is the Task equivalent of getOk.

Call Signature

<T>(task: Task<T, never, D>): Promise<T>;
Type Parameters
Type Parameter
T
Parameters
ParameterType
taskTask<T, never, D>
Returns

Promise<T>

Call Signature

<T, Deps>(task: Task<T, never, Deps>, deps: RunCustomDeps<Deps>): Promise<T>;
Type Parameters
Type Parameter
T
Deps extends object
Parameters
ParameterType
taskTask<T, never, Deps>
depsRunCustomDeps<Deps>
Returns

Promise<T>

Inherited from

Run.ok


onAbort

readonly onAbort: (callback: (abortError: AbortError) => void) =>
  | Disposable
  | null;

Defined in: packages/common/src/Task.ts:1471

Registers a synchronous callback for observed run.signal aborts.

The callback runs when this Run observes abort, including normal Run disposal. Masked Runs can record an abort request without aborting run.signal, so this callback does not run for every recorded request. If this Run is already aborted, the callback runs immediately and no callback is registered. Dispose the returned registration to release the callback before abort. Returns null when already aborted, which is safe in a using declaration.

Example

import { AbortError, createRun, ok, sleep } from "@evolu/common";

let socketClosed = false;
const openSocket = () => ({
  close: () => {
    socketClosed = true;
  },
  read: async () => "message",
});

await using run = createRun();
const fiber = run.abortable(async (run) => {
  const socket = openSocket();
  using closeOnAbort = run.onAbort(() => {
    socket.close();
  });

  await run.ok(sleep("1s"));
  const message = await socket.read();
  return ok(message);
});
fiber.abort();
const result = await fiber;
assert(!result.ok);
expect(AbortError.is(result.error)).toBe(true);
expect(socketClosed).toBe(true);

Inherited from

Run.onAbort


onEvent

onEvent:
  | ((event: RunEvent) => void)
  | undefined;

Defined in: packages/common/src/Task.ts:1491

Callback for monitoring Run events emitted by this Run or descendants.

Event handlers are observers, not part of Task control flow. Handler defects are reported via ReportDefectDep.reportDefect; they do not panic the root Run or change Run state.

Do not call Run APIs or Fiber control methods from event handlers. Event handlers must only observe and report.

Inherited from

Run.onEvent


orThrow

readonly orThrow: {
<TTask>  (task: TaskWithError<TTask>): Promise<InferTaskOk<TTask>>;
<Deps, TTask>  (task: TaskWithError<TTask>, deps: RunCustomDeps<Deps>): Promise<InferTaskOk<TTask>>;
};

Defined in: packages/common/src/Task.ts:1161

Runs a Task whose error type is not never and throws if the returned Result is an error.

This is the Task equivalent of getOrThrow. Use it where a declared Result error should crash the current flow instead of being handled locally.

Call Signature

<TTask>(task: TaskWithError<TTask>): Promise<InferTaskOk<TTask>>;
Type Parameters
Type Parameter
TTask extends Task<any, any, D>
Parameters
ParameterType
taskTaskWithError<TTask>
Returns

Promise<InferTaskOk<TTask>>

Call Signature

<Deps, TTask>(task: TaskWithError<TTask>, deps: RunCustomDeps<Deps>): Promise<InferTaskOk<TTask>>;
Type Parameters
Type Parameter
Deps extends object
TTask extends Task<any, any, Deps>
Parameters
ParameterType
taskTaskWithError<TTask>
depsRunCustomDeps<Deps>
Returns

Promise<InferTaskOk<TTask>>

Inherited from

Run.orThrow


panic

readonly panic: (defect: unknown) => AbortError;

Defined in: packages/common/src/Task.ts:1593

Shuts down the Run tree because of a defect.

Panic creates a PanicAbortReason from the defect, wraps it in an AbortError, and reports that AbortError through ReportDefectDep. The original defect is available as abortError.reason.defect for diagnostics. The first panic records the AbortError as the root Run's aborted exit and starts root disposal, which aborts running Tasks, prevents new Tasks from starting, and waits for running Tasks to settle. Later panics still report and return their own AbortError, but do not replace the root Run exit.

parent

readonly parent:
  | Run<unknown>
  | null;

Defined in: packages/common/src/Task.ts:1399

The parent Run, if this Run was created as a child.

Inherited from

Run.parent


signal

readonly signal: AbortSignal;

Defined in: packages/common/src/Task.ts:1427

Abort signal for the Task.

Aborts when this Run is disposed. While the Task is running, it also aborts when a parent abort request reaches this Run and the Task is not wrapped with unabortable.

After the Task settles, this Run is disposed. If the signal has not already aborted, disposal aborts it with the recorded abort error for aborted or panicked exits, and with runDisposedAbortReason after successful completion.

In masked Tasks, an abort request can be recorded in Run.getState without being observed by this signal. If the masked Task completes successfully, this signal still aborts with runDisposedAbortReason during disposal.

Pass this signal to cancellation-aware APIs such as fetch. For cleanup callbacks, use Run.onAbort instead of addEventListener. Run's internal cleanup also listens on this signal, so abort listeners must not call stopImmediatePropagation — it would suppress later-registered listeners, including Run.onAbort callbacks.

Inherited from

Run.signal


snapshot

readonly snapshot: () => RunSnapshot;

Defined in: packages/common/src/Task.ts:1479

Creates a memoized recursive RunSnapshot of the current Run tree.

Inherited from

Run.snapshot