@gesslar/actioneer 0.2.1 → 0.2.2

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,4 +1,4 @@
1
- import {Data, Sass, Valid} from "@gesslar/toolkit"
1
+ import {Sass, Valid} from "@gesslar/toolkit"
2
2
 
3
3
  import ActionBuilder from "./ActionBuilder.js"
4
4
  import {ACTIVITY} from "./Activity.js"
@@ -20,10 +20,7 @@ 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} */
24
23
  #actionBuilder = null
25
- /** @type {import("./ActionWrapper.js").default|null} */
26
- #actionWrapper = null
27
24
 
28
25
  /**
29
26
  * Logger invoked for diagnostics.
@@ -51,160 +48,82 @@ export default class ActionRunner extends Piper {
51
48
 
52
49
  this.#actionBuilder = actionBuilder
53
50
 
54
- this.addStep(this.run, {
55
- name: `ActionRunner for ${actionBuilder.tag.description}`
56
- })
51
+ this.addStep(this.run)
57
52
  }
58
53
 
59
54
  /**
60
55
  * 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.
63
56
  *
64
57
  * @param {unknown} context - Seed value passed to the first activity.
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.
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.
67
60
  */
68
61
  async run(context) {
69
- if(!this.#actionWrapper)
70
- this.#actionWrapper = await this.#actionBuilder.build()
71
-
72
- const actionWrapper = this.#actionWrapper
62
+ const actionWrapper = await this.#actionBuilder.build()
73
63
  const activities = actionWrapper.activities
74
64
 
75
65
  for(const activity of activities) {
76
- try {
77
- // await timeout(500)
66
+ const kind = activity.kind
78
67
 
79
- const kind = activity.kind
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
80
77
 
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)
85
- } else {
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)
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
90
+
91
+ context = await this.#executeActivity(activity,context)
92
+
93
+ if(kindUntil)
94
+ if(await this.#predicateCheck(activity,pred,context))
95
+ break
133
96
  }
97
+ } else {
98
+ context = await this.#executeActivity(activity, context)
134
99
  }
135
- } catch(error) {
136
- throw Sass.new("ActionRunner running activity", error)
137
100
  }
101
+
138
102
  }
139
103
 
140
104
  return context
141
105
  }
142
106
 
143
107
  /**
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}).
108
+ * Execute a single activity, recursing into nested action wrappers when needed.
151
109
  *
152
110
  * @param {import("./Activity.js").default} activity Pipeline activity descriptor.
153
111
  * @param {unknown} context Current pipeline context.
154
- * @param {boolean} [parallel] Whether to use parallel execution (via pipe() instead of run()). Default: false.
155
112
  * @returns {Promise<unknown>} Resolved activity result.
156
- * @throws {Sass} If the operation kind is invalid, or if SPLIT activity lacks splitter/rejoiner.
157
113
  * @private
158
114
  */
159
- async #execute(activity, context, parallel=false) {
115
+ async #executeActivity(activity, context) {
160
116
  // What kind of op are we looking at? Is it a function?
161
117
  // Or a class instance of type ActionBuilder?
162
118
  const opKind = activity.opKind
163
-
164
119
  if(opKind === "ActionBuilder") {
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
- })
120
+ const runner = new this.constructor(activity.op, {debug: this.#debug})
171
121
 
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
- }
122
+ return await runner.run(context, true)
179
123
  } else if(opKind === "Function") {
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
- }
124
+ return await activity.run(context)
204
125
  }
205
126
 
206
- console.log(activity.opKind + " " + JSON.stringify(activity))
207
-
208
127
  throw Sass.new("We buy Functions and ActionBuilders. Only. Not whatever that was.")
209
128
  }
210
129
 
@@ -217,7 +136,7 @@ export default class ActionRunner extends Piper {
217
136
  * @returns {Promise<boolean>} True when the predicate allows another iteration.
218
137
  * @private
219
138
  */
220
- async #hasPredicate(activity,predicate,context) {
139
+ async #predicateCheck(activity,predicate,context) {
221
140
  Valid.type(predicate, "Function")
222
141
 
223
142
  return !!(await predicate.call(activity.action, context))
@@ -3,11 +3,9 @@ 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>|import("./ActionBuilder.js").default} op Operation or nested ActionBuilder to execute.
6
+ * @property {(context: unknown) => unknown|Promise<unknown>|ActionWrapper} op Operation or nested wrapper 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.
11
9
  * @property {unknown} [action] Parent action instance supplied when invoking the op.
