first commit
This commit is contained in:
120
docs/foundation/flow/workflow/src/compensation/saga/saga.md
Normal file
120
docs/foundation/flow/workflow/src/compensation/saga/saga.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# saga
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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<TCtx>` — `{ sagaId; status; stepResults; compensationRecords; startedAt; completedAt; error?; }`.
|
||||
- `SagaOptions` — `{ id; name; continueCompensationOnError?; clock?; }`.
|
||||
- `Saga<TCtx>` —
|
||||
- `size`, `steps` accessors
|
||||
- `addStep(step)` returns `this`
|
||||
- `execute(context): Promise<SagaResult<TCtx>>`
|
||||
- `createSaga<TCtx>(options): Saga<TCtx>`.
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user