@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 +1 -1
- package/src/lib/ActionBuilder.js +106 -22
- package/src/lib/ActionHooks.js +46 -30
- package/src/lib/ActionRunner.js +123 -42
- package/src/lib/ActionWrapper.js +17 -7
- package/src/lib/Activity.js +74 -12
- package/src/lib/Piper.js +105 -55
- package/src/types/lib/ActionBuilder.d.ts +42 -2
- 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 +4 -2
- 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 +54 -19
- 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/package.json
CHANGED
package/src/lib/ActionBuilder.js
CHANGED
|
@@ -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,
|
|
158
|
+
const activityDefinition = {name,action,debug}
|
|
133
159
|
|
|
134
160
|
if(args.length === 1) {
|
|
135
|
-
const [op,
|
|
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,
|
|
165
|
+
Object.assign(activityDefinition, {op,kind})
|
|
140
166
|
} else if(args.length === 3) {
|
|
141
|
-
const [kind,
|
|
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,
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
322
|
+
if(hooksFile && hooksKind) {
|
|
323
|
+
this.#actionHooks = await newHooks({hooksFile,hooksKind}, this.#debug)
|
|
324
|
+
|
|
325
|
+
return this.#actionHooks
|
|
326
|
+
}
|
|
243
327
|
}
|
|
244
328
|
}
|
package/src/lib/ActionHooks.js
CHANGED
|
@@ -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} [
|
|
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
|
-
#
|
|
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(
|
|
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,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
|
-
*
|
|
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
120
|
const instance = new ActionHooks(config, debug)
|
|
118
|
-
if(!instance.#
|
|
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.#
|
|
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
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
const hooks = this.#hooks
|
|
172
|
+
const debug = this.#debug
|
|
173
|
+
const hooks = this.#hooksObject
|
|
169
174
|
|
|
170
|
-
|
|
171
|
-
|
|
175
|
+
if(!hooks)
|
|
176
|
+
return
|
|
172
177
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
const stringActivityName = Data.isType(activityName, "Symbol")
|
|
179
|
+
? activityName.description()
|
|
180
|
+
: activityName
|
|
176
181
|
|
|
177
|
-
|
|
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.#
|
|
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 ${
|
|
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 ${
|
|
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(" ")
|
package/src/lib/ActionRunner.js
CHANGED
|
@@ -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
|
|
59
|
-
* @throws {Sass} When no activities are registered
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
const kind = activity.kind
|
|
92
80
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
|
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 #
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 #
|
|
220
|
+
async #hasPredicate(activity,predicate,context) {
|
|
140
221
|
Valid.type(predicate, "Function")
|
|
141
222
|
|
|
142
223
|
return !!(await predicate.call(activity.action, context))
|
package/src/lib/ActionWrapper.js
CHANGED
|
@@ -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>|
|
|
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 {
|
|
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(
|
|
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
|
|