12
10
  * @property {(message: string, level?: number, ...args: Array<unknown>) => void} [debug] Optional logger reference.
13
11
  */
@@ -33,29 +31,21 @@ export default class ActionWrapper {
33
31
  */
34
32
  #debug = () => {}
35
33
 
36
- /**
37
- * ActionHooks instance shared across all activities.
38
- *
39
- * @type {import("./ActionHooks.js").default|null}
40
- */
41
34
  #hooks = null
42
35
 
43
36
  /**
44
37
  * Create a wrapper from the builder payload.
45
38
  *
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
39
+ * @param {{activities: Map<string|symbol, WrappedActivityConfig>, debug: (message: string, level?: number, ...args: Array<unknown>) => void}} init Builder payload containing activities + logger.
50
40
  */
51
- constructor(config) {
52
- this.#debug = config.debug
53
- this.#hooks = config.hooks
54
- this.#activities = config.activities
41
+ constructor({activities,hooks,debug}) {
42
+ this.#debug = debug
43
+ this.#hooks = hooks
44
+ this.#activities = activities
55
45
  this.#debug(
56
46
  "Instantiating ActionWrapper with %o activities.",
57
47
  2,
58
- this.#activities.size,
48
+ activities.size,
59
49
  )
60
50
  }
61
51
 
@@ -8,58 +8,32 @@ 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 false (4)
13
- * @property {number} SPLIT - Execute activity with split/rejoin pattern for parallel execution (8)
14
11
  */
15
12
  export const ACTIVITY = Object.freeze({
16
13
  WHILE: 1<<1,
17
14
  UNTIL: 1<<2,
18
- SPLIT: 1<<3,
19
15
  })
20
16
 
21
17
  export default class Activity {
22
- /** @type {unknown} */
23
18
  #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} */
31
19
  #name = null
32
- /** @type {((context: unknown) => unknown|Promise<unknown>)|import("./ActionBuilder.js").default} */
33
20
  #op = null
34
- /** @type {((context: unknown) => boolean|Promise<boolean>)|null} */
21
+ #kind = null
35
22
  #pred = null
36
- /** @type {((originalContext: unknown, splitResults: unknown) => unknown)|null} */
37
- #rejoiner = null
38
- /** @type {((context: unknown) => unknown)|null} */
39
- #splitter = null
23
+ #hooks = null
40
24
 
41
25
  /**
42
26
  * Construct an Activity definition wrapper.
43
27
  *
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
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
53
29
  */
54
- constructor({action,name,op,kind,pred,hooks,splitter,rejoiner}) {
55
- this.#action = action
56
- this.#hooks = hooks
57
- this.#kind = kind
30
+ constructor({action,name,op,kind,pred,hooks}) {
58
31
  this.#name = name
59
32
  this.#op = op
33
+ this.#kind = kind
34
+ this.#action = action
60
35
  this.#pred = pred
61
- this.#rejoiner = rejoiner
62
- this.#splitter = splitter
36
+ this.#hooks = hooks
63
37
  }
64
38
 
65
39
  /**
@@ -90,16 +64,7 @@ export default class Activity {
90
64
  }
91
65
 
92
66
  /**
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).
67
+ * The operator kind name (Function or ActionWrapper).
103
68
  *
104
69
  * @returns {string} - Kind name extracted via Data.typeOf
105
70
  */
@@ -108,32 +73,14 @@ export default class Activity {
108
73
  }
109
74
 
110
75
  /**
111
- * The operator to execute (function or nested ActionBuilder).
76
+ * The operator to execute (function or nested wrapper).
112
77
  *
113
- * @returns {(context: unknown) => unknown|Promise<unknown>|import("./ActionBuilder.js").default} - Activity operation
78
+ * @returns {unknown} - Activity operation
114
79
  */
115
80
  get op() {
116
81
  return this.#op
117
82
  }
118
83
 
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
-
137
84
  /**
138
85
  * The action instance this activity belongs to.
139
86
  *
@@ -147,7 +94,7 @@ export default class Activity {
147
94
  * Execute the activity with before/after hooks.
148
95
  *
149
96
  * @param {unknown} context - Mutable context flowing through the pipeline
150
- * @returns {Promise<unknown>} - Activity result
97
+ * @returns {Promise<{activityResult: unknown}>} - Activity result wrapper with new context
151
98
  */
152
99
  async run(context) {
153
100
  // before hook
@@ -165,7 +112,7 @@ export default class Activity {
165
112
  /**
166
113
  * Attach hooks to this activity instance.
167
114
  *
168
- * @param {ActionHooks} hooks - Hooks instance with optional before$/after$ methods
115
+ * @param {unknown} hooks - Hooks instance with optional before$/after$ methods
169
116
  * @returns {this} - This activity for chaining
170
117
  */
171
118
  setActionHooks(hooks) {
@@ -174,13 +121,4 @@ export default class Activity {
174
121
 
175
122
  return this
176
123
  }
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
- }
186
124
  }
package/src/lib/Piper.js CHANGED
@@ -2,20 +2,18 @@
2
2
  * Generic Pipeline - Process items through a series of steps with concurrency control
3
3
  *
4
4
  * This abstraction handles:
5
- * - Concurrent processing with configurable limits using worker pool pattern
6
- * - Pipeline of processing steps executed sequentially per item
5
+ * - Concurrent processing with configurable limits
6
+ * - Pipeline of processing steps
7
+ * - Result categorization (success/warning/error)
7
8
  * - Setup/cleanup lifecycle hooks
8
9
  * - Error handling and reporting
9
- * - Dynamic worker spawning to maintain concurrency
10
10
  */
11
11
 
12
12
  import {Sass, Tantrum, Util} from "@gesslar/toolkit"
13
13
 
14
14
  export default class Piper {
15
- /** @type {(message: string, level?: number, ...args: Array<unknown>) => void} */
16
15
  #debug
17
16
 
18
- /** @type {Map<string, Set<unknown>>} */
19
17
  #lifeCycle = new Map([
20
18
  ["setup", new Set()],
21
19
  ["process", new Set()],
@@ -32,19 +30,14 @@ export default class Piper {
32
30
  }
33
31
 
34
32
  /**
35
- * Add a processing step to the pipeline.
36
- * Each step is executed sequentially per item.
33
+ * Add a processing step to the pipeline
37
34
  *
38
35
  * @param {(context: unknown) => Promise<unknown>|unknown} fn Function that processes an item
39
- * @param {{name: string, required?: boolean}} options Step options (name is required)
36
+ * @param {{name?: string, required?: boolean}} [options] Step options
40
37
  * @param {unknown} [newThis] Optional this binding
41
38
  * @returns {Piper} The pipeline instance (for chaining)
42
- * @throws {Sass} If name is not provided in options
43
39
  */
44
40
  addStep(fn, options = {}, newThis) {
45
- if(options.name == null)
46
- throw Sass.new("Missing name for step.")
47
-
48
41
  this.#lifeCycle.get("process").add({
49
42
  fn: fn.bind(newThis ?? this),
50
43
  name: options.name || `Step ${this.#lifeCycle.get("process").size + 1}`,
@@ -82,70 +75,33 @@ export default class Piper {
82
75
  }
83
76
 
84
77
  /**
85
- * Process items through the pipeline with concurrency control using a worker pool pattern.
86
- * Workers are spawned up to maxConcurrent limit, and as workers complete, new workers
87
- * are spawned to maintain concurrency until all items are processed.
88
- *
89
- * This implementation uses dynamic worker spawning to maintain concurrency:
90
- * - Initial workers are spawned up to maxConcurrent limit
91
- * - As each worker completes (success OR failure), a replacement worker is spawned if items remain
92
- * - Worker spawning occurs in finally block to ensure resilience to individual worker failures
93
- * - All results are collected with {ok, value} or {ok: false, error} structure
94
- * - Processing continues even if individual workers fail, collecting all errors
78
+ * Process items through the pipeline with concurrency control
95
79
  *
96
80
  * @param {Array<unknown>|unknown} items - Items to process
97
- * @param {number} [maxConcurrent] - Maximum concurrent items to process (default: 10)
98
- * @returns {Promise<Array<{ok: boolean, value?: unknown, error?: Sass}>>} - Results with success/failure status
99
- * @throws {Sass} If setup or teardown fails
81
+ * @param {number} maxConcurrent - Maximum concurrent items to process
82
+ * @returns {Promise<Array<unknown>>} - Collected results from steps
100
83
  */
101
84
  async pipe(items, maxConcurrent = 10) {
102
85
  items = Array.isArray(items)
103
86
  ? items
104
87
  : [items]
105
88
 
106
- const pipeResult = []
107
-
108
- let pendingCount = 0
109
- let resolveAll
110
- const allDone = new Promise(resolve => {
111
- resolveAll = resolve
112
- })
89
+ let itemIndex = 0
90
+ const allResults = []
113
91
 
114
- /**
115
- * Worker function that processes one item and potentially spawns a replacement.
116
- * Uses shift() to atomically retrieve items from the queue, ensuring no duplicate processing.
117
- * Spawns replacement workers in the finally block to guarantee resilience to errors.
118
- *
119
- * @private
120
- */
121
92
  const processWorker = async() => {
122
- if(items.length === 0) {
123
- pendingCount--
124
-
125
- if(pendingCount === 0)
126
- resolveAll()
127
-
128
- return
129
- }
130
-
131
- const item = items.shift()
132
-
133
- try {
134
- const result = await this.#processWorker(item)
135
- pipeResult.push({ok: true, value: result})
136
- } catch(error) {
137
- pipeResult.push({ok: false, error: Sass.new("Processing pipeline item.", error)})
138
- } finally {
139
- // Spawn a replacement worker if there are more items
140
- if(items.length > 0) {
141
- pendingCount++
142
- processWorker() // Don't await - let it run in parallel
93
+ while(true) {
94
+ const currentIndex = itemIndex++
95
+ if(currentIndex >= items.length)
96
+ break
97
+
98
+ const item = items[currentIndex]
99
+ try {
100
+ const result = await this.#processItem(item)
101
+ allResults.push(result)
102
+ } catch(error) {
103
+ throw Sass.new("Processing pipeline item.", error)
143
104
  }
144
-
145
- if(--pendingCount === 0)
146
- resolveAll()
147
-
148
- this.#debug("pendingCount = %o", 2, pendingCount)
149
105
  }
150
106
  }
151
107
 
@@ -156,19 +112,15 @@ export default class Piper {
156
112
 
157
113
  try {
158
114
  // Start workers up to maxConcurrent limit
115
+ const workers = []
159
116
  const workerCount = Math.min(maxConcurrent, items.length)
160
- pendingCount = workerCount
161
117
 
162
- if(workerCount === 0) {
163
- resolveAll() // No items to process
164
- } else {
165
- for(let i = 0; i < workerCount; i++) {
166
- processWorker() // Don't await - let them all run in parallel
167
- }
168
- }
118
+ for(let i = 0; i < workerCount; i++)
119
+ workers.push(processWorker())
169
120
 
170
121
  // Wait for all workers to complete
171
- await allDone
122
+ const processResult = await Util.settleAll(workers)
123
+ this.#processResult("Processing pipeline.", processResult)
172
124
  } finally {
173
125
  // Run cleanup hooks
174
126
  const teardownResult = await Util.settleAll(
@@ -177,31 +129,7 @@ export default class Piper {
177
129
  this.#processResult("Tearing down the pipeline.", teardownResult)
178
130
  }
179
131
 
180
- return pipeResult
181
- }
182
-
183
- /**
184
- * Process a single item through all pipeline steps.
185
- *
186
- * @param {unknown} item The item to process
187
- * @returns {Promise<unknown>} Result from the final step
188
- * @private
189
- */
190
- async #processWorker(item) {
191
- try {
192
- // Execute each step in sequence
193
- let result = item
194
-
195
- for(const step of this.#lifeCycle.get("process")) {
196
- this.#debug("Executing step: %o", 4, step.name)
197
-
198
- result = await step.fn(result) ?? result
199
- }
200
-
201
- return result
202
- } catch(error) {
203
- throw Sass.new("Processing an item.", error)
204
- }
132
+ return allResults
205
133
  }
206
134
 
207
135
  /**
@@ -218,4 +146,29 @@ export default class Piper {
218
146
  settled.filter(r => r.status==="rejected").map(r => r.reason)
219
147
  )
220
148
  }
149
+
150
+ /**
151
+ * Process a single item through all pipeline steps
152
+ *
153
+ * @param {unknown} item The item to process
154
+ * @returns {Promise<unknown>} Result from the final step
155
+ * @private
156
+ */
157
+ async #processItem(item) {
158
+ // Execute each step in sequence
159
+ let result = item
160
+
161
+ for(const step of this.#lifeCycle.get("process")) {
162
+ this.#debug("Executing step: %o", 4, step.name)
163
+
164
+ try {
165
+ result = await step.fn(result) ?? result
166
+ } catch(error) {
167
+ if(step.required)
168
+ throw Sass.new(`Processing required step "${step.name}".`, error)
169
+ }
170
+ }
171
+
172
+ return result
173
+ }
221
174
  }