Initial Commit
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# conditional-step
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`ConditionalStep` is the branching primitive in `@suaveplan/workflow`. It
|
||||
evaluates a `GuardFn` against the context and dispatches to one of two
|
||||
`Step` instances (`onTrue` or `onFalse`). When the false branch is omitted
|
||||
and the condition resolves falsy, the step is recorded as `"skipped"` —
|
||||
giving callers a way to express "do this only if the predicate holds"
|
||||
without filling in a no-op alternative.
|
||||
|
||||
This class is the workflow analogue of a ternary expression: it never
|
||||
declares its own retry policy or compensation directly; both branches are
|
||||
fully-fledged `Step` instances whose own configuration governs those
|
||||
concerns. `compensate` rolls back only the branch that actually ran.
|
||||
|
||||
## Features
|
||||
|
||||
- `onTrue` / optional `onFalse` branches.
|
||||
- `lastBranch` accessor reflecting which path executed.
|
||||
- Skipped branch records a `"skipped"` `StepResult` and emits an OTel
|
||||
`"skipped"` counter increment (the duration histogram is intentionally
|
||||
skipped — see `otel/instruments`).
|
||||
- `compensate(ctx)` rolls back only the branch that ran.
|
||||
- Injectable `Clock`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createConditionalStep, createStep } from "@suaveplan/workflow";
|
||||
|
||||
const branch = createConditionalStep<{ premium: boolean }>({
|
||||
id: "discount",
|
||||
name: "Discount",
|
||||
condition: (ctx) => ctx.premium,
|
||||
onTrue: createStep({
|
||||
id: "premium",
|
||||
name: "Premium discount",
|
||||
execute: () => ({ discountPct: 30 }),
|
||||
}),
|
||||
onFalse: createStep({
|
||||
id: "standard",
|
||||
name: "Standard discount",
|
||||
execute: () => ({ discountPct: 5 }),
|
||||
}),
|
||||
});
|
||||
const result = await branch.run({ premium: true });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createConditionalStep, createStep } from "@suaveplan/workflow";
|
||||
|
||||
const ship = createStep<{ shipped: boolean }>({
|
||||
id: "ship",
|
||||
name: "Ship",
|
||||
execute: async (ctx) => {
|
||||
await dispatch(ctx);
|
||||
},
|
||||
compensate: async (ctx) => {
|
||||
await recall(ctx);
|
||||
},
|
||||
});
|
||||
|
||||
const guard = createConditionalStep<{ ready: boolean }>({
|
||||
id: "maybe-ship",
|
||||
name: "Maybe ship",
|
||||
condition: (ctx) => ctx.ready,
|
||||
onTrue: ship,
|
||||
});
|
||||
|
||||
await guard.run({ ready: true });
|
||||
// Later, on outer-step rollback:
|
||||
await guard.compensate({ ready: true });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ConditionalStepOptions<TCtx>` —
|
||||
`{ id; name; condition; onTrue; onFalse?; clock?; }`.
|
||||
- `ConditionalStep<TCtx>` —
|
||||
- `trueBranch`, `falseBranch?`, `lastBranch?`
|
||||
- `run(context): Promise<StepResult>`
|
||||
- `compensate(context): Promise<void>`
|
||||
- `createConditionalStep<TCtx>(options): ConditionalStep<TCtx>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`lastBranch` is set before the branch runs so callers that observe the
|
||||
step (e.g. via hooks) can attribute downstream side-effects to the chosen
|
||||
arm. `compensate` checks both the branch identity and the underlying
|
||||
step's `hasCompensation` flag, so a guard whose `onTrue` step has no
|
||||
compensation is a no-op rather than a thrown error.
|
||||
|
||||
`recordStepExecuted(stepId, "skipped", 0)` is emitted on the no-false-branch
|
||||
fallthrough so the metric for skipped steps lines up with what
|
||||
`WorkflowEngine` emits when a top-level `Step.shouldExecute` returns false.
|
||||
Reference in New Issue
Block a user