@gesslar/actioneer 0.2.3 → 0.2.4

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 CHANGED
@@ -335,13 +335,22 @@ Examples of minimal configs and one-liners to run them are in the project discus
335
335
 
336
336
  ## Testing
337
337
 
338
- Run the small smoke tests with Node's built-in test runner:
338
+ Run the comprehensive test suite with Node's built-in test runner:
339
339
 
340
340
  ```bash
341
341
  npm test
342
342
  ```
343
343
 
344
- The test suite is intentionally small; it verifies public exports and a few core behaviors. Add more unit tests under `tests/` if you need deeper coverage.
344
+ The test suite includes 150+ tests covering all core classes and behaviors:
345
+
346
+ - **Activity** - Activity definitions, ACTIVITY flags (WHILE, UNTIL, SPLIT), and execution
347
+ - **ActionBuilder** - Fluent builder API, activity registration, and hooks configuration
348
+ - **ActionWrapper** - Activity iteration and integration with ActionBuilder
349
+ - **ActionRunner** - Pipeline execution, loop semantics, nested builders, and error handling
350
+ - **ActionHooks** - Hook lifecycle, loading from files, and timeout handling
351
+ - **Piper** - Concurrent processing, worker pools, and lifecycle hooks
352
+
353
+ Tests are organized in `tests/unit/` with one file per class. All tests use Node's native test runner and assertion library.
345
354
 
346
355
  ## Publishing
347
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/actioneer",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Ready? Set?? ACTION!! pew! pew! pew!",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import {Data, Sass, Valid} from "@gesslar/toolkit"
2
2
 
3
3
  import ActionWrapper from "./ActionWrapper.js"
4
4
  import ActionHooks from "./ActionHooks.js"
5
+ import {ACTIVITY} from "./Activity.js"
5
6
 
6
7
  /** @typedef {import("./ActionRunner.js").default} ActionRunner */
7
8
  /** @typedef {typeof import("./Activity.js").ACTIVITY} ActivityFlags */
