API reference
Three exports:
import { defineSaga, defineTask, defineMapTask } from "minisagas";defineSaga
function defineSaga<TInput = {}, TContext = {}>(name: string): SagaBuilder<TInput, TContext>;Throws if name is not a non-empty string.
SagaBuilder
Every method except build returns a new builder with the result type widened. Builders are immutable, so a partial one is safe to reuse as a base:
const base = defineSaga<In, Ctx>("checkout").task(validate);
const withCard = base.task(chargeCard); // validate, chargeCard
const withCredit = base.task(useCredit); // validate, useCredit, not chargeCardDiscarding the return value therefore does nothing: builder.task(t) on its own is a no-op.
.task(config, options?)
task(config: TaskConfig, options?: RuntimeTaskOptions): SagaBuilderAdds one task. Result lands under config.name.
.mapTask(config, options?)
mapTask(config: MapTaskConfig, options?: RuntimeTaskOptions): SagaBuilderRuns execute once per item from iterator, sequentially. Result is an array under config.name.
.mapParallelTask(config, options?)
mapParallelTask(config: MapTaskConfig, options?: ParallelTaskOptions): SagaBuilderSame, all at once, or options.concurrency at a time. Results keep input order regardless of settle order.
.parallelTasks(configs, options?)
parallelTasks(configs: TaskConfig[], options?: ParallelTaskOptions): SagaBuilderRuns 2+ tasks concurrently; each result lands under its own name. Throws at build time with fewer than two tasks.
.saga(saga, options?)
saga(saga: Saga, options?: { adapt?: (args) => TChildInput }): SagaBuilderInlines another saga's nodes. The child's context, hooks and name are discarded. See Composing sagas.
adapt is also a per-step option on .task(), .mapTask(), .mapParallelTask() and .parallelTasks(), where it does the same job for a task defined against its own input. See adapt.
.use(hooks)
use(hooks: SagaHooks): SagaBuilderMerges lifecycle hooks. Repeated keys replace.
.build(context)
build(context: TContext): Saga<TInput, TContext, TResult>Snapshots the nodes and hooks against a context. Reusable across runs.
Saga
.execute(input, options?)
execute(
input: TInput,
options?: { signal?: AbortSignal },
): Promise<SagaResult<TInput, TContext, TResult>>Runs the saga. Never rejects: failures come back as success: false, after compensation has run.
Abort signal and the run stops before its next task, ends any retry loop in flight, and then rolls back. See Cancellation.
defineTask / defineMapTask
defineTask<TInput, TContext, TPreviousResults>()(config): TaskConfig
defineMapTask<TInput, TContext, TPreviousResults>()(config): MapTaskConfigValidate and freeze a reusable definition. Curried; see why.
Types
Every type below is exported from minisagas and shipped with source maps, so go-to-definition on any of them lands in the annotated source rather than a stripped .d.ts. Rather than mirror the declarations here, this section covers only what the types cannot say for themselves.
TaskConfig and MapTaskConfig are what defineTask / defineMapTask take. TaskArgs is what every callback is handed (input, results, context); MapTaskArgs adds value, index and array; ExecuteArgs and MapExecuteArgs are those two plus the signal.
ExecutionOptions (retry, timeout) describes a task's execute and nothing else. Compensation is never retried or timed out, see Compensation. RuntimeTaskOptions adds the call-site-only optional, when and adapt; ParallelTaskOptions adds failFast and concurrency. See Task options for what each one does.
SagaResult is a union on success. Both halves carry input, context, executedTasks, duration and an optional failedTask; the failing half adds error, compensated and compensationErrors, and narrows results to Partial. failedTask names the task that threw, and is absent when the saga failed outside a task. On failure results holds only what completed before the error, and rolled-back tasks are still in there, since they did produce a result.
SagaHooks is eight optional lifecycle handlers, plus onHookError for when one of them throws. See Hooks.
Errors thrown
Everything the library throws about your configuration is a SagaError, and carries a kind to match on: messages get reworded, kinds do not. They all throw while defining or building, so a saga that built never throws one.
import { SagaError } from "minisagas";
try {
buildCheckout();
} catch (error) {
if (error instanceof SagaError && error.kind === "DUPLICATE_TASK_NAME") {
// ...
}
}kind | When |
|---|---|
INVALID_NAME | defineSaga(""), or a task defined without a name |
INVALID_OPTIONS | retry.attempts, retry.delay, retry.maxDelay, timeout or concurrency out of range or not finite |
DUPLICATE_TASK_NAME | .build() on a saga with two tasks of the same name |
TOO_FEW_PARALLEL_TASKS | .parallelTasks() with fewer than two tasks |
The messages name the offending saga, task and field, but they are not part of the contract. You should match on kind.
A run's own failures are not SagaErrors: your task's error travels untouched, and the platform shapes keep theirs. A tripped timeout rejects with a TimeoutError, a cancelled task with an AbortError, and a parallel group without failFast with an AggregateError carrying each branch's error in .errors and naming those branches in its message.