@gesslar/actioneer 0.2.0 → 0.2.2
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 +152 -0
- package/package.json +1 -1
- package/src/lib/ActionRunner.js +1 -1
- package/src/lib/Piper.js +29 -26
package/README.md
CHANGED
|
@@ -44,6 +44,158 @@ import { ActionBuilder, ActionRunner } from "@gesslar/actioneer"
|
|
|
44
44
|
|
|
45
45
|
If you'd like more complete typings or additional JSDoc, open an issue or send a PR — contributions welcome.
|
|
46
46
|
|
|
47
|
+
## Activity Modes
|
|
48
|
+
|
|
49
|
+
Actioneer supports four distinct execution modes for activities, allowing you to control how operations are executed:
|
|
50
|
+
|
|
51
|
+
### Execute Once (Default)
|
|
52
|
+
|
|
53
|
+
The simplest mode executes an activity exactly once per context:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
class MyAction {
|
|
57
|
+
setup(builder) {
|
|
58
|
+
builder.do("processItem", ctx => {
|
|
59
|
+
ctx.result = ctx.input * 2
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### WHILE Mode
|
|
66
|
+
|
|
67
|
+
Loops while a predicate returns `true`. The predicate is evaluated **before** each iteration:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { ActionBuilder, ACTIVITY } from "@gesslar/actioneer"
|
|
71
|
+
|
|
72
|
+
class CounterAction {
|
|
73
|
+
#shouldContinue = (ctx) => ctx.count < 10
|
|
74
|
+
|
|
75
|
+
#increment = (ctx) => {
|
|
76
|
+
ctx.count += 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setup(builder) {
|
|
80
|
+
builder
|
|
81
|
+
.do("initialize", ctx => { ctx.count = 0 })
|
|
82
|
+
.do("countUp", ACTIVITY.WHILE, this.#shouldContinue, this.#increment)
|
|
83
|
+
.do("finish", ctx => { return ctx.count })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The activity will continue executing as long as the predicate returns `true`. Once it returns `false`, execution moves to the next activity.
|
|
89
|
+
|
|
90
|
+
### UNTIL Mode
|
|
91
|
+
|
|
92
|
+
Loops until a predicate returns `true`. The predicate is evaluated **after** each iteration:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
import { ActionBuilder, ACTIVITY } from "@gesslar/actioneer"
|
|
96
|
+
|
|
97
|
+
class ProcessorAction {
|
|
98
|
+
#queueIsEmpty = (ctx) => ctx.queue.length === 0
|
|
99
|
+
|
|
100
|
+
#processItem = (ctx) => {
|
|
101
|
+
const item = ctx.queue.shift()
|
|
102
|
+
ctx.processed.push(item)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setup(builder) {
|
|
106
|
+
builder
|
|
107
|
+
.do("initialize", ctx => {
|
|
108
|
+
ctx.queue = [1, 2, 3, 4, 5]
|
|
109
|
+
ctx.processed = []
|
|
110
|
+
})
|
|
111
|
+
.do("process", ACTIVITY.UNTIL, this.#queueIsEmpty, this.#processItem)
|
|
112
|
+
.do("finish", ctx => { return ctx.processed })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The activity executes at least once, then continues while the predicate returns `false`. Once it returns `true`, execution moves to the next activity.
|
|
118
|
+
|
|
119
|
+
### SPLIT Mode
|
|
120
|
+
|
|
121
|
+
Executes with a split/rejoin pattern for parallel execution. This mode requires a splitter function to divide the context and a rejoiner function to recombine results:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
import { ActionBuilder, ACTIVITY } from "@gesslar/actioneer"
|
|
125
|
+
|
|
126
|
+
class ParallelProcessor {
|
|
127
|
+
#split = (ctx) => {
|
|
128
|
+
// Split context into multiple items for parallel processing
|
|
129
|
+
return ctx.items.map(item => ({ item, processedBy: "worker" }))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#rejoin = (originalCtx, splitResults) => {
|
|
133
|
+
// Recombine parallel results back into original context
|
|
134
|
+
originalCtx.results = splitResults.map(r => r.item)
|
|
135
|
+
return originalCtx
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#processItem = (ctx) => {
|
|
139
|
+
ctx.item = ctx.item.toUpperCase()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setup(builder) {
|
|
143
|
+
builder
|
|
144
|
+
.do("initialize", ctx => {
|
|
145
|
+
ctx.items = ["apple", "banana", "cherry"]
|
|
146
|
+
})
|
|
147
|
+
.do("parallel", ACTIVITY.SPLIT, this.#split, this.#rejoin, this.#processItem)
|
|
148
|
+
.do("finish", ctx => { return ctx.results })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**How SPLIT Mode Works:**
|
|
154
|
+
|
|
155
|
+
1. The **splitter** function receives the context and returns an array of contexts (one per parallel task)
|
|
156
|
+
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 processed results
|
|
158
|
+
4. The rejoiner combines the results and returns the updated context
|
|
159
|
+
|
|
160
|
+
**Nested Pipelines with SPLIT:**
|
|
161
|
+
|
|
162
|
+
You can use nested ActionBuilders with SPLIT mode for complex parallel workflows:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
class NestedParallel {
|
|
166
|
+
#split = (ctx) => ctx.batches.map(batch => ({ batch }))
|
|
167
|
+
|
|
168
|
+
#rejoin = (original, results) => {
|
|
169
|
+
original.processed = results.flatMap(r => r.batch)
|
|
170
|
+
return original
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
setup(builder) {
|
|
174
|
+
builder
|
|
175
|
+
.do("parallel", ACTIVITY.SPLIT, this.#split, this.#rejoin,
|
|
176
|
+
new ActionBuilder(this)
|
|
177
|
+
.do("step1", ctx => { /* ... */ })
|
|
178
|
+
.do("step2", ctx => { /* ... */ })
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Mode Constraints
|
|
185
|
+
|
|
186
|
+
- **Only one mode per activity**: You cannot combine WHILE, UNTIL, and SPLIT. Attempting to use multiple modes will throw an error: `"You can't combine activity kinds. Pick one: WHILE, UNTIL, or SPLIT!"`
|
|
187
|
+
- **SPLIT requires both functions**: The splitter and rejoiner are both mandatory for SPLIT mode
|
|
188
|
+
- **Predicates must return boolean**: For WHILE and UNTIL modes, predicates should return `true` or `false`
|
|
189
|
+
|
|
190
|
+
### Mode Summary Table
|
|
191
|
+
|
|
192
|
+
| Mode | Signature | Predicate Timing | Use Case |
|
|
193
|
+
| ----------- | ---------------------------------------------------------- | ---------------- | ------------------------------------ |
|
|
194
|
+
| **Default** | `.do(name, operation)` | N/A | Execute once per context |
|
|
195
|
+
| **WHILE** | `.do(name, ACTIVITY.WHILE, predicate, operation)` | Before iteration | Loop while condition is true |
|
|
196
|
+
| **UNTIL** | `.do(name, ACTIVITY.UNTIL, predicate, operation)` | After iteration | Loop until condition is true |
|
|
197
|
+
| **SPLIT** | `.do(name, ACTIVITY.SPLIT, splitter, rejoiner, operation)` | N/A | Parallel execution with split/rejoin |
|
|
198
|
+
|
|
47
199
|
## ActionHooks
|
|
48
200
|
|
|
49
201
|
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
package/src/lib/ActionRunner.js
CHANGED
|
@@ -91,7 +91,7 @@ export default class ActionRunner extends Piper {
|
|
|
91
91
|
context = await this.#executeActivity(activity,context)
|
|
92
92
|
|
|
93
93
|
if(kindUntil)
|
|
94
|
-
if(
|
|
94
|
+
if(await this.#predicateCheck(activity,pred,context))
|
|
95
95
|
break
|
|
96
96
|
}
|
|
97
97
|
} else {
|
package/src/lib/Piper.js
CHANGED
|
@@ -41,7 +41,7 @@ export default class Piper {
|
|
|
41
41
|
this.#lifeCycle.get("process").add({
|
|
42
42
|
fn: fn.bind(newThis ?? this),
|
|
43
43
|
name: options.name || `Step ${this.#lifeCycle.get("process").size + 1}`,
|
|
44
|
-
required:
|
|
44
|
+
required: options.required ?? true,
|
|
45
45
|
...options
|
|
46
46
|
})
|
|
47
47
|
|
|
@@ -110,22 +110,24 @@ export default class Piper {
|
|
|
110
110
|
)
|
|
111
111
|
this.#processResult("Setting up the pipeline.", setupResult)
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
113
|
+
try {
|
|
114
|
+
// Start workers up to maxConcurrent limit
|
|
115
|
+
const workers = []
|
|
116
|
+
const workerCount = Math.min(maxConcurrent, items.length)
|
|
117
|
+
|
|
118
|
+
for(let i = 0; i < workerCount; i++)
|
|
119
|
+
workers.push(processWorker())
|
|
120
|
+
|
|
121
|
+
// Wait for all workers to complete
|
|
122
|
+
const processResult = await Util.settleAll(workers)
|
|
123
|
+
this.#processResult("Processing pipeline.", processResult)
|
|
124
|
+
} finally {
|
|
125
|
+
// Run cleanup hooks
|
|
126
|
+
const teardownResult = await Util.settleAll(
|
|
127
|
+
[...this.#lifeCycle.get("teardown")].map(e => e())
|
|
128
|
+
)
|
|
129
|
+
this.#processResult("Tearing down the pipeline.", teardownResult)
|
|
130
|
+
}
|
|
129
131
|
|
|
130
132
|
return allResults
|
|
131
133
|
}
|
|
@@ -153,19 +155,20 @@ export default class Piper {
|
|
|
153
155
|
* @private
|
|
154
156
|
*/
|
|
155
157
|
async #processItem(item) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let result = item
|
|
158
|
+
// Execute each step in sequence
|
|
159
|
+
let result = item
|
|
159
160
|
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
for(const step of this.#lifeCycle.get("process")) {
|
|
162
|
+
this.#debug("Executing step: %o", 4, step.name)
|
|
162
163
|
|
|
164
|
+
try {
|
|
163
165
|
result = await step.fn(result) ?? result
|
|
166
|
+
} catch(error) {
|
|
167
|
+
if(step.required)
|
|
168
|
+
throw Sass.new(`Processing required step "${step.name}".`, error)
|
|
164
169
|
}
|
|
165
|
-
|
|
166
|
-
return result
|
|
167
|
-
} catch(error) {
|
|
168
|
-
throw Sass.new("Processing an item.", error)
|
|
169
170
|
}
|
|
171
|
+
|
|
172
|
+
return result
|
|
170
173
|
}
|
|
171
174
|
}
|