@@ -100,6 +101,7 @@ export default class ActionBuilder {
100
101
  * Overloads:
101
102
  * - do(name, op)
102
103
  * - do(name, kind, pred, opOrWrapper)
104
+ * - do(name, kind, splitter, rejoiner, opOrWrapper)
103
105
  *
104
106
  * @overload
105
107
  * @param {string|symbol} name Activity name
@@ -116,6 +118,16 @@ export default class ActionBuilder {
116
118
  * @returns {ActionBuilder}
117
119
  */
118
120
 
121
+ /**
122
+ * @overload
123
+ * @param {string|symbol} name Activity name
124
+ * @param {number} kind ACTIVITY.SPLIT flag.
125
+ * @param {(context: unknown) => unknown} splitter Splitter function for SPLIT mode.
126
+ * @param {(originalContext: unknown, splitResults: unknown) => unknown} rejoiner Rejoiner function for SPLIT mode.
127
+ * @param {ActionFunction|import("./ActionWrapper.js").default} op Operation or nested wrapper to execute.
128
+ * @returns {ActionBuilder}
129
+ */
130
+
119
131
  /**
120
132
  * Handles runtime dispatch across the documented overloads.
121
133
  *
@@ -128,7 +140,8 @@ export default class ActionBuilder {
128
140
 
129
141
  // signatures
130
142
  // name, [function] => once
131
- // name, [number,function,function] => some kind of control operation
143
+ // name, [number,function,function] => some kind of control operation (WHILE/UNTIL)
144
+ // name, [number,function,function,function] => SPLIT operation with splitter/rejoiner
132
145
  // name, [number,function,ActionBuilder] => some kind of branch
133
146
 
134
147
  const action = this.#action
@@ -149,6 +162,19 @@ export default class ActionBuilder {
149
162
  Valid.type(op, "Function|ActionBuilder")
150
163
 
151
164
  Object.assign(activityDefinition, {kind, pred, op})
165
+ } else if(args.length === 4) {
166
+ const [kind, splitter, rejoiner, op] = args
167
+
168
+ Valid.type(kind, "Number")
169
+ Valid.type(splitter, "Function")
170
+ Valid.type(rejoiner, "Function")
171
+ Valid.type(op, "Function|ActionBuilder")
172
+
173
+ // Validate that kind is SPLIT
174
+ if((kind & ACTIVITY.SPLIT) !== ACTIVITY.SPLIT)
175
+ throw Sass.new("4-argument form of 'do' is only valid for ACTIVITY.SPLIT")
176
+
177
+ Object.assign(activityDefinition, {kind, splitter, rejoiner, op})
152
178
  } else {
153
179
  throw Sass.new("Invalid number of arguments passed to 'do'")
154
180
  }
@@ -182,9 +208,14 @@ export default class ActionBuilder {
182
208
  *
183
209
  * @param {import("./ActionHooks.js").default} hooks An already-instantiated hooks instance.
184
210
  * @returns {ActionBuilder} The builder instance for chaining.
185
- * @throws {Sass} If hooks have already been configured.
211
+ * @throws {Sass} If hooks have already been configured with a different instance.
186
212
  */
187
213
  withHooks(hooks) {
214
+ // If the same hooks instance is already set, this is idempotent - just return
215
+ if(this.#hooks === hooks) {
216
+ return this
217
+ }
218
+
188
219
  Valid.assert(this.#hooksFile === null, "Hooks have already been configured.")
189
220
  Valid.assert(this.#hooksKind === null, "Hooks have already been configured.")
190
221
  Valid.assert(this.#hooks === null, "Hooks have already been configured.")
@@ -236,8 +267,14 @@ export default class ActionBuilder {
236
267
  const newHooks = ActionHooks.new
237
268
 
238
269
  const hooks = this.#hooks
239
- if(hooks)
270
+ if(hooks) {
271
+ // If hooks is already an ActionHooks instance, use it directly
272
+ if(hooks instanceof ActionHooks)
273
+ return hooks
274
+
275
+ // Otherwise, wrap it in a new ActionHooks instance
240
276
  return await newHooks({hooks}, this.#debug)
277
+ }
241
278
 
242
279
  const hooksFile = this.#hooksFile
243
280
  const hooksKind = this.#hooksKind
@@ -114,7 +114,7 @@ export default class ActionHooks {
114
114
  static async new(config, debug) {
115
115
  debug("Creating new HookManager instance with args: %o", 2, config)
116
116
 
117
- const instance = new ActionHooks(config, debug)
117
+ const instance = new ActionHooks({...config, debug})
118
118
  if(!instance.#hooks) {
119
119
  const hooksFile = new FileObject(instance.#hooksFile)
120
120
 
@@ -151,7 +151,7 @@ export default class ActionHooks {
151
151
  }
152
152
  }
153
153
 
154
- return this
154
+ return instance
155
155
  }
156
156
 
157
157
  /**
@@ -170,8 +170,8 @@ export default class ActionHooks {
170
170
  if(!hooks)
171
171
  return
172
172
 
173
- const stringActivityName = Data.isType("Symbol")
174
- ? activityName.description()
173
+ const stringActivityName = Data.isType(activityName, "Symbol")
174
+ ? activityName.description
175
175
  : activityName
176
176
 
177
177
  const hookName = this.#getActivityHookName(kind, stringActivityName)
@@ -112,7 +112,7 @@ export default class ActionRunner extends Piper {
112
112
  if(await this.#hasPredicate(activity,predicate,context))
113
113
  break
114
114
  }
115
- } else if(kindSplit && activity.opKind === "ActionBuilder") {
115
+ } else if(kindSplit) {
116
116
  // SPLIT activity: parallel execution with splitter/rejoiner pattern
117
117
  const splitter = activity.splitter
118
118
  const rejoiner = activity.rejoiner
@@ -123,8 +123,19 @@ export default class ActionRunner extends Piper {
123
123
  )
124
124
 
125
125
  const original = context
126
- const splitContext = splitter.call(activity.action,context)
127
- const newContext = await this.#execute(activity,splitContext,true)
126
+ const splitContexts = splitter.call(activity.action,context)
127
+
128
+ let newContext
129
+ if(activity.opKind === "ActionBuilder") {
130
+ // Use parallel execution for ActionBuilder
131
+ newContext = await this.#execute(activity,splitContexts,true)
132
+ } else {
133
+ // For plain functions, process each split context
134
+ newContext = await Promise.all(
135
+ splitContexts.map(ctx => this.#execute(activity,ctx))
136
+ )
137
+ }
138
+
128
139
  const rejoined = rejoiner.call(activity.action, original,newContext)
129
140
 
130
141
  context = rejoined
@@ -147,7 +158,7 @@ export default class ActionRunner extends Piper {
147
158
  *
148
159
  * When parallel=true, uses Piper.pipe() for concurrent execution with worker pool pattern.
149
160
  * 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}).
161
+ * Results from parallel execution are returned directly as an array from Piper.pipe().
151
162
  *
152
163
  * @param {import("./Activity.js").default} activity Pipeline activity descriptor.
153
164
  * @param {unknown} context Current pipeline context.
@@ -162,17 +173,15 @@ export default class ActionRunner extends Piper {
162
173
  const opKind = activity.opKind
163
174
 
164
175
  if(opKind === "ActionBuilder") {
165
- if(activity.hooks && !activity.op.hasActionHooks)
166
- activity.op.withActionHooks(activity.hooks)
176
+ if(activity.hooks)
177
+ activity.op.withHooks(activity.hooks)
167
178
 
168
179
  const runner = new this.constructor(activity.op, {
169
180
  debug: this.#debug, name: activity.name
170
181
  })
171
182
 
172
183
  if(parallel) {
173
- const piped = await runner.pipe(context)
174
-
175
- return piped.filter(p => p.ok).map(p => p.value)
184
+ return await runner.pipe(context)
176
185
  } else {
177
186
  return await runner.run(context)
178
187
  }
@@ -182,16 +191,14 @@ export default class ActionRunner extends Piper {
182
191
 
183
192
  if(Data.isType(result, "ActionBuilder")) {
184
193
  if(activity.hooks)
185
- result.withActionHooks(activity.hooks)
194
+ result.withHooks(activity.hooks)
186
195
 
187
196
  const runner = new this.constructor(result, {
188
197
  debug: this.#debug, name: result.name
189
198
  })
190
199
 
191
200
  if(parallel) {
192
- const piped = await runner.pipe(context)
193
-
194
- return piped.filter(p => p.ok).map(p => p.value)
201
+ return await runner.pipe(context)
195
202
  } else {
196
203
  return await runner.run(context)
197
204
  }
@@ -89,7 +89,7 @@ export default class ActionBuilder {
89
89
  *
90
90
  * @param {import("./ActionHooks.js").default} hooks An already-instantiated hooks instance.
91
91
  * @returns {ActionBuilder} The builder instance for chaining.
92
- * @throws {Sass} If hooks have already been configured.
92
+ * @throws {Sass} If hooks have already been configured with a different instance.
93
93
  */
94
94
  withHooks(hooks: import('./ActionHooks.js').default): ActionBuilder
95
95
  /**