@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 +128 -2
- package/package.json +1 -1
- package/src/lib/ActionBuilder.js +158 -16
- package/src/lib/ActionHooks.js +90 -50
- package/src/lib/ActionRunner.js +131 -138
- package/src/lib/ActionWrapper.js +20 -7
- package/src/lib/Activity.js +79 -19
- package/src/lib/Piper.js +106 -57
- package/src/types/lib/ActionBuilder.d.ts +63 -6
- package/src/types/lib/ActionBuilder.d.ts.map +1 -1
- package/src/types/lib/ActionHooks.d.ts +23 -24
- package/src/types/lib/ActionHooks.d.ts.map +1 -1
- package/src/types/lib/ActionRunner.d.ts +7 -20
- package/src/types/lib/ActionRunner.d.ts.map +1 -1
- package/src/types/lib/ActionWrapper.d.ts +19 -5
- package/src/types/lib/ActionWrapper.d.ts.map +1 -1
- package/src/types/lib/Activity.d.ts +56 -18
- package/src/types/lib/Activity.d.ts.map +1 -1
- package/src/types/lib/Piper.d.ts +24 -7
- package/src/types/lib/Piper.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ class MyAction {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const
|
|
32
|
-
const runner = new ActionRunner(
|
|
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
package/src/lib/ActionBuilder.js
CHANGED
|
@@ -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
|
-
*
|
|
42
|
-
*
|
|
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,
|
|
158
|
+
const activityDefinition = {name,action,debug}
|
|
129
159
|
|
|
130
160
|
if(args.length === 1) {
|
|
131
|
-
const [op,
|
|
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,
|
|
165
|
+
Object.assign(activityDefinition, {op,kind})
|
|
136
166
|
} else if(args.length === 3) {
|
|
137
|
-
const [kind,
|
|
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|
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
}
|
package/src/lib/ActionHooks.js
CHANGED
|
@@ -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} [
|
|
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
|
-
#
|
|
29
|
+
#hooksObject = null
|
|
31
30
|
/** @type {string|null} */
|
|
32
31
|
#actionKind = null
|
|
33
32
|
/** @type {number} */
|
|
34
|
-
#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(
|
|
43
|
+
constructor(
|
|
44
|
+
{actionKind, hooksFile, hooksObject, hookTimeout = 1_000},
|
|
45
|
+
debug,
|
|
46
|
+
) {
|
|
44
47
|
this.#actionKind = actionKind
|
|
45
48
|
this.#hooksFile = hooksFile
|
|
46
|
-
this.#
|
|
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 {
|
|
75
|
+
* @returns {HookModule|null} Hooks object or null if not loaded
|
|
73
76
|
*/
|
|
74
77
|
get hooks() {
|
|
75
|
-
return this.#
|
|
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
|
-
*
|
|
111
|
+
* If a hooksObject is provided in config, it's used directly; otherwise, hooks are loaded from file.
|
|
109
112
|
*
|
|
110
|
-
* @param {ActionHooksConfig} config
|
|
113
|
+
* @param {ActionHooksConfig} config Configuration object with hooks settings
|
|
111
114
|
* @param {DebugFn} debug The debug function.
|
|
112
|
-
* @returns {Promise<ActionHooks
|
|
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
|
|
118
|
-
|
|
120
|
+
const instance = new ActionHooks(config, debug)
|
|
121
|
+
if(!instance.#hooksObject) {
|
|
122
|
+
const hooksFile = new FileObject(instance.#hooksFile)
|
|
119
123
|
|
|
120
|
-
|
|
124
|
+
debug("Loading hooks from %o", 2, hooksFile.uri)
|
|
121
125
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if(!hooksImport)
|
|
130
|
-
return null
|
|
130
|
+
try {
|
|
131
|
+
const hooksImport = await hooksFile.import()
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
if(!hooksImport)
|
|
134
|
+
return null
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
if(!hooksImport[actionKind])
|
|
136
|
-
return null
|
|
136
|
+
debug("Hooks file imported successfully as a module", 2)
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
const actionKind = instance.actionKind
|
|
139
|
+
if(!hooksImport[actionKind])
|
|
140
|
+
return null
|
|
139
141
|
|
|
140
|
-
|
|
142
|
+
const hooks = new hooksImport[actionKind]({debug})
|
|
141
143
|
|
|
142
|
-
|
|
143
|
-
instance.#hooks = hooks
|
|
144
|
+
debug(hooks.constructor.name, 4)
|
|
144
145
|
|
|
145
|
-
|
|
146
|
+
instance.#hooksObject = hooks
|
|
147
|
+
debug("Hooks %o loaded successfully for %o", 2, hooksFile.uri, instance.actionKind)
|
|
146
148
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
return instance
|
|
150
|
+
} catch(error) {
|
|
151
|
+
debug("Failed to load hooks %o: %o", 1, hooksFile.uri, error.message)
|
|
150
152
|
|
|
151
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
const hooks = this.#hooks
|
|
172
|
+
const debug = this.#debug
|
|
173
|
+
const hooks = this.#hooksObject
|
|
167
174
|
|
|
168
|
-
|
|
169
|
-
|
|
175
|
+
if(!hooks)
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
const stringActivityName = Data.isType(activityName, "Symbol")
|
|
179
|
+
? activityName.description()
|
|
180
|
+
: activityName
|
|
170
181
|
|
|
171
|
-
|
|
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.#
|
|
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 ${
|
|
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 ${
|
|
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
|
}
|