Parallelism with SPLIT
SPLIT mode executes a split/rejoin pattern for parallel work. You provide a
splitter that divides the context into many, an operation that runs on
each split in parallel, and a rejoiner that recombines the results.
.do(name, ACTIVITY.SPLIT, splitter, rejoiner, operation)A complete example
Section titled “A complete example”import {ActionBuilder, ACTIVITY} from "@gesslar/actioneer"
class ParallelProcessor { #split = ctx => { // Split the context into one context per parallel task return ctx.items.map(item => ({item, processedBy: "worker"})) }
#rejoin = (originalCtx, settledResults) => { // settledResults are settlement objects — pull the value out of each originalCtx.results = settledResults .filter(r => r.status === "fulfilled") .map(r => r.value.item)
return originalCtx }
#processItem = ctx => { ctx.item = ctx.item.toUpperCase()
return ctx // operations must return the context they want to pass on }
setup(builder) { builder .do("initialize", ctx => { ctx.items = ["apple", "banana", "cherry"]
return ctx }) .do("parallel", ACTIVITY.SPLIT, this.#split, this.#rejoin, this.#processItem) .do("finish", ctx => { return ctx.results }) }}How SPLIT works
Section titled “How SPLIT works”- The splitter receives the context and returns an array of contexts — one per parallel task. (The splitter returns the array directly; it is not subject to the “return the context” rule that operations follow.)
- Each split context is processed in parallel through the operation. The
value an operation returns becomes that split’s settled
value. - The rejoiner receives the original (pre-split) context and an array of
settlement objects — the same shape
Promise.allSettled()produces. - The rejoiner combines the results and returns the context that flows to the next activity.
SPLIT settles every operation
Section titled “SPLIT settles every operation”This is the part to internalize: your rejoiner receives settlement objects,
not raw values — whether the operation is a plain function or a nested
ActionBuilder. Each element of the array is one of:
{status: "fulfilled", value: <result>}for a successful operation{status: "rejected", reason: <error>}for a failed operation
Your rejoiner must handle them accordingly — check each status manually, or
lean on the Promised helpers from
@gesslar/toolkit, which are built for
exactly this shape of data:
import {Promised} from "@gesslar/toolkit"
#rejoin = (originalCtx, settledResults) => { // settledResults is an array of settlement objects.
// Keep only the successful values originalCtx.results = Promised.values(settledResults)
// Collect any failures if (Promised.hasRejected(settledResults)) { originalCtx.errors = Promised.reasons(settledResults) }
return originalCtx}Nested pipelines with SPLIT
Section titled “Nested pipelines with SPLIT”The operation can itself be a nested ActionBuilder,
letting each parallel task run a whole sub-pipeline:
class NestedParallel { #split = ctx => ctx.batches.map(batch => ({batch}))
#rejoin = (original, settledResults) => { // A nested ActionBuilder still settles — go through .value original.processed = Promised.values(settledResults).flatMap(r => r.batch) return original }
setup(builder) { builder .do("parallel", ACTIVITY.SPLIT, this.#split, this.#rejoin, new ActionBuilder() .do("step1", ctx => { /* ... */ return ctx }) .do("step2", ctx => { /* ... */ return ctx }) ) }}SPLIT and done()
Section titled “SPLIT and done()”Each split context runs as an independent execution, which changes how
done() callbacks fire — but only for a nested-builder
operation. There are three cases to keep straight:
- The outer pipeline’s
done()runs once. The SPLIT is a single activity in that pipeline, so itsdone()fires exactly once, after the rejoiner — not per split. - A nested
ActionBuilderoperation’s owndone()runs once per split. Because each split is executed as its own top-level run, the nested builder’sdone()is not suppressed (this is the opposite ofWHILE/UNTILloops, where a nested builder’sdone()never runs). If thatdone()has side effects — closing a connection, writing a file — it happens once for every split context. - A plain-function operation has no
done()of its own, so there is nothing extra to fire.
Keep the second case in mind when a nested split-operation pipeline finalizes
resources in its done().
Requirements
Section titled “Requirements”- Both functions are mandatory. SPLIT requires a splitter and a rejoiner; omitting either throws.
- The splitter returns an array. Each element becomes the context for one parallel operation.
- The rejoiner returns the context. Whatever it returns flows to the next activity.