first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
# parallel-step
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ParallelStep` groups multiple `Step` instances and runs them concurrently
against a shared context. It is the only place in `@suaveplan/workflow`
that introduces concurrency, which keeps the engine's main loop strictly
sequential and easy to reason about. The class supports two execution
modes: an unbounded mode (`maxConcurrency: Infinity` — default) that fans
all steps out via a single `Promise.allSettled`, and a bounded mode that
batches them in groups of `maxConcurrency`.
The group also exposes a `compensateAll` helper that walks every member
step in reverse insertion order and invokes its compensation hook, which is
how the engine and saga unwind a partially-completed parallel group on
failure.
## Features
- `addStep(step)` fluent API to build up the group after construction.
- `failFast` short-circuit between batches when any error has been observed.
- `maxConcurrency` bounded batching (when finite).
- `Promise.allSettled` semantics inside each batch so a single throw never
aborts sibling steps in the same batch.
- Guard short-circuit: a step whose `shouldExecute` returns false is
recorded as `"skipped"` without invoking `run`.
- Injectable `Clock` for deterministic timestamps.
## Usage: Basic Example
```ts
import { createParallelStep, createStep } from "@suaveplan/workflow";
const group = createParallelStep<{ url: string }>({
groupId: "fetch-group",
name: "Fetch resources",
});
group
.addStep(createStep({ id: "a", name: "A", execute: () => "a" }))
.addStep(createStep({ id: "b", name: "B", execute: () => "b" }));
const result = await group.run({ url: "https://example.com" });
```
## Usage: Advanced Example
```ts
import { createParallelStep, createStep } from "@suaveplan/workflow";
const group = createParallelStep<{ jobIds: string[] }>({
groupId: "process",
name: "Process jobs",
maxConcurrency: 2,
failFast: true,
});
for (const id of ["a", "b", "c", "d"]) {
group.addStep(
createStep({
id,
name: `process-${id}`,
execute: async (ctx) => {
if (id === "b") throw new Error("nope");
return id;
},
compensate: async () => {
await unprocess(id);
},
}),
);
}
const result = await group.run({ jobIds: [] });
if (result.status === "failed") {
await group.compensateAll({ jobIds: [] });
}
```
## API Reference
- `ParallelResult<TCtx>` — `{ groupId; status; results; startedAt; completedAt; errors; }`.
- `ParallelStepOptions` — `{ groupId; name; failFast?; maxConcurrency?; clock?; }`.
- `ParallelStep<TCtx>` —
- `size`, `steps` accessors
- `addStep(step)` returns `this`
- `run(context): Promise<ParallelResult<TCtx>>`
- `compensateAll(context): Promise<void>`
- `createParallelStep<TCtx>(options): ParallelStep<TCtx>`.
## Implementation Notes
In bounded mode, batches are computed by slicing `_steps` at
`maxConcurrency` boundaries (so a `[a, b, c, d]` group with
`maxConcurrency: 2` produces `[[a, b], [c, d]]`). When `failFast: true`,
the second batch is skipped if the first produced any error — but errors
are already collected for every step in the failing batch, so callers see
every sibling outcome that ran before the short-circuit.
`compensateAll` reverses the original insertion order so the most recently
added (and therefore most recently completed) step rolls back first. This
matches saga semantics and matches what callers expect when authoring
inverse pairs (`reserve → release`, `charge → refund`).