@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/actioneer",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Ready? Set?? ACTION!! pew! pew! pew!",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -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(!await this.#predicateCheck(activity,pred,context))
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: !!options.required, // Default to 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
- // Start workers up to maxConcurrent limit
114
- const workers = []
115
- const workerCount = Math.min(maxConcurrent, items.length)
116
-
117
- for(let i = 0; i < workerCount; i++)
118
- workers.push(processWorker())
119
-
120
- // Wait for all workers to complete
121
- const processResult = await Util.settleAll(workers)
122
- this.#processResult("Processing pipeline.", processResult)
123
-
124
- // Run cleanup hooks
125
- const teardownResult = await Util.settleAll(
126
- [...this.#lifeCycle.get("teardown")].map(e => e())
127
- )
128
- this.#processResult("Tearing down the pipeline.", teardownResult)
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
- try {
157
- // Execute each step in sequence
158
- let result = item
158
+ // Execute each step in sequence
159
+ let result = item
159
160
 
160
- for(const step of this.#lifeCycle.get("process")) {
161
- this.#debug("Executing step: %o", 4, step.name)
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
  }