@gesslar/actioneer 2.2.0 → 2.4.0

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.
@@ -1,17 +1,17 @@
1
- import {Promised, Data, Sass, Valid, Notify} from "@gesslar/toolkit"
1
+ import {Promised, Data, Sass, Tantrum, Valid} from "@gesslar/toolkit"
2
2
 
3
3
  import {ACTIVITY} from "./Activity.js"
4
4
  import Piper from "./Piper.js"
5
5
 
6
6
  /**
7
- * @typedef {(message: string, level?: number, ...args: Array<unknown>) => void} DebugFn
8
- */
9
-
10
- /**
11
- * @typedef {import("./ActionBuilder.js").default} ActionBuilder
7
+ * Types
8
+ *
9
+ * @import {default as ActionBuilder} from "./ActionBuilder.js"
10
+ * @import {default as ActionWrapper} from "./ActionWrapper.js"
12
11
  */
13
-
14
12
  /**
13
+ * @typedef {(message: string, level?: number, ...args: Array<unknown>) => void} DebugFn
14
+ *
15
15
  * @typedef {object} ActionRunnerOptions
16
16
  * @property {DebugFn} [debug] Logger function.
17
17
  */
@@ -24,9 +24,9 @@ import Piper from "./Piper.js"
24
24
  * context object under `result.value` that can be replaced or enriched.
25
25
  */
26
26
  export default class ActionRunner extends Piper {
27
- /** @type {import("./ActionBuilder.js").default|null} */
27
+ /** @type {ActionBuilder?} */
28
28
  #actionBuilder = null
29
- /** @type {import("./ActionWrapper.js").default|null} */
29
+ /** @type {ActionWrapper?} */
30
30
  #actionWrapper = null
31
31
 
32
32
  /**
@@ -36,13 +36,6 @@ export default class ActionRunner extends Piper {
36
36
  */
37
37
  #debug = () => {}
38
38
 
39
- /**
40
- * Event emitter for cross-runner communication (BREAK/CONTINUE signals).
41
- *
42
- * @type {typeof Notify}
43
- */
44
- #notify = Notify
45
-
46
39
  /**
47
40
  * Instantiate a runner over an optional action wrapper.
48
41
  *
@@ -62,9 +55,45 @@ export default class ActionRunner extends Piper {
62
55
 
63
56
  this.#actionBuilder = actionBuilder
64
57
 
58
+ this.addSetup(this.#setupHooks)
65
59
  this.addStep(this.run, {
66
60
  name: `ActionRunner for ${actionBuilder.tag.description}`
67
61
  })
62
+ this.addCleanup(this.#cleanupHooks)
63
+ }
64
+
65
+ /**
66
+ * Invokes the `setup` lifecycle hook on the raw hooks object, if defined.
67
+ * Registered as a Piper setup step so it fires before any items are processed.
68
+ *
69
+ * @param {unknown} ctx - Value passed by {@link Piper#pipe} (the items array).
70
+ * @returns {Promise<void>}
71
+ * @private
72
+ */
73
+ async #setupHooks(ctx) {
74
+ const ab = this.#actionBuilder
75
+ const ah = ab?.hooks
76
+ const setup = ah?.setup
77
+
78
+ if(setup)
79
+ await setup.call(ah, ctx)
80
+ }
81
+
82
+ /**
83
+ * Invokes the `cleanup` lifecycle hook on the raw hooks object, if defined.
84
+ * Registered as a Piper teardown step so it fires after all items are processed.
85
+ *
86
+ * @param {unknown} ctx - Value passed by {@link Piper#pipe} (the items array).
87
+ * @returns {Promise<void>}
88
+ * @private
89
+ */
90
+ async #cleanupHooks(ctx) {
91
+ const ab = this.#actionBuilder
92
+ const ah = ab?.hooks
93
+ const cleanup = ah?.cleanup
94
+
95
+ if(cleanup)
96
+ await cleanup.call(ah, ctx)
68
97
  }
