first commit
This commit is contained in:
@@ -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`).
|
||||
Reference in New Issue
Block a user