@gesslar/actioneer 0.2.4 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -3
- package/package.json +2 -2
- package/src/lib/ActionBuilder.js +1 -1
- package/src/lib/ActionRunner.js +27 -12
- package/src/lib/Piper.js +11 -7
package/README.md
CHANGED
|
@@ -30,8 +30,16 @@ class MyAction {
|
|
|
30
30
|
|
|
31
31
|
const builder = new ActionBuilder(new MyAction())
|
|
32
32
|
const runner = new ActionRunner(builder)
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const results = await runner.pipe([{}], 4) // run up to 4 contexts concurrently
|
|
34
|
+
|
|
35
|
+
// pipe() returns settled results: {status: "fulfilled", value: ...} or {status: "rejected", reason: ...}
|
|
36
|
+
results.forEach(result => {
|
|
37
|
+
if (result.status === "fulfilled") {
|
|
38
|
+
console.log("Count:", result.value)
|
|
39
|
+
} else {
|
|
40
|
+
console.error("Error:", result.reason)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
35
43
|
```
|
|
36
44
|
|
|
37
45
|
## Types (TypeScript / VS Code)
|
|
@@ -154,9 +162,40 @@ class ParallelProcessor {
|
|
|
154
162
|
|
|
155
163
|
1. The **splitter** function receives the context and returns an array of contexts (one per parallel task)
|
|
156
164
|
2. Each split context is processed in parallel through the **operation** function
|
|
157
|
-
3. The **rejoiner** function receives the original context and the array of
|
|
165
|
+
3. The **rejoiner** function receives the original context and the array of settled results from `Promise.allSettled()`
|
|
158
166
|
4. The rejoiner combines the results and returns the updated context
|
|
159
167
|
|
|
168
|
+
**Important: SPLIT uses `Promise.allSettled()`**
|
|
169
|
+
|
|
170
|
+
The SPLIT mode uses `Promise.allSettled()` internally to execute parallel operations. This means your **rejoiner** function will receive an array of settlement objects, not the raw context values. Each element in the array will be either:
|
|
171
|
+
|
|
172
|
+
- `{ status: "fulfilled", value: <result> }` for successful operations
|
|
173
|
+
- `{ status: "rejected", reason: <error> }` for failed operations
|
|
174
|
+
|
|
175
|
+
Your rejoiner must handle settled results accordingly. You can process them however you need - check each `status` manually, or use helper utilities like those in `@gesslar/toolkit`:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
import { Util } from "@gesslar/toolkit"
|
|
179
|
+
|
|
180
|
+
#rejoin = (originalCtx, settledResults) => {
|
|
181
|
+
// settledResults is an array of settlement objects
|
|
182
|
+
// Each has either { status: "fulfilled", value: ... }
|
|
183
|
+
// or { status: "rejected", reason: ... }
|
|
184
|
+
|
|
185
|
+
// Example: extract only successful results
|
|
186
|
+
originalCtx.results = Util.fulfilledValues(settledResults)
|
|
187
|
+
|
|
188
|
+
// Example: check for any failures
|
|
189
|
+
if (Util.anyRejected(settledResults)) {
|
|
190
|
+
originalCtx.errors = Util.rejectedReasons(
|
|
191
|
+
Util.settledAndRejected(settledResults)
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return originalCtx
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
160
199
|
**Nested Pipelines with SPLIT:**
|
|
161
200
|
|
|
162
201
|
You can use nested ActionBuilders with SPLIT mode for complex parallel workflows:
|
|
@@ -196,6 +235,68 @@ class NestedParallel {
|
|
|
196
235
|
| **UNTIL** | `.do(name, ACTIVITY.UNTIL, predicate, operation)` | After iteration | Loop until condition is true |
|
|
197
236
|
| **SPLIT** | `.do(name, ACTIVITY.SPLIT, splitter, rejoiner, operation)` | N/A | Parallel execution with split/rejoin |
|
|
198
237
|
|
|
238
|
+
## Running Actions: `run()` vs `pipe()`
|
|
239
|
+
|
|
240
|
+
ActionRunner provides two methods for executing your action pipelines:
|
|
241
|
+
|
|
242
|
+
### `run(context)` - Single Context Execution
|
|
243
|
+
|
|
244
|
+
Executes the pipeline once with a single context. Returns the final context value directly, or throws if an error occurs.
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
const builder = new ActionBuilder(new MyAction())
|
|
248
|
+
const runner = new ActionRunner(builder)
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const result = await runner.run({input: "data"})
|
|
252
|
+
console.log(result) // Final context value
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error("Pipeline failed:", error)
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Use `run()` when:**
|
|
259
|
+
|
|
260
|
+
- Processing a single context
|
|
261
|
+
- You want errors to throw immediately
|
|
262
|
+
- You prefer traditional try/catch error handling
|
|
263
|
+
|
|
264
|
+
### `pipe(contexts, maxConcurrent)` - Concurrent Batch Execution
|
|
265
|
+
|
|
266
|
+
Executes the pipeline concurrently across multiple contexts with a configurable concurrency limit. Returns an array of **settled results** - never throws on individual pipeline failures.
|
|
267
|
+
|
|
268
|
+
```js
|
|
269
|
+
const builder = new ActionBuilder(new MyAction())
|
|
270
|
+
const runner = new ActionRunner(builder)
|
|
271
|
+
|
|
272
|
+
const contexts = [{id: 1}, {id: 2}, {id: 3}]
|
|
273
|
+
const results = await runner.pipe(contexts, 4) // Max 4 concurrent
|
|
274
|
+
|
|
275
|
+
results.forEach((result, i) => {
|
|
276
|
+
if (result.status === "fulfilled") {
|
|
277
|
+
console.log(`Context ${i} succeeded:`, result.value)
|
|
278
|
+
} else {
|
|
279
|
+
console.error(`Context ${i} failed:`, result.reason)
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Use `pipe()` when:**
|
|
285
|
+
|
|
286
|
+
- Processing multiple contexts in parallel
|
|
287
|
+
- You want to control concurrency (default: 10)
|
|
288
|
+
- You need all results (both successes and failures)
|
|
289
|
+
- Error handling should be at the call site
|
|
290
|
+
|
|
291
|
+
**Important: `pipe()` returns settled results**
|
|
292
|
+
|
|
293
|
+
The `pipe()` method uses `Promise.allSettled()` internally and returns an array of settlement objects:
|
|
294
|
+
|
|
295
|
+
- `{status: "fulfilled", value: <result>}` for successful executions
|
|
296
|
+
- `{status: "rejected", reason: <error>}` for failed executions
|
|
297
|
+
|
|
298
|
+
This design ensures error handling responsibility stays at the call site - you decide how to handle failures rather than the framework deciding for you.
|
|
299
|
+
|
|
199
300
|
## ActionHooks
|
|
200
301
|
|
|
201
302
|
Actioneer supports lifecycle hooks that can execute before and after each activity in your pipeline. Hooks are loaded from a module and can be configured either by file path or by providing a pre-instantiated hooks object.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gesslar/actioneer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Ready? Set?? ACTION!! pew! pew! pew!",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"typescript": "^5.9.3"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@gesslar/toolkit": "^
|
|
59
|
+
"@gesslar/toolkit": "^1.9.1"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/src/lib/ActionBuilder.js
CHANGED
package/src/lib/ActionRunner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Data, Sass, Valid} from "@gesslar/toolkit"
|
|
1
|
+
import {Data, Sass, Util, Valid} from "@gesslar/toolkit"
|
|
2
2
|
|
|
3
3
|
import ActionBuilder from "./ActionBuilder.js"
|
|
4
4
|
import {ACTIVITY} from "./Activity.js"
|
|
@@ -113,30 +113,45 @@ export default class ActionRunner extends Piper {
|
|
|
113
113
|
break
|
|
114
114
|
}
|
|
115
115
|
} else if(kindSplit) {
|
|
116
|
-
// SPLIT activity: parallel execution with splitter/rejoiner
|
|
117
|
-
|
|
118
|
-
const rejoiner = activity
|
|
116
|
+
// SPLIT activity: parallel execution with splitter/rejoiner
|
|
117
|
+
// pattern
|
|
118
|
+
const {splitter, rejoiner} = activity
|
|
119
119
|
|
|
120
120
|
if(!splitter || !rejoiner)
|
|
121
121
|
throw Sass.new(
|
|
122
|
-
`SPLIT activity "${String(activity.name)}" requires both
|
|
122
|
+
`SPLIT activity "${String(activity.name)}" requires both ` +
|
|
123
|
+
`splitter and rejoiner functions.`
|
|
123
124
|
)
|
|
124
125
|
|
|
125
126
|
const original = context
|
|
126
|
-
const splitContexts = splitter.call(activity.action,context)
|
|
127
|
+
const splitContexts = await splitter.call(activity.action, context)
|
|
128
|
+
|
|
129
|
+
let settled
|
|
127
130
|
|
|
128
|
-
let newContext
|
|
129
131
|
if(activity.opKind === "ActionBuilder") {
|
|
130
|
-
// Use parallel execution for ActionBuilder
|
|
131
|
-
|
|
132
|
+
// Use parallel execution for ActionBuilder with concurrency control
|
|
133
|
+
// pipe() now returns settled results
|
|
134
|
+
if(activity.hooks)
|
|
135
|
+
activity.op.withHooks(activity.hooks)
|
|
136
|
+
|
|
137
|
+
const runner = new this.constructor(activity.op, {
|
|
138
|
+
debug: this.#debug, name: activity.name
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// pipe() returns settled results with concurrency control
|
|
142
|
+
settled = await runner.pipe(splitContexts)
|
|
132
143
|
} else {
|
|
133
144
|
// For plain functions, process each split context
|
|
134
|
-
|
|
135
|
-
splitContexts.map(ctx => this.#execute(activity,ctx))
|
|
145
|
+
settled = await Util.settleAll(
|
|
146
|
+
splitContexts.map(ctx => this.#execute(activity, ctx))
|
|
136
147
|
)
|
|
137
148
|
}
|
|
138
149
|
|
|
139
|
-
const rejoined = rejoiner.call(
|
|
150
|
+
const rejoined = await rejoiner.call(
|
|
151
|
+
activity.action,
|
|
152
|
+
original,
|
|
153
|
+
settled
|
|
154
|
+
)
|
|
140
155
|
|
|
141
156
|
context = rejoined
|
|
142
157
|
} else {
|
package/src/lib/Piper.js
CHANGED
|
@@ -79,7 +79,7 @@ export default class Piper {
|
|
|
79
79
|
*
|
|
80
80
|
* @param {Array<unknown>|unknown} items - Items to process
|
|
81
81
|
* @param {number} maxConcurrent - Maximum concurrent items to process
|
|
82
|
-
* @returns {Promise<Array<unknown>>} -
|
|
82
|
+
* @returns {Promise<Array<{status: string, value?: unknown, reason?: unknown}>>} - Settled results from processing
|
|
83
83
|
*/
|
|
84
84
|
async pipe(items, maxConcurrent = 10) {
|
|
85
85
|
items = Array.isArray(items)
|
|
@@ -87,20 +87,23 @@ export default class Piper {
|
|
|
87
87
|
: [items]
|
|
88
88
|
|
|
89
89
|
let itemIndex = 0
|
|
90
|
-
const allResults =
|
|
90
|
+
const allResults = new Array(items.length)
|
|
91
91
|
|
|
92
92
|
const processWorker = async() => {
|
|
93
93
|
while(true) {
|
|
94
94
|
const currentIndex = itemIndex++
|
|
95
|
+
|
|
95
96
|
if(currentIndex >= items.length)
|
|
96
97
|
break
|
|
97
98
|
|
|
98
99
|
const item = items[currentIndex]
|
|
100
|
+
|
|
99
101
|
try {
|
|
100
102
|
const result = await this.#processItem(item)
|
|
101
|
-
|
|
103
|
+
|
|
104
|
+
allResults[currentIndex] = {status: "fulfilled", value: result}
|
|
102
105
|
} catch(error) {
|
|
103
|
-
|
|
106
|
+
allResults[currentIndex] = {status: "rejected", reason: error}
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
}
|
|
@@ -108,6 +111,7 @@ export default class Piper {
|
|
|
108
111
|
const setupResult = await Util.settleAll(
|
|
109
112
|
[...this.#lifeCycle.get("setup")].map(e => e())
|
|
110
113
|
)
|
|
114
|
+
|
|
111
115
|
this.#processResult("Setting up the pipeline.", setupResult)
|
|
112
116
|
|
|
113
117
|
try {
|
|
@@ -118,14 +122,14 @@ export default class Piper {
|
|
|
118
122
|
for(let i = 0; i < workerCount; i++)
|
|
119
123
|
workers.push(processWorker())
|
|
120
124
|
|
|
121
|
-
// Wait for all workers to complete
|
|
122
|
-
|
|
123
|
-
this.#processResult("Processing pipeline.", processResult)
|
|
125
|
+
// Wait for all workers to complete - don't throw on worker failures
|
|
126
|
+
await Promise.all(workers)
|
|
124
127
|
} finally {
|
|
125
128
|
// Run cleanup hooks
|
|
126
129
|
const teardownResult = await Util.settleAll(
|
|
127
130
|
[...this.#lifeCycle.get("teardown")].map(e => e())
|
|
128
131
|
)
|
|
132
|
+
|
|
129
133
|
this.#processResult("Tearing down the pipeline.", teardownResult)
|
|
130
134
|
}
|
|
131
135
|
|