first commit
This commit is contained in:
95
docs/api-stack/api/api-saga/src/state/state-machine.md
Normal file
95
docs/api-stack/api/api-saga/src/state/state-machine.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# `state/state-machine`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The saga lifecycle has two coupled finite-state machines: the instance
|
||||
state (overall saga health) and the per-step state. This module pins
|
||||
both transition tables in one place so every other module — orchestrator,
|
||||
recovery, compensation — funnels every write through a single guard. The
|
||||
guard refuses transitions the table does not declare and raises
|
||||
`SAGA.INVALID_STATE_TRANSITION` so a buggy orchestrator path can never
|
||||
silently demote `completed` back into `running` (or, more dangerously,
|
||||
out of a terminal state into a runnable one).
|
||||
|
||||
## Features
|
||||
|
||||
- Instance machine: `running → {completed, compensating, cancelled}` and
|
||||
`compensating → {compensated, compensation_failed}`. Terminal states
|
||||
(`completed`, `compensated`, `compensation_failed`, `cancelled`) have
|
||||
no outgoing transitions.
|
||||
- Step machine: `pending → in_flight → {completed, failed}`; completed
|
||||
steps may roll back via `compensating → {compensated, failed}`.
|
||||
- `assertInstanceTransition` / `assertStepTransition` throw
|
||||
`SagaError(SAGA.INVALID_STATE_TRANSITION)` with full context.
|
||||
- `isTerminalInstance` / `isTerminalStep` predicates so the recovery
|
||||
runner can skip already-settled instances.
|
||||
- Idempotent same-state transitions (`from === to`) are accepted as a
|
||||
no-op — useful for re-asserting state during recovery replay.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
assertInstanceTransition,
|
||||
canTransitionInstance,
|
||||
} from "@suaveplan/api-saga/state/state-machine";
|
||||
|
||||
assertInstanceTransition("s_1", "running", "completed"); // ok
|
||||
canTransitionInstance("completed", "running"); // false
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Compose with the persistence layer so every DB row write is gated on
|
||||
the state machine. Same-state writes are accepted but a forbidden
|
||||
transition halts the orchestrator before the DB is mutated:
|
||||
|
||||
```ts
|
||||
import {
|
||||
assertStepTransition,
|
||||
canTransitionStep,
|
||||
} from "@suaveplan/api-saga";
|
||||
|
||||
function updateStep(
|
||||
sagaId: string,
|
||||
stepName: string,
|
||||
current: SagaStepState,
|
||||
next: SagaStepState
|
||||
): void {
|
||||
assertStepTransition(sagaId, stepName, current, next);
|
||||
if (current === next) {
|
||||
return; // idempotent — DB already reflects the state
|
||||
}
|
||||
persistence.updateStep(sagaId, stepName, next);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `SagaInstanceState` — `"running" | "completed" | "compensating" |
|
||||
"compensated" | "compensation_failed" | "cancelled"`.
|
||||
- `SagaStepState` — `"pending" | "in_flight" | "completed" | "failed" |
|
||||
"compensating" | "compensated"`.
|
||||
- `TERMINAL_INSTANCE_STATES`, `TERMINAL_STEP_STATES` — frozen sets.
|
||||
- `canTransitionInstance(from, to): boolean`,
|
||||
`canTransitionStep(from, to): boolean` — predicates.
|
||||
- `assertInstanceTransition(sagaId, from, to): void`,
|
||||
`assertStepTransition(sagaId, stepName, from, to): void` — guard
|
||||
functions that throw on rejected transitions.
|
||||
- `isTerminalInstance(state): boolean`, `isTerminalStep(state): boolean`
|
||||
— predicates the recovery runner uses to filter.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The transition tables are `ReadonlyMap<State, ReadonlySet<State>>` so
|
||||
they're immutable at the type level — a downstream module cannot patch
|
||||
the table at runtime. The compensation machine intentionally allows
|
||||
`compensating → failed`: a single compensate-handler failure does not
|
||||
yet mean the saga's terminal failure (the compensation runner retries
|
||||
up to `maxCompensationAttempts`); the per-step `failed` state is
|
||||
transient in that sense, and the instance-level transition into
|
||||
`compensation_failed` only fires once every retry budget is exhausted.
|
||||
Reference in New Issue
Block a user