@gesslar/actioneer 0.2.1 → 0.2.2
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 +152 -0
- package/package.json +1 -1
- package/src/lib/ActionBuilder.js +22 -106
- package/src/lib/ActionHooks.js +30 -46
- package/src/lib/ActionRunner.js +42 -123
- package/src/lib/ActionWrapper.js +7 -17
- package/src/lib/Activity.js +12 -74
- package/src/lib/Piper.js +52 -99
- package/src/types/lib/ActionBuilder.d.ts +2 -42
- package/src/types/lib/ActionBuilder.d.ts.map +1 -1
- package/src/types/lib/ActionHooks.d.ts +24 -23
- package/src/types/lib/ActionHooks.d.ts.map +1 -1
- package/src/types/lib/ActionRunner.d.ts +2 -4
- package/src/types/lib/ActionRunner.d.ts.map +1 -1
- package/src/types/lib/ActionWrapper.d.ts +5 -19
- package/src/types/lib/ActionWrapper.d.ts.map +1 -1
- package/src/types/lib/Activity.d.ts +19 -54
- package/src/types/lib/Activity.d.ts.map +1 -1
- package/src/types/lib/Piper.d.ts +7 -24
- package/src/types/lib/Piper.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,158 @@ 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
|
+
## Activity Modes
|
|
48
|
+
|
|
49
|
+
Actioneer supports four distinct execution modes for activities, allowing you to control how operations are executed:
|
|
50
|
+
|
|
51
|
+
### Execute Once (Default)
|
|
52
|
+
|
|
53
|
+
The simplest mode executes an activity exactly once per context:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
class MyAction {
|
|
57
|
+
setup(builder) {
|
|
58
|
+
builder.do("processItem", ctx => {
|
|
59
|
+
ctx.result = ctx.input * 2
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### WHILE Mode
|
|
66
|
+
|
|
67
|
+
Loops while a predicate returns `true`. The predicate is evaluated **before** each iteration:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { ActionBuilder, ACTIVITY } from "@gesslar/actioneer"
|
|
71
|
+
|
|
72
|
+
class CounterAction {
|
|
73
|
+
#shouldContinue = (ctx) => ctx.count < 10
|
|
74
|
+
|
|
75
|
+
#increment = (ctx) => {
|
|
76
|
+
ctx.count += 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setup(builder) {
|
|
80
|
+
builder
|
|
81
|
+
.do("initialize", ctx => { ctx.count = 0 })
|
|
82
|
+
.do("countUp", ACTIVITY.WHILE, this.#shouldContinue, this.#increment)
|
|
83
|
+
.do("finish", ctx => { return ctx.count })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The activity will continue executing as long as the predicate returns `true`. Once it returns `false`, execution moves to the next activity.
|
|
89
|
+
|
|
90
|
+
### UNTIL Mode
|
|
91
|
+
|
|
92
|
+
Loops until a predicate returns `true`. The predicate is evaluated **after** each iteration:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
import { ActionBuilder, ACTIVITY } from "@gesslar/actioneer"
|
|
96
|
+
|
|
97
|
+
class ProcessorAction {
|
|
98
|
+
#queueIsEmpty = (ctx) => ctx.queue.length === 0
|
|
99
|
+
|
|
100
|
+
#processItem = (ctx) => {
|
|
101
|
+
const item = ctx.queue.shift()
|
|
102
|
+
ctx.processed.push(item)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setup(builder) {
|
|
106
|
+
builder
|
|
107
|
+
.do("initialize", ctx => {
|
|
108
|
+
ctx.queue = [1, 2, 3, 4, 5]
|
|
109
|
+
ctx.processed = []
|
|
110
|
+
})
|
|
111
|
+
.do("process", ACTIVITY.UNTIL, this.#queueIsEmpty, this.#processItem)
|
|
112
|
+
.do("finish", ctx => { return ctx.processed })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The activity executes at least once, then continues while the predicate returns `false`. Once it returns `true`, execution moves to the next activity.
|
|
118
|
+
|
|
119
|
+
### SPLIT Mode
|
|
120
|
+
|
|
121
|
+
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:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
import { ActionBuilder, ACTIVITY } from "@gesslar/actioneer"
|
|
125
|
+
|
|
126
|
+
class ParallelProcessor {
|
|
127
|
+
#split = (ctx) => {
|
|
128
|
+
// Split context into multiple items for parallel processing
|
|
129
|
+
return ctx.items.map(item => ({ item, processedBy: "worker" }))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#rejoin = (originalCtx, splitResults) => {
|
|
133
|
+
// Recombine parallel results back into original context
|
|
134
|
+
originalCtx.results = splitResults.map(r => r.item)
|
|
135
|
+
return originalCtx
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#processItem = (ctx) => {
|
|
139
|
+
ctx.item = ctx.item.toUpperCase()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setup(builder) {
|
|
143
|
+
builder
|
|
144
|
+
.do("initialize", ctx => {
|
|
145
|
+
ctx.items = ["apple", "banana", "cherry"]
|
|
146
|
+
})
|
|
147
|
+
.do("parallel", ACTIVITY.SPLIT, this.#split, this.#rejoin, this.#processItem)
|
|
148
|
+
.do("finish", ctx => { return ctx.results })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**How SPLIT Mode Works:**
|
|
154
|
+
|
|
155
|
+
1. The **splitter** function receives the context and returns an array of contexts (one per parallel task)
|
|
156
|
+
2. Each split context is processed in parallel through the **operation** function
|
|
157
|
+
3. The **rejoiner** function receives the original context and the array of processed results
|
|
158
|
+
4. The rejoiner combines the results and returns the updated context
|
|
159
|
+
|
|
160
|
+
**Nested Pipelines with SPLIT:**
|
|
161
|
+
|
|
162
|
+
You can use nested ActionBuilders with SPLIT mode for complex parallel workflows:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
class NestedParallel {
|
|
166
|
+
#split = (ctx) => ctx.batches.map(batch => ({ batch }))
|
|
167
|
+
|
|
168
|
+
#rejoin = (original, results) => {
|
|
169
|
+
original.processed = results.flatMap(r => r.batch)
|
|
170
|
+
return original
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
setup(builder) {
|
|
174
|
+
builder
|
|
175
|
+
.do("parallel", ACTIVITY.SPLIT, this.#split, this.#rejoin,
|
|
176
|
+
new ActionBuilder(this)
|
|
177
|
+
.do("step1", ctx => { /* ... */ })
|
|
178
|
+
.do("step2", ctx => { /* ... */ })
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Mode Constraints
|
|
185
|
+
|
|
186
|
+
- **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!"`
|
|
187
|
+
- **SPLIT requires both functions**: The splitter and rejoiner are both mandatory for SPLIT mode
|
|
188
|
+
- **Predicates must return boolean**: For WHILE and UNTIL modes, predicates should return `true` or `false`
|
|
189
|
+
|
|
190
|
+
### Mode Summary Table
|
|
191
|
+
|
|
192
|
+
| Mode | Signature | Predicate Timing | Use Case |
|
|
193
|
+
| ----------- | ---------------------------------------------------------- | ---------------- | ------------------------------------ |
|
|
194
|
+
| **Default** | `.do(name, operation)` | N/A | Execute once per context |
|
|
195
|
+
| **WHILE** | `.do(name, ACTIVITY.WHILE, predicate, operation)` | Before iteration | Loop while condition is true |
|
|
196
|
+
| **UNTIL** | `.do(name, ACTIVITY.UNTIL, predicate, operation)` | After iteration | Loop until condition is true |
|
|
197
|
+
| **SPLIT** | `.do(name, ACTIVITY.SPLIT, splitter, rejoiner, operation)` | N/A | Parallel execution with split/rejoin |
|
|
198
|
+
|
|
47
199
|
## ActionHooks
|
|
48
200
|
|
|
49
201
|
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.
|
package/package.json
CHANGED
package/src/lib/ActionBuilder.js
CHANGED
|
@@ -30,8 +30,6 @@ 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).
|
|
35
33
|
*/
|
|
36
34
|
|
|
37
35
|
/**
|
|
@@ -67,29 +65,15 @@ export default class ActionBuilder {
|
|
|
67
65
|
#debug = null
|
|
68
66
|
/** @type {symbol|null} */
|
|
69
67
|
#tag = null
|
|
70
|
-
/** @type {string|null} */
|
|
71
68
|
#hooksFile = null
|
|
72
|
-
/** @type {string|null} */
|
|
73
69
|
#hooksKind = null
|
|
74
|
-
/** @type {unknown|null} */
|
|
75
70
|
#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
|
-
}
|
|
87
71
|
|
|
88
72
|
/**
|
|
89
73
|
* Creates a new ActionBuilder instance with the provided action callback.
|
|
90
74
|
*
|
|
91
|
-
* @param {ActionBuilderAction} [action]
|
|
92
|
-
* @param {ActionBuilderConfig} [config]
|
|
75
|
+
* @param {ActionBuilderAction} [action] Base action invoked by the runner when a block satisfies the configured structure.
|
|
76
|
+
* @param {ActionBuilderConfig} [config] Options
|
|
93
77
|
*/
|
|
94
78
|
constructor(
|
|
95
79
|
action,
|
|
@@ -128,16 +112,6 @@ export default class ActionBuilder {
|
|
|
128
112
|
* @returns {ActionBuilder}
|
|
129
113
|
*/
|
|
130
114
|
|
|
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
|
-
|
|
141
115
|
/**
|
|
142
116
|
* Handles runtime dispatch across the documented overloads.
|
|
143
117
|
*
|
|
@@ -155,32 +129,22 @@ export default class ActionBuilder {
|
|
|
155
129
|
|
|
156
130
|
const action = this.#action
|
|
157
131
|
const debug = this.#debug
|
|
158
|
-
const activityDefinition = {name,action,debug}
|
|
132
|
+
const activityDefinition = {name, action, debug}
|
|
159
133
|
|
|
160
134
|
if(args.length === 1) {
|
|
161
|
-
const [op,kind] = args
|
|
135
|
+
const [op, kind] = args
|
|
162
136
|
Valid.type(kind, "Number|undefined")
|
|
163
137
|
Valid.type(op, "Function")
|
|
164
138
|
|
|
165
|
-
Object.assign(activityDefinition, {op,kind})
|
|
139
|
+
Object.assign(activityDefinition, {op, kind})
|
|
166
140
|
} else if(args.length === 3) {
|
|
167
|
-
const [kind,pred,op] = args
|
|
141
|
+
const [kind, pred, op] = args
|
|
168
142
|
|
|
169
143
|
Valid.type(kind, "Number")
|
|
170
144
|
Valid.type(pred, "Function")
|
|
171
145
|
Valid.type(op, "Function|ActionBuilder")
|
|
172
146
|
|
|
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
|
-
|
|
147
|
+
Object.assign(activityDefinition, {kind, pred, op})
|
|
184
148
|
} else {
|
|
185
149
|
throw Sass.new("Invalid number of arguments passed to 'do'")
|
|
186
150
|
}
|
|
@@ -199,7 +163,9 @@ export default class ActionBuilder {
|
|
|
199
163
|
* @throws {Sass} If hooks have already been configured.
|
|
200
164
|
*/
|
|
201
165
|
withHooksFile(hooksFile, hooksKind) {
|
|
202
|
-
Valid.assert(this.#
|
|
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.")
|
|
203
169
|
|
|
204
170
|
this.#hooksFile = hooksFile
|
|
205
171
|
this.#hooksKind = hooksKind
|
|
@@ -215,40 +181,15 @@ export default class ActionBuilder {
|
|
|
215
181
|
* @throws {Sass} If hooks have already been configured.
|
|
216
182
|
*/
|
|
217
183
|
withHooks(hooks) {
|
|
218
|
-
Valid.assert(this.#
|
|
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.")
|
|
219
187
|
|
|
220
188
|
this.#hooks = hooks
|
|
221
189
|
|
|
222
190
|
return this
|
|
223
191
|
}
|
|
224
192
|
|
|
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
|
-
|
|
252
193
|
/**
|
|
253
194
|
* Validates that an activity name has not been reused.
|
|
254
195
|
*
|
|
@@ -270,8 +211,6 @@ export default class ActionBuilder {
|
|
|
270
211
|
*/
|
|
271
212
|
async build() {
|
|
272
213
|
const action = this.#action
|
|
273
|
-
const activities = this.#activities
|
|
274
|
-
const debug = this.#debug
|
|
275
214
|
|
|
276
215
|
if(!action.tag) {
|
|
277
216
|
action.tag = this.#tag
|
|
@@ -282,47 +221,24 @@ export default class ActionBuilder {
|
|
|
282
221
|
// All children in a branch also get the same hooks.
|
|
283
222
|
const hooks = await this.#getHooks()
|
|
284
223
|
|
|
285
|
-
return new ActionWrapper({
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
*
|
|
291
|
-
* @returns {boolean} True if ActionHooks have been configured.
|
|
292
|
-
*/
|
|
293
|
-
get hasActionHooks() {
|
|
294
|
-
return this.#actionHooks !== null
|
|
224
|
+
return new ActionWrapper({
|
|
225
|
+
activities: this.#activities,
|
|
226
|
+
debug: this.#debug,
|
|
227
|
+
hooks,
|
|
228
|
+
})
|
|
295
229
|
}
|
|
296
230
|
|
|
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
231
|
async #getHooks() {
|
|
305
|
-
if(this.#actionHooks) {
|
|
306
|
-
return this.#actionHooks
|
|
307
|
-
}
|
|
308
|
-
|
|
309
232
|
const newHooks = ActionHooks.new
|
|
310
233
|
|
|
311
234
|
const hooks = this.#hooks
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
this.#actionHooks = await newHooks({hooks}, this.#debug)
|
|
315
|
-
|
|
316
|
-
return this.#actionHooks
|
|
317
|
-
}
|
|
235
|
+
if(hooks)
|
|
236
|
+
return await newHooks({hooks}, this.#debug)
|
|
318
237
|
|
|
319
238
|
const hooksFile = this.#hooksFile
|
|
320
239
|
const hooksKind = this.#hooksKind
|
|
321
240
|
|
|
322
|
-
if(hooksFile && hooksKind)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return this.#actionHooks
|
|
326
|
-
}
|
|
241
|
+
if(hooksFile && hooksKind)
|
|
242
|
+
return await newHooks({hooksFile,hooksKind}, this.#debug)
|
|
327
243
|
}
|
|
328
244
|
}
|
package/src/lib/ActionHooks.js
CHANGED
|
@@ -7,10 +7,11 @@ import {Data, FileObject, Sass, Util, Valid} from "@gesslar/toolkit"
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {object} ActionHooksConfig
|
|
10
|
-
* @property {string}
|
|
11
|
-
* @property {FileObject
|
|
12
|
-
* @property {unknown} [
|
|
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).
|
|
13
13
|
* @property {number} [hookTimeout] Timeout applied to hook execution in milliseconds.
|
|
14
|
+
* @property {DebugFn} debug Logger to emit diagnostics.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -26,7 +27,7 @@ export default class ActionHooks {
|
|
|
26
27
|
/** @type {FileObject|null} */
|
|
27
28
|
#hooksFile = null
|
|
28
29
|
/** @type {HookModule|null} */
|
|
29
|
-
#
|
|
30
|
+
#hooks = null
|
|
30
31
|
/** @type {string|null} */
|
|
31
32
|
#actionKind = null
|
|
32
33
|
/** @type {number} */
|
|
@@ -38,15 +39,11 @@ export default class ActionHooks {
|
|
|
38
39
|
* Creates a new ActionHook instance.
|
|
39
40
|
*
|
|
40
41
|
* @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(
|
|
44
|
-
{actionKind, hooksFile, hooksObject, hookTimeout = 1_000},
|
|
45
|
-
debug,
|
|
46
|
-
) {
|
|
43
|
+
constructor({actionKind, hooksFile, hooks, hookTimeout = 1_000, debug}) {
|
|
47
44
|
this.#actionKind = actionKind
|
|
48
45
|
this.#hooksFile = hooksFile
|
|
49
|
-
this.#
|
|
46
|
+
this.#hooks = hooks
|
|
50
47
|
this.#timeout = hookTimeout
|
|
51
48
|
this.#debug = debug
|
|
52
49
|
}
|
|
@@ -54,7 +51,7 @@ export default class ActionHooks {
|
|
|
54
51
|
/**
|
|
55
52
|
* Gets the action identifier.
|
|
56
53
|
*
|
|
57
|
-
* @returns {string
|
|
54
|
+
* @returns {string} Action identifier or instance
|
|
58
55
|
*/
|
|
59
56
|
get actionKind() {
|
|
60
57
|
return this.#actionKind
|
|
@@ -63,7 +60,7 @@ export default class ActionHooks {
|
|
|
63
60
|
/**
|
|
64
61
|
* Gets the hooks file object.
|
|
65
62
|
*
|
|
66
|
-
* @returns {FileObject
|
|
63
|
+
* @returns {FileObject} File object containing hooks
|
|
67
64
|
*/
|
|
68
65
|
get hooksFile() {
|
|
69
66
|
return this.#hooksFile
|
|
@@ -72,10 +69,10 @@ export default class ActionHooks {
|
|
|
72
69
|
/**
|
|
73
70
|
* Gets the loaded hooks object.
|
|
74
71
|
*
|
|
75
|
-
* @returns {
|
|
72
|
+
* @returns {object|null} Hooks object or null if not loaded
|
|
76
73
|
*/
|
|
77
74
|
get hooks() {
|
|
78
|
-
return this.#
|
|
75
|
+
return this.#hooks
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
/**
|
|
@@ -108,17 +105,17 @@ export default class ActionHooks {
|
|
|
108
105
|
/**
|
|
109
106
|
* Static factory method to create and initialize a hook manager.
|
|
110
107
|
* Loads hooks from the specified file and returns an initialized instance.
|
|
111
|
-
*
|
|
108
|
+
* Override loadHooks() in subclasses to customize hook loading logic.
|
|
112
109
|
*
|
|
113
|
-
* @param {ActionHooksConfig} config
|
|
110
|
+
* @param {ActionHooksConfig} config Same configuration object as constructor
|
|
114
111
|
* @param {DebugFn} debug The debug function.
|
|
115
|
-
* @returns {Promise<ActionHooks>} Initialized hook manager
|
|
112
|
+
* @returns {Promise<ActionHooks|null>} Initialized hook manager or null if no hooks found
|
|
116
113
|
*/
|
|
117
114
|
static async new(config, debug) {
|
|
118
115
|
debug("Creating new HookManager instance with args: %o", 2, config)
|
|
119
116
|
|
|
120
117
|
const instance = new ActionHooks(config, debug)
|
|
121
|
-
if(!instance.#
|
|
118
|
+
if(!instance.#hooks) {
|
|
122
119
|
const hooksFile = new FileObject(instance.#hooksFile)
|
|
123
120
|
|
|
124
121
|
debug("Loading hooks from %o", 2, hooksFile.uri)
|
|
@@ -143,7 +140,7 @@ export default class ActionHooks {
|
|
|
143
140
|
|
|
144
141
|
debug(hooks.constructor.name, 4)
|
|
145
142
|
|
|
146
|
-
instance.#
|
|
143
|
+
instance.#hooks = hooks
|
|
147
144
|
debug("Hooks %o loaded successfully for %o", 2, hooksFile.uri, instance.actionKind)
|
|
148
145
|
|
|
149
146
|
return instance
|
|
@@ -154,34 +151,31 @@ export default class ActionHooks {
|
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
153
|
|
|
157
|
-
return
|
|
154
|
+
return this
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
/**
|
|
161
|
-
* Invoke a dynamically-named hook such as `before$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.
|
|
158
|
+
* Invoke a dynamically-named hook such as `before$foo`.
|
|
164
159
|
*
|
|
165
160
|
* @param {'before'|'after'|'setup'|'cleanup'|string} kind Hook namespace.
|
|
166
161
|
* @param {string|symbol} activityName Activity identifier.
|
|
167
162
|
* @param {unknown} context Pipeline context supplied to the hook.
|
|
168
163
|
* @returns {Promise<void>}
|
|
169
|
-
* @throws {Sass} If the hook execution fails or exceeds timeout.
|
|
170
164
|
*/
|
|
171
165
|
async callHook(kind, activityName, context) {
|
|
172
|
-
|
|
173
|
-
|
|
166
|
+
try {
|
|
167
|
+
const debug = this.#debug
|
|
168
|
+
const hooks = this.#hooks
|
|
174
169
|
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
if(!hooks)
|
|
171
|
+
return
|
|
177
172
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
173
|
+
const stringActivityName = Data.isType("Symbol")
|
|
174
|
+
? activityName.description()
|
|
175
|
+
: activityName
|
|
181
176
|
|
|
182
|
-
|
|
177
|
+
const hookName = this.#getActivityHookName(kind, stringActivityName)
|
|
183
178
|
|
|
184
|
-
try {
|
|
185
179
|
debug("Looking for hook: %o", 4, hookName)
|
|
186
180
|
|
|
187
181
|
const hook = hooks[hookName]
|
|
@@ -195,7 +189,7 @@ export default class ActionHooks {
|
|
|
195
189
|
debug("Hook function starting execution: %o", 4, hookName)
|
|
196
190
|
|
|
197
191
|
const duration = (
|
|
198
|
-
await Util.time(() => hook.call(this.#
|
|
192
|
+
await Util.time(() => hook.call(this.#hooks, context))
|
|
199
193
|
).cost
|
|
200
194
|
|
|
201
195
|
debug("Hook function completed successfully: %o, after %oms", 4, hookName, duration)
|
|
@@ -214,26 +208,16 @@ export default class ActionHooks {
|
|
|
214
208
|
expireAsync
|
|
215
209
|
])
|
|
216
210
|
} catch(error) {
|
|
217
|
-
throw Sass.new(`Processing hook ${
|
|
211
|
+
throw Sass.new(`Processing hook ${kind}$${activityName}`, error)
|
|
218
212
|
}
|
|
219
213
|
|
|
220
214
|
debug("We made it throoough the wildernessss", 4)
|
|
221
215
|
|
|
222
216
|
} catch(error) {
|
|
223
|
-
throw Sass.new(`Processing hook ${
|
|
217
|
+
throw Sass.new(`Processing hook ${kind}$${activityName}`, error)
|
|
224
218
|
}
|
|
225
219
|
}
|
|
226
220
|
|
|
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
221
|
#getActivityHookName(event, activityName) {
|
|
238
222
|
const name = activityName
|
|
239
223
|
.split(" ")
|