API reference@evolu/commonTask › callback

function callback<T, E, D>(
  fn: (options: {
    reject: (defect: unknown) => void;
    resolve: (result: Result<T, E>) => void;
    run: Run<D>;
  }) => void | (() => void),
): Task<T, E, D>;

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

Creates a Task from a callback-based API.

Use this to wrap callback-style APIs such as event listeners and Node.js callbacks. Resolve with ok(value) or err(error), or reject with a defect, to complete the Task. Settlement is one-shot: the first resolve or reject wins, and later settlement calls are ignored, matching Promise semantics. When reject wins settlement, the defect panics the Run tree and is observed at Fiber boundaries as AbortError with PanicAbortReason. Rejecting AbortError is reserved for Task abort control flow: direct run(task) rejects with it, and run.abortable(task) returns it as an Err.

This helper is a callback bridge. If reject forwards an Error created in a separate async chain, V8 cannot reconstruct the caller's zero-cost async stack through this bridge. Prefer native promise APIs and await when the wrapped API already returns a Promise.

One-shot settlement applies only to resolve and reject. A synchronous throw from the setup function is a defect that panics the Run tree even after resolve was called — setup throws are bugs and must not be masked by an earlier resolution.

Optionally return a synchronous cleanup callback. It runs when the Task Run signal aborts, including normal completion and explicit abort. The callback must not throw. Cleanup defects panic the root Run; if the callback Task already settled, its Fiber keeps the original Result while the root still reports the panic. Cleanup must be synchronous; returned promises are not awaited. For async cleanup, prefer acquireUseRelease, await using with AsyncDisposableStack, or a Task that owns the resource explicitly.

If setup can throw after acquiring any resource, use a local DisposableStack before returning cleanup. Register each cleanup as soon as the resource is acquired, then move the disposer and return a cleanup callback. If setup throws before cleanup is returned, the local using disposal releases already-acquired resources.

Example

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

const listeners = new Set<(message: string) => void>();
const subscribe = (listener: (message: string) => void): (() => void) => {
  listeners.add(listener);
  return () => {
    listeners.delete(listener);
  };
};
const nextMessage: Task<string> = callback(({ resolve }) =>
  subscribe((message) => resolve(ok(message))),
);

await using run = createRun();
const fiber = run(nextMessage);
assertEqual(listeners.size, 1);
for (const listener of listeners) listener("ready");
assertOk(await fiber, "ready");
// The callback cleanup unsubscribes after settlement.
assertEqual(listeners.size, 0);