@gesslar/actioneer 0.2.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/actioneer",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Ready? Set?? ACTION!! pew! pew! pew!",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -30,6 +30,8 @@ import ActionHooks from "./ActionHooks.js"
30
30
  * @property {ActionFunction|import("./ActionWrapper.js").default} op Operation to execute.
31
31
  * @property {number} [kind] Optional kind flags from {@link ActivityFlags}.
32
32
  * @property {(context: unknown) => boolean|Promise<boolean>} [pred] Loop predicate.
33
+ * @property {(context: unknown) => unknown} [splitter] Function to split context for parallel execution (SPLIT activities).
34
+ * @property {(originalContext: unknown, splitResults: unknown) => unknown} [rejoiner] Function to rejoin split results (SPLIT activities).
33
35
  */
34
36
 
35
37
  /**
@@ -65,15 +67,29 @@ export default class ActionBuilder {
65
67
  #debug = null
66
68
  /** @type {symbol|null} */
67
69
  #tag = null
70
+ /** @type {string|null} */
68
71
  #hooksFile = null
72
+ /** @type {string|null} */
69
73
  #hooksKind = null
74
+ /** @type {unknown|null} */
70
75
  #hooks = null
76
+ /** @type {import("./ActionHooks.js").default|null} */
77
+ #actionHooks = null
78
+
79
+ /**
80
+ * Get the builder's tag symbol.
81
+ *
82
+ * @returns {symbol|null} The tag symbol for this builder instance
83
+ */
84
+ get tag() {
85
+ return this.#tag
86
+ }
71
87
 
72
88
  /**
73
89
  * Creates a new ActionBuilder instance with the provided action callback.
74
90
  *
75
- * @param {ActionBuilderAction} [action] Base action invoked by the runner when a block satisfies the configured structure.
76
- * @param {ActionBuilderConfig} [config] Options
91
+ * @param {ActionBuilderAction} [action] - Base action invoked by the runner when a block satisfies the configured structure.
92
+ * @param {ActionBuilderConfig} [config] - Options
77
93
  */
78
94
  constructor(
79
95
  action,
@@ -112,6 +128,16 @@ export default class ActionBuilder {
112
128
  * @returns {ActionBuilder}
113
129
  */
114
130
 
131
+ /**
132
+ * @overload
133
+ * @param {string|symbol} name Activity name
134
+ * @param {number} kind Kind bitfield (ACTIVITY.SPLIT).
135
+ * @param {(context: unknown) => unknown} splitter Function to split context for parallel execution.
136
+ * @param {(originalContext: unknown, splitResults: unknown) => unknown} rejoiner Function to rejoin split results with original context.
137
+ * @param {ActionFunction|ActionBuilder} op Operation or nested ActionBuilder to execute on split context.
138
+ * @returns {ActionBuilder}
139
+ */
140
+
115
141
  /**
116
142
  * Handles runtime dispatch across the documented overloads.
117
143
  *
@@ -129,22 +155,32 @@ export default class ActionBuilder {
129
155
 
130
156
  const action = this.#action
131
157
  const debug = this.#debug
132
- const activityDefinition = {name, action, debug}
158
+ const activityDefinition = {name,action,debug}
133
159
 
134
160
  if(args.length === 1) {
135
- const [op, kind] = args
161
+ const [op,kind] = args
136
162
  Valid.type(kind, "Number|undefined")
137
163
  Valid.type(op, "Function")
138
164
 
139
- Object.assign(activityDefinition, {op, kind})
165
+ Object.assign(activityDefinition, {op,kind})
140
166
  } else if(args.length === 3) {
141
- const [kind, pred, op] = args
167
+ const [kind,pred,op] = args
142
168
 
143
169
  Valid.type(kind, "Number")
144
170
  Valid.type(pred, "Function")
145
171
  Valid.type(op, "Function|ActionBuilder")
146
172
 
147
- Object.assign(activityDefinition, {kind, pred, op})
173
+ Object.assign(activityDefinition, {kind,pred,op})
174
+ } else if(args.length === 4) {
175
+ const [kind,splitter,rejoiner,op] = args
176
+
177
+ Valid.type(kind, "Number")
178
+ Valid.type(splitter, "Function")
179
+ Valid.type(rejoiner, "Function")
180
+ Valid.type(op, "Function|ActionBuilder")
181
+
182
+ Object.assign(activityDefinition, {kind,splitter,rejoiner,op})
183
+
148
184
  } else {
149
185
  throw Sass.new("Invalid number of arguments passed to 'do'")
150
186
  }
@@ -163,9 +199,7 @@ export default class ActionBuilder {
163
199
  * @throws {Sass} If hooks have already been configured.
164
200
  */
165
201
  withHooksFile(hooksFile, hooksKind) {
166
- Valid.assert(this.#hooksFile === null, "Hooks have already been configured.")
167
- Valid.assert(this.#hooksKind === null, "Hooks have already been configured.")
168
- Valid.assert(this.#hooks === null, "Hooks have already been configured.")
202
+ Valid.assert(this.#exclusiveHooksCheck(), "Hooks have already been configured.")
169
203
 
170
204
  this.#hooksFile = hooksFile
171
205
  this.#hooksKind = hooksKind
@@ -181,15 +215,40 @@ export default class ActionBuilder {
181
215
  * @throws {Sass} If hooks have already been configured.
182
216
  */
183
217
  withHooks(hooks) {
184
- Valid.assert(this.#hooksFile === null, "Hooks have already been configured.")
185
- Valid.assert(this.#hooksKind === null, "Hooks have already been configured.")
186
- Valid.assert(this.#hooks === null, "Hooks have already been configured.")
218
+ Valid.assert(this.#exclusiveHooksCheck(), "Hooks have already been configured.")
187
219
 
188
220
  this.#hooks = hooks
189
221
 
190
222
  return this
191
223
  }
192
224
 
225
+ /**
226
+ * Configure hooks using an ActionHooks instance directly (typically used internally).
227
+ *
228
+ * @param {import("./ActionHooks.js").default} actionHooks Pre-configured ActionHooks instance.
229
+ * @returns {ActionBuilder} The builder instance for chaining.
230
+ * @throws {Sass} If hooks have already been configured.
231
+ */
232
+ withActionHooks(actionHooks) {
233
+ Valid.assert(this.#exclusiveHooksCheck(), "Hooks have already been configured.")
234
+
235
+ this.#actionHooks = actionHooks
236
+
237
+ return this
238
+ }
239
+
240
+ /**
241
+ * Ensures only one hooks configuration method is used at a time.
242
+ *
243
+ * @returns {boolean} True if no hooks have been configured yet, false otherwise.
244
+ * @private
245
+ */
246
+ #exclusiveHooksCheck() {
247
+ return !!(this.#hooksFile && this.#hooksKind) +
248
+ !!(this.#hooks) +
249
+ !!(this.#actionHooks) === 0
250
+ }
251
+
193
252
  /**
194
253
  * Validates that an activity name has not been reused.
195
254
  *
@@ -211,6 +270,8 @@ export default class ActionBuilder {
211
270
  */
212
271
  async build() {
213
272
  const action = this.#action
273
+ const activities = this.#activities
274
+ const debug = this.#debug
214
275
 
215
276
  if(!action.tag) {
216
277
  action.tag = this.#tag
@@ -221,24 +282,47 @@ export default class ActionBuilder {
221
282
  // All children in a branch also get the same hooks.
222
283
  const hooks = await this.#getHooks()
223
284
 
224
- return new ActionWrapper({
225
- activities: this.#activities,
226
- debug: this.#debug,
227
- hooks,
228
- })
285
+ return new ActionWrapper({activities,hooks,debug})
286
+ }
287
+
288
+ /**
289
+ * Check if this builder has ActionHooks configured.
290
+ *
291
+ * @returns {boolean} True if ActionHooks have been configured.
292
+ */
293
+ get hasActionHooks() {
294
+ return this.#actionHooks !== null
229
295
  }
230
296
 
297
+ /**
298
+ * Internal method to retrieve or create ActionHooks instance.
299
+ * Caches the hooks instance to avoid redundant instantiation.
300
+ *
301
+ * @returns {Promise<import("./ActionHooks.js").default|undefined>} The ActionHooks instance if configured.
302
+ * @private
303
+ */
231
304
  async #getHooks() {
305
+ if(this.#actionHooks) {
306
+ return this.#actionHooks
307
+ }
308
+
232
309
  const newHooks = ActionHooks.new
233
310
 
234
311
  const hooks = this.#hooks
235
- if(hooks)
236
- return await newHooks({hooks}, this.#debug)
312
+
313
+ if(hooks) {
314
+ this.#actionHooks = await newHooks({hooks}, this.#debug)
315
+
316
+ return this.#actionHooks
317
+ }
237
318
 
238
319
  const hooksFile = this.#hooksFile
239
320
  const hooksKind = this.#hooksKind
240
321
 
241
- if(hooksFile && hooksKind)
242
- return await newHooks({hooksFile,hooksKind}, this.#debug)
322
+ if(hooksFile && hooksKind) {
323
+ this.#actionHooks = await newHooks({hooksFile,hooksKind}, this.#debug)
324
+
325
+ return this.#actionHooks
326
+ }
243
327
  }
244
328
  }
@@ -7,11 +7,10 @@ import {Data, FileObject, Sass, Util, Valid} from "@gesslar/toolkit"
7
7
 
8
8
  /**
9
9
  * @typedef {object} ActionHooksConfig
10
- * @property {string} actionKind Action identifier shared between runner and hooks.
11
- * @property {FileObject} hooksFile File handle used to import the hooks module.
12
- * @property {unknown} [hooks] Already-instantiated hooks implementation (skips loading).
10
+ * @property {string} [actionKind] Action identifier shared between runner and hooks.
11
+ * @property {FileObject|string} [hooksFile] File handle or path used to import the hooks module.
12
+ * @property {unknown} [hooksObject] Already-instantiated hooks implementation (skips loading).
13
13
  * @property {number} [hookTimeout] Timeout applied to hook execution in milliseconds.
14
- * @property {DebugFn} debug Logger to emit diagnostics.
15
14
  */
16
15
 
17
16
  /**
@@ -27,7 +26,7 @@ export default class ActionHooks {
27
26
  /** @type {FileObject|null} */
28
27
  #hooksFile = null
29
28
  /** @type {HookModule|null} */
30
- #hooks = null
29
+ #hooksObject = null
31
30
  /** @type {string|null} */
32
31
  #actionKind = null
33
32
  /** @type {number} */
@@ -39,11 +38,15 @@ export default class ActionHooks {
39
38
  * Creates a new ActionHook instance.
40
39
  *
41
40
  * @param {ActionHooksConfig} config Configuration values describing how to load the hooks.
41
+ * @param {(message: string, level?: number, ...args: Array<unknown>) => void} debug Debug function
42
42
  */
43
- constructor({actionKind, hooksFile, hooks, hookTimeout = 1_000, debug}) {
43
+ constructor(
44
+ {actionKind, hooksFile, hooksObject, hookTimeout = 1_000},
45
+ debug,
46
+ ) {
44
47
  this.#actionKind = actionKind
45
48
  this.#hooksFile = hooksFile
46
- this.#hooks = hooks
49
+ this.#hooksObject = hooksObject
47
50
  this.#timeout = hookTimeout
48
51
  this.#debug = debug
49
52
  }
@@ -51,7 +54,7 @@ export default class ActionHooks {
51
54
  /**
52
55
  * Gets the action identifier.
53
56
  *
54
- * @returns {string} Action identifier or instance
57
+ * @returns {string|null} Action identifier or instance
55
58
  */
56
59
  get actionKind() {
57
60
  return this.#actionKind
@@ -60,7 +63,7 @@ export default class ActionHooks {
60
63
  /**
61
64
  * Gets the hooks file object.
62
65
  *
63
- * @returns {FileObject} File object containing hooks
66
+ * @returns {FileObject|null} File object containing hooks
64
67
  */
65
68
  get hooksFile() {
66
69
  return this.#hooksFile
@@ -69,10 +72,10 @@ export default class ActionHooks {
69
72
  /**
70
73
  * Gets the loaded hooks object.
71
74
  *
72
- * @returns {object|null} Hooks object or null if not loaded
75
+ * @returns {HookModule|null} Hooks object or null if not loaded
73
76
  */
74
77
  get hooks() {
75
- return this.#hooks
78
+ return this.#hooksObject
76
79
  }
77
80
 
78
81
  /**
@@ -105,17 +108,17 @@ export default class ActionHooks {
105
108
  /**
106
109
  * Static factory method to create and initialize a hook manager.
107
110
  * Loads hooks from the specified file and returns an initialized instance.
108
- * Override loadHooks() in subclasses to customize hook loading logic.
111
+ * If a hooksObject is provided in config, it's used directly; otherwise, hooks are loaded from file.
109
112
  *
110
- * @param {ActionHooksConfig} config Same configuration object as constructor
113
+ * @param {ActionHooksConfig} config Configuration object with hooks settings
111
114
  * @param {DebugFn} debug The debug function.
112
- * @returns {Promise<ActionHooks|null>} Initialized hook manager or null if no hooks found
115
+ * @returns {Promise<ActionHooks>} Initialized hook manager
113
116
  */
114
117
  static async new(config, debug) {
115
118
  debug("Creating new HookManager instance with args: %o", 2, config)
116
119
 
117
120
  const instance = new ActionHooks(config, debug)
118
- if(!instance.#hooks) {
121
+ if(!instance.#hooksObject) {
119
122
  const hooksFile = new FileObject(instance.#hooksFile)
120
123
 
121
124
  debug("Loading hooks from %o", 2, hooksFile.uri)
@@ -140,7 +143,7 @@ export default class ActionHooks {
140
143
 
141
144
  debug(hooks.constructor.name, 4)
142
145
 
143
- instance.#hooks = hooks
146
+ instance.#hooksObject = hooks
144
147
  debug("Hooks %o loaded successfully for %o", 2, hooksFile.uri, instance.actionKind)
145
148
 
146
149
  return instance
@@ -151,31 +154,34 @@ export default class ActionHooks {
151
154
  }
152
155
  }
153
156
 
154
- return this
157
+ return instance
155
158
  }
156
159
 
157
160
  /**
158
- * Invoke a dynamically-named hook such as `before$foo`.
161
+ * Invoke a dynamically-named hook such as `before$foo` or `after$foo`.
162
+ * The hook name is constructed by combining the kind with the activity name.
163
+ * Symbols are converted to their description. Non-alphanumeric characters are filtered out.
159
164
  *
160
165
  * @param {'before'|'after'|'setup'|'cleanup'|string} kind Hook namespace.
161
166
  * @param {string|symbol} activityName Activity identifier.
162
167
  * @param {unknown} context Pipeline context supplied to the hook.
163
168
  * @returns {Promise<void>}
169
+ * @throws {Sass} If the hook execution fails or exceeds timeout.
164
170
  */
165
171
  async callHook(kind, activityName, context) {
166
- try {
167
- const debug = this.#debug
168
- const hooks = this.#hooks
172
+ const debug = this.#debug
173
+ const hooks = this.#hooksObject
169
174
 
170
- if(!hooks)
171
- return
175
+ if(!hooks)
176
+ return
172
177
 
173
- const stringActivityName = Data.isType("Symbol")
174
- ? activityName.description()
175
- : activityName
178
+ const stringActivityName = Data.isType(activityName, "Symbol")
179
+ ? activityName.description()
180
+ : activityName
176
181
 
177
- const hookName = this.#getActivityHookName(kind, stringActivityName)
182
+ const hookName = this.#getActivityHookName(kind, stringActivityName)
178
183
 
184
+ try {
179
185
  debug("Looking for hook: %o", 4, hookName)
180
186
 
181
187
  const hook = hooks[hookName]
@@ -189,7 +195,7 @@ export default class ActionHooks {
189
195
  debug("Hook function starting execution: %o", 4, hookName)
190
196
 
191
197
  const duration = (
192
- await Util.time(() => hook.call(this.#hooks, context))
198
+ await Util.time(() => hook.call(this.#hooksObject, context))
193
199
  ).cost
194
200
 
195
201
  debug("Hook function completed successfully: %o, after %oms", 4, hookName, duration)
@@ -208,16 +214,26 @@ export default class ActionHooks {
208
214
  expireAsync
209
215
  ])
210
216
  } catch(error) {
211
- throw Sass.new(`Processing hook ${kind}$${activityName}`, error)
217
+ throw Sass.new(`Processing hook ${hookName}`, error)
212
218
  }
213
219
 
214
220
  debug("We made it throoough the wildernessss", 4)
215
221
 
216
222
  } catch(error) {
217
- throw Sass.new(`Processing hook ${kind}$${activityName}`, error)
223
+ throw Sass.new(`Processing hook ${hookName}`, error)
218
224
  }
219
225
  }
220
226
 
227
+ /**
228
+ * Transforms an activity name into a hook-compatible name.
229
+ * Converts "my activity name" to "myActivityName" and combines with event kind.
230
+ * Example: ("before", "my activity") => "before$myActivity"
231
+ *
232
+ * @param {string} event Hook event type (before, after, etc.)
233
+ * @param {string} activityName The raw activity name
234
+ * @returns {string} The formatted hook name
235
+ * @private
236
+ */
221
237
  #getActivityHookName(event, activityName) {
222
238
  const name = activityName
223
239
  .split(" ")
@@ -1,4 +1,4 @@
1
- import {Sass, Valid} from "@gesslar/toolkit"
1
+ import {Data, Sass, Valid} from "@gesslar/toolkit"
2
2
 
3
3
  import ActionBuilder from "./ActionBuilder.js"
4
4
  import {ACTIVITY} from "./Activity.js"
@@ -20,7 +20,10 @@ import Piper from "./Piper.js"
20
20
  * context object under `result.value` that can be replaced or enriched.
21
21
  */
22
22
  export default class ActionRunner extends Piper {
23
+ /** @type {import("./ActionBuilder.js").default|null} */
23
24
  #actionBuilder = null
25
+ /** @type {import("./ActionWrapper.js").default|null} */
26
+ #actionWrapper = null
24
27
 
25
28
  /**
26
29
  * Logger invoked for diagnostics.
@@ -48,82 +51,160 @@ export default class ActionRunner extends Piper {
48
51
 
49
52
  this.#actionBuilder = actionBuilder
50
53
 
51
- this.addStep(this.run)
54
+ this.addStep(this.run, {
55
+ name: `ActionRunner for ${actionBuilder.tag.description}`
56
+ })
52
57
  }
53
58
 
54
59
  /**
55
60
  * Executes the configured action pipeline.
61
+ * Builds the ActionWrapper on first run and caches it for subsequent calls.
62
+ * Supports WHILE, UNTIL, and SPLIT activity kinds.
56
63
  *
57
64
  * @param {unknown} context - Seed value passed to the first activity.
58
- * @returns {Promise<unknown>} Final value produced by the pipeline, or null when a parallel stage reports failures.
59
- * @throws {Sass} When no activities are registered or required parallel builders are missing.
65
+ * @returns {Promise<unknown>} Final value produced by the pipeline.
66
+ * @throws {Sass} When no activities are registered, conflicting activity kinds are used, or execution fails.
60
67
  */
61
68
  async run(context) {
62
- const actionWrapper = await this.#actionBuilder.build()
69
+ if(!this.#actionWrapper)
70
+ this.#actionWrapper = await this.#actionBuilder.build()
71
+
72
+ const actionWrapper = this.#actionWrapper
63
73
  const activities = actionWrapper.activities
64
74
 
65
75
  for(const activity of activities) {
66
- const kind = activity.kind
67
-
68
- // If we have no kind, then it's just a once.
69
- // Get it over and done with!
70
- if(!kind) {
71
- context = await this.#executeActivity(activity, context)
72
- } else {
73
- const {WHILE,UNTIL} = ACTIVITY
74
- const pred = activity.pred
75
- const kindWhile = kind & WHILE
76
- const kindUntil = kind & UNTIL
77
-
78
- if(kindWhile && kindUntil)
79
- throw Sass.new(
80
- "For Kathy Griffin's sake! You can't do something while AND " +
81
- "until. Pick one!"
82
- )
83
-
84
- if(kindWhile || kindUntil) {
85
- for(;;) {
86
-
87
- if(kindWhile)
88
- if(!await this.#predicateCheck(activity,pred,context))
89
- break
76
+ try {
77
+ // await timeout(500)
90
78
 
91
- context = await this.#executeActivity(activity,context)
79
+ const kind = activity.kind
92
80
 
93
- if(kindUntil)
94
- if(!await this.#predicateCheck(activity,pred,context))
95
- break
96
- }
81
+ // If we have no kind, then it's just a once.
82
+ // Get it over and done with!
83
+ if(!kind) {
84
+ context = await this.#execute(activity, context)
97
85
  } else {
98
- context = await this.#executeActivity(activity, context)
86
+ // Validate that only one activity kind bit is set
87
+ // (kind & (kind - 1)) !== 0 means multiple bits are set
88
+ const multipleBitsSet = (kind & (kind - 1)) !== 0
89
+ if(multipleBitsSet)
90
+ throw Sass.new(
91
+ "For Kathy Griffin's sake! You can't combine activity kinds. " +
92
+ "Pick one: WHILE, UNTIL, or SPLIT!"
93
+ )
94
+
95
+ const {WHILE,UNTIL,SPLIT} = ACTIVITY
96
+ const kindWhile = kind & WHILE
97
+ const kindUntil = kind & UNTIL
98
+ const kindSplit = kind & SPLIT
99
+
100
+ if(kindWhile || kindUntil) {
101
+ const predicate = activity.pred
102
+
103
+ for(;;) {
104
+
105
+ if(kindWhile)
106
+ if(!await this.#hasPredicate(activity,predicate,context))
107
+ break
108
+
109
+ context = await this.#execute(activity,context)
110
+
111
+ if(kindUntil)
112
+ if(!await this.#hasPredicate(activity,predicate,context))
113
+ break
114
+ }
115
+ } else if(kindSplit && activity.opKind === "ActionBuilder") {
116
+ // SPLIT activity: parallel execution with splitter/rejoiner pattern
117
+ const splitter = activity.splitter
118
+ const rejoiner = activity.rejoiner
119
+
120
+ if(!splitter || !rejoiner)
121
+ throw Sass.new(
122
+ `SPLIT activity "${String(activity.name)}" requires both splitter and rejoiner functions.`
123
+ )
124
+
125
+ const original = context
126
+ const splitContext = splitter.call(activity.action,context)
127
+ const newContext = await this.#execute(activity,splitContext,true)
128
+ const rejoined = rejoiner.call(activity.action, original,newContext)
129
+
130
+ context = rejoined
131
+ } else {
132
+ context = await this.#execute(activity, context)
133
+ }
99
134
  }
135
+ } catch(error) {
136
+ throw Sass.new("ActionRunner running activity", error)
100
137
  }
101
-
102
138
  }
103
139
 
104
140
  return context
105
141
  }
106
142
 
107
143
  /**
108
- * Execute a single activity, recursing into nested action wrappers when needed.
144
+ * Execute a single activity, recursing into nested ActionBuilders when needed.
145
+ * Handles both function-based activities and ActionBuilder-based nested pipelines.
146
+ * Automatically propagates hooks to nested builders and handles dynamic ActionBuilder returns.
147
+ *
148
+ * When parallel=true, uses Piper.pipe() for concurrent execution with worker pool pattern.
149
+ * This is triggered by SPLIT activities where context is divided for parallel processing.
150
+ * Results from parallel execution are filtered to only include successful outcomes ({ok: true}).
109
151
  *
110
152
  * @param {import("./Activity.js").default} activity Pipeline activity descriptor.
111
153
  * @param {unknown} context Current pipeline context.
154
+ * @param {boolean} [parallel] Whether to use parallel execution (via pipe() instead of run()). Default: false.
112
155
  * @returns {Promise<unknown>} Resolved activity result.
156
+ * @throws {Sass} If the operation kind is invalid, or if SPLIT activity lacks splitter/rejoiner.
113
157
  * @private
114
158
  */
115
- async #executeActivity(activity, context) {
159
+ async #execute(activity, context, parallel=false) {
116
160
  // What kind of op are we looking at? Is it a function?
117
161
  // Or a class instance of type ActionBuilder?
118
162
  const opKind = activity.opKind
163
+
119
164
  if(opKind === "ActionBuilder") {
120
- const runner = new this.constructor(activity.op, {debug: this.#debug})
165
+ if(activity.hooks && !activity.op.hasActionHooks)
166
+ activity.op.withActionHooks(activity.hooks)
167
+
168
+ const runner = new this.constructor(activity.op, {
169
+ debug: this.#debug, name: activity.name
170
+ })
121
171
 
122
- return await runner.run(context, true)
172
+ if(parallel) {
173
+ const piped = await runner.pipe(context)
174
+
175
+ return piped.filter(p => p.ok).map(p => p.value)
176
+ } else {
177
+ return await runner.run(context)
178
+ }
123
179
  } else if(opKind === "Function") {
124
- return await activity.run(context)
180
+ try {
181
+ const result = await activity.run(context)
182
+
183
+ if(Data.isType(result, "ActionBuilder")) {
184
+ if(activity.hooks)
185
+ result.withActionHooks(activity.hooks)
186
+
187
+ const runner = new this.constructor(result, {
188
+ debug: this.#debug, name: result.name
189
+ })
190
+
191
+ if(parallel) {
192
+ const piped = await runner.pipe(context)
193
+
194
+ return piped.filter(p => p.ok).map(p => p.value)
195
+ } else {
196
+ return await runner.run(context)
197
+ }
198
+ } else {
199
+ return result
200
+ }
201
+ } catch(error) {
202
+ throw Sass.new("Executing activity", error)
203
+ }
125
204
  }
126
205
 
206
+ console.log(activity.opKind + " " + JSON.stringify(activity))
207
+
127
208
  throw Sass.new("We buy Functions and ActionBuilders. Only. Not whatever that was.")
128
209
  }
129
210
 
@@ -136,7 +217,7 @@ export default class ActionRunner extends Piper {
136
217
  * @returns {Promise<boolean>} True when the predicate allows another iteration.
137
218
  * @private
138
219
  */
139
- async #predicateCheck(activity,predicate,context) {
220
+ async #hasPredicate(activity,predicate,context) {
140
221
  Valid.type(predicate, "Function")
141
222
 
142
223
  return !!(await predicate.call(activity.action, context))
@@ -3,9 +3,11 @@ import Activity from "./Activity.js"
3
3
  /**
4
4
  * @typedef {object} WrappedActivityConfig
5
5
  * @property {string|symbol} name Activity identifier used by hooks/logs.
6
- * @property {(context: unknown) => unknown|Promise<unknown>|ActionWrapper} op Operation or nested wrapper to execute.
6
+ * @property {(context: unknown) => unknown|Promise<unknown>|import("./ActionBuilder.js").default} op Operation or nested ActionBuilder to execute.
7
7
  * @property {number} [kind] Optional loop semantic flags.
8
8
  * @property {(context: unknown) => boolean|Promise<boolean>} [pred] Predicate tied to WHILE/UNTIL semantics.
9
+ * @property {(context: unknown) => unknown} [splitter] Splitter function for SPLIT activities.
10
+ * @property {(originalContext: unknown, splitResults: unknown) => unknown} [rejoiner] Rejoiner function for SPLIT activities.
9
11
  * @property {unknown} [action] Parent action instance supplied when invoking the op.
10
12
  * @property {(message: string, level?: number, ...args: Array<unknown>) => void} [debug] Optional logger reference.
11
13
  */
@@ -31,21 +33,29 @@ export default class ActionWrapper {
31
33
  */
32
34
  #debug = () => {}
33
35
 
36
+ /**
37
+ * ActionHooks instance shared across all activities.
38
+ *
39
+ * @type {import("./ActionHooks.js").default|null}
40
+ */
34
41
  #hooks = null
35
42
 
36
43
  /**
37
44
  * Create a wrapper from the builder payload.
38
45
  *
39
- * @param {{activities: Map<string|symbol, WrappedActivityConfig>, debug: (message: string, level?: number, ...args: Array<unknown>) => void}} init Builder payload containing activities + logger.
46
+ * @param {object} config Builder payload containing activities + logger
47
+ * @param {Map<string|symbol, WrappedActivityConfig>} config.activities Activities map
48
+ * @param {(message: string, level?: number, ...args: Array<unknown>) => void} config.debug Debug function
49
+ * @param {object} config.hooks Hooks object
40
50
  */
41
- constructor({activities,hooks,debug}) {
42
- this.#debug = debug
43
- this.#hooks = hooks
44
- this.#activities = activities
51
+ constructor(config) {
52
+ this.#debug = config.debug
53
+ this.#hooks = config.hooks
54
+ this.#activities = config.activities
45
55
  this.#debug(
46
56
  "Instantiating ActionWrapper with %o activities.",
47
57
  2,
48
- activities.size,
58
+ this.#activities.size,
49
59
  )
50
60
  }
51
61