@entergreat/step-pipeline 1.1.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.
package/.npmrc.example ADDED
@@ -0,0 +1,5 @@
1
+ # NPM Registry Configuration
2
+ # For private packages, add your auth token:
3
+ # //registry.npmjs.org/:_authToken=${NPM_TOKEN}
4
+
5
+ registry=https://registry.npmjs.org/
package/README.md ADDED
@@ -0,0 +1,496 @@
1
+ # @entergreat/step-pipeline
2
+
3
+ Reusable step-based pipeline framework for EnterGreat services. Build robust, resumable, and maintainable data processing pipelines with ease.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Step-based architecture** - Break complex workflows into manageable steps
8
+ - 🔄 **Resume capability** - Start from any step in the pipeline
9
+ - 🔁 **Automatic retries** - Configure retries per step
10
+ - ⏱️ **Timeout support** - Set execution timeouts for steps
11
+ - 📊 **Built-in metrics** - Track execution time and success rates
12
+ - 🎯 **Conditional execution** - Execute steps based on conditions
13
+ - 🔌 **Event system** - Hook into step lifecycle events
14
+ - 📝 **Comprehensive logging** - Configurable log levels
15
+ - 🛡️ **Error handling** - Graceful error handling with detailed reporting
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @entergreat/step-pipeline
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```javascript
26
+ import { StepPipeline, StepDefinition } from '@entergreat/step-pipeline';
27
+
28
+ // Create a pipeline
29
+ const pipeline = new StepPipeline();
30
+
31
+ // Define your steps
32
+ const steps = [
33
+ new StepDefinition(
34
+ 0,
35
+ 'Load Data',
36
+ 'Load data from source',
37
+ async (context) => {
38
+ // Your logic here
39
+ return { data: 'loaded' };
40
+ }
41
+ ),
42
+ new StepDefinition(
43
+ 1,
44
+ 'Process Data',
45
+ 'Process the loaded data',
46
+ async (context) => {
47
+ // Your logic here
48
+ return { processed: true };
49
+ }
50
+ ),
51
+ new StepDefinition(
52
+ 2,
53
+ 'Save Results',
54
+ 'Save processed results',
55
+ async (context) => {
56
+ // Your logic here
57
+ return { saved: true };
58
+ }
59
+ )
60
+ ];
61
+
62
+ // Register steps
63
+ pipeline.registerSteps(steps);
64
+
65
+ // Execute the pipeline
66
+ const context = { userId: '123', timestamp: Date.now() };
67
+ const results = await pipeline.executeAll(context);
68
+
69
+ // Print summary
70
+ pipeline.printSummary(results);
71
+ ```
72
+
73
+ ## Core Concepts
74
+
75
+ ### StepDefinition
76
+
77
+ Defines a single step in your pipeline:
78
+
79
+ ```javascript
80
+ new StepDefinition(
81
+ id, // Unique step ID (number)
82
+ name, // Display name (string)
83
+ description, // What this step does (string)
84
+ handler, // Async function to execute (async function)
85
+ options // Optional configuration (object)
86
+ )
87
+ ```
88
+
89
+ ### StepPipeline
90
+
91
+ Main class for managing and executing pipelines:
92
+
93
+ ```javascript
94
+ const pipeline = new StepPipeline({
95
+ logLevel: 'info', // 'error', 'warn', 'info', 'debug'
96
+ stopOnError: true, // Stop pipeline on step failure
97
+ enableMetrics: true // Track execution metrics
98
+ });
99
+ ```
100
+
101
+ ### Context
102
+
103
+ Data object that flows through all steps:
104
+
105
+ ```javascript
106
+ const context = {
107
+ // Your custom data
108
+ userId: '123',
109
+ input: 'some data',
110
+ // Context is mutable and can be updated by steps
111
+ };
112
+ ```
113
+
114
+ ## API Reference
115
+
116
+ ### StepPipeline Methods
117
+
118
+ #### `registerSteps(stepDefinitions)`
119
+ Register multiple steps at once.
120
+
121
+ ```javascript
122
+ pipeline.registerSteps([step1, step2, step3]);
123
+ ```
124
+
125
+ #### `executeAll(context, startStep?)`
126
+ Execute all registered steps from start to end.
127
+
128
+ ```javascript
129
+ // Execute all steps
130
+ await pipeline.executeAll(context);
131
+
132
+ // Resume from step 5
133
+ await pipeline.executeAll(context, 5);
134
+ ```
135
+
136
+ #### `executeStep(stepId, context, skip?)`
137
+ Execute a single step by ID.
138
+
139
+ ```javascript
140
+ const result = await pipeline.executeStep(2, context);
141
+ ```
142
+
143
+ #### `executeRange(startStep, endStep, context)`
144
+ Execute a specific range of steps.
145
+
146
+ ```javascript
147
+ // Execute steps 2 through 5
148
+ await pipeline.executeRange(2, 5, context);
149
+ ```
150
+
151
+ #### `executeSteps(stepIds, context)`
152
+ Execute only specific steps by IDs.
153
+
154
+ ```javascript
155
+ // Execute only steps 1, 3, and 5
156
+ await pipeline.executeSteps([1, 3, 5], context);
157
+ ```
158
+
159
+ #### `listSteps()`
160
+ Get a list of all registered steps.
161
+
162
+ ```javascript
163
+ const steps = pipeline.listSteps();
164
+ // Returns: [{ id, name, description, options }, ...]
165
+ ```
166
+
167
+ #### `getStepCount()`
168
+ Get the total number of registered steps.
169
+
170
+ ```javascript
171
+ const count = pipeline.getStepCount();
172
+ ```
173
+
174
+ #### `getSummary(results)`
175
+ Get execution summary with statistics.
176
+
177
+ ```javascript
178
+ const summary = pipeline.getSummary(results);
179
+ // Returns: { total, successful, failed, skipped, successRate, totalDuration }
180
+ ```
181
+
182
+ #### `printSummary(results)`
183
+ Print formatted execution summary to console.
184
+
185
+ ```javascript
186
+ pipeline.printSummary(results);
187
+ ```
188
+
189
+ ### Step Options
190
+
191
+ Configure individual steps with advanced options:
192
+
193
+ ```javascript
194
+ new StepDefinition(
195
+ 5,
196
+ 'Flaky API Call',
197
+ 'Call external API with retries',
198
+ handler,
199
+ {
200
+ retries: 3, // Retry 3 times on failure
201
+ retryDelay: 1000, // Wait 1s between retries
202
+ timeout: 30000, // Timeout after 30s
203
+ skip: false, // Skip this step
204
+ condition: (context) => context.shouldRun // Conditional execution
205
+ }
206
+ )
207
+ ```
208
+
209
+ ### Event System
210
+
211
+ Hook into pipeline lifecycle events:
212
+
213
+ ```javascript
214
+ pipeline
215
+ .on('stepStart', (stepId, stepName, context) => {
216
+ console.log(`Starting ${stepName}...`);
217
+ })
218
+ .on('stepComplete', (stepId, stepName, result, duration) => {
219
+ console.log(`Completed ${stepName} in ${duration}s`);
220
+ })
221
+ .on('stepError', (stepId, stepName, error, duration) => {
222
+ console.error(`Failed ${stepName}: ${error.message}`);
223
+ })
224
+ .on('stepSkipped', (stepId, stepName) => {
225
+ console.log(`Skipped ${stepName}`);
226
+ })
227
+ .on('sequenceStart', (startId, endId, context) => {
228
+ console.log(`Starting sequence ${startId} to ${endId}`);
229
+ })
230
+ .on('sequenceComplete', (results) => {
231
+ console.log(`Sequence completed with ${results.length} steps`);
232
+ });
233
+ ```
234
+
235
+ ## Usage Examples
236
+
237
+ ### Example 1: Basic Pipeline
238
+
239
+ ```javascript
240
+ import { StepPipeline, StepDefinition } from '@entergreat/step-pipeline';
241
+
242
+ class DataProcessor {
243
+ constructor() {
244
+ this.pipeline = new StepPipeline();
245
+ this.registerSteps();
246
+ }
247
+
248
+ registerSteps() {
249
+ const steps = [
250
+ new StepDefinition(0, 'Validate Input', 'Validate input data', this.validate.bind(this)),
251
+ new StepDefinition(1, 'Transform Data', 'Transform to required format', this.transform.bind(this)),
252
+ new StepDefinition(2, 'Save Results', 'Save to database', this.save.bind(this))
253
+ ];
254
+
255
+ this.pipeline.registerSteps(steps);
256
+ }
257
+
258
+ async validate(context) {
259
+ if (!context.data) throw new Error('No data provided');
260
+ return { valid: true };
261
+ }
262
+
263
+ async transform(context) {
264
+ context.transformed = context.data.toUpperCase();
265
+ return { transformed: true };
266
+ }
267
+
268
+ async save(context) {
269
+ // Save to database
270
+ return { saved: true };
271
+ }
272
+
273
+ async process(data) {
274
+ const context = { data, timestamp: Date.now() };
275
+ const results = await this.pipeline.executeAll(context);
276
+ return this.pipeline.getSummary(results);
277
+ }
278
+ }
279
+
280
+ // Usage
281
+ const processor = new DataProcessor();
282
+ const summary = await processor.process('hello world');
283
+ ```
284
+
285
+ ### Example 2: With Retries and Timeouts
286
+
287
+ ```javascript
288
+ const pipeline = new StepPipeline();
289
+
290
+ const steps = [
291
+ new StepDefinition(
292
+ 0,
293
+ 'Fetch Data',
294
+ 'Fetch from external API',
295
+ async (context) => {
296
+ const response = await fetch(context.url);
297
+ return await response.json();
298
+ },
299
+ {
300
+ retries: 3,
301
+ retryDelay: 2000,
302
+ timeout: 10000
303
+ }
304
+ )
305
+ ];
306
+
307
+ pipeline.registerSteps(steps);
308
+ ```
309
+
310
+ ### Example 3: Conditional Execution
311
+
312
+ ```javascript
313
+ const steps = [
314
+ new StepDefinition(
315
+ 0,
316
+ 'Optional Enhancement',
317
+ 'Run only if premium user',
318
+ async (context) => {
319
+ // Enhancement logic
320
+ return { enhanced: true };
321
+ },
322
+ {
323
+ condition: (context) => context.user.isPremium
324
+ }
325
+ )
326
+ ];
327
+ ```
328
+
329
+ ### Example 4: Resume from Checkpoint
330
+
331
+ ```javascript
332
+ // First run - fails at step 5
333
+ try {
334
+ await pipeline.executeAll(context);
335
+ } catch (error) {
336
+ console.error('Pipeline failed at step 5');
337
+ }
338
+
339
+ // Resume from step 5 after fixing the issue
340
+ const results = await pipeline.executeAll(context, 5);
341
+ ```
342
+
343
+ ### Example 5: With Event Tracking
344
+
345
+ ```javascript
346
+ const pipeline = new StepPipeline();
347
+
348
+ // Track metrics
349
+ const metrics = {
350
+ stepsExecuted: 0,
351
+ totalDuration: 0,
352
+ errors: []
353
+ };
354
+
355
+ pipeline
356
+ .on('stepComplete', (stepId, stepName, result, duration) => {
357
+ metrics.stepsExecuted++;
358
+ metrics.totalDuration += parseFloat(duration);
359
+ })
360
+ .on('stepError', (stepId, stepName, error) => {
361
+ metrics.errors.push({ stepId, stepName, error: error.message });
362
+ });
363
+
364
+ await pipeline.executeAll(context);
365
+ console.log('Metrics:', metrics);
366
+ ```
367
+
368
+ ## Integration with EnterGreat Services
369
+
370
+ ### Before (without package)
371
+
372
+ ```javascript
373
+ // Each service had duplicate code
374
+ import { StepRegistry, StepDefinition } from './core/StepRegistry.js';
375
+ import { StepExecutor } from './core/StepExecutor.js';
376
+
377
+ export class StepService {
378
+ constructor() {
379
+ this.registry = new StepRegistry();
380
+ this.executor = new StepExecutor(this.registry);
381
+ this.registerSteps();
382
+ }
383
+
384
+ async executeAll(context, startStep = 0) {
385
+ const endStep = 8; // Hardcoded
386
+ return await this.executor.executeSequence(startStep, endStep, context);
387
+ }
388
+
389
+ // More duplicate code...
390
+ }
391
+ ```
392
+
393
+ ### After (with package)
394
+
395
+ ```javascript
396
+ import { StepPipeline, StepDefinition } from '@entergreat/step-pipeline';
397
+
398
+ export class StepService {
399
+ constructor() {
400
+ // One line instead of multiple!
401
+ this.pipeline = new StepPipeline();
402
+ this.registerSteps();
403
+ }
404
+
405
+ async executeAll(context, startStep = 0) {
406
+ // Uses the package's built-in method
407
+ return await this.pipeline.executeAll(context, startStep);
408
+ }
409
+
410
+ listSteps() {
411
+ // Uses the package's built-in method
412
+ return this.pipeline.listSteps();
413
+ }
414
+ }
415
+ ```
416
+
417
+ ## Advanced Features
418
+
419
+ ### Custom Logging
420
+
421
+ ```javascript
422
+ const pipeline = new StepPipeline({
423
+ logLevel: 'debug' // Show all logs including debug messages
424
+ });
425
+ ```
426
+
427
+ ### Stop on Error
428
+
429
+ ```javascript
430
+ const pipeline = new StepPipeline({
431
+ stopOnError: false // Continue even if steps fail
432
+ });
433
+ ```
434
+
435
+ ### Heartbeat Messages
436
+
437
+ Useful for long-running steps:
438
+
439
+ ```javascript
440
+ async longRunningStep(context) {
441
+ for (let i = 0; i < 100; i++) {
442
+ if (i % 10 === 0) {
443
+ this.pipeline.heartbeat(`Processing item ${i}/100`);
444
+ }
445
+ await processItem(i);
446
+ }
447
+ }
448
+ ```
449
+
450
+ ## Testing
451
+
452
+ ```javascript
453
+ import { StepPipeline, StepDefinition } from '@entergreat/step-pipeline';
454
+
455
+ describe('My Pipeline', () => {
456
+ it('should execute all steps successfully', async () => {
457
+ const pipeline = new StepPipeline();
458
+ const steps = [
459
+ new StepDefinition(0, 'Test Step', 'Test', async () => ({ success: true }))
460
+ ];
461
+
462
+ pipeline.registerSteps(steps);
463
+ const results = await pipeline.executeAll({});
464
+
465
+ expect(results[0].success).toBe(true);
466
+ });
467
+ });
468
+ ```
469
+
470
+ ## Best Practices
471
+
472
+ 1. **Use descriptive names** - Make step names and descriptions clear
473
+ 2. **Keep steps focused** - Each step should do one thing well
474
+ 3. **Handle errors gracefully** - Return meaningful error messages
475
+ 4. **Use context wisely** - Don't mutate context unnecessarily
476
+ 5. **Log important events** - Use heartbeat for long-running operations
477
+ 6. **Test steps independently** - Unit test each step handler
478
+ 7. **Use retries for flaky operations** - Network calls, external APIs
479
+ 8. **Set appropriate timeouts** - Prevent hanging operations
480
+
481
+ ## Migration Guide
482
+
483
+ See [MIGRATION.md](./MIGRATION.md) for detailed migration instructions from local implementations.
484
+
485
+ ## Contributing
486
+
487
+ Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md).
488
+
489
+ ## License
490
+
491
+ MIT License - see [LICENSE](./LICENSE) file for details.
492
+
493
+ ## Support
494
+
495
+ - GitHub Issues: [Create an issue](https://github.com/entergreat/step-pipeline/issues)
496
+ - Email: support@entergreat.com
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@entergreat/step-pipeline",
3
+ "version": "1.1.0",
4
+ "description": "Reusable step-based pipeline framework for EnterGreat services",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "keywords": [
11
+ "pipeline",
12
+ "steps",
13
+ "workflow",
14
+ "orchestration",
15
+ "entergreat"
16
+ ],
17
+ "author": "EnterGreat",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/entergreat/step-pipeline.git"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ }
26
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Represents a single step in the pipeline
3
+ * Supports both numeric IDs (legacy) and string IDs (recommended)
4
+ */
5
+ export class StepDefinition {
6
+ constructor(id, name, description, handler, options = {}) {
7
+ this.id = id;
8
+ this.name = name;
9
+ this.description = description;
10
+ this.handler = handler;
11
+ this.options = {
12
+ // Execution options
13
+ skip: options.skip || false,
14
+ retries: options.retries || 0,
15
+ retryDelay: options.retryDelay || 1000,
16
+ timeout: options.timeout || null,
17
+ condition: options.condition || null,
18
+
19
+ // Message options
20
+ startMessage: options.startMessage || `Starting ${name}...`,
21
+ successMessage: options.successMessage || `${name} completed successfully`,
22
+ errorMessage: options.errorMessage || `${name} failed`,
23
+
24
+ // Organization options
25
+ phase: options.phase || null,
26
+ tags: options.tags || [],
27
+
28
+ // Dependencies
29
+ dependsOn: options.dependsOn || [],
30
+
31
+ ...options
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Check if this step should be executed based on condition
37
+ */
38
+ shouldExecute(context) {
39
+ if (typeof this.options.condition === 'function') {
40
+ return this.options.condition(context);
41
+ }
42
+ return !this.options.skip;
43
+ }
44
+
45
+ /**
46
+ * Get formatted start message with context interpolation
47
+ */
48
+ getStartMessage(context = {}) {
49
+ return this._interpolate(this.options.startMessage, context);
50
+ }
51
+
52
+ /**
53
+ * Get formatted success message with context interpolation
54
+ */
55
+ getSuccessMessage(context = {}, result = {}) {
56
+ return this._interpolate(this.options.successMessage, { ...context, ...result });
57
+ }
58
+
59
+ /**
60
+ * Get formatted error message with context interpolation
61
+ */
62
+ getErrorMessage(context = {}, error = {}) {
63
+ return this._interpolate(this.options.errorMessage, { ...context, error: error.message });
64
+ }
65
+
66
+ /**
67
+ * Interpolate variables in message templates
68
+ * Example: "Processing {count} items" -> "Processing 5 items"
69
+ */
70
+ _interpolate(template, data) {
71
+ if (!template) return '';
72
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
73
+ return data[key] !== undefined ? data[key] : match;
74
+ });
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Builder class for creating step definitions with a fluent API
80
+ */
81
+ export class StepBuilder {
82
+ constructor(id, name) {
83
+ this.id = id;
84
+ this.name = name;
85
+ this._description = '';
86
+ this._handler = null;
87
+ this._options = {};
88
+ }
89
+
90
+ description(desc) {
91
+ this._description = desc;
92
+ return this;
93
+ }
94
+
95
+ handler(fn) {
96
+ this._handler = fn;
97
+ return this;
98
+ }
99
+
100
+ startMessage(msg) {
101
+ this._options.startMessage = msg;
102
+ return this;
103
+ }
104
+
105
+ successMessage(msg) {
106
+ this._options.successMessage = msg;
107
+ return this;
108
+ }
109
+
110
+ errorMessage(msg) {
111
+ this._options.errorMessage = msg;
112
+ return this;
113
+ }
114
+
115
+ retries(count, delay = 1000) {
116
+ this._options.retries = count;
117
+ this._options.retryDelay = delay;
118
+ return this;
119
+ }
120
+
121
+ timeout(ms) {
122
+ this._options.timeout = ms;
123
+ return this;
124
+ }
125
+
126
+ condition(fn) {
127
+ this._options.condition = fn;
128
+ return this;
129
+ }
130
+
131
+ phase(phaseName) {
132
+ this._options.phase = phaseName;
133
+ return this;
134
+ }
135
+
136
+ tags(...tags) {
137
+ this._options.tags = tags;
138
+ return this;
139
+ }
140
+
141
+ dependsOn(...stepIds) {
142
+ this._options.dependsOn = stepIds;
143
+ return this;
144
+ }
145
+
146
+ build() {
147
+ if (!this._handler) {
148
+ throw new Error(`Step ${this.id}: handler is required`);
149
+ }
150
+ return new StepDefinition(
151
+ this.id,
152
+ this.name,
153
+ this._description,
154
+ this._handler,
155
+ this._options
156
+ );
157
+ }
158
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Executes pipeline steps with logging, error handling, and retries
3
+ */
4
+ export class StepExecutor {
5
+ constructor(registry, options = {}) {
6
+ this.registry = registry;
7
+ this.options = {
8
+ logLevel: options.logLevel || 'info',
9
+ stopOnError: options.stopOnError !== false,
10
+ enableMetrics: options.enableMetrics || false,
11
+ ...options
12
+ };
13
+ this.eventHandlers = new Map();
14
+ }
15
+
16
+ /**
17
+ * Register an event handler
18
+ */
19
+ on(event, handler) {
20
+ if (!this.eventHandlers.has(event)) {
21
+ this.eventHandlers.set(event, []);
22
+ }
23
+ this.eventHandlers.get(event).push(handler);
24
+ return this;
25
+ }
26
+
27
+ /**
28
+ * Emit an event
29
+ */
30
+ emit(event, ...args) {
31
+ const handlers = this.eventHandlers.get(event) || [];
32
+ handlers.forEach(handler => {
33
+ try {
34
+ handler(...args);
35
+ } catch (error) {
36
+ console.error(`Error in event handler for ${event}:`, error);
37
+ }
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Execute a single step
43
+ */
44
+ async execute(stepId, context, skip = false) {
45
+ const step = this.registry.get(stepId);
46
+
47
+ this.log('info', `\n${'='.repeat(60)}`);
48
+ this.log('info', `Step ${stepId}: ${step.name}`);
49
+ this.log('info', `Description: ${step.description}`);
50
+ this.log('info', '='.repeat(60));
51
+
52
+ // Check if step should be skipped
53
+ if (skip || !step.shouldExecute(context)) {
54
+ this.log('info', `⏭️ Skipping step ${stepId}...`);
55
+ this.emit('stepSkipped', stepId, step.name);
56
+ return { skipped: true, stepId, stepName: step.name };
57
+ }
58
+
59
+ const startTime = Date.now();
60
+ let lastError = null;
61
+ const maxAttempts = step.options.retries + 1;
62
+
63
+ // Emit step start event
64
+ this.emit('stepStart', stepId, step.name, context);
65
+
66
+ // Retry logic
67
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
68
+ try {
69
+ if (attempt > 1) {
70
+ this.log('warn', `🔄 Retry attempt ${attempt}/${maxAttempts} for step ${stepId}`);
71
+ await this.sleep(step.options.retryDelay);
72
+ }
73
+
74
+ // Use custom start message from step definition
75
+ const startMessage = step.getStartMessage(context);
76
+ this.log('info', `🚀 ${startMessage}`);
77
+
78
+ // Execute with timeout if specified
79
+ const result = step.options.timeout
80
+ ? await this.executeWithTimeout(step.handler, context, step.options.timeout)
81
+ : await step.handler(context);
82
+
83
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
84
+
85
+ // Use custom success message from step definition
86
+ const successMessage = step.getSuccessMessage(context, result);
87
+ this.log('info', `✅ ${successMessage} (${duration}s)`);
88
+
89
+ // Emit step complete event
90
+ this.emit('stepComplete', stepId, step.name, result, duration);
91
+
92
+ return {
93
+ success: true,
94
+ stepId,
95
+ stepName: step.name,
96
+ duration,
97
+ result,
98
+ attempts: attempt
99
+ };
100
+ } catch (error) {
101
+ lastError = error;
102
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
103
+
104
+ if (attempt < maxAttempts) {
105
+ this.log('warn', `⚠️ Step ${stepId} failed (attempt ${attempt}/${maxAttempts})`);
106
+ this.log('warn', `Error: ${error.message}`);
107
+ } else {
108
+ // Use custom error message from step definition
109
+ const errorMessage = step.getErrorMessage(context, error);
110
+ this.log('error', `❌ ${errorMessage} after ${duration}s and ${attempt} attempt(s)`);
111
+
112
+ // Emit step error event
113
+ this.emit('stepError', stepId, step.name, error, duration);
114
+
115
+ return {
116
+ success: false,
117
+ stepId,
118
+ stepName: step.name,
119
+ duration,
120
+ error: error.message,
121
+ stack: error.stack,
122
+ attempts: attempt
123
+ };
124
+ }
125
+ }
126
+ }
127
+
128
+ // This should never be reached, but just in case
129
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
130
+ return {
131
+ success: false,
132
+ stepId,
133
+ stepName: step.name,
134
+ duration,
135
+ error: lastError?.message || 'Unknown error',
136
+ attempts: maxAttempts
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Execute a sequence of steps
142
+ * Supports both numeric sequences (0, 1, 2) and string ID ranges
143
+ */
144
+ async executeSequence(startStepId, endStepId, context) {
145
+ const results = [];
146
+
147
+ this.log('info', `\n${'*'.repeat(60)}`);
148
+ this.log('info', `Starting execution from step ${startStepId} to ${endStepId}`);
149
+ this.log('info', `${'*'.repeat(60)}\n`);
150
+
151
+ // Emit sequence start event
152
+ this.emit('sequenceStart', startStepId, endStepId, context);
153
+
154
+ // Get all step IDs in order
155
+ const allStepIds = this.registry.getStepIds();
156
+
157
+ // Find the start and end indices
158
+ const startIndex = allStepIds.indexOf(startStepId);
159
+ const endIndex = endStepId ? allStepIds.indexOf(endStepId) : allStepIds.length - 1;
160
+
161
+ if (startIndex === -1) {
162
+ throw new Error(`Start step ${startStepId} not found in registry`);
163
+ }
164
+
165
+ if (endIndex === -1 && endStepId) {
166
+ throw new Error(`End step ${endStepId} not found in registry`);
167
+ }
168
+
169
+ // Execute steps in range
170
+ for (let i = startIndex; i <= endIndex; i++) {
171
+ const stepId = allStepIds[i];
172
+
173
+ const result = await this.execute(stepId, context);
174
+ results.push(result);
175
+
176
+ // Stop execution if step failed and stopOnError is true
177
+ if (!result.success && !result.skipped && this.options.stopOnError) {
178
+ this.log('error', `\n❌ Execution stopped at step ${stepId} due to error`);
179
+ break;
180
+ }
181
+ }
182
+
183
+ this.log('info', `\n${'*'.repeat(60)}`);
184
+ this.log('info', `Execution completed`);
185
+ this.log('info', `${'*'.repeat(60)}\n`);
186
+
187
+ // Emit sequence complete event
188
+ this.emit('sequenceComplete', results);
189
+
190
+ return results;
191
+ }
192
+
193
+ /**
194
+ * Execute handler with timeout
195
+ */
196
+ async executeWithTimeout(handler, context, timeout) {
197
+ return Promise.race([
198
+ handler(context),
199
+ new Promise((_, reject) =>
200
+ setTimeout(() => reject(new Error(`Step timeout after ${timeout}ms`)), timeout)
201
+ )
202
+ ]);
203
+ }
204
+
205
+ /**
206
+ * Sleep utility for retry delays
207
+ */
208
+ sleep(ms) {
209
+ return new Promise(resolve => setTimeout(resolve, ms));
210
+ }
211
+
212
+ /**
213
+ * Print a heartbeat message
214
+ */
215
+ printHeartbeat(message) {
216
+ this.log('info', `💓 ${message}`);
217
+ }
218
+
219
+ /**
220
+ * Log a message based on log level
221
+ */
222
+ log(level, message) {
223
+ const levels = { error: 0, warn: 1, info: 2, debug: 3 };
224
+ const currentLevel = levels[this.options.logLevel] || 2;
225
+ const messageLevel = levels[level] || 2;
226
+
227
+ if (messageLevel <= currentLevel) {
228
+ if (level === 'error') {
229
+ console.error(message);
230
+ } else if (level === 'warn') {
231
+ console.warn(message);
232
+ } else {
233
+ console.log(message);
234
+ }
235
+ }
236
+ }
237
+ }
@@ -0,0 +1,178 @@
1
+ import { StepRegistry } from './StepRegistry.js';
2
+ import { StepExecutor } from './StepExecutor.js';
3
+
4
+ /**
5
+ * High-level API for managing and executing step-based pipelines
6
+ * This is the main class that services will use
7
+ */
8
+ export class StepPipeline {
9
+ constructor(options = {}) {
10
+ this.registry = new StepRegistry();
11
+ this.executor = new StepExecutor(this.registry, options);
12
+ this.options = options;
13
+ }
14
+
15
+ /**
16
+ * Register steps (convenience method)
17
+ */
18
+ registerSteps(stepDefinitions) {
19
+ this.registry.registerMany(stepDefinitions);
20
+ return this;
21
+ }
22
+
23
+ /**
24
+ * Execute a single step by ID
25
+ */
26
+ async executeStep(stepId, context, skip = false) {
27
+ return await this.executor.execute(stepId, context, skip);
28
+ }
29
+
30
+ /**
31
+ * Execute all registered steps
32
+ */
33
+ async executeAll(context, startStep = null) {
34
+ const firstStep = startStep !== null ? startStep : this.registry.getFirstStepId();
35
+ const lastStep = this.registry.getLastStepId();
36
+
37
+ if (firstStep === -1 || lastStep === -1) {
38
+ throw new Error('No steps registered in pipeline');
39
+ }
40
+
41
+ return await this.executor.executeSequence(firstStep, lastStep, context);
42
+ }
43
+
44
+ /**
45
+ * Execute a range of steps
46
+ */
47
+ async executeRange(startStep, endStep, context) {
48
+ return await this.executor.executeSequence(startStep, endStep, context);
49
+ }
50
+
51
+ /**
52
+ * Execute only specific steps by IDs
53
+ */
54
+ async executeSteps(stepIds, context) {
55
+ const results = [];
56
+
57
+ for (const stepId of stepIds) {
58
+ if (!this.registry.has(stepId)) {
59
+ console.warn(`⚠️ Step ${stepId} not found, skipping...`);
60
+ continue;
61
+ }
62
+
63
+ const result = await this.executor.execute(stepId, context);
64
+ results.push(result);
65
+
66
+ // Stop if step failed and stopOnError is enabled
67
+ if (!result.success && !result.skipped && this.executor.options.stopOnError) {
68
+ break;
69
+ }
70
+ }
71
+
72
+ return results;
73
+ }
74
+
75
+ /**
76
+ * List all registered steps
77
+ */
78
+ listSteps() {
79
+ return this.registry.getAll().map(step => ({
80
+ id: step.id,
81
+ name: step.name,
82
+ description: step.description,
83
+ options: step.options
84
+ }));
85
+ }
86
+
87
+ /**
88
+ * Get a specific step definition
89
+ */
90
+ getStep(stepId) {
91
+ return this.registry.get(stepId);
92
+ }
93
+
94
+ /**
95
+ * Check if a step exists
96
+ */
97
+ hasStep(stepId) {
98
+ return this.registry.has(stepId);
99
+ }
100
+
101
+ /**
102
+ * Get total number of steps
103
+ */
104
+ getStepCount() {
105
+ return this.registry.count();
106
+ }
107
+
108
+ /**
109
+ * Get the first step ID
110
+ */
111
+ getFirstStepId() {
112
+ return this.registry.getFirstStepId();
113
+ }
114
+
115
+ /**
116
+ * Get the last step ID
117
+ */
118
+ getLastStepId() {
119
+ return this.registry.getLastStepId();
120
+ }
121
+
122
+ /**
123
+ * Register event handlers
124
+ */
125
+ on(event, handler) {
126
+ this.executor.on(event, handler);
127
+ return this;
128
+ }
129
+
130
+ /**
131
+ * Print a heartbeat message (useful for long-running steps)
132
+ */
133
+ heartbeat(message) {
134
+ this.executor.printHeartbeat(message);
135
+ }
136
+
137
+ /**
138
+ * Get execution summary from results
139
+ */
140
+ getSummary(results) {
141
+ const successful = results.filter(r => r.success).length;
142
+ const failed = results.filter(r => !r.success && !r.skipped).length;
143
+ const skipped = results.filter(r => r.skipped).length;
144
+ const totalDuration = results.reduce((sum, r) => {
145
+ return sum + (parseFloat(r.duration) || 0);
146
+ }, 0);
147
+
148
+ return {
149
+ total: results.length,
150
+ successful,
151
+ failed,
152
+ skipped,
153
+ successRate: results.length > 0 ? (successful / results.length * 100).toFixed(2) : 0,
154
+ totalDuration: totalDuration.toFixed(2),
155
+ results
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Print execution summary
161
+ */
162
+ printSummary(results) {
163
+ const summary = this.getSummary(results);
164
+
165
+ console.log('\n' + '='.repeat(60));
166
+ console.log('📊 Execution Summary');
167
+ console.log('='.repeat(60));
168
+ console.log(`Total Steps: ${summary.total}`);
169
+ console.log(`✅ Successful: ${summary.successful}`);
170
+ console.log(`❌ Failed: ${summary.failed}`);
171
+ console.log(`⏭️ Skipped: ${summary.skipped}`);
172
+ console.log(`Success Rate: ${summary.successRate}%`);
173
+ console.log(`Total Duration: ${summary.totalDuration}s`);
174
+ console.log('='.repeat(60) + '\n');
175
+
176
+ return summary;
177
+ }
178
+ }
@@ -0,0 +1,124 @@
1
+ import { StepDefinition } from './StepDefinition.js';
2
+
3
+ /**
4
+ * Registry for managing step definitions
5
+ */
6
+ export class StepRegistry {
7
+ constructor() {
8
+ this.steps = new Map();
9
+ }
10
+
11
+ /**
12
+ * Register a new step
13
+ */
14
+ register(stepDefinition) {
15
+ if (!(stepDefinition instanceof StepDefinition)) {
16
+ throw new Error('Invalid step definition');
17
+ }
18
+ this.steps.set(stepDefinition.id, stepDefinition);
19
+ return this;
20
+ }
21
+
22
+ /**
23
+ * Register multiple steps at once
24
+ */
25
+ registerMany(stepDefinitions) {
26
+ stepDefinitions.forEach(step => this.register(step));
27
+ return this;
28
+ }
29
+
30
+ /**
31
+ * Get a step by ID
32
+ */
33
+ get(stepId) {
34
+ const step = this.steps.get(stepId);
35
+ if (!step) {
36
+ throw new Error(`Step ${stepId} not found in registry`);
37
+ }
38
+ return step;
39
+ }
40
+
41
+ /**
42
+ * Get all registered steps
43
+ * Sorts by numeric ID if all IDs are numbers, otherwise maintains insertion order
44
+ */
45
+ getAll() {
46
+ const steps = Array.from(this.steps.values());
47
+
48
+ // Check if all IDs are numeric
49
+ const allNumeric = steps.every(step => typeof step.id === 'number');
50
+
51
+ if (allNumeric) {
52
+ return steps.sort((a, b) => a.id - b.id);
53
+ }
54
+
55
+ // For string IDs, maintain insertion order (Map preserves insertion order)
56
+ return steps;
57
+ }
58
+
59
+ /**
60
+ * Check if a step exists
61
+ */
62
+ has(stepId) {
63
+ return this.steps.has(stepId);
64
+ }
65
+
66
+ /**
67
+ * Get total number of steps
68
+ */
69
+ count() {
70
+ return this.steps.size;
71
+ }
72
+
73
+ /**
74
+ * Get the last step ID
75
+ */
76
+ getLastStepId() {
77
+ const steps = this.getAll();
78
+ return steps.length > 0 ? steps[steps.length - 1].id : null;
79
+ }
80
+
81
+ /**
82
+ * Get the first step ID
83
+ */
84
+ getFirstStepId() {
85
+ const steps = this.getAll();
86
+ return steps.length > 0 ? steps[0].id : null;
87
+ }
88
+
89
+ /**
90
+ * Get steps by phase
91
+ */
92
+ getByPhase(phase) {
93
+ return this.getAll().filter(step => step.options.phase === phase);
94
+ }
95
+
96
+ /**
97
+ * Get steps by tag
98
+ */
99
+ getByTag(tag) {
100
+ return this.getAll().filter(step => step.options.tags.includes(tag));
101
+ }
102
+
103
+ /**
104
+ * Get step IDs in order
105
+ */
106
+ getStepIds() {
107
+ return this.getAll().map(step => step.id);
108
+ }
109
+
110
+ /**
111
+ * Clear all steps
112
+ */
113
+ clear() {
114
+ this.steps.clear();
115
+ return this;
116
+ }
117
+
118
+ /**
119
+ * Remove a specific step
120
+ */
121
+ remove(stepId) {
122
+ return this.steps.delete(stepId);
123
+ }
124
+ }
package/src/index.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @entergreat/step-pipeline
3
+ *
4
+ * Reusable step-based pipeline framework for EnterGreat services
5
+ */
6
+
7
+ import { StepDefinition, StepBuilder } from './core/StepDefinition.js';
8
+ import { StepRegistry } from './core/StepRegistry.js';
9
+ import { StepExecutor } from './core/StepExecutor.js';
10
+ import { StepPipeline } from './core/StepPipeline.js';
11
+ import { createStepsFromConfig } from './utils/helpers.js';
12
+
13
+ // Named exports
14
+ export {
15
+ StepDefinition,
16
+ StepBuilder,
17
+ StepRegistry,
18
+ StepExecutor,
19
+ StepPipeline,
20
+ createStepsFromConfig
21
+ };
22
+
23
+ // Default export
24
+ export default {
25
+ StepDefinition,
26
+ StepBuilder,
27
+ StepRegistry,
28
+ StepExecutor,
29
+ StepPipeline,
30
+ createStepsFromConfig
31
+ };
@@ -0,0 +1,79 @@
1
+ import { StepDefinition, StepBuilder } from '../core/StepDefinition.js';
2
+
3
+ /**
4
+ * Create step definitions from a configuration array
5
+ *
6
+ * @param {Array} config - Array of step configurations
7
+ * @param {Object} handlers - Object containing handler functions
8
+ * @returns {Array<StepDefinition>} Array of step definitions
9
+ *
10
+ * @example
11
+ * const steps = createStepsFromConfig([
12
+ * {
13
+ * id: 'loadData',
14
+ * name: 'Load Data',
15
+ * startMessage: 'start loading data from source',
16
+ * successMessage: 'done loading data successfully'
17
+ * }
18
+ * ], handlers);
19
+ */
20
+ export function createStepsFromConfig(config, handlers) {
21
+ if (!Array.isArray(config)) {
22
+ throw new Error('Config must be an array');
23
+ }
24
+
25
+ if (!handlers || typeof handlers !== 'object') {
26
+ throw new Error('Handlers must be an object');
27
+ }
28
+
29
+ return config.map((stepConfig, index) => {
30
+ const {
31
+ id,
32
+ name,
33
+ description,
34
+ startMessage,
35
+ successMessage,
36
+ errorMessage,
37
+ ...options
38
+ } = stepConfig;
39
+
40
+ // Validate required fields
41
+ if (!id) {
42
+ throw new Error(`Step at index ${index}: 'id' is required`);
43
+ }
44
+
45
+ if (!name) {
46
+ throw new Error(`Step ${id}: 'name' is required`);
47
+ }
48
+
49
+ // Get handler function
50
+ const handler = handlers[id];
51
+ if (typeof handler !== 'function') {
52
+ throw new Error(
53
+ `Step ${id}: handler function not found in handlers object. ` +
54
+ `Expected handlers['${id}'] to be a function.`
55
+ );
56
+ }
57
+
58
+ // Create step definition
59
+ return new StepDefinition(
60
+ id,
61
+ name,
62
+ description || startMessage || '',
63
+ handler,
64
+ {
65
+ startMessage,
66
+ successMessage,
67
+ errorMessage,
68
+ ...options
69
+ }
70
+ );
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Create a fluent step builder
76
+ */
77
+ export function step(id, name) {
78
+ return new StepBuilder(id, name);
79
+ }