zena:task
import {…} from 'zena:task';
zena:task — operations as values (docs/design/cancellation.md; PLAN.md, async roadmap).
An operation that can be started is a value, Op<T> = () => Future<T>, and this module is the algebra over such values:
combinators from Op<T> to Op<T>, so composition order is
syntax: retry(3, timeout(milliseconds(100), op)) gives every
attempt its own 100ms budget, timeout(milliseconds(500), retry(3, op)) one budget over all of them.
Two rules make the algebra sound. Policies act on FAILURES and
never on cancellation: an ambient cancel passes through every
combinator untouched — catch cannot observe the cancellation
channel, so retrying cancelled work is not even expressible here —
because a retry after cancel would outlive the scope tree that
cancelled it. And racing combinators start their candidates inside
a TaskGroup, so a loser or an expired attempt is actually
cancelled rather than left running (the thing a resilience layer
over bare promises cannot do).
An Op is called once per attempt: the operation must be a
future-RETURNING function, not a future. A future is work already
started — it cannot be attempted again.
The planned Task class — an operation's observable lifecycle
(initial, pending, complete, error), in the style of @lit/task —
belongs here too: a Task runs an Op, and these combinators are
the policies a Task runs it under.
Classes
TimeoutError
class TimeoutError extends Error
How timeout and deadline report an attempt that ran out of
budget. Carries the budget so a retry layer's log can say which.
TaskState
sealed class TaskState<T>
Where a task's latest run stands. Cancellation is not a state: a cancelled run restores the state the task held before it (docs/design/task.md, "The state, as data").
Initial
final class Initial<T> extends TaskState<T>
No run has settled yet.
Pending
final class Pending<T> extends TaskState<T>
A run is in flight. Deliberately carries no future: only the latest run's outcome ever becomes the state, and a held future could outlive its run's relevance.
Complete
final class Complete<T>(value: T) extends TaskState<T>
The latest settled run produced this value.
value: T
Errored
final class Errored<T>(error: Error) extends TaskState<T>
The latest settled run failed with this error.
error: Error
Task
class Task<T>
Runs an operation and exposes where the run stands — Initial,
Pending, Complete, or Errored — across however many runs,
latest-wins (docs/design/task.md).
The op is fixed at construction with its policies already composed:
new Task(retry(3, timeout(milliseconds(100), fetchUser))). Each
run starts the op inside a child CancelScope, so a superseding
run() cancels the previous attempt's whole subtree rather than
racing it for the state.
new(op: Op<T>, onChange: (() => void)? = null)
changed(): Future<void>
Settles at the next state transition after the call.
run(): Future<T>
Starts a run, cancelling a pending previous one — superseded before it arrived, its outcome discarded. Returns this run's future, which settles the way the run does even if superseded.
cancel(): void
Cancels a pending run; the previous settled state is restored when the cancellation lands. A task with no run pending is unchanged.
Type aliases
Op
type Op<T> = () => Future<T>
An operation that can be attempted. Each call starts one fresh attempt and hands back its future.
Functions
timeout
function timeout<T>(budget: Duration, op: Op<T>): Op<T>
Bounds each attempt to budget of wall time: the attempt races its
budget, and whichever loses is cancelled. An attempt that exceeds
the budget fails with TimeoutError — a failure like any other, so
an outer retry treats a timeout as retryable.
deadline
function deadline<T>(at: Instant, op: Op<T>): Op<T>
Bounds each attempt to finish before an absolute point on the
monotonic clock (monotonic()), however many layers retry it:
where timeout gives every attempt a fresh budget, a deadline
spends one budget across all of them. An attempt starting past the
deadline fails immediately, without running the operation.
retry
function retry<T>(attempts: i32, op: Op<T>, firstDelay: Duration = milliseconds(0)): Op<T>
Runs up to attempts attempts, handing back the first value; the
last attempt's failure propagates. With firstDelay, waits that
long before the second attempt and doubles the wait each time after.
Retrying is the reason Op is a function: each attempt is a fresh
call. Cancellation is not retried — see the module comment.
fallback
function fallback<T>(op: Op<T>, alt: Op<T>): Op<T>
Tries op; on failure, answers with alt instead. The failure
that triggered the fallback is discarded — when it matters, catch
it yourself. Cancellation is not a failure: if op's attempt
completes cancelled, the unwind passes through and alt never
runs.
hedge
function hedge<T>(delay: Duration, op: Op<T>): Op<T>
Starts a second attempt if the first has not settled within
delay, and hands back whichever settles first, value or
failure — the tool for an operation that is usually fast and
occasionally very slow. The loser is cancelled. A hedge trims
slowness and does not mask errors; layer retry on top for that.