@gesslar/actioneer 2.1.0 → 2.3.1
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 +4 -4
- package/src/browser/lib/ActionBuilder.js +91 -59
- package/src/browser/lib/ActionHooks.js +19 -21
- package/src/browser/lib/ActionRunner.js +104 -65
- package/src/browser/lib/ActionWrapper.js +37 -12
- package/src/browser/lib/Activity.js +52 -17
- package/src/browser/lib/Piper.js +37 -17
- package/src/types/browser/lib/ActionBuilder.d.ts +82 -60
- package/src/types/browser/lib/ActionBuilder.d.ts.map +1 -1
- package/src/types/browser/lib/ActionHooks.d.ts +24 -25
- package/src/types/browser/lib/ActionHooks.d.ts.map +1 -1
- package/src/types/browser/lib/ActionRunner.d.ts +10 -7
- package/src/types/browser/lib/ActionRunner.d.ts.map +1 -1
- package/src/types/browser/lib/ActionWrapper.d.ts +22 -7
- package/src/types/browser/lib/ActionWrapper.d.ts.map +1 -1
- package/src/types/browser/lib/Activity.d.ts +45 -18
- package/src/types/browser/lib/Activity.d.ts.map +1 -1
- package/src/types/browser/lib/Piper.d.ts +7 -5
- package/src/types/browser/lib/Piper.d.ts.map +1 -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.1
|
|
8
|
+
"version": "2.3.1",
|
|
9
9
|
"license": "Unlicense",
|
|
10
10
|
"homepage": "https://github.com/gesslar/toolkit#readme",
|
|
11
11
|
"repository": {
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"node": ">=22"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@gesslar/toolkit": "^3.
|
|
52
|
+
"@gesslar/toolkit": "^3.34.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@gesslar/uglier": "^1.1
|
|
56
|
-
"eslint": "^
|
|
55
|
+
"@gesslar/uglier": "^1.4.1",
|
|
56
|
+
"eslint": "^10.0.0",
|
|
57
57
|
"typescript": "^5.9.3"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
@@ -4,36 +4,29 @@ import ActionWrapper from "./ActionWrapper.js"
|
|
|
4
4
|
import ActionHooks from "./ActionHooks.js"
|
|
5
5
|
import {ACTIVITY} from "./Activity.js"
|
|
6
6
|
|
|
7
|
-
/** @typedef {import("./ActionRunner.js").default} ActionRunner */
|
|
8
|
-
/** @typedef {typeof import("./Activity.js").ACTIVITY} ActivityFlags */
|
|
9
|
-
|
|
10
7
|
/**
|
|
8
|
+
* Type imports and definitions.
|
|
9
|
+
*
|
|
10
|
+
* @import {default as ActionRunner} from "./ActionRunner.js"
|
|
11
|
+
*
|
|
11
12
|
* @typedef {(message: string, level?: number, ...args: Array<unknown>) => void} DebugFn
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
13
|
+
*
|
|
15
14
|
* @typedef {object} ActionBuilderAction
|
|
16
|
-
* @property {(builder: ActionBuilder) => void} setup Function invoked during {@link ActionBuilder
|
|
17
|
-
* @property {symbol} [tag] Optional tag to reuse when reconstructing builders.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/**
|
|
15
|
+
* @property {(builder: ActionBuilder) => void} setup - Function invoked during {@link ActionBuilder} to register activities.
|
|
16
|
+
* @property {symbol} [tag] - Optional tag to reuse when reconstructing builders.
|
|
17
|
+
*
|
|
21
18
|
* @typedef {object} ActionBuilderConfig
|
|
22
|
-
* @property {symbol} [tag] Optional tag for the builder instance.
|
|
23
|
-
* @property {DebugFn} [debug] Logger used by the pipeline internals.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
/**
|
|
19
|
+
* @property {symbol} [tag] - Optional tag for the builder instance.
|
|
20
|
+
* @property {DebugFn} [debug] - Logger used by the pipeline internals.
|
|
21
|
+
*
|
|
27
22
|
* @typedef {object} ActivityDefinition
|
|
28
|
-
* @property {ActionBuilderAction|null} action Parent action instance when available.
|
|
29
|
-
* @property {DebugFn|null} debug Logger function.
|
|
30
|
-
* @property {string|symbol} name Activity identifier.
|
|
31
|
-
* @property {ActionFunction|import("./ActionWrapper.js").default} op Operation to execute.
|
|
32
|
-
* @property {number} [kind] Optional kind flags from {@link
|
|
33
|
-
* @property {(context: unknown) => boolean|Promise<boolean>} [pred] Loop predicate.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/**
|
|
23
|
+
* @property {ActionBuilderAction|null} action - Parent action instance when available.
|
|
24
|
+
* @property {DebugFn|null} debug - Logger function.
|
|
25
|
+
* @property {string|symbol} name - Activity identifier.
|
|
26
|
+
* @property {ActionFunction|import("./ActionWrapper.js").default} op - Operation to execute.
|
|
27
|
+
* @property {number} [kind] - Optional kind flags from {@link ACTIVITY}.
|
|
28
|
+
* @property {(context: unknown) => boolean|Promise<boolean>} [pred] - Loop predicate.
|
|
29
|
+
*
|
|
37
30
|
* @typedef {(context: unknown) => unknown|Promise<unknown>} ActionFunction
|
|
38
31
|
*/
|
|
39
32
|
|
|
@@ -58,24 +51,28 @@ import {ACTIVITY} from "./Activity.js"
|
|
|
58
51
|
* @class ActionBuilder
|
|
59
52
|
*/
|
|
60
53
|
export default class ActionBuilder {
|
|
61
|
-
/** @type {ActionBuilderAction
|
|
54
|
+
/** @type {ActionBuilderAction?} */
|
|
62
55
|
#action = null
|
|
63
56
|
/** @type {Map<string|symbol, ActivityDefinition>} */
|
|
64
57
|
#activities = new Map([])
|
|
65
|
-
/** @type {DebugFn
|
|
58
|
+
/** @type {DebugFn?} */
|
|
66
59
|
#debug = null
|
|
67
|
-
/** @type {symbol
|
|
60
|
+
/** @type {symbol?} */
|
|
68
61
|
#tag = null
|
|
62
|
+
/** @type {string?} */
|
|
69
63
|
#hooksFile = null
|
|
64
|
+
/** @type {string?} */
|
|
70
65
|
#hooksKind = null
|
|
66
|
+
/** @type {ActionHooks?} */
|
|
71
67
|
#hooks = null
|
|
68
|
+
/** @type {ActionFunction?} */
|
|
72
69
|
#done = null
|
|
73
70
|
|
|
74
71
|
/**
|
|
75
72
|
* Creates a new ActionBuilder instance with the provided action callback.
|
|
76
73
|
*
|
|
77
|
-
* @param {ActionBuilderAction} [action] Base action invoked by the runner when a block satisfies the configured structure.
|
|
78
|
-
* @param {ActionBuilderConfig} [config] Options
|
|
74
|
+
* @param {ActionBuilderAction} [action] - Base action invoked by the runner when a block satisfies the configured structure.
|
|
75
|
+
* @param {ActionBuilderConfig} [config] - Options
|
|
79
76
|
*/
|
|
80
77
|
constructor(
|
|
81
78
|
action,
|
|
@@ -85,6 +82,9 @@ export default class ActionBuilder {
|
|
|
85
82
|
this.#tag = this.#tag || tag
|
|
86
83
|
|
|
87
84
|
if(action) {
|
|
85
|
+
if(action.tag)
|
|
86
|
+
throw Sass.new("Action has already been consumed by a builder and cannot be reused.")
|
|
87
|
+
|
|
88
88
|
if(Data.typeOf(action.setup) !== "Function")
|
|
89
89
|
throw Sass.new("Setup must be a function.")
|
|
90
90
|
|
|
@@ -100,41 +100,50 @@ export default class ActionBuilder {
|
|
|
100
100
|
* Register an activity that the runner can execute.
|
|
101
101
|
*
|
|
102
102
|
* Overloads:
|
|
103
|
-
* - do(name, op)
|
|
104
|
-
* - do(name, kind, pred,
|
|
105
|
-
* - do(name, kind,
|
|
103
|
+
* - do(name, op) - Simple once-off activity
|
|
104
|
+
* - do(name, kind, pred) - BREAK/CONTINUE control flow (no op, just predicate)
|
|
105
|
+
* - do(name, kind, pred, opOrWrapper) - WHILE/UNTIL/IF with predicate and operation
|
|
106
|
+
* - do(name, kind, splitter, rejoiner, opOrWrapper) - SPLIT with parallel execution
|
|
106
107
|
*
|
|
107
108
|
* @overload
|
|
108
|
-
* @param {string|symbol} name Activity name
|
|
109
|
-
* @param {ActionFunction} op Operation to execute once.
|
|
109
|
+
* @param {string|symbol} name - Activity name
|
|
110
|
+
* @param {ActionFunction} op - Operation to execute once.
|
|
110
111
|
* @returns {ActionBuilder}
|
|
111
112
|
*/
|
|
112
113
|
|
|
113
114
|
/**
|
|
114
115
|
* @overload
|
|
115
|
-
* @param {string|symbol} name Activity name
|
|
116
|
-
* @param {number} kind
|
|
117
|
-
* @param {(context: unknown) => boolean|Promise<boolean>} pred Predicate
|
|
118
|
-
* @param {ActionFunction|import("./ActionWrapper.js").default} op Operation or nested wrapper to execute.
|
|
116
|
+
* @param {string|symbol} name - Activity name
|
|
117
|
+
* @param {number} kind - ACTIVITY.BREAK or ACTIVITY.CONTINUE flag.
|
|
118
|
+
* @param {(context: unknown) => boolean|Promise<boolean>} pred - Predicate to evaluate for control flow.
|
|
119
119
|
* @returns {ActionBuilder}
|
|
120
120
|
*/
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* @overload
|
|
124
|
-
* @param {string|symbol} name Activity name
|
|
125
|
-
* @param {number} kind ACTIVITY.
|
|
126
|
-
* @param {(context: unknown) =>
|
|
127
|
-
* @param {
|
|
128
|
-
* @
|
|
124
|
+
* @param {string|symbol} name - Activity name
|
|
125
|
+
* @param {number} kind - Activity kind (WHILE, UNTIL, or IF) from {@link ACTIVITY}.
|
|
126
|
+
* @param {(context: unknown) => boolean|Promise<boolean>} pred - Predicate executed before/after the op.
|
|
127
|
+
* @param {ActionFunction|ActionBuilder} op - Operation or nested builder to execute.
|
|
128
|
+
* @returns {ActionBuilder}
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @overload
|
|
133
|
+
* @param {string|symbol} name - Activity name
|
|
134
|
+
* @param {number} kind - ACTIVITY.SPLIT flag.
|
|
135
|
+
* @param {(context: unknown) => unknown} splitter - Splitter function for SPLIT mode.
|
|
136
|
+
* @param {(originalContext: unknown, splitResults: unknown) => unknown} rejoiner - Rejoiner function for SPLIT mode.
|
|
137
|
+
* @param {ActionFunction|ActionBuilder} op - Operation or nested builder to execute.
|
|
129
138
|
* @returns {ActionBuilder}
|
|
130
139
|
*/
|
|
131
140
|
|
|
132
141
|
/**
|
|
133
142
|
* Handles runtime dispatch across the documented overloads.
|
|
134
143
|
*
|
|
135
|
-
* @param {string|symbol} name Activity name
|
|
136
|
-
* @param {...unknown} args See overloads
|
|
137
|
-
* @returns {ActionBuilder} The builder instance for chaining
|
|
144
|
+
* @param {string|symbol} name - Activity name
|
|
145
|
+
* @param {...unknown} args - See overloads
|
|
146
|
+
* @returns {ActionBuilder} - The builder instance for chaining
|
|
138
147
|
*/
|
|
139
148
|
do(name, ...args) {
|
|
140
149
|
this.#dupeActivityCheck(name)
|
|
@@ -142,6 +151,7 @@ export default class ActionBuilder {
|
|
|
142
151
|
// signatures
|
|
143
152
|
// name, [function] => once
|
|
144
153
|
// name, [number,function,function] => some kind of control operation (WHILE/UNTIL)
|
|
154
|
+
// name, [number,function] => some kind of control operation (BREAK/CONTINUE)
|
|
145
155
|
// name, [number,function,function,function] => SPLIT operation with splitter/rejoiner
|
|
146
156
|
// name, [number,function,ActionBuilder] => some kind of branch
|
|
147
157
|
|
|
@@ -155,6 +165,13 @@ export default class ActionBuilder {
|
|
|
155
165
|
Valid.type(op, "Function")
|
|
156
166
|
|
|
157
167
|
Object.assign(activityDefinition, {op, kind})
|
|
168
|
+
} else if(args.length === 2) {
|
|
169
|
+
const [kind, pred] = args
|
|
170
|
+
Valid.type(kind, "Number")
|
|
171
|
+
Valid.type(pred, "Function")
|
|
172
|
+
Valid.assert(kind === ACTIVITY.BREAK || kind === ACTIVITY.CONTINUE, "Invalid arguments for BREAK/CONTINUE.")
|
|
173
|
+
|
|
174
|
+
Object.assign(activityDefinition, {kind, pred})
|
|
158
175
|
} else if(args.length === 3) {
|
|
159
176
|
const [kind, pred, op] = args
|
|
160
177
|
|
|
@@ -172,7 +189,7 @@ export default class ActionBuilder {
|
|
|
172
189
|
Valid.type(op, "Function|ActionBuilder")
|
|
173
190
|
|
|
174
191
|
// Validate that kind is SPLIT
|
|
175
|
-
if(
|
|
192
|
+
if(kind !== ACTIVITY.SPLIT)
|
|
176
193
|
throw Sass.new("4-argument form of 'do' is only valid for ACTIVITY.SPLIT")
|
|
177
194
|
|
|
178
195
|
Object.assign(activityDefinition, {kind, splitter, rejoiner, op})
|
|
@@ -188,9 +205,9 @@ export default class ActionBuilder {
|
|
|
188
205
|
/**
|
|
189
206
|
* Configure hooks to be loaded from a file when the action is built.
|
|
190
207
|
*
|
|
191
|
-
* @param {string} hooksFile Path to the hooks module file.
|
|
192
|
-
* @param {string} hooksKind Name of the exported hooks class to instantiate.
|
|
193
|
-
* @returns {ActionBuilder} The builder instance for chaining.
|
|
208
|
+
* @param {string} hooksFile - Path to the hooks module file.
|
|
209
|
+
* @param {string} hooksKind - Name of the exported hooks class to instantiate.
|
|
210
|
+
* @returns {ActionBuilder} - The builder instance for chaining.
|
|
194
211
|
* @throws {Sass} If hooks have already been configured.
|
|
195
212
|
*/
|
|
196
213
|
withHooksFile(hooksFile, hooksKind) {
|
|
@@ -207,8 +224,8 @@ export default class ActionBuilder {
|
|
|
207
224
|
/**
|
|
208
225
|
* Configure hooks using a pre-instantiated hooks object.
|
|
209
226
|
*
|
|
210
|
-
* @param {
|
|
211
|
-
* @returns {ActionBuilder} The builder instance for chaining.
|
|
227
|
+
* @param {ActionHooks} hooks - An already-instantiated hooks instance.
|
|
228
|
+
* @returns {ActionBuilder} - The builder instance for chaining.
|
|
212
229
|
* @throws {Sass} If hooks have already been configured with a different instance.
|
|
213
230
|
*/
|
|
214
231
|
withHooks(hooks) {
|
|
@@ -230,7 +247,7 @@ export default class ActionBuilder {
|
|
|
230
247
|
* Configure the action instance if not already set.
|
|
231
248
|
* Used to propagate parent action context to nested builders.
|
|
232
249
|
*
|
|
233
|
-
* @param {ActionBuilderAction} action The action instance to inherit.
|
|
250
|
+
* @param {ActionBuilderAction} action - The action instance to inherit.
|
|
234
251
|
* @returns {ActionBuilder} The builder instance for chaining.
|
|
235
252
|
*/
|
|
236
253
|
withAction(action) {
|
|
@@ -250,7 +267,7 @@ export default class ActionBuilder {
|
|
|
250
267
|
/**
|
|
251
268
|
* Register a callback to be executed after all activities complete.
|
|
252
269
|
*
|
|
253
|
-
* @param {ActionFunction} callback Function to execute at the end of the pipeline.
|
|
270
|
+
* @param {ActionFunction} callback - Function to execute at the end of the pipeline.
|
|
254
271
|
* @returns {ActionBuilder} The builder instance for chaining.
|
|
255
272
|
*/
|
|
256
273
|
done(callback) {
|
|
@@ -264,7 +281,7 @@ export default class ActionBuilder {
|
|
|
264
281
|
* Validates that an activity name has not been reused.
|
|
265
282
|
*
|
|
266
283
|
* @private
|
|
267
|
-
* @param {string
|
|
284
|
+
* @param {string|symbol} name Activity identifier.
|
|
268
285
|
*/
|
|
269
286
|
#dupeActivityCheck(name) {
|
|
270
287
|
Valid.assert(
|
|
@@ -277,9 +294,9 @@ export default class ActionBuilder {
|
|
|
277
294
|
* Finalises the builder and returns a payload that can be consumed by the
|
|
278
295
|
* runner.
|
|
279
296
|
*
|
|
280
|
-
* @returns {Promise<
|
|
297
|
+
* @returns {Promise<ActionWrapper>} Payload consumed by the {@link ActionRunner} constructor.
|
|
281
298
|
*/
|
|
282
|
-
async build() {
|
|
299
|
+
async build(runner) {
|
|
283
300
|
const action = this.#action
|
|
284
301
|
|
|
285
302
|
if(action && !action.tag) {
|
|
@@ -288,6 +305,20 @@ export default class ActionBuilder {
|
|
|
288
305
|
await Promise.resolve(action.setup.call(action, this))
|
|
289
306
|
}
|
|
290
307
|
|
|
308
|
+
if(action) {
|
|
309
|
+
// Inject a method to the action for emission, but only if it's undefined.
|
|
310
|
+
if(Data.isType(action.emit, "Undefined"))
|
|
311
|
+
action.emit = (...args) => runner.emit(...args)
|
|
312
|
+
|
|
313
|
+
// Inject a method to the action for onission, but only if it's undefined.
|
|
314
|
+
if(Data.isType(action.on, "Undefined"))
|
|
315
|
+
action.on = (event, cb) => runner.on(event, cb)
|
|
316
|
+
|
|
317
|
+
// Inject a method to the action for offission, but only if it's undefined.
|
|
318
|
+
if(Data.isType(action.off, "Undefined"))
|
|
319
|
+
action.off = (event, cb) => runner.off(event, cb)
|
|
320
|
+
}
|
|
321
|
+
|
|
291
322
|
// All children in a branch also get the same hooks.
|
|
292
323
|
const hooks = await this.#getHooks()
|
|
293
324
|
|
|
@@ -296,7 +327,8 @@ export default class ActionBuilder {
|
|
|
296
327
|
debug: this.#debug,
|
|
297
328
|
hooks,
|
|
298
329
|
done: this.#done,
|
|
299
|
-
action
|
|
330
|
+
action,
|
|
331
|
+
runner: runner,
|
|
300
332
|
})
|
|
301
333
|
}
|
|
302
334
|
|