@gesslar/actioneer 0.2.2 → 0.2.3
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 +4 -0
- package/src/lib/ActionRunner.js +123 -42
- package/src/lib/Activity.js +74 -12
package/package.json
CHANGED
package/src/lib/ActionBuilder.js
CHANGED
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/Activity.js
CHANGED
|
@@ -8,32 +8,58 @@ import {Data} from "@gesslar/toolkit"
|
|
|
8
8
|
*
|
|
9
9
|
* @readonly
|
|
10
10
|
* @enum {number}
|
|
11
|
+
* @property {number} WHILE - Execute activity while predicate returns true (2)
|
|
12
|
+
* @property {number} UNTIL - Execute activity until predicate returns true (4)
|
|
13
|
+
* @property {number} SPLIT - Execute activity with split/rejoin pattern for parallel execution (8)
|
|
11
14
|
*/
|
|
12
15
|
export const ACTIVITY = Object.freeze({
|
|
13
16
|
WHILE: 1<<1,
|
|
14
17
|
UNTIL: 1<<2,
|
|
18
|
+
SPLIT: 1<<3,
|
|
15
19
|
})
|
|
16
20
|
|
|
17
21
|
export default class Activity {
|
|
22
|
+
/** @type {unknown} */
|
|
18
23
|
#action = null
|
|
24
|
+
/** @type {unknown} */
|
|
25
|
+
#context = null
|
|
26
|
+
/** @type {ActionHooks|null} */
|
|
27
|
+
#hooks = null
|
|
28
|
+
/** @type {number|null} */
|
|
29
|
+
#kind = null
|
|
30
|
+
/** @type {string|symbol} */
|
|
19
31
|
#name = null
|
|
32
|
+
/** @type {((context: unknown) => unknown|Promise<unknown>)|import("./ActionBuilder.js").default} */
|
|
20
33
|
#op = null
|
|
21
|
-
|
|
34
|
+
/** @type {((context: unknown) => boolean|Promise<boolean>)|null} */
|
|
22
35
|
#pred = null
|
|
23
|
-
|
|
36
|
+
/** @type {((originalContext: unknown, splitResults: unknown) => unknown)|null} */
|
|
37
|
+
#rejoiner = null
|
|
38
|
+
/** @type {((context: unknown) => unknown)|null} */
|
|
39
|
+
#splitter = null
|
|
24
40
|
|
|
25
41
|
/**
|
|
26
42
|
* Construct an Activity definition wrapper.
|
|
27
43
|
*
|
|
28
|
-
* @param {
|
|
44
|
+
* @param {object} init - Initial properties describing the activity operation, loop semantics, and predicate
|
|
45
|
+
* @param {unknown} init.action - Parent action instance
|
|
46
|
+
* @param {string|symbol} init.name - Activity identifier
|
|
47
|
+
* @param {(context: unknown) => unknown|Promise<unknown>|import("./ActionBuilder.js").default} init.op - Operation to execute
|
|
48
|
+
* @param {number} [init.kind] - Optional loop semantics flags
|
|
49
|
+
* @param {(context: unknown) => boolean|Promise<boolean>} [init.pred] - Optional predicate for WHILE/UNTIL
|
|
50
|
+
* @param {ActionHooks} [init.hooks] - Optional hooks instance
|
|
51
|
+
* @param {(context: unknown) => unknown} [init.splitter] - Optional splitter function for SPLIT activities
|
|
52
|
+
* @param {(originalContext: unknown, splitResults: unknown) => unknown} [init.rejoiner] - Optional rejoiner function for SPLIT activities
|
|
29
53
|
*/
|
|
30
|
-
constructor({action,name,op,kind,pred,hooks}) {
|
|
54
|
+
constructor({action,name,op,kind,pred,hooks,splitter,rejoiner}) {
|
|
55
|
+
this.#action = action
|
|
56
|
+
this.#hooks = hooks
|
|
57
|
+
this.#kind = kind
|
|
31
58
|
this.#name = name
|
|
32
59
|
this.#op = op
|
|
33
|
-
this.#kind = kind
|
|
34
|
-
this.#action = action
|
|
35
60
|
this.#pred = pred
|
|
36
|
-
this.#
|
|
61
|
+
this.#rejoiner = rejoiner
|
|
62
|
+
this.#splitter = splitter
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
/**
|
|
@@ -64,7 +90,16 @@ export default class Activity {
|
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
/**
|
|
67
|
-
* The
|
|
93
|
+
* The current context (if set).
|
|
94
|
+
*
|
|
95
|
+
* @returns {unknown} Current context value
|
|
96
|
+
*/
|
|
97
|
+
get context() {
|
|
98
|
+
return this.#context
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The operator kind name (Function or ActionBuilder).
|
|
68
103
|
*
|
|
69
104
|
* @returns {string} - Kind name extracted via Data.typeOf
|
|
70
105
|
*/
|
|
@@ -73,14 +108,32 @@ export default class Activity {
|
|
|
73
108
|
}
|
|
74
109
|
|
|
75
110
|
/**
|
|
76
|
-
* The operator to execute (function or nested
|
|
111
|
+
* The operator to execute (function or nested ActionBuilder).
|
|
77
112
|
*
|
|
78
|
-
* @returns {unknown} - Activity operation
|
|
113
|
+
* @returns {(context: unknown) => unknown|Promise<unknown>|import("./ActionBuilder.js").default} - Activity operation
|
|
79
114
|
*/
|
|
80
115
|
get op() {
|
|
81
116
|
return this.#op
|
|
82
117
|
}
|
|
83
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The splitter function for SPLIT activities.
|
|
121
|
+
*
|
|
122
|
+
* @returns {((context: unknown) => unknown)|null} Splitter function or null
|
|
123
|
+
*/
|
|
124
|
+
get splitter() {
|
|
125
|
+
return this.#splitter
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The rejoiner function for SPLIT activities.
|
|
130
|
+
*
|
|
131
|
+
* @returns {((originalContext: unknown, splitResults: unknown) => unknown)|null} Rejoiner function or null
|
|
132
|
+
*/
|
|
133
|
+
get rejoiner() {
|
|
134
|
+
return this.#rejoiner
|
|
135
|
+
}
|
|
136
|
+
|
|
84
137
|
/**
|
|
85
138
|
* The action instance this activity belongs to.
|
|
86
139
|
*
|
|
@@ -94,7 +147,7 @@ export default class Activity {
|
|
|
94
147
|
* Execute the activity with before/after hooks.
|
|
95
148
|
*
|
|
96
149
|
* @param {unknown} context - Mutable context flowing through the pipeline
|
|
97
|
-
* @returns {Promise<
|
|
150
|
+
* @returns {Promise<unknown>} - Activity result
|
|
98
151
|
*/
|
|
99
152
|
async run(context) {
|
|
100
153
|
// before hook
|
|
@@ -112,7 +165,7 @@ export default class Activity {
|
|
|
112
165
|
/**
|
|
113
166
|
* Attach hooks to this activity instance.
|
|
114
167
|
*
|
|
115
|
-
* @param {
|
|
168
|
+
* @param {ActionHooks} hooks - Hooks instance with optional before$/after$ methods
|
|
116
169
|
* @returns {this} - This activity for chaining
|
|
117
170
|
*/
|
|
118
171
|
setActionHooks(hooks) {
|
|
@@ -121,4 +174,13 @@ export default class Activity {
|
|
|
121
174
|
|
|
122
175
|
return this
|
|
123
176
|
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get the hooks instance attached to this activity.
|
|
180
|
+
*
|
|
181
|
+
* @returns {ActionHooks|null} The hooks instance or null
|
|
182
|
+
*/
|
|
183
|
+
get hooks() {
|
|
184
|
+
return this.#hooks
|
|
185
|
+
}
|
|
124
186
|
}
|