# parallel-step _(no submodules)_ ## 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` — `{ groupId; status; results; startedAt; completedAt; errors; }`. - `ParallelStepOptions` — `{ groupId; name; failFast?; maxConcurrency?; clock?; }`. - `ParallelStep` — - `size`, `steps` accessors - `addStep(step)` returns `this` - `run(context): Promise>` - `compensateAll(context): Promise` - `createParallelStep(options): ParallelStep`. ## 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`).