69
98
 
70
99
  /**
@@ -76,14 +105,17 @@ export default class ActionRunner extends Piper {
76
105
  * @param {import("./ActionWrapper.js").default|null} [parentWrapper] - Parent wrapper for BREAK/CONTINUE signaling.
77
106
  * @returns {Promise<unknown>} Final value produced by the pipeline.
78
107
  * @throws {Sass} When no activities are registered, conflicting activity kinds are used, or execution fails.
108
+ * @throws {Tantrum} When both an activity and the done callback fail.
79
109
  */
80
110
  async run(context, parentWrapper=null) {
81
111
  if(!this.#actionWrapper)
82
- this.#actionWrapper = await this.#actionBuilder.build()
112
+ this.#actionWrapper = await this.#actionBuilder.build(this)
83
113
 
84
114
  const actionWrapper = this.#actionWrapper
85
115
  const activities = Array.from(actionWrapper.activities)
86
116
 
117
+ let caughtError = null
118
+
87
119
  try {
88
120
  for(
89
121
  let cursor = 0, max = activities.length;
@@ -114,7 +146,7 @@ export default class ActionRunner extends Piper {
114
146
 
115
147
  if(await this.#evalPredicate(activity, context)) {
116
148
  if(kindBreak) {
117
- this.#notify.emit("loop.break", parentWrapper)
149
+ this.emit("loop.break", parentWrapper)
118
150
  break
119
151
  }
120
152
 
@@ -132,7 +164,7 @@ export default class ActionRunner extends Piper {
132
164
  break
133
165
 
134
166
  let weWereOnABreak = false
135
- const breakReceiver = this.#notify.on("loop.break", wrapper => {
167
+ const breakReceiver = this.on("loop.break", wrapper => {
136
168
  if(wrapper.id === actionWrapper.id) {
137
169
  weWereOnABreak = true
138
170
  }
@@ -158,10 +190,7 @@ export default class ActionRunner extends Piper {
158
190
  )
159
191
 
160
192
  const original = context
161
- const splitContexts = await splitter.call(
162
- activity.action,
163
- context
164
- )
193
+ const splitContexts = await splitter.call(activity.action,context)
165
194
 
166
195
  let settled
167
196
 
@@ -199,20 +228,28 @@ export default class ActionRunner extends Piper {
199
228
  throw Sass.new("ActionRunner running activity", error)
200
229
  }
201
230
  }
202
- } finally {
203
- // Execute done callback if registered - always runs, even on error
204
- // Only run for top-level pipelines, not nested builders (inside loops)
205
- if(actionWrapper.done && !parentWrapper) {
206
- try {
207
- context = await actionWrapper.done.call(
208
- actionWrapper.action, context
209
- )
210
- } catch(error) {
211
- throw Sass.new("ActionRunner running done callback", error)
212
- }
231
+ } catch(err) {
232
+ caughtError = err
233
+ }
234
+
235
+ // Execute done callback if registered - always runs, even on error
236
+ // Only run for top-level pipelines, not nested builders (inside loops)
237
+ if(actionWrapper.done && !parentWrapper) {
238
+ try {
239
+ context = await actionWrapper.done.call(
240
+ actionWrapper.action, caughtError ?? context
241
+ )
242
+ } catch(error) {
243
+ if(caughtError)
244
+ caughtError = new Tantrum("ActionRunner running done callback", [caughtError, error])
245
+ else
246
+ caughtError = Sass.new("ActionRunner running done callback", error)
213
247
  }
214
248
  }
215
249
 
250
+ if(caughtError)
251
+ throw caughtError
252
+
216
253
  return context
217
254
  }
218
255
 
@@ -248,10 +285,20 @@ export default class ActionRunner extends Piper {
248
285
  debug: this.#debug, name: activity.name
249
286
  })
250
287
 
251
- if(parallel) {
252
- return await runner.pipe(context)
253
- } else {
254
- return await runner.run(context, activity.wrapper)
288
+ // Forward loop.break events from nested runner to this runner
289
+ // so that parent WHILE/UNTIL loops can receive break signals.
290
+ const forwarder = runner.on("loop.break",
291
+ wrapper => this.emit("loop.break", wrapper)
292
+ )
293
+
294
+ try {
295
+ if(parallel) {
296
+ return await runner.pipe(context)
297
+ } else {
298
+ return await runner.run(context, activity.wrapper)
299
+ }
300
+ } finally {
301
+ forwarder()
255
302
  }
256
303
  } else if(opKind === "Function") {
257
304
  try {
@@ -281,8 +328,6 @@ export default class ActionRunner extends Piper {
281
328
  }
282
329
  }
283
330
 
284
- console.log(activity.opKind + " " + JSON.stringify(activity))
285
-
286
331
  throw Sass.new("We buy Functions and ActionBuilders. Only. Not whatever that was.")
287
332
  }
288
333
 
@@ -1,5 +1,11 @@
1
1
  import Activity from "./Activity.js"
2
2
 
3
+ /**
4
+ * Type imports
5
+ *
6
+ * @import {default as ActionHooks} from "./ActionHooks.js"
7
+ */
8
+
3
9
  /**
4
10
  * @typedef {object} WrappedActivityConfig
5
11
  * @property {string|symbol} name Activity identifier used by hooks/logs.
@@ -10,10 +16,6 @@ import Activity from "./Activity.js"
10
16
  * @property {(message: string, level?: number, ...args: Array<unknown>) => void} [debug] Optional logger reference.
11
17
  */
12
18
 
13
- /**
14
- * @typedef {import("@gesslar/toolkit").Generator<Activity, void, unknown>} ActivityIterator
15
- */
16
-
17
19
  /**
18
20
  * Thin wrapper that materialises {@link Activity} instances on demand.
19
21
  */
@@ -32,7 +34,7 @@ export default class ActionWrapper {
32
34
  */
33
35
  #debug = () => {}
34
36
 
35
- /** @type {import("./ActionHooks.js").default|null} */
37
+ /** @type {ActionHooks} */
36
38
  #hooks = null
37
39
  /** @type {((context: unknown) => unknown|Promise<unknown>)|null} */
38
40
  #done = null
@@ -44,7 +46,12 @@ export default class ActionWrapper {
44
46
  /**
45
47
  * Create a wrapper from the builder payload.
46
48
  *
47
- * @param {{activities: Map<string|symbol, WrappedActivityConfig>, debug: (message: string, level?: number, ...args: Array<unknown>) => void}} init Builder payload containing activities + logger.
49
+ * @param {object} init - Builder payload.
50
+ * @param {Map<string|symbol, WrappedActivityConfig>} init.activities - Registered activities.
51
+ * @param {(message: string, level?: number, ...args: Array<unknown>) => void} init.debug - Logger.
52
+ * @param {import("./ActionHooks.js").default?} init.hooks - Optional hooks instance.
53
+ * @param {((context: unknown) => unknown|Promise<unknown>)|null} [init.done] - Optional done callback.
54
+ * @param {unknown} [init.action] - Optional parent action instance.
48
55
  */
49
56
  constructor({activities,hooks,debug,done: doneCallback,action}) {
50
57
  this.#debug = debug
@@ -1,6 +1,10 @@
1
1
  import {Data} from "@gesslar/toolkit"
2
2
 
3
- /** @typedef {import("./ActionHooks.js").default} ActionHooks */
3
+ /**
4
+ * @import {default as ActionBuilder} from "./ActionBuilder.js"
5
+ * @import {default as ActionHooks} from "./ActionHooks.js"
6
+ * @import {default as ActionWrapper} from "./ActionWrapper.js"
7
+ **/
4
8
 
5
9
  /**
6
10
  * Activity bit flags recognised by the builder. The flag decides
@@ -29,13 +33,13 @@ export default class Activity {
29
33
  #action = null
30
34
  /** @type {unknown} */
31
35
  #context = null
32
- /** @type {ActionHooks|null} */
36
+ /** @type {ActionHooks?} */
33
37
  #hooks = null
34
- /** @type {number|null} */
38
+ /** @type {number?} */
35
39
  #kind = null
36
40
  /** @type {string|symbol} */
37
41
  #name = null
38
- /** @type {((context: unknown) => unknown|Promise<unknown>)|import("./ActionBuilder.js").default} */
42
+ /** @type {((context: unknown) => unknown|Promise<unknown>)|ActionBuilder} */
39
43
  #op = null
40
44
  /** @type {((context: unknown) => boolean|Promise<boolean>)|null} */
41
45
  #pred = null
@@ -43,7 +47,7 @@ export default class Activity {
43
47
  #rejoiner = null
44
48
  /** @type {((context: unknown) => unknown)|null} */
45
49
  #splitter = null
46
- /** @type {import("./ActionWrapper.js").default|null} */
50
+ /** @type {ActionWrapper?} */
47
51
  #wrapper = null
48
52
  /** @type {symbol} */
49
53
  #id = Symbol(performance.now())
@@ -54,13 +58,13 @@ export default class Activity {
54
58
  * @param {object} init - Initial properties describing the activity operation, loop semantics, and predicate
55
59
  * @param {unknown} init.action - Parent action instance
56
60
  * @param {string|symbol} init.name - Activity identifier
57
- * @param {(context: unknown) => unknown|Promise<unknown>|import("./ActionBuilder.js").default} init.op - Operation to execute
61
+ * @param {(context: unknown) => unknown|Promise<unknown>|ActionBuilder} init.op - Operation to execute
58
62
  * @param {number} [init.kind] - Optional loop semantics flags
59
63
  * @param {(context: unknown) => boolean|Promise<boolean>} [init.pred] - Optional predicate for WHILE/UNTIL
60
64
  * @param {ActionHooks} [init.hooks] - Optional hooks instance
61
65
  * @param {(context: unknown) => unknown} [init.splitter] - Optional splitter function for SPLIT activities
62
66
  * @param {(originalContext: unknown, splitResults: unknown) => unknown} [init.rejoiner] - Optional rejoiner function for SPLIT activities
63
- * @param {import("./ActionWrapper.js").default} [init.wrapper] - Optional wrapper containing this activity
67
+ * @param {ActionWrapper} [init.wrapper] - Optional wrapper containing this activity
64
68
  */
65
69
  constructor({action,name,op,kind,pred,hooks,splitter,rejoiner,wrapper}) {
66
70
  this.#action = action
@@ -131,7 +135,7 @@ export default class Activity {
131
135
  /**
132
136
  * The operator to execute (function or nested ActionBuilder).
133
137
  *
134
- * @returns {(context: unknown) => unknown|Promise<unknown>|import("./ActionBuilder.js").default} - Activity operation
138
+ * @returns {(context: unknown) => unknown|Promise<unknown>|ActionBuilder} - Activity operation
135
139
  */
136
140
  get op() {
137
141
  return this.#op
@@ -140,7 +144,7 @@ export default class Activity {
140
144
  /**
141
145
  * The splitter function for SPLIT activities.
142
146
  *
143
- * @returns {((context: unknown) => unknown)|null} Splitter function or null
147
+ * @returns {((context: unknown) => unknown)?} Splitter function or null
144
148
  */
145
149
  get splitter() {
146
150
  return this.#splitter
@@ -149,7 +153,7 @@ export default class Activity {
149
153
  /**
150
154
  * The rejoiner function for SPLIT activities.
151
155
  *
152
- * @returns {((originalContext: unknown, splitResults: unknown) => unknown)|null} Rejoiner function or null
156
+ * @returns {((originalContext: unknown, splitResults: unknown) => unknown)?} Rejoiner function or null
153
157
  */
154
158
  get rejoiner() {
155
159
  return this.#rejoiner
@@ -168,7 +172,7 @@ export default class Activity {
168
172
  * Get the ActionWrapper containing this activity.
169
173
  * Used by BREAK/CONTINUE to signal the parent loop.
170
174
  *
171
- * @returns {import("./ActionWrapper.js").default|null} The wrapper or null
175
+ * @returns {ActionWrapper?} The wrapper or null
172
176
  */
173
177
  get wrapper() {
174
178
  return this.#wrapper ?? null
@@ -188,7 +192,7 @@ export default class Activity {
188
192
  const result = await this.#op.call(this.#action, context)
189
193
 
190
194
  // after hook
191
- await this.#hooks?.callHook("after", this.#name, context)
195
+ await this.#hooks?.callHook("after", this.#name, result, context)
192
196
 
193
197
  return result
194
198
  }
@@ -209,7 +213,7 @@ export default class Activity {
209
213
  /**
210
214
  * Get the hooks instance attached to this activity.
211
215
  *
212
- * @returns {ActionHooks|null} The hooks instance or null
216
+ * @returns {ActionHooks?} The hooks instance or null
213
217
  */
214
218
  get hooks() {
215
219
  return this.#hooks
@@ -9,10 +9,16 @@
9
9
  * - Error handling and reporting
10
10
  */
11
11
 
12
- import {Promised, Sass, Tantrum} from "@gesslar/toolkit"
12
+ import {Data, Disposer, NotifyClass, Promised, Sass} from "@gesslar/toolkit"
13
13
 
14
- export default class Piper {
14
+ /**
15
+ * @import {Tantrum} from "@gesslar/toolkit"
16
+ */
17
+
18
+ export default class Piper extends NotifyClass {
15
19
  #debug
20
+ #disposer = Disposer
21
+ #abortedReason
16
22
 
17
23
  #lifeCycle = new Map([
18
24
  ["setup", new Set()],
@@ -23,18 +29,32 @@ export default class Piper {
23
29
  /**
24
30
  * Create a Piper instance.
25
31
  *
26
- * @param {{debug?: (message: string, level?: number, ...args: Array<unknown>) => void}} [config] Optional configuration with debug function
32
+ * @param {{debug?: (message: string, level?: number, ...args: Array<unknown>) => void}} [config] - Optional configuration with debug function
27
33
  */
28
34
  constructor({debug = (() => {})} = {}) {
35
+ super()
36
+
29
37
  this.#debug = debug
38
+
39
+ this.#disposer.register(
40
+ this.on("abort", this.#abortCalled.bind(this))
41
+ )
42
+ }
43
+
44
+ #abortCalled(reason) {
45
+ this.#abortedReason = reason
46
+ }
47
+
48
+ get reason() {
49
+ return this.#abortedReason
30
50
  }
31
51
 
32
52
  /**
33
53
  * Add a processing step to the pipeline
34
54
  *
35
- * @param {(context: unknown) => Promise<unknown>|unknown} fn Function that processes an item
36
- * @param {{name?: string, required?: boolean}} [options] Step options
37
- * @param {unknown} [newThis] Optional this binding
55
+ * @param {(context: unknown) => Promise<unknown>|unknown} fn - Function that processes an item
56
+ * @param {{name?: string, required?: boolean}} [options] - Step options
57
+ * @param {unknown} [newThis] - Optional this binding
38
58
  * @returns {Piper} The pipeline instance (for chaining)
39
59
  */
40
60
  addStep(fn, options = {}, newThis) {
@@ -51,7 +71,7 @@ export default class Piper {
51
71
  /**
52
72
  * Add setup hook that runs before processing starts.
53
73
  *
54
- * @param {() => Promise<void>|void} fn - Setup function executed before processing
74
+ * @param {(items: Array<unknown>) => Promise<void>|void} fn - Setup function executed before processing; receives the full items array.
55
75
  * @param {unknown} [thisArg] - Optional this binding for the setup function
56
76
  * @returns {Piper} - The pipeline instance
57
77
  */
@@ -64,7 +84,7 @@ export default class Piper {
64
84
  /**
65
85
  * Add cleanup hook that runs after processing completes
66
86
  *
67
- * @param {() => Promise<void>|void} fn - Cleanup function executed after processing
87
+ * @param {(items: Array<unknown>) => Promise<void>|void} fn - Cleanup function executed after processing; receives the full items array.
68
88
  * @param {unknown} [thisArg] - Optional this binding for the cleanup function
69
89
  * @returns {Piper} - The pipeline instance
70
90
  */
@@ -90,7 +110,7 @@ export default class Piper {
90
110
  const allResults = new Array(items.length)
91
111
 
92
112
  const processWorker = async() => {
93
- while(true) {
113
+ while(true && !this.reason) {
94
114
  const currentIndex = itemIndex++
95
115
 
96
116
  if(currentIndex >= items.length)
@@ -101,7 +121,10 @@ export default class Piper {
101
121
  try {
102
122
  const result = await this.#processItem(item)
103
123
 
104
- allResults[currentIndex] = {status: "fulfilled", value: result}
124
+ if(Data.isType(result, "Error"))
125
+ allResults[currentIndex] = {status: "rejected", reason: result}
126
+ else
127
+ allResults[currentIndex] = {status: "fulfilled", value: result}
105
128
  } catch(error) {
106
129
  allResults[currentIndex] = {status: "rejected", reason: error}
107
130
  }
@@ -109,7 +132,7 @@ export default class Piper {
109
132
  }
110
133
 
111
134
  const setupResult = await Promised.settle(
112
- [...this.#lifeCycle.get("setup")].map(e => Promise.resolve(e()))
135
+ [...this.#lifeCycle.get("setup")].map(e => Promise.resolve(e(items)))
113
136
  )
114
137
 
115
138
  this.#processResult("Setting up the pipeline.", setupResult)
@@ -127,36 +150,37 @@ export default class Piper {
127
150
  } finally {
128
151
  // Run cleanup hooks
129
152
  const teardownResult = await Promised.settle(
130
- [...this.#lifeCycle.get("teardown")].map(e => Promise.resolve(e()))
153
+ [...this.#lifeCycle.get("teardown")].map(e => Promise.resolve(e(items)))
131
154
  )
132
155
 
133
156
  this.#processResult("Tearing down the pipeline.", teardownResult)
134
157
  }
135
158
 
159
+ if(this.reason)
160
+ this.emit("aborted", this.reason)
161
+
136
162
  return allResults
137
163
  }
138
164
 
139
165
  /**
140
166
  * Validate settleAll results and throw a combined error when rejected.
141
167
  *
142
- * @param {string} message Context message
143
- * @param {Array<unknown>} settled Results from settleAll
144
168
  * @private
169
+ * @param {string} message - Context message
170
+ * @param {Array<unknown>} settled - Results from settleAll
171
+ * @throws {Tantrum} - If any settled result was rejected
145
172
  */
146
- #processResult(message, settled) {
147
- if(settled.some(r => r.status === "rejected"))
148
- throw Tantrum.new(
149
- message,
150
- settled.filter(r => r.status==="rejected").map(r => r.reason)
151
- )
173
+ #processResult(_message, settled) {
174
+ if(Promised.hasRejected(settled))
175
+ Promised.throw(settled)
152
176
  }
153
177
 
154
178
  /**
155
179
  * Process a single item through all pipeline steps
156
180
  *
157
- * @param {unknown} item The item to process
158
- * @returns {Promise<unknown>} Result from the final step
159
181
  * @private
182
+ * @param {unknown} item - The item to process
183
+ * @returns {Promise<unknown>} Result from the final step
160
184
  */
161
185
  async #processItem(item) {
162
186
  // Execute each step in sequence