@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.
- package/README.md +152 -0
- package/package.json +1 -1
- package/src/lib/ActionBuilder.js +22 -106
- package/src/lib/ActionHooks.js +30 -46
- package/src/lib/ActionRunner.js +42 -123
- package/src/lib/ActionWrapper.js +7 -17
- package/src/lib/Activity.js +12 -74
- package/src/lib/Piper.js +52 -99
- package/src/types/lib/ActionBuilder.d.ts +2 -42
- package/src/types/lib/ActionBuilder.d.ts.map +1 -1
- package/src/types/lib/ActionHooks.d.ts +24 -23
- package/src/types/lib/ActionHooks.d.ts.map +1 -1
- package/src/types/lib/ActionRunner.d.ts +2 -4
- package/src/types/lib/ActionRunner.d.ts.map +1 -1
- package/src/types/lib/ActionWrapper.d.ts +5 -19
- package/src/types/lib/ActionWrapper.d.ts.map +1 -1
- package/src/types/lib/Activity.d.ts +19 -54
- package/src/types/lib/Activity.d.ts.map +1 -1
- package/src/types/lib/Piper.d.ts +7 -24
- package/src/types/lib/Piper.d.ts.map +1 -1
package/src/lib/ActionRunner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
// await timeout(500)
|
|
66
|
+
const kind = activity.kind
|
|
78
67
|
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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 #
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 #
|
|
139
|
+
async #predicateCheck(activity,predicate,context) {
|
|
221
140
|
Valid.type(predicate, "Function")
|
|
222
141
|
|
|
223
142
|
return !!(await predicate.call(activity.action, context))
|
package/src/lib/ActionWrapper.js
CHANGED
|
@@ -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>|
|
|
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 {
|
|
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(
|
|
52
|
-
this.#debug =
|
|
53
|
-
this.#hooks =
|
|
54
|
-
this.#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
|
-
|
|
48
|
+
activities.size,
|
|
59
49
|
)
|
|
60
50
|
}
|
|
61
51
|
|
package/src/lib/Activity.js
CHANGED
|
@@ -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
|
-
|
|
21
|
+
#kind = null
|
|
35
22
|
#pred = null
|
|
36
|
-
|
|
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 {
|
|
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
|
|
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.#
|
|
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
|
|
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
|
|
76
|
+
* The operator to execute (function or nested wrapper).
|
|
112
77
|
*
|
|
113
|
-
* @returns {
|
|
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 {
|
|
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
|
|
6
|
-
* - Pipeline of processing steps
|
|
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
|
|
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
|
|
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}
|
|
98
|
-
* @returns {Promise<Array<
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
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
|
|
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
|
}
|