Skip to content

@unthrown/saga


@unthrown/saga

Saga

SagaAsyncBuilder

ts
type SagaAsyncBuilder<T, E> = object;

Defined in: saga.ts:43

The builder SagaAsync returns: steps in, one AsyncResult out.

Type Parameters

Type ParameterDescription
Twhat the last step produced, and what run() answers.
Ethe union of every step's modeled error type.

Properties

PropertyModifierTypeDescriptionDefined in
runreadonly() => AsyncResult<T, E>Run the steps in order, unwinding LIFO on the first failure.saga.ts:63
stepreadonly<T2, E2>(run, undo?) => SagaAsyncBuilder<T2, E | E2>Add a step, with the undo that takes it back. Remarks run is a thunk: an AsyncResult starts on construction, so a step built eagerly would run before the saga reached it. It takes no argument; undo receives the value its own step produced. Either may answer a plain Result in place of an AsyncResult, so a synchronous compensation needs no toAsync(). An undo answers unknown in the Ok channel and never in the Err one: compensation may not invent a new way for the saga to fail, because the caller is already handling the failure that triggered it.saga.ts:58

SagaAsync()

ts
function SagaAsync(): SagaAsyncBuilder<undefined, never>;

Defined in: saga.ts:183

Start a saga: a sequence of steps, each with an optional compensating undo, unwound last-in, first-out the moment a step fails.

Returns

SagaAsyncBuilder<undefined, never>

Remarks

DoAsync is the sibling for a sequence that only goes forward. Reach for a saga when a later step's failure must take back what the earlier ones did — a placement to cancel, a reservation to release — and the alternative is the hand-written walk-back, where two things go wrong quietly: the undos run in the wrong order, and an AsyncResult built outside the failure branch runs whether or not it was needed (an AsyncResult starts on construction, which is why every argument here is a thunk).

The saga answers what the last step produced. A failure — a modeled Err or a Defect — unwinds every undo recorded so far and then comes back unchanged, so a caller triages exactly what it would have without the saga. The one exception is a defect inside an undo: it wins over the failure that triggered it, because a compensation that broke is the more urgent report. Every remaining undo still runs first.

It is pure control flow — no timers, no clock, no randomness — so it replays deterministically inside a workflow sandbox.

Examples

ts
import { SagaAsync } from "@unthrown/saga";

const fulfilled = await SagaAsync()
  .step(() => place(order), () => cancelPlacement(order))
  .step(() => reserveStock(order), () => releaseStock(order))
  .step(() => arrangeShipping(order))
  .run();
// shipping failed → stock released, then placement cancelled, then Err(ShippingUnavailable)
ts
import { ErrAsync, OkAsync } from "unthrown";
import { SagaAsync } from "@unthrown/saga";

// The undo receives the step's own value, so it can take back exactly what
// that step created:
await SagaAsync()
  .step(() => OkAsync({ id: "o-1" }), (order) => releaseStock(order.id))
  .step(() => ErrAsync("shipping is down"))
  .run(); // => Err("shipping is down"), with the stock released first

Released under the MIT License.