Skip to content

API reference

Three exports:

ts
import { defineSaga, defineTask, defineMapTask } from "minisagas";

defineSaga

ts
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:

ts
const base = defineSaga<In, Ctx>("checkout").task(validate);

const withCard = base.task(chargeCard); // validate, chargeCard
const withCredit = base.task(useCredit); // validate, useCredit, not chargeCard

Discarding the return value therefore does nothing: builder.task(t) on its own is a no-op.

.task(config, options?)

ts
task(config: TaskConfig, options?: RuntimeTaskOptions): SagaBuilder

Adds one task. Result lands under config.name.

.mapTask(config, options?)

ts
mapTask(config: MapTaskConfig, options?: RuntimeTaskOptions): SagaBuilder

Runs execute once per item from iterator, sequentially. Result is an array under config.name.

.mapParallelTask(config, options?)

ts
mapParallelTask(config: MapTaskConfig, options?: ParallelTaskOptions): SagaBuilder

Same, all at once, or options.concurrency at a time. Results keep input order regardless of settle order.

.parallelTasks(configs, options?)

ts
parallelTasks(configs: TaskConfig[], options?: ParallelTaskOptions): SagaBuilder

Runs 2+ tasks concurrently; each result lands under its own name. Throws at build time with fewer than two tasks.

.saga(saga, options?)

ts
saga(saga: Saga, options?: { adapt?: (args) => TChildInput }): SagaBuilder

Inlines 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)

ts
use(hooks: SagaHooks): SagaBuilder

Merges lifecycle hooks. Repeated keys replace.

.build(context)

ts
build(context: TContext): Saga<TInput, TContext, TResult>

Snapshots the nodes and hooks against a context. Reusable across runs.

Saga

.execute(input, options?)

ts
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

ts
defineTask<TInput, TContext, TPreviousResults>()(config): TaskConfig
defineMapTask<TInput, TContext, TPreviousResults>()(config): MapTaskConfig

Validate 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.

ts
import { SagaError } from "minisagas";

try {
  buildCheckout();
} catch (error) {
  if (error instanceof SagaError && error.kind === "DUPLICATE_TASK_NAME") {
    // ...
  }
}
kindWhen
INVALID_NAMEdefineSaga(""), or a task defined without a name
INVALID_OPTIONSretry.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.

MIT Licensed