# saga _(no submodules)_ ## Purpose `Saga` is the dedicated, opinionated orchestrator for the textbook saga pattern: sequential business steps where every successful step registers its inverse so that a later failure can roll back every side effect in strict reverse order. Compared to `WorkflowEngine`, `Saga` is intentionally narrower — it has no `onError: "continue"` mode, no transition hooks, and no lifecycle hook registry. Callers reach for it when they want the smaller surface area and the explicit per-step semantics. The class owns its own `Compensator` and resets it at the start of every `execute` call, so a single `Saga` instance can be run repeatedly without leaking state across runs. ## Features - `addStep(step)` fluent registration. - Sequential execution with first-failure short-circuit. - Automatic reverse-order compensation via the internal `Compensator`. - `continueCompensationOnError` (default `true`) — forwards to the compensator. - Skipped steps recorded but not compensated. - Injectable `Clock` propagates to the compensator too. ## Usage: Basic Example ```ts import { createSaga, createStep } from "@suaveplan/workflow"; const saga = createSaga<{ orderId: string }>({ id: "booking", name: "Booking saga", }); saga .addStep( createStep({ id: "reserve", name: "Reserve seats", execute: (ctx) => reserveSeats(ctx), compensate: (ctx) => releaseSeats(ctx), }), ) .addStep( createStep({ id: "charge", name: "Charge payment", execute: (ctx) => chargeCard(ctx), compensate: (ctx) => refundCard(ctx), }), ); const result = await saga.execute({ orderId: "ord-1" }); ``` ## Usage: Advanced Example ```ts import { createSaga, createStep } from "@suaveplan/workflow"; const fakeClock = () => 1_000; const saga = createSaga<{ id: string }>({ id: "two-phase", name: "Two-phase commit", continueCompensationOnError: false, clock: fakeClock, }); saga.addStep( createStep({ id: "lock", name: "Lock resource", execute: (ctx) => acquireLock(ctx.id), compensate: (ctx) => releaseLock(ctx.id), }), ); saga.addStep( createStep({ id: "commit", name: "Commit", execute: () => { throw new Error("conflict"); }, }), ); const result = await saga.execute({ id: "row-7" }); expect(result.status).toBe("failed"); expect(result.compensationRecords.length).toBe(1); ``` ## API Reference - `SagaResult` — `{ sagaId; status; stepResults; compensationRecords; startedAt; completedAt; error?; }`. - `SagaOptions` — `{ id; name; continueCompensationOnError?; clock?; }`. - `Saga` — - `size`, `steps` accessors - `addStep(step)` returns `this` - `execute(context): Promise>` - `createSaga(options): Saga`. ## Implementation Notes The compensator is rebuilt at construction time and `clear()`-ed at the start of every `execute`, so the saga is genuinely reusable. The internal `extractStepError` helper guarantees that a failed step always surfaces a real `Error` instance even if the underlying `StepResult.error` was absent — that case is rare (a step that returns `status: "failed"` without populating `error` is technically allowed by the type) but the saga has to handle it cleanly. Skipped steps do NOT register a compensation; this preserves the saga contract that compensations correspond strictly to side effects that happened, not to side effects that were considered.