@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/actioneer",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Ready? Set?? ACTION!! pew! pew! pew!",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -90,6 +90,10 @@ export default class ActionBuilder {
90
90
  }
91
91
  }
92
92
 
93
+ get tag() {
94
+ return this.#tag
95
+ }
96
+
93
97
  /**
94
98
  * Register an activity that the runner can execute.
95
99
  *
@@ -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, or null when a parallel stage reports failures.
59
- * @throws {Sass} When no activities are registered or required parallel builders are missing.
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
- const actionWrapper = await this.#actionBuilder.build()
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
- const kind = activity.kind
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
- context = await this.#executeActivity(activity,context)
79
+ const kind = activity.kind
92
80
 
93
- if(kindUntil)
94
- if(await this.#predicateCheck(activity,pred,context))
95
- break
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
- context = await this.#executeActivity(activity, context)
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 action wrappers when needed.
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 #executeActivity(activity, context) {
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
- const runner = new this.constructor(activity.op, {debug: this.#debug})
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
- return await runner.run(context, true)
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
- return await activity.run(context)
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 #predicateCheck(activity,predicate,context) {
220
+ async #hasPredicate(activity,predicate,context) {
140
221
  Valid.type(predicate, "Function")
141
222
 
142
223
  return !!(await predicate.call(activity.action, context))
@@ -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
- #kind = null
34
+ /** @type {((context: unknown) => boolean|Promise<boolean>)|null} */
22
35
  #pred = null
23
- #hooks = null
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 {{action: unknown, name: string, op: (context: unknown) => unknown|Promise<unknown>|unknown, kind?: number, pred?: (context: unknown) => boolean|Promise<boolean>, hooks?: ActionHooks}} init - Initial properties describing the activity operation, loop semantics, and predicate
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.#hooks = hooks
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 operator kind name (Function or ActionWrapper).
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 wrapper).
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<{activityResult: unknown}>} - Activity result wrapper with new context
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 {unknown} hooks - Hooks instance with optional before$/after$ methods
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
  }