@gesslar/actioneer 0.1.3 → 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/README.md CHANGED
@@ -28,8 +28,8 @@ class MyAction {
28
28
  }
29
29
  }
30
30
 
31
- const wrapper = new ActionBuilder(new MyAction()).build()
32
- const runner = new ActionRunner(wrapper)
31
+ const builder = new ActionBuilder(new MyAction())
32
+ const runner = new ActionRunner(builder)
33
33
  const result = await runner.pipe([{}], 4) // run up to 4 contexts concurrently
34
34
  console.log(result)
35
35
  ```
@@ -44,6 +44,132 @@ import { ActionBuilder, ActionRunner } from "@gesslar/actioneer"
44
44
 
45
45
  If you'd like more complete typings or additional JSDoc, open an issue or send a PR — contributions welcome.
46
46
 
47
+ ## ActionHooks
48
+
49
+ Actioneer supports lifecycle hooks that can execute before and after each activity in your pipeline. Hooks are loaded from a module and can be configured either by file path or by providing a pre-instantiated hooks object.
50
+
51
+ ### Hook System Overview
52
+
53
+ The hook system allows you to:
54
+
55
+ - Execute code before and after each activity in your pipeline
56
+ - Implement setup and cleanup logic
57
+ - Add observability and logging to your pipelines
58
+ - Modify or inspect the context flowing through activities
59
+
60
+ ### Configuring Hooks
61
+
62
+ You can attach hooks to an ActionBuilder in two ways:
63
+
64
+ #### 1. Load hooks from a file
65
+
66
+ ```js
67
+ import { ActionBuilder, ActionRunner } from "@gesslar/actioneer"
68
+
69
+ class MyAction {
70
+ setup(builder) {
71
+ builder
72
+ .withHooksFile("./hooks/MyActionHooks.js", "MyActionHooks")
73
+ .do("prepare", ctx => { ctx.count = 0 })
74
+ .do("work", ctx => { ctx.count += 1 })
75
+ }
76
+ }
77
+
78
+ const builder = new ActionBuilder(new MyAction())
79
+ const runner = new ActionRunner(builder)
80
+ const result = await runner.pipe([{}], 4)
81
+ ```
82
+
83
+ #### 2. Provide a pre-instantiated hooks object
84
+
85
+ ```js
86
+ import { ActionBuilder, ActionRunner } from "@gesslar/actioneer"
87
+ import { MyActionHooks } from "./hooks/MyActionHooks.js"
88
+
89
+ const hooks = new MyActionHooks({ debug: console.log })
90
+
91
+ class MyAction {
92
+ setup(builder) {
93
+ builder
94
+ .withHooks(hooks)
95
+ .do("prepare", ctx => { ctx.count = 0 })
96
+ .do("work", ctx => { ctx.count += 1 })
97
+ }
98
+ }
99
+
100
+ const builder = new ActionBuilder(new MyAction())
101
+ const runner = new ActionRunner(builder)
102
+ const result = await runner.pipe([{}], 4)
103
+ ```
104
+
105
+ ### Writing Hooks
106
+
107
+ Hooks are classes exported from a module. The hook methods follow a naming convention: `event$activityName`.
108
+
109
+ ```js
110
+ // hooks/MyActionHooks.js
111
+ export class MyActionHooks {
112
+ constructor({ debug }) {
113
+ this.debug = debug
114
+ }
115
+
116
+ // Hook that runs before the "prepare" activity
117
+ async before$prepare(context) {
118
+ this.debug("About to prepare", context)
119
+ }
120
+
121
+ // Hook that runs after the "prepare" activity
122
+ async after$prepare(context) {
123
+ this.debug("Finished preparing", context)
124
+ }
125
+
126
+ // Hook that runs before the "work" activity
127
+ async before$work(context) {
128
+ this.debug("Starting work", context)
129
+ }
130
+
131
+ // Hook that runs after the "work" activity
132
+ async after$work(context) {
133
+ this.debug("Work complete", context)
134
+ }
135
+
136
+ // Optional: setup hook runs once at initialization
137
+ async setup(args) {
138
+ this.debug("Hooks initialized")
139
+ }
140
+
141
+ // Optional: cleanup hook for teardown
142
+ async cleanup(args) {
143
+ this.debug("Hooks cleaned up")
144
+ }
145
+ }
146
+ ```
147
+
148
+ ### Hook Naming Convention
149
+
150
+ Activity names are transformed to hook method names:
151
+
152
+ - Spaces are removed and words are camelCased: `"do work"` → `before$doWork` / `after$doWork`
153
+ - Non-word characters are stripped: `"step-1"` → `before$step1` / `after$step1`
154
+ - First word stays lowercase: `"Prepare Data"` → `before$prepareData` / `after$prepareData`
155
+
156
+ ### Hook Timeout
157
+
158
+ By default, hooks have a 1-second (1000ms) timeout. If a hook exceeds this timeout, the pipeline will throw a `Sass` error. You can configure the timeout when creating the hooks:
159
+
160
+ ```js
161
+ new ActionHooks({
162
+ actionKind: "MyActionHooks",
163
+ hooksFile: "./hooks.js",
164
+ hookTimeout: 5000, // 5 seconds
165
+ debug: console.log
166
+ })
167
+ ```
168
+
169
+ ### Nested Pipelines and Hooks
170
+
171
+ When you nest ActionBuilders (for branching or parallel execution), the parent's hooks are automatically passed to all children, ensuring consistent hook behavior throughout the entire pipeline hierarchy.
172
+
47
173
  ### Optional TypeScript (local, opt-in)
48
174
 
49
175
  This project intentionally avoids committing TypeScript tool configuration. If you'd like to use TypeScript's checker locally (for editor integration or optional JSDoc checking), you can drop a `tsconfig.json` in your working copy — `tsconfig.json` is already in the repository `.gitignore`, so feel free to typecheck yourselves into oblivion.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/actioneer",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Ready? Set?? ACTION!! pew! pew! pew!",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -1,6 +1,7 @@
1
1
  import {Data, Sass, Valid} from "@gesslar/toolkit"
2
2
 
3
3
  import ActionWrapper from "./ActionWrapper.js"
4
+ import ActionHooks from "./ActionHooks.js"
4
5
 
5
6
  /** @typedef {import("./ActionRunner.js").default} ActionRunner */
6
7
  /** @typedef {typeof import("./Activity.js").ACTIVITY} ActivityFlags */
@@ -29,6 +30,8 @@ import ActionWrapper from "./ActionWrapper.js"
29
30
  * @property {ActionFunction|import("./ActionWrapper.js").default} op Operation to execute.
30
31
  * @property {number} [kind] Optional kind flags from {@link ActivityFlags}.
31
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).
32
35
  */
33
36
 
34
37
  /**
@@ -38,8 +41,8 @@ import ActionWrapper from "./ActionWrapper.js"
38
41
  /**
39
42
  * Fluent builder for describing how an action should process the context that
40
43
  * flows through the {@link ActionRunner}. Consumers register named activities,
41
- * optional hook pairs, and nested parallel pipelines before handing the
42
- * builder back to the runner for execution.
44
+ * and nested parallel pipelines before handing the builder back to the runner
45
+ * for execution.
43
46
  *
44
47
  * Typical usage:
45
48
  *
@@ -64,12 +67,29 @@ export default class ActionBuilder {
64
67
  #debug = null
65
68
  /** @type {symbol|null} */
66
69
  #tag = null
70
+ /** @type {string|null} */
71
+ #hooksFile = null
72
+ /** @type {string|null} */
73
+ #hooksKind = null
74
+ /** @type {unknown|null} */
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
+ }
67
87
 
68
88
  /**
69
89
  * Creates a new ActionBuilder instance with the provided action callback.
70
90
  *
71
- * @param {ActionBuilderAction} [action] Base action invoked by the runner when a block satisfies the configured structure.
72
- * @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
73
93
  */
74
94
  constructor(
75
95
  action,
@@ -108,6 +128,16 @@ export default class ActionBuilder {
108
128
  * @returns {ActionBuilder}
109
129
  */
110
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
+
111
141
  /**
112
142
  * Handles runtime dispatch across the documented overloads.
113
143
  *
@@ -125,22 +155,32 @@ export default class ActionBuilder {
125
155
 
126
156
  const action = this.#action
127
157
  const debug = this.#debug
128
- const activityDefinition = {name, action, debug}
158
+ const activityDefinition = {name,action,debug}
129
159
 
130
160
  if(args.length === 1) {
131
- const [op, kind] = args
161
+ const [op,kind] = args
132
162
  Valid.type(kind, "Number|undefined")
133
163
  Valid.type(op, "Function")
134
164
 
135
- Object.assign(activityDefinition, {op, kind})
165
+ Object.assign(activityDefinition, {op,kind})
136
166
  } else if(args.length === 3) {
137
- const [kind, pred, op] = args
167
+ const [kind,pred,op] = args
138
168
 
139
169
  Valid.type(kind, "Number")
140
170
  Valid.type(pred, "Function")
141
- Valid.type(op, "Function|ActionWrapper")
171
+ Valid.type(op, "Function|ActionBuilder")
172
+
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})
142
183
 
143
- Object.assign(activityDefinition, {kind, pred, op})
144
184
  } else {
145
185
  throw Sass.new("Invalid number of arguments passed to 'do'")
146
186
  }
@@ -150,6 +190,65 @@ export default class ActionBuilder {
150
190
  return this
151
191
  }
152
192
 
193
+ /**
194
+ * Configure hooks to be loaded from a file when the action is built.
195
+ *
196
+ * @param {string} hooksFile Path to the hooks module file.
197
+ * @param {string} hooksKind Name of the exported hooks class to instantiate.
198
+ * @returns {ActionBuilder} The builder instance for chaining.
199
+ * @throws {Sass} If hooks have already been configured.
200
+ */
201
+ withHooksFile(hooksFile, hooksKind) {
202
+ Valid.assert(this.#exclusiveHooksCheck(), "Hooks have already been configured.")
203
+
204
+ this.#hooksFile = hooksFile
205
+ this.#hooksKind = hooksKind
206
+
207
+ return this
208
+ }
209
+
210
+ /**
211
+ * Configure hooks using a pre-instantiated hooks object.
212
+ *
213
+ * @param {import("./ActionHooks.js").default} hooks An already-instantiated hooks instance.
214
+ * @returns {ActionBuilder} The builder instance for chaining.
215
+ * @throws {Sass} If hooks have already been configured.
216
+ */
217
+ withHooks(hooks) {
218
+ Valid.assert(this.#exclusiveHooksCheck(), "Hooks have already been configured.")
219
+
220
+ this.#hooks = hooks
221
+
222
+ return this
223
+ }
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
+
153
252
  /**
154
253
  * Validates that an activity name has not been reused.
155
254
  *
@@ -167,10 +266,12 @@ export default class ActionBuilder {
167
266
  * Finalises the builder and returns a payload that can be consumed by the
168
267
  * runner.
169
268
  *
170
- * @returns {import("./ActionWrapper.js").default} Payload consumed by the {@link ActionRunner} constructor.
269
+ * @returns {Promise<import("./ActionWrapper.js").default>} Payload consumed by the {@link ActionRunner} constructor.
171
270
  */
172
- build() {
271
+ async build() {
173
272
  const action = this.#action
273
+ const activities = this.#activities
274
+ const debug = this.#debug
174
275
 
175
276
  if(!action.tag) {
176
277
  action.tag = this.#tag
@@ -178,9 +279,50 @@ export default class ActionBuilder {
178
279
  action.setup.call(action, this)
179
280
  }
180
281
 
181
- return new ActionWrapper({
182
- activities: this.#activities,
183
- debug: this.#debug,
184
- })
282
+ // All children in a branch also get the same hooks.
283
+ const hooks = await this.#getHooks()
284
+
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
295
+ }
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
+ */
304
+ async #getHooks() {
305
+ if(this.#actionHooks) {
306
+ return this.#actionHooks
307
+ }
308
+
309
+ const newHooks = ActionHooks.new
310
+
311
+ const hooks = this.#hooks
312
+
313
+ if(hooks) {
314
+ this.#actionHooks = await newHooks({hooks}, this.#debug)
315
+
316
+ return this.#actionHooks
317
+ }
318
+
319
+ const hooksFile = this.#hooksFile
320
+ const hooksKind = this.#hooksKind
321
+
322
+ if(hooksFile && hooksKind) {
323
+ this.#actionHooks = await newHooks({hooksFile,hooksKind}, this.#debug)
324
+
325
+ return this.#actionHooks
326
+ }
185
327
  }
186
328
  }
@@ -1,5 +1,5 @@
1
1
  import {setTimeout as timeout} from "timers/promises"
2
- import {FileObject, Sass, Util, Valid} from "@gesslar/toolkit"
2
+ import {Data, FileObject, Sass, Util, Valid} from "@gesslar/toolkit"
3
3
 
4
4
  /**
5
5
  * @typedef {(message: string, level?: number, ...args: Array<unknown>) => void} DebugFn
@@ -7,11 +7,10 @@ import {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,11 +26,11 @@ 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} */
34
- #timeout = 1000 // Default 1 second timeout
33
+ #timeout = 1_000 // Default 1 second timeout
35
34
  /** @type {DebugFn|null} */
36
35
  #debug = null
37
36
 
@@ -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 = 1000, 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,71 +108,80 @@ 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
- const instance = new this(config, debug)
118
- const hooksFile = instance.hooksFile
120
+ const instance = new ActionHooks(config, debug)
121
+ if(!instance.#hooksObject) {
122
+ const hooksFile = new FileObject(instance.#hooksFile)
119
123
 
120
- debug("Loading hooks from %o", 2, hooksFile.uri)
124
+ debug("Loading hooks from %o", 2, hooksFile.uri)
121
125
 
122
- debug("Checking hooks file exists: %o", 2, hooksFile.uri)
123
- if(!await hooksFile.exists)
124
- throw Sass.new(`No such hooks file, ${hooksFile.uri}`)
126
+ debug("Checking hooks file exists: %o", 2, hooksFile.uri)
127
+ if(!await hooksFile.exists)
128
+ throw Sass.new(`No such hooks file, ${hooksFile.uri}`)
125
129
 
126
- try {
127
- const hooksImport = await hooksFile.import()
128
-
129
- if(!hooksImport)
130
- return null
130
+ try {
131
+ const hooksImport = await hooksFile.import()
131
132
 
132
- debug("Hooks file imported successfully as a module", 2)
133
+ if(!hooksImport)
134
+ return null
133
135
 
134
- const actionKind = instance.actionKind
135
- if(!hooksImport[actionKind])
136
- return null
136
+ debug("Hooks file imported successfully as a module", 2)
137
137
 
138
- const hooks = new hooksImport[actionKind]({debug})
138
+ const actionKind = instance.actionKind
139
+ if(!hooksImport[actionKind])
140
+ return null
139
141
 
140
- debug(hooks.constructor.name, 4)
142
+ const hooks = new hooksImport[actionKind]({debug})
141
143
 
142
- // Attach common properties to hooks
143
- instance.#hooks = hooks
144
+ debug(hooks.constructor.name, 4)
144
145
 
145
- debug("Hooks %o loaded successfully for %o", 2, hooksFile.uri, instance.actionKind)
146
+ instance.#hooksObject = hooks
147
+ debug("Hooks %o loaded successfully for %o", 2, hooksFile.uri, instance.actionKind)
146
148
 
147
- return instance
148
- } catch(error) {
149
- debug("Failed to load hooks %o: %o", 1, hooksFile.uri, error.message)
149
+ return instance
150
+ } catch(error) {
151
+ debug("Failed to load hooks %o: %o", 1, hooksFile.uri, error.message)
150
152
 
151
- return null
153
+ return null
154
+ }
152
155
  }
156
+
157
+ return instance
153
158
  }
154
159
 
155
160
  /**
156
- * 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.
157
164
  *
158
165
  * @param {'before'|'after'|'setup'|'cleanup'|string} kind Hook namespace.
159
166
  * @param {string|symbol} activityName Activity identifier.
160
167
  * @param {unknown} context Pipeline context supplied to the hook.
161
168
  * @returns {Promise<void>}
169
+ * @throws {Sass} If the hook execution fails or exceeds timeout.
162
170
  */
163
171
  async callHook(kind, activityName, context) {
164
- try {
165
- const debug = this.#debug
166
- const hooks = this.#hooks
172
+ const debug = this.#debug
173
+ const hooks = this.#hooksObject
167
174
 
168
- if(!hooks)
169
- return
175
+ if(!hooks)
176
+ return
177
+
178
+ const stringActivityName = Data.isType(activityName, "Symbol")
179
+ ? activityName.description()
180
+ : activityName
170
181
 
171
- const hookName = `${kind}$${activityName}`
182
+ const hookName = this.#getActivityHookName(kind, stringActivityName)
172
183
 
184
+ try {
173
185
  debug("Looking for hook: %o", 4, hookName)
174
186
 
175
187
  const hook = hooks[hookName]
@@ -183,7 +195,7 @@ export default class ActionHooks {
183
195
  debug("Hook function starting execution: %o", 4, hookName)
184
196
 
185
197
  const duration = (
186
- await Util.time(() => hook.call(this.#hooks, context))
198
+ await Util.time(() => hook.call(this.#hooksObject, context))
187
199
  ).cost
188
200
 
189
201
  debug("Hook function completed successfully: %o, after %oms", 4, hookName, duration)
@@ -202,13 +214,41 @@ export default class ActionHooks {
202
214
  expireAsync
203
215
  ])
204
216
  } catch(error) {
205
- throw Sass.new(`Processing hook ${kind}$${activityName}`, error)
217
+ throw Sass.new(`Processing hook ${hookName}`, error)
206
218
  }
207
219
 
208
220
  debug("We made it throoough the wildernessss", 4)
209
221
 
210
222
  } catch(error) {
211
- throw Sass.new(`Processing hook ${kind}$${activityName}`, error)
223
+ throw Sass.new(`Processing hook ${hookName}`, error)
212
224
  }
213
225
  }
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
+ */
237
+ #getActivityHookName(event, activityName) {
238
+ const name = activityName
239
+ .split(" ")
240
+ .map(a => a.trim())
241
+ .filter(Boolean)
242
+ .map(a => a
243
+ .split("")
244
+ .filter(b => /[\w]/.test(b))
245
+ .filter(Boolean)
246
+ .join("")
247
+ )
248
+ .map(a => a.toLowerCase())
249
+ .map((a, i) => i === 0 ? a : Util.capitalize(a))
250
+ .join("")
251
+
252
+ return `${event}$${name}`
253
+ }
214
254
  }