3.4 KiB
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.failFastshort-circuit between batches when any error has been observed.maxConcurrencybounded batching (when finite).Promise.allSettledsemantics inside each batch so a single throw never aborts sibling steps in the same batch.- Guard short-circuit: a step whose
shouldExecutereturns false is recorded as"skipped"without invokingrun. - Injectable
Clockfor deterministic timestamps.
Usage: Basic Example
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
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,stepsaccessorsaddStep(step)returnsthisrun(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).