@gesslar/actioneer 2.0.2 → 2.2.0
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 +212 -13
- package/package.json +5 -5
- package/src/browser/lib/ActionBuilder.js +44 -7
- package/src/browser/lib/ActionRunner.js +139 -90
- package/src/browser/lib/ActionWrapper.js +52 -8
- package/src/browser/lib/Activity.js +39 -8
- package/src/browser/lib/Piper.js +2 -2
- package/src/lib/ActionHooks.js +8 -0
- package/src/types/browser/lib/ActionBuilder.d.ts +24 -8
- package/src/types/browser/lib/ActionBuilder.d.ts.map +1 -1
- package/src/types/browser/lib/ActionRunner.d.ts +7 -2
- package/src/types/browser/lib/ActionRunner.d.ts.map +1 -1
- package/src/types/browser/lib/ActionWrapper.d.ts +24 -5
- package/src/types/browser/lib/ActionWrapper.d.ts.map +1 -1
- package/src/types/browser/lib/Activity.d.ts +29 -8
- package/src/types/browser/lib/Activity.d.ts.map +1 -1
- package/src/types/lib/ActionBuilder.d.ts +7 -0
- package/src/types/lib/ActionHooks.d.ts +25 -1
- package/src/types/lib/ActionHooks.d.ts.map +1 -1
- package/src/types/lib/ActionWrapper.d.ts +7 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Actioneer
|
|
2
2
|
|
|
3
|
-
Actioneer is a small, focused action orchestration library for Node.js and browser environments. It provides a fluent builder for composing activities and a concurrent runner with lifecycle hooks and
|
|
3
|
+
Actioneer is a small, focused action orchestration library for Node.js and browser environments. It provides a fluent builder for composing activities and a concurrent runner with lifecycle hooks and control flow semantics (while/until/if/break/continue). The project is written as ES modules and targets Node 20+ and modern browsers.
|
|
4
4
|
|
|
5
5
|
This repository extracts the action orchestration pieces from a larger codebase and exposes a compact API for building pipelines of work that can run concurrently with hook support and nested pipelines.
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ These classes work in browsers, Node.js, and browser-like environments such as T
|
|
|
16
16
|
| ActionHooks | Lifecycle hook management (requires pre-instantiated hooks in browser) |
|
|
17
17
|
| ActionRunner | Concurrent pipeline executor with configurable concurrency |
|
|
18
18
|
| ActionWrapper | Activity container and iterator |
|
|
19
|
-
| Activity | Activity definitions with WHILE, UNTIL, and SPLIT modes |
|
|
19
|
+
| Activity | Activity definitions with WHILE, UNTIL, IF, BREAK, CONTINUE, and SPLIT modes |
|
|
20
20
|
| Piper | Base concurrent processing with worker pools |
|
|
21
21
|
|
|
22
22
|
### Node.js
|
|
@@ -133,7 +133,7 @@ If you'd like more complete typings or additional JSDoc, open an issue or send a
|
|
|
133
133
|
|
|
134
134
|
## Activity Modes
|
|
135
135
|
|
|
136
|
-
Actioneer supports
|
|
136
|
+
Actioneer supports six distinct execution modes for activities, allowing you to control how operations are executed:
|
|
137
137
|
|
|
138
138
|
### Execute Once (Default)
|
|
139
139
|
|
|
@@ -203,6 +203,133 @@ class ProcessorAction {
|
|
|
203
203
|
|
|
204
204
|
The activity executes at least once, then continues while the predicate returns `false`. Once it returns `true`, execution moves to the next activity.
|
|
205
205
|
|
|
206
|
+
### IF Mode
|
|
207
|
+
|
|
208
|
+
Conditionally executes an activity based on a predicate. Unlike WHILE/UNTIL, IF executes at most once:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
import { ActionBuilder, ACTIVITY } from "@gesslar/actioneer"
|
|
212
|
+
|
|
213
|
+
class ConditionalAction {
|
|
214
|
+
#shouldProcess = (ctx) => ctx.value > 10
|
|
215
|
+
|
|
216
|
+
#processLargeValue = (ctx) => {
|
|
217
|
+
ctx.processed = ctx.value * 2
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
setup(builder) {
|
|
221
|
+
builder
|
|
222
|
+
.do("initialize", ctx => { ctx.value = 15 })
|
|
223
|
+
.do("maybeProcess", ACTIVITY.IF, this.#shouldProcess, this.#processLargeValue)
|
|
224
|
+
.do("finish", ctx => { return ctx })
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
If the predicate returns `true`, the activity executes once. If `false`, the activity is skipped entirely and execution moves to the next activity.
|
|
230
|
+
|
|
231
|
+
### BREAK Mode
|
|
232
|
+
|
|
233
|
+
Breaks out of a WHILE or UNTIL loop when a predicate returns `true`. BREAK must be used inside a nested ActionBuilder within a loop:
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
import { ActionBuilder, ACTIVITY } from "@gesslar/actioneer"
|
|
237
|
+
|
|
238
|
+
class BreakExample {
|
|
239
|
+
setup(builder) {
|
|
240
|
+
builder
|
|
241
|
+
.do("initialize", ctx => {
|
|
242
|
+
ctx.count = 0
|
|
243
|
+
ctx.items = []
|
|
244
|
+
})
|
|
245
|
+
.do("loop", ACTIVITY.WHILE, ctx => ctx.count < 100,
|
|
246
|
+
new ActionBuilder()
|
|
247
|
+
.do("increment", ctx => {
|
|
248
|
+
ctx.count++
|
|
249
|
+
ctx.items.push(ctx.count)
|
|
250
|
+
return ctx
|
|
251
|
+
})
|
|
252
|
+
.do("earlyExit", ACTIVITY.BREAK, ctx => ctx.count >= 5)
|
|
253
|
+
)
|
|
254
|
+
.do("finish", ctx => { return ctx.items }) // Returns [1, 2, 3, 4, 5]
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
When the BREAK predicate returns `true`, the loop terminates immediately and execution continues with the next activity after the loop.
|
|
260
|
+
|
|
261
|
+
**Important:** BREAK only works inside a nested ActionBuilder that is the operation of a WHILE or UNTIL activity. Using BREAK outside of a loop context will throw an error.
|
|
262
|
+
|
|
263
|
+
### CONTINUE Mode
|
|
264
|
+
|
|
265
|
+
Skips the remaining activities in the current loop iteration and continues to the next iteration. Like BREAK, CONTINUE must be used inside a nested ActionBuilder within a loop:
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
import { ActionBuilder, ACTIVITY } from "@gesslar/actioneer"
|
|
269
|
+
|
|
270
|
+
class ContinueExample {
|
|
271
|
+
setup(builder) {
|
|
272
|
+
builder
|
|
273
|
+
.do("initialize", ctx => {
|
|
274
|
+
ctx.count = 0
|
|
275
|
+
ctx.processed = []
|
|
276
|
+
})
|
|
277
|
+
.do("loop", ACTIVITY.WHILE, ctx => ctx.count < 5,
|
|
278
|
+
new ActionBuilder()
|
|
279
|
+
.do("increment", ctx => {
|
|
280
|
+
ctx.count++
|
|
281
|
+
return ctx
|
|
282
|
+
})
|
|
283
|
+
.do("skipEvens", ACTIVITY.CONTINUE, ctx => ctx.count % 2 === 0)
|
|
284
|
+
.do("process", ctx => {
|
|
285
|
+
ctx.processed.push(ctx.count)
|
|
286
|
+
return ctx
|
|
287
|
+
})
|
|
288
|
+
)
|
|
289
|
+
.do("finish", ctx => { return ctx.processed }) // Returns [1, 3, 5]
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
When the CONTINUE predicate returns `true`, the remaining activities in that iteration are skipped, and the loop continues with its next iteration (re-evaluating the loop predicate for WHILE, or executing the operation then evaluating for UNTIL).
|
|
295
|
+
|
|
296
|
+
**Important:** Like BREAK, CONTINUE only works inside a nested ActionBuilder within a WHILE or UNTIL loop.
|
|
297
|
+
|
|
298
|
+
### Combining Control Flow
|
|
299
|
+
|
|
300
|
+
You can combine IF, BREAK, and CONTINUE within the same loop for complex control flow:
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
class CombinedExample {
|
|
304
|
+
setup(builder) {
|
|
305
|
+
builder
|
|
306
|
+
.do("initialize", ctx => {
|
|
307
|
+
ctx.count = 0
|
|
308
|
+
ctx.results = []
|
|
309
|
+
})
|
|
310
|
+
.do("loop", ACTIVITY.WHILE, ctx => ctx.count < 100,
|
|
311
|
+
new ActionBuilder()
|
|
312
|
+
.do("increment", ctx => { ctx.count++; return ctx })
|
|
313
|
+
.do("exitAt10", ACTIVITY.BREAK, ctx => ctx.count > 10)
|
|
314
|
+
.do("skipEvens", ACTIVITY.CONTINUE, ctx => ctx.count % 2 === 0)
|
|
315
|
+
.do("processLarge", ACTIVITY.IF, ctx => ctx.count > 5, ctx => {
|
|
316
|
+
ctx.results.push(ctx.count * 10)
|
|
317
|
+
return ctx
|
|
318
|
+
})
|
|
319
|
+
.do("processAll", ctx => {
|
|
320
|
+
ctx.results.push(ctx.count)
|
|
321
|
+
return ctx
|
|
322
|
+
})
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Results: [1, 3, 5, 70, 7, 90, 9]
|
|
327
|
+
// - 1, 3, 5: odd numbers <= 5, just pushed
|
|
328
|
+
// - 7, 9: odd numbers > 5, pushed with *10 first, then pushed
|
|
329
|
+
// - evens skipped by CONTINUE
|
|
330
|
+
// - loop exits when count > 10
|
|
331
|
+
```
|
|
332
|
+
|
|
206
333
|
### SPLIT Mode
|
|
207
334
|
|
|
208
335
|
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:
|
|
@@ -301,18 +428,22 @@ class NestedParallel {
|
|
|
301
428
|
|
|
302
429
|
### Mode Constraints
|
|
303
430
|
|
|
304
|
-
- **Only one mode per activity**:
|
|
431
|
+
- **Only one mode per activity**: Each activity can have only one mode. Attempting to combine modes will throw an error
|
|
305
432
|
- **SPLIT requires both functions**: The splitter and rejoiner are both mandatory for SPLIT mode
|
|
306
|
-
- **Predicates must return boolean**:
|
|
433
|
+
- **Predicates must return boolean**: All predicates (WHILE, UNTIL, IF, BREAK, CONTINUE) should return `true` or `false`
|
|
434
|
+
- **BREAK/CONTINUE require loop context**: These modes only work inside a nested ActionBuilder within a WHILE or UNTIL loop
|
|
307
435
|
|
|
308
436
|
### Mode Summary Table
|
|
309
437
|
|
|
310
|
-
| Mode
|
|
311
|
-
|
|
|
312
|
-
| **Default**
|
|
313
|
-
| **WHILE**
|
|
314
|
-
| **UNTIL**
|
|
315
|
-
| **
|
|
438
|
+
| Mode | Signature | Predicate Timing | Use Case |
|
|
439
|
+
| ------------ | ---------------------------------------------------------- | ---------------- | ------------------------------------------- |
|
|
440
|
+
| **Default** | `.do(name, operation)` | N/A | Execute once per context |
|
|
441
|
+
| **WHILE** | `.do(name, ACTIVITY.WHILE, predicate, operation)` | Before iteration | Loop while condition is true |
|
|
442
|
+
| **UNTIL** | `.do(name, ACTIVITY.UNTIL, predicate, operation)` | After iteration | Loop until condition is true |
|
|
443
|
+
| **IF** | `.do(name, ACTIVITY.IF, predicate, operation)` | Before execution | Conditional execution (once or skip) |
|
|
444
|
+
| **BREAK** | `.do(name, ACTIVITY.BREAK, predicate)` | When reached | Exit enclosing WHILE/UNTIL loop |
|
|
445
|
+
| **CONTINUE** | `.do(name, ACTIVITY.CONTINUE, predicate)` | When reached | Skip to next iteration of enclosing loop |
|
|
446
|
+
| **SPLIT** | `.do(name, ACTIVITY.SPLIT, splitter, rejoiner, operation)` | N/A | Parallel execution with split/rejoin |
|
|
316
447
|
|
|
317
448
|
## Running Actions: `run()` vs `pipe()`
|
|
318
449
|
|
|
@@ -376,6 +507,74 @@ The `pipe()` method uses `Promise.allSettled()` internally and returns an array
|
|
|
376
507
|
|
|
377
508
|
This design ensures error handling responsibility stays at the call site - you decide how to handle failures rather than the framework deciding for you.
|
|
378
509
|
|
|
510
|
+
## Pipeline Completion: `done()`
|
|
511
|
+
|
|
512
|
+
The `done()` method registers a callback that executes after all activities in the pipeline complete, regardless of whether an error occurred. This is useful for cleanup, finalization, or returning a transformed result.
|
|
513
|
+
|
|
514
|
+
```js
|
|
515
|
+
import { ActionBuilder, ActionRunner } from "@gesslar/actioneer"
|
|
516
|
+
|
|
517
|
+
class MyAction {
|
|
518
|
+
setup(builder) {
|
|
519
|
+
builder
|
|
520
|
+
.do("step1", ctx => { ctx.a = 1 })
|
|
521
|
+
.do("step2", ctx => { ctx.b = 2 })
|
|
522
|
+
.done(ctx => {
|
|
523
|
+
// This runs after all activities complete
|
|
524
|
+
return { total: ctx.a + ctx.b }
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const builder = new ActionBuilder(new MyAction())
|
|
530
|
+
const runner = new ActionRunner(builder)
|
|
531
|
+
const result = await runner.run({})
|
|
532
|
+
console.log(result) // { total: 3 }
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Key Behaviors
|
|
536
|
+
|
|
537
|
+
- **Always executes**: The `done()` callback runs even if an earlier activity throws an error (similar to `finally` in try/catch)
|
|
538
|
+
- **Top-level only**: The callback only runs for the outermost pipeline, not for nested builders inside loops (WHILE/UNTIL). However, for SPLIT activities, `done()` runs for each split context since each is an independent execution
|
|
539
|
+
- **Transform the result**: Whatever you return from `done()` becomes the final pipeline result
|
|
540
|
+
- **Access to action context**: The callback is bound to the action instance, so `this` refers to your action class
|
|
541
|
+
- **Async support**: The callback can be async and return a Promise
|
|
542
|
+
|
|
543
|
+
### Use Cases
|
|
544
|
+
|
|
545
|
+
**Cleanup resources:**
|
|
546
|
+
|
|
547
|
+
```js
|
|
548
|
+
builder
|
|
549
|
+
.do("openConnection", ctx => { ctx.conn = openDb() })
|
|
550
|
+
.do("query", ctx => { ctx.data = ctx.conn.query("SELECT *") })
|
|
551
|
+
.done(ctx => {
|
|
552
|
+
ctx.conn.close() // Always close, even on error
|
|
553
|
+
return ctx.data
|
|
554
|
+
})
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
**Transform the final result:**
|
|
558
|
+
|
|
559
|
+
```js
|
|
560
|
+
builder
|
|
561
|
+
.do("gather", ctx => { ctx.items = [1, 2, 3] })
|
|
562
|
+
.do("process", ctx => { ctx.items = ctx.items.map(x => x * 2) })
|
|
563
|
+
.done(ctx => ctx.items) // Return just the items array, not the whole context
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Logging and metrics:**
|
|
567
|
+
|
|
568
|
+
```js
|
|
569
|
+
builder
|
|
570
|
+
.do("start", ctx => { ctx.startTime = Date.now() })
|
|
571
|
+
.do("work", ctx => { /* ... */ })
|
|
572
|
+
.done(ctx => {
|
|
573
|
+
console.log(`Pipeline completed in ${Date.now() - ctx.startTime}ms`)
|
|
574
|
+
return ctx
|
|
575
|
+
})
|
|
576
|
+
```
|
|
577
|
+
|
|
379
578
|
## ActionHooks
|
|
380
579
|
|
|
381
580
|
Actioneer supports lifecycle hooks that can execute before and after each activity in your pipeline. Hooks can be configured by file path (Node.js only) or by providing a pre-instantiated hooks object (Node.js and browser).
|
|
@@ -558,9 +757,9 @@ Run the comprehensive test suite with Node's built-in test runner:
|
|
|
558
757
|
npm test
|
|
559
758
|
```
|
|
560
759
|
|
|
561
|
-
The test suite includes
|
|
760
|
+
The test suite includes 200+ tests covering all core classes and behaviors:
|
|
562
761
|
|
|
563
|
-
- **Activity** - Activity definitions, ACTIVITY flags (WHILE, UNTIL, SPLIT), and execution
|
|
762
|
+
- **Activity** - Activity definitions, ACTIVITY flags (WHILE, UNTIL, IF, BREAK, CONTINUE, SPLIT), and execution
|
|
564
763
|
- **ActionBuilder** - Fluent builder API, activity registration, and hooks configuration
|
|
565
764
|
- **ActionWrapper** - Activity iteration and integration with ActionBuilder
|
|
566
765
|
- **ActionRunner** - Pipeline execution, loop semantics, nested builders, and error handling
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"name": "gesslar",
|
|
6
6
|
"url": "https://gesslar.dev"
|
|
7
7
|
},
|
|
8
|
-
"version": "2.0
|
|
8
|
+
"version": "2.2.0",
|
|
9
9
|
"license": "Unlicense",
|
|
10
10
|
"homepage": "https://github.com/gesslar/toolkit#readme",
|
|
11
11
|
"repository": {
|
|
@@ -49,19 +49,19 @@
|
|
|
49
49
|
"node": ">=22"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@gesslar/toolkit": "^3.
|
|
52
|
+
"@gesslar/toolkit": "^3.24.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@gesslar/uglier": "^
|
|
55
|
+
"@gesslar/uglier": "^1.2.0",
|
|
56
56
|
"eslint": "^9.39.2",
|
|
57
57
|
"typescript": "^5.9.3"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"lint": "eslint src/",
|
|
61
61
|
"lint:fix": "eslint src/ --fix",
|
|
62
|
-
"types:
|
|
62
|
+
"types": "node -e \"require('fs').rmSync('types',{recursive:true,force:true});\" && tsc -p tsconfig.types.json",
|
|
63
63
|
"submit": "pnpm publish --access public --//registry.npmjs.org/:_authToken=\"${NPM_ACCESS_TOKEN}\"",
|
|
64
|
-
"update": "pnpm
|
|
64
|
+
"update": "pnpm self-update && pnpx npm-check-updates -u && pnpm install",
|
|
65
65
|
"test": "node --test tests/**/*.test.js",
|
|
66
66
|
"test:browser": "node --test tests/browser/*.test.js",
|
|
67
67
|
"test:node": "node --test tests/node/*.test.js",
|
|
@@ -66,9 +66,14 @@ export default class ActionBuilder {
|
|
|
66
66
|
#debug = null
|
|
67
67
|
/** @type {symbol|null} */
|
|
68
68
|
#tag = null
|
|
69
|
+
/** @type {string|null} */
|
|
69
70
|
#hooksFile = null
|
|
71
|
+
/** @type {string|null} */
|
|
70
72
|
#hooksKind = null
|
|
73
|
+
/** @type {import("./ActionHooks.js").default|null} */
|
|
71
74
|
#hooks = null
|
|
75
|
+
/** @type {ActionFunction|null} */
|
|
76
|
+
#done = null
|
|
72
77
|
|
|
73
78
|
/**
|
|
74
79
|
* Creates a new ActionBuilder instance with the provided action callback.
|
|
@@ -99,9 +104,10 @@ export default class ActionBuilder {
|
|
|
99
104
|
* Register an activity that the runner can execute.
|
|
100
105
|
*
|
|
101
106
|
* Overloads:
|
|
102
|
-
* - do(name, op)
|
|
103
|
-
* - do(name, kind, pred,
|
|
104
|
-
* - do(name, kind,
|
|
107
|
+
* - do(name, op) - Simple once-off activity
|
|
108
|
+
* - do(name, kind, pred) - BREAK/CONTINUE control flow (no op, just predicate)
|
|
109
|
+
* - do(name, kind, pred, opOrWrapper) - WHILE/UNTIL/IF with predicate and operation
|
|
110
|
+
* - do(name, kind, splitter, rejoiner, opOrWrapper) - SPLIT with parallel execution
|
|
105
111
|
*
|
|
106
112
|
* @overload
|
|
107
113
|
* @param {string|symbol} name Activity name
|
|
@@ -112,9 +118,17 @@ export default class ActionBuilder {
|
|
|
112
118
|
/**
|
|
113
119
|
* @overload
|
|
114
120
|
* @param {string|symbol} name Activity name
|
|
115
|
-
* @param {number} kind
|
|
121
|
+
* @param {number} kind ACTIVITY.BREAK or ACTIVITY.CONTINUE flag.
|
|
122
|
+
* @param {(context: unknown) => boolean|Promise<boolean>} pred Predicate to evaluate for control flow.
|
|
123
|
+
* @returns {ActionBuilder}
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @overload
|
|
128
|
+
* @param {string|symbol} name Activity name
|
|
129
|
+
* @param {number} kind Activity kind (WHILE, UNTIL, or IF) from {@link ActivityFlags}.
|
|
116
130
|
* @param {(context: unknown) => boolean|Promise<boolean>} pred Predicate executed before/after the op.
|
|
117
|
-
* @param {ActionFunction|
|
|
131
|
+
* @param {ActionFunction|ActionBuilder} op Operation or nested builder to execute.
|
|
118
132
|
* @returns {ActionBuilder}
|
|
119
133
|
*/
|
|
120
134
|
|
|
@@ -124,7 +138,7 @@ export default class ActionBuilder {
|
|
|
124
138
|
* @param {number} kind ACTIVITY.SPLIT flag.
|
|
125
139
|
* @param {(context: unknown) => unknown} splitter Splitter function for SPLIT mode.
|
|
126
140
|
* @param {(originalContext: unknown, splitResults: unknown) => unknown} rejoiner Rejoiner function for SPLIT mode.
|
|
127
|
-
* @param {ActionFunction|
|
|
141
|
+
* @param {ActionFunction|ActionBuilder} op Operation or nested builder to execute.
|
|
128
142
|
* @returns {ActionBuilder}
|
|
129
143
|
*/
|
|
130
144
|
|
|
@@ -141,6 +155,7 @@ export default class ActionBuilder {
|
|
|
141
155
|
// signatures
|
|
142
156
|
// name, [function] => once
|
|
143
157
|
// name, [number,function,function] => some kind of control operation (WHILE/UNTIL)
|
|
158
|
+
// name, [number,function] => some kind of control operation (BREAK/CONTINUE)
|
|
144
159
|
// name, [number,function,function,function] => SPLIT operation with splitter/rejoiner
|
|
145
160
|
// name, [number,function,ActionBuilder] => some kind of branch
|
|
146
161
|
|
|
@@ -154,6 +169,13 @@ export default class ActionBuilder {
|
|
|
154
169
|
Valid.type(op, "Function")
|
|
155
170
|
|
|
156
171
|
Object.assign(activityDefinition, {op, kind})
|
|
172
|
+
} else if(args.length === 2) {
|
|
173
|
+
const [kind, pred] = args
|
|
174
|
+
Valid.type(kind, "Number")
|
|
175
|
+
Valid.type(pred, "Function")
|
|
176
|
+
Valid.assert(kind === ACTIVITY.BREAK || kind === ACTIVITY.CONTINUE, "Invalid arguments for BREAK/CONTINUE.")
|
|
177
|
+
|
|
178
|
+
Object.assign(activityDefinition, {kind, pred})
|
|
157
179
|
} else if(args.length === 3) {
|
|
158
180
|
const [kind, pred, op] = args
|
|
159
181
|
|
|
@@ -171,7 +193,7 @@ export default class ActionBuilder {
|
|
|
171
193
|
Valid.type(op, "Function|ActionBuilder")
|
|
172
194
|
|
|
173
195
|
// Validate that kind is SPLIT
|
|
174
|
-
if(
|
|
196
|
+
if(kind !== ACTIVITY.SPLIT)
|
|
175
197
|
throw Sass.new("4-argument form of 'do' is only valid for ACTIVITY.SPLIT")
|
|
176
198
|
|
|
177
199
|
Object.assign(activityDefinition, {kind, splitter, rejoiner, op})
|
|
@@ -246,6 +268,19 @@ export default class ActionBuilder {
|
|
|
246
268
|
return this
|
|
247
269
|
}
|
|
248
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Register a callback to be executed after all activities complete.
|
|
273
|
+
*
|
|
274
|
+
* @param {ActionFunction} callback Function to execute at the end of the pipeline.
|
|
275
|
+
* @returns {ActionBuilder} The builder instance for chaining.
|
|
276
|
+
*/
|
|
277
|
+
done(callback) {
|
|
278
|
+
Valid.type(callback, "Function")
|
|
279
|
+
this.#done = callback
|
|
280
|
+
|
|
281
|
+
return this
|
|
282
|
+
}
|
|
283
|
+
|
|
249
284
|
/**
|
|
250
285
|
* Validates that an activity name has not been reused.
|
|
251
286
|
*
|
|
@@ -281,6 +316,8 @@ export default class ActionBuilder {
|
|
|
281
316
|
activities: this.#activities,
|
|
282
317
|
debug: this.#debug,
|
|
283
318
|
hooks,
|
|
319
|
+
done: this.#done,
|
|
320
|
+
action: this.#action,
|
|
284
321
|
})
|
|
285
322
|
}
|
|
286
323
|
|