@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 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 simple loop semantics (while/until). The project is written as ES modules and targets Node 20+ and modern browsers.
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 four distinct execution modes for activities, allowing you to control how operations are executed:
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**: 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!"`
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**: For WHILE and UNTIL modes, predicates should return `true` or `false`
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 | Signature | Predicate Timing | Use Case |
311
- | ----------- | ---------------------------------------------------------- | ---------------- | ------------------------------------ |
312
- | **Default** | `.do(name, operation)` | N/A | Execute once per context |
313
- | **WHILE** | `.do(name, ACTIVITY.WHILE, predicate, operation)` | Before iteration | Loop while condition is true |
314
- | **UNTIL** | `.do(name, ACTIVITY.UNTIL, predicate, operation)` | After iteration | Loop until condition is true |
315
- | **SPLIT** | `.do(name, ACTIVITY.SPLIT, splitter, rejoiner, operation)` | N/A | Parallel execution with split/rejoin |
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 150+ tests covering all core classes and behaviors:
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.0",
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.23.0"
52
+ "@gesslar/toolkit": "^3.34.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@gesslar/uglier": "^1.1.0",
56
- "eslint": "^9.39.2",
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#build} to register activities.
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 ActivityFlags}.
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|null} */
54
+ /** @type {ActionBuilderAction?} */
62
55
  #action = null
63
56
  /** @type {Map<string|symbol, ActivityDefinition>} */
64
57
  #activities = new Map([])
65
- /** @type {DebugFn|null} */
58
+ /** @type {DebugFn?} */
66
59
  #debug = null
67
- /** @type {symbol|null} */
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, opOrWrapper)
105
- * - do(name, kind, splitter, rejoiner, opOrWrapper)
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 Kind bitfield from {@link ActivityFlags}.
117
- * @param {(context: unknown) => boolean|Promise<boolean>} pred Predicate executed before/after the op.
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.SPLIT flag.
126
- * @param {(context: unknown) => unknown} splitter Splitter function for SPLIT mode.
127
- * @param {(originalContext: unknown, splitResults: unknown) => unknown} rejoiner Rejoiner function for SPLIT mode.
128
- * @param {ActionFunction|import("./ActionWrapper.js").default} op Operation or nested wrapper to execute.
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((kind & ACTIVITY.SPLIT) !== ACTIVITY.SPLIT)
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 {import("./ActionHooks.js").default} hooks An already-instantiated hooks instance.
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 | symbol} name Activity identifier.
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<import("./ActionWrapper.js").default>} Payload consumed by the {@link ActionRunner} constructor.
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: this.#action,
330
+ action,
331
+ runner: runner,
300
332
  })
301
333
  }
302
334