Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
# `orchestrator/compensate-runner`
## Purpose
`runFullCompensation(...)` is the orchestrator's per-instance
compensation driver. It bridges the generic `compensation` module
(which only knows about `CompensableStep` + retry loop) with the
saga-specific concerns: looking up each completed step's registered
compensate handler, transitioning the step through `compensating →
compensated`, recording the compensation counter + span, and emitting
`saga.step.compensated`. The result of the walk is returned as a
structured outcome so the orchestrator can settle the instance state.
This module is split out from `orchestrator.ts` to keep the main
file under the ~250-line soft limit and to isolate the compensation
concerns from the forward step-dispatch loop.
## Features
- Walks completed step rows in reverse via the generic
`runCompensation` helper.
- Per-step: opens a `saga.compensate` span, transitions to
`compensating`, invokes the registered compensate handler under a
fresh `RequestContext`, increments
`saga.compensations_total`, transitions to `compensated`, emits
`saga.step.compensated`.
- Honours per-saga `maxCompensationAttempts` (defaults to 5 via the
`compensation` module).
- On terminal failure: transitions the failed step row back to
`failed` so the persistence layer reflects the broken state.
## Usage: Basic
Internal helper — typically only consumed by `createSagaOrchestrator`:
```ts
import { runFullCompensation } from "@suaveplan/api-saga/orchestrator/compensate-runner";
const result = await runFullCompensation({
instance,
maxCompensationAttempts: 5,
principal,
persistence,
registry,
emitter,
instruments,
requestContextFactory,
nowMs: () => Date.now(),
});
if (result.outcome === "compensated") {
persistence.transitionInstance(instance.id, "compensated");
}
```
## Usage: Advanced
A consumer that wants to drive compensation manually (e.g. an
operator-triggered "rollback this saga" admin command) can call
this directly without going through `start()` or `resume()`. The
caller is responsible for transitioning the instance into the
matching terminal state afterwards:
```ts
import { runFullCompensation } from "@suaveplan/api-saga";
const result = await runFullCompensation({
instance: persistence.requireInstance(id),
maxCompensationAttempts: 3,
principal: null,
persistence,
registry,
emitter,
instruments,
requestContextFactory,
nowMs: () => Date.now(),
});
persistence.transitionInstance(
id,
result.outcome === "compensated"
? "compensated"
: "compensation_failed"
);
```
## API Reference
- `runFullCompensation(args): Promise<CompensationFlowOutcome>`.
- `args`:
- `instance: SagaInstanceRow`,
- `maxCompensationAttempts: number`,
- `principal: Principal | null`,
- `persistence, registry, emitter, instruments,
requestContextFactory, nowMs` — injected dependencies.
- `CompensationFlowOutcome`:
- `{ outcome: "compensated" }`,
- `{ outcome: "compensation_failed", failedStep: string }`.
## Implementation Notes
The compensation walk only considers step rows whose persisted
state is `completed`. Steps in `pending`, `in_flight`, or `failed`
have either not run or already failed; running compensation against
them would either invoke a compensate handler with no matching
output or repeat work the runner already declared lost.
`updateStepPayload` is invoked AFTER the compensate handler so the
persistence layer records the final attempt count rather than the
initial one. This matters for the operator-facing dashboards that
read the row to display "compensation took N attempts".