@emmvish/stable-request 1.5.3 → 1.6.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/README.md CHANGED
@@ -32,7 +32,8 @@ All in all, it provides you with the **entire ecosystem** to build **API-integra
32
32
  | Batch or fan-out requests | `stableApiGateway` |
33
33
  | Multi-step orchestration | `stableWorkflow` |
34
34
 
35
- Start small and scale
35
+
36
+ Start small and scale.
36
37
 
37
38
  ---
38
39
 
@@ -41,11 +42,8 @@ Start small and scale
41
42
  - [Installation](#installation)
42
43
  - [Core Features](#core-features)
43
44
  - [Quick Start](#quick-start)
44
- - [API Reference](#api-reference)
45
- - [stableRequest](#stableRequest)
46
- - [stableApiGateway](#stableApiGateway)
47
- - [stableWorkflow](#stableWorkflow)
48
45
  - [Advanced Features](#advanced-features)
46
+ - [Non-Linear Workflows](#non-linear-workflows)
49
47
  - [Retry Strategies](#retry-strategies)
50
48
  - [Circuit Breaker](#circuit-breaker)
51
49
  - [Rate Limiting](#rate-limiting)
@@ -57,7 +55,6 @@ Start small and scale
57
55
  - [Response Analysis](#response-analysis)
58
56
  - [Error Handling](#error-handling)
59
57
  - [Advanced Use Cases](#advanced-use-cases)
60
- - [Configuration Options](#configuration-options)
61
58
  - [License](#license)
62
59
  <!-- TOC END -->
63
60
 
@@ -76,7 +73,7 @@ npm install @emmvish/stable-request
76
73
  - ✅ **Rate Limiting**: Control request throughput across single or multiple requests
77
74
  - ✅ **Response Caching**: Built-in TTL-based caching with global cache manager
78
75
  - ✅ **Batch Processing**: Execute multiple requests concurrently or sequentially via API Gateway
79
- - ✅ **Multi-Phase Workflows**: Orchestrate complex request workflows with phase dependencies
76
+ - ✅ **Multi-Phase Non-Linear Workflows**: Orchestrate complex request workflows with phase dependencies
80
77
  - ✅ **Pre-Execution Hooks**: Transform requests before execution with dynamic configuration
81
78
  - ✅ **Shared Buffer**: Share state across requests in workflows and gateways
82
79
  - ✅ **Request Grouping**: Apply different configurations to request groups
@@ -165,84 +162,729 @@ const result = await stableWorkflow(phases, {
165
162
  console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
166
163
  ```
167
164
 
168
- ## API Reference
165
+ ### Non-Linear Workflow with Dynamic Routing
166
+
167
+ ```typescript
168
+ import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
169
+
170
+ const phases: STABLE_WORKFLOW_PHASE[] = [
171
+ {
172
+ id: 'check-status',
173
+ requests: [
174
+ { id: 'status', requestOptions: { reqData: { path: '/status' }, resReq: true } }
175
+ ],
176
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
177
+ const status = phaseResult.responses[0]?.data?.status;
178
+
179
+ if (status === 'completed') {
180
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' };
181
+ } else if (status === 'processing') {
182
+ await new Promise(resolve => setTimeout(resolve, 2000));
183
+ return { action: PHASE_DECISION_ACTIONS.REPLAY }; // Replay this phase
184
+ } else {
185
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-handler' };
186
+ }
187
+ },
188
+ allowReplay: true,
189
+ maxReplayCount: 10
190
+ },
191
+ {
192
+ id: 'process',
193
+ requests: [
194
+ { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
195
+ ]
196
+ },
197
+ {
198
+ id: 'error-handler',
199
+ requests: [
200
+ { id: 'error', requestOptions: { reqData: { path: '/error' }, resReq: true } }
201
+ ]
202
+ },
203
+ {
204
+ id: 'finalize',
205
+ requests: [
206
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
207
+ ]
208
+ }
209
+ ];
210
+
211
+ const result = await stableWorkflow(phases, {
212
+ workflowId: 'dynamic-workflow',
213
+ commonRequestData: { hostname: 'api.example.com' },
214
+ enableNonLinearExecution: true,
215
+ maxWorkflowIterations: 50,
216
+ sharedBuffer: {}
217
+ });
218
+
219
+ console.log('Execution history:', result.executionHistory);
220
+ console.log('Terminated early:', result.terminatedEarly);
221
+ ```
222
+
223
+ ## Advanced Features
224
+
225
+ ### Non-Linear Workflows
226
+
227
+ Non-linear workflows enable dynamic phase execution based on runtime decisions, allowing you to build complex orchestrations with conditional branching, polling loops, error recovery, and adaptive routing.
228
+
229
+ #### Phase Decision Actions
169
230
 
170
- ### stableRequest
231
+ Each phase can make decisions about workflow execution:
171
232
 
172
- Execute a single HTTP request with retry logic and observability.
233
+ - **`continue`**: Proceed to the next sequential phase
234
+ - **`jump`**: Jump to a specific phase by ID
235
+ - **`replay`**: Re-execute the current phase
236
+ - **`skip`**: Skip to a target phase or skip the next phase
237
+ - **`terminate`**: Stop the workflow immediately
238
+
239
+ #### Basic Non-Linear Workflow
173
240
 
174
- **Signature:**
175
241
  ```typescript
176
- async function stableRequest<RequestDataType, ResponseDataType>(
177
- options: STABLE_REQUEST<RequestDataType, ResponseDataType>
178
- ): Promise<ResponseDataType | boolean>
242
+ import { stableWorkflow, STABLE_WORKFLOW_PHASE, PhaseExecutionDecision, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
243
+
244
+ const phases: STABLE_WORKFLOW_PHASE[] = [
245
+ {
246
+ id: 'validate-input',
247
+ requests: [
248
+ { id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
249
+ ],
250
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
251
+ const isValid = phaseResult.responses[0]?.data?.valid;
252
+
253
+ if (isValid) {
254
+ sharedBuffer.validationPassed = true;
255
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to next phase
256
+ } else {
257
+ return {
258
+ action: PHASE_DECISION_ACTIONS.TERMINATE,
259
+ metadata: { reason: 'Validation failed' }
260
+ };
261
+ }
262
+ }
263
+ },
264
+ {
265
+ id: 'process-data',
266
+ requests: [
267
+ { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
268
+ ]
269
+ }
270
+ ];
271
+
272
+ const result = await stableWorkflow(phases, {
273
+ workflowId: 'validation-workflow',
274
+ commonRequestData: { hostname: 'api.example.com' },
275
+ enableNonLinearExecution: true,
276
+ sharedBuffer: {}
277
+ });
278
+
279
+ if (result.terminatedEarly) {
280
+ console.log('Workflow terminated:', result.terminationReason);
281
+ }
179
282
  ```
180
283
 
181
- **Key Options:**
182
- - `reqData`: Request configuration (hostname, path, method, headers, body, etc.)
183
- - `resReq`: If `true`, returns response data; if `false`, returns boolean success status
184
- - `attempts`: Number of retry attempts (default: 1)
185
- - `wait`: Base wait time between retries in milliseconds (default: 1000)
186
- - `retryStrategy`: `FIXED`, `LINEAR`, or `EXPONENTIAL` (default: FIXED)
187
- - `responseAnalyzer`: Custom function to validate response content
188
- - `finalErrorAnalyzer`: Handle final errors gracefully (return `true` to suppress error)
189
- - `cache`: Enable response caching with TTL
190
- - `circuitBreaker`: Circuit breaker configuration
191
- - `preExecution`: Pre-execution hooks for dynamic request transformation
192
- - `commonBuffer`: Shared state object across hooks
284
+ #### Conditional Branching
285
+
286
+ ```typescript
287
+ const phases: STABLE_WORKFLOW_PHASE[] = [
288
+ {
289
+ id: 'check-user-type',
290
+ requests: [
291
+ { id: 'user', requestOptions: { reqData: { path: '/user/info' }, resReq: true } }
292
+ ],
293
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
294
+ const userType = phaseResult.responses[0]?.data?.type;
295
+ sharedBuffer.userType = userType;
296
+
297
+ if (userType === 'premium') {
298
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'premium-flow' };
299
+ } else if (userType === 'trial') {
300
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'trial-flow' };
301
+ } else {
302
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'free-flow' };
303
+ }
304
+ }
305
+ },
306
+ {
307
+ id: 'premium-flow',
308
+ requests: [
309
+ { id: 'premium', requestOptions: { reqData: { path: '/premium/data' }, resReq: true } }
310
+ ],
311
+ phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
312
+ },
313
+ {
314
+ id: 'trial-flow',
315
+ requests: [
316
+ { id: 'trial', requestOptions: { reqData: { path: '/trial/data' }, resReq: true } }
317
+ ],
318
+ phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
319
+ },
320
+ {
321
+ id: 'free-flow',
322
+ requests: [
323
+ { id: 'free', requestOptions: { reqData: { path: '/free/data' }, resReq: true } }
324
+ ]
325
+ },
326
+ {
327
+ id: 'finalize',
328
+ requests: [
329
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
330
+ ]
331
+ }
332
+ ];
193
333
 
194
- ### stableApiGateway
334
+ const result = await stableWorkflow(phases, {
335
+ enableNonLinearExecution: true,
336
+ sharedBuffer: {},
337
+ handlePhaseDecision: (decision, phaseResult) => {
338
+ console.log(`Phase ${phaseResult.phaseId} decided: ${decision.action}`);
339
+ }
340
+ });
341
+ ```
195
342
 
196
- Execute multiple requests concurrently or sequentially with shared configuration.
343
+ #### Polling with Replay
197
344
 
198
- **Signature:**
199
345
  ```typescript
200
- async function stableApiGateway<RequestDataType, ResponseDataType>(
201
- requests: API_GATEWAY_REQUEST<RequestDataType, ResponseDataType>[],
202
- options: API_GATEWAY_OPTIONS<RequestDataType, ResponseDataType>
203
- ): Promise<API_GATEWAY_RESPONSE<ResponseDataType>[]>
346
+ const phases: STABLE_WORKFLOW_PHASE[] = [
347
+ {
348
+ id: 'poll-job-status',
349
+ allowReplay: true,
350
+ maxReplayCount: 20,
351
+ requests: [
352
+ { id: 'check', requestOptions: { reqData: { path: '/job/status' }, resReq: true } }
353
+ ],
354
+ phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
355
+ const status = phaseResult.responses[0]?.data?.status;
356
+ const attempts = executionHistory.filter(h => h.phaseId === 'poll-job-status').length;
357
+
358
+ if (status === 'completed') {
359
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
360
+ } else if (status === 'failed') {
361
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-recovery' };
362
+ } else if (attempts < 20) {
363
+ // Still processing, wait and replay
364
+ await new Promise(resolve => setTimeout(resolve, 2000));
365
+ return { action: PHASE_DECISION_ACTIONS.REPLAY };
366
+ } else {
367
+ return {
368
+ action: PHASE_DECISION_ACTIONS.TERMINATE,
369
+ metadata: { reason: 'Job timeout after 20 attempts' }
370
+ };
371
+ }
372
+ }
373
+ },
374
+ {
375
+ id: 'process-results',
376
+ requests: [
377
+ { id: 'process', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
378
+ ]
379
+ },
380
+ {
381
+ id: 'error-recovery',
382
+ requests: [
383
+ { id: 'recover', requestOptions: { reqData: { path: '/job/retry' }, resReq: true } }
384
+ ]
385
+ }
386
+ ];
387
+
388
+ const result = await stableWorkflow(phases, {
389
+ workflowId: 'polling-workflow',
390
+ commonRequestData: { hostname: 'api.example.com' },
391
+ enableNonLinearExecution: true,
392
+ maxWorkflowIterations: 100
393
+ });
394
+
395
+ console.log('Total iterations:', result.executionHistory.length);
396
+ console.log('Phases executed:', result.completedPhases);
204
397
  ```
205
398
 
206
- **Key Options:**
207
- - `concurrentExecution`: Execute requests concurrently (default: true)
208
- - `stopOnFirstError`: Stop processing on first error (sequential mode only)
209
- - `maxConcurrentRequests`: Limit concurrent execution
210
- - `rateLimit`: Rate limiting configuration
211
- - `circuitBreaker`: Shared circuit breaker across requests
212
- - `requestGroups`: Apply different configurations to request groups
213
- - `sharedBuffer`: Shared state across all requests
214
- - `common*`: Common configuration applied to all requests (e.g., `commonAttempts`, `commonCache`)
399
+ #### Retry Logic with Replay
400
+
401
+ ```typescript
402
+ const phases: STABLE_WORKFLOW_PHASE[] = [
403
+ {
404
+ id: 'attempt-operation',
405
+ allowReplay: true,
406
+ maxReplayCount: 3,
407
+ requests: [
408
+ {
409
+ id: 'operation',
410
+ requestOptions: {
411
+ reqData: { path: '/risky-operation', method: 'POST' },
412
+ resReq: true,
413
+ attempts: 1 // No retries at request level
414
+ }
415
+ }
416
+ ],
417
+ phaseDecisionHook: async ({ phaseResult, executionHistory, sharedBuffer }) => {
418
+ const success = phaseResult.responses[0]?.success;
419
+ const attemptCount = executionHistory.filter(h => h.phaseId === 'attempt-operation').length;
420
+
421
+ if (success) {
422
+ sharedBuffer.operationResult = phaseResult.responses[0]?.data;
423
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
424
+ } else if (attemptCount < 3) {
425
+ // Exponential backoff
426
+ const delay = 1000 * Math.pow(2, attemptCount);
427
+ await new Promise(resolve => setTimeout(resolve, delay));
428
+
429
+ sharedBuffer.retryAttempts = attemptCount;
430
+ return { action: PHASE_DECISION_ACTIONS.REPLAY };
431
+ } else {
432
+ return {
433
+ action: PHASE_DECISION_ACTIONS.JUMP,
434
+ targetPhaseId: 'fallback-operation',
435
+ metadata: { reason: 'Max retries exceeded' }
436
+ };
437
+ }
438
+ }
439
+ },
440
+ {
441
+ id: 'primary-flow',
442
+ requests: [
443
+ { id: 'primary', requestOptions: { reqData: { path: '/primary' }, resReq: true } }
444
+ ]
445
+ },
446
+ {
447
+ id: 'fallback-operation',
448
+ requests: [
449
+ { id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
450
+ ]
451
+ }
452
+ ];
215
453
 
216
- ### stableWorkflow
454
+ const result = await stableWorkflow(phases, {
455
+ enableNonLinearExecution: true,
456
+ sharedBuffer: { retryAttempts: 0 },
457
+ logPhaseResults: true
458
+ });
459
+ ```
217
460
 
218
- Execute multi-phase workflows with sequential or concurrent phase execution.
461
+ #### Skip Phases
219
462
 
220
- **Signature:**
221
463
  ```typescript
222
- async function stableWorkflow<RequestDataType, ResponseDataType>(
223
- phases: STABLE_WORKFLOW_PHASE<RequestDataType, ResponseDataType>[],
224
- options: STABLE_WORKFLOW_OPTIONS<RequestDataType, ResponseDataType>
225
- ): Promise<STABLE_WORKFLOW_RESULT<ResponseDataType>>
464
+ const phases: STABLE_WORKFLOW_PHASE[] = [
465
+ {
466
+ id: 'check-cache',
467
+ allowSkip: true,
468
+ requests: [
469
+ { id: 'cache', requestOptions: { reqData: { path: '/cache/check' }, resReq: true } }
470
+ ],
471
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
472
+ const cached = phaseResult.responses[0]?.data?.cached;
473
+
474
+ if (cached) {
475
+ sharedBuffer.cachedData = phaseResult.responses[0]?.data;
476
+ // Skip expensive-computation and go directly to finalize
477
+ return { action: PHASE_DECISION_ACTIONS.SKIP, targetPhaseId: 'finalize' };
478
+ }
479
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
480
+ }
481
+ },
482
+ {
483
+ id: 'expensive-computation',
484
+ requests: [
485
+ { id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
486
+ ]
487
+ },
488
+ {
489
+ id: 'save-to-cache',
490
+ requests: [
491
+ { id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
492
+ ]
493
+ },
494
+ {
495
+ id: 'finalize',
496
+ requests: [
497
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
498
+ ]
499
+ }
500
+ ];
501
+
502
+ const result = await stableWorkflow(phases, {
503
+ enableNonLinearExecution: true,
504
+ sharedBuffer: {}
505
+ });
226
506
  ```
227
507
 
228
- **Key Options:**
229
- - `workflowId`: Unique workflow identifier
230
- - `concurrentPhaseExecution`: Execute phases concurrently (default: false)
231
- - `stopOnFirstPhaseError`: Stop workflow on first phase failure
232
- - `enableMixedExecution`: Allow mixed concurrent/sequential phase execution
233
- - `handlePhaseCompletion`: Hook called after each phase completes
234
- - `handlePhaseError`: Hook called when phase fails
235
- - `sharedBuffer`: Shared state across all phases and requests
236
-
237
- **Phase Configuration:**
238
- - `id`: Phase identifier
239
- - `requests`: Array of requests in this phase
240
- - `concurrentExecution`: Execute phase requests concurrently
241
- - `stopOnFirstError`: Stop phase on first request error
242
- - `markConcurrentPhase`: Mark phase for concurrent execution in mixed mode
243
- - `commonConfig`: Phase-level configuration overrides
508
+ #### Execution History and Tracking
244
509
 
245
- ## Advanced Features
510
+ ```typescript
511
+ const result = await stableWorkflow(phases, {
512
+ workflowId: 'tracked-workflow',
513
+ enableNonLinearExecution: true,
514
+ handlePhaseCompletion: ({ phaseResult, workflowId }) => {
515
+ console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
516
+ executionNumber: phaseResult.executionNumber,
517
+ success: phaseResult.success,
518
+ decision: phaseResult.decision
519
+ });
520
+ },
521
+ handlePhaseDecision: (decision, phaseResult) => {
522
+ console.log(`Decision made:`, {
523
+ phase: phaseResult.phaseId,
524
+ action: decision.action,
525
+ target: decision.targetPhaseId,
526
+ metadata: decision.metadata
527
+ });
528
+ }
529
+ });
530
+
531
+ // Analyze execution history
532
+ console.log('Total phase executions:', result.executionHistory.length);
533
+ console.log('Unique phases executed:', new Set(result.executionHistory.map(h => h.phaseId)).size);
534
+ console.log('Replay count:', result.executionHistory.filter(h => h.decision?.action === 'replay').length);
535
+
536
+ result.executionHistory.forEach(record => {
537
+ console.log(`- ${record.phaseId} (attempt ${record.executionNumber}): ${record.success ? '✓' : '✗'}`);
538
+ });
539
+ ```
540
+
541
+ #### Loop Protection
542
+
543
+ ```typescript
544
+ const result = await stableWorkflow(phases, {
545
+ enableNonLinearExecution: true,
546
+ maxWorkflowIterations: 50, // Prevent infinite loops
547
+ handlePhaseCompletion: ({ phaseResult }) => {
548
+ if (phaseResult.executionNumber && phaseResult.executionNumber > 10) {
549
+ console.warn(`Phase ${phaseResult.phaseId} executed ${phaseResult.executionNumber} times`);
550
+ }
551
+ }
552
+ });
553
+
554
+ if (result.terminatedEarly && result.terminationReason?.includes('iterations')) {
555
+ console.error('Workflow hit iteration limit - possible infinite loop');
556
+ }
557
+ ```
558
+
559
+ #### Mixed Serial and Parallel Execution
560
+
561
+ Non-linear workflows support mixing serial and parallel phase execution. Mark consecutive phases with `markConcurrentPhase: true` to execute them in parallel, while other phases execute serially.
562
+
563
+ **Basic Mixed Execution:**
564
+
565
+ ```typescript
566
+ import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
567
+
568
+ const phases: STABLE_WORKFLOW_PHASE[] = [
569
+ {
570
+ id: 'init',
571
+ requests: [
572
+ { id: 'init', requestOptions: { reqData: { path: '/init' }, resReq: true } }
573
+ ],
574
+ phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.CONTINUE })
575
+ },
576
+ // These two phases execute in parallel
577
+ {
578
+ id: 'check-inventory',
579
+ markConcurrentPhase: true,
580
+ requests: [
581
+ { id: 'inv', requestOptions: { reqData: { path: '/inventory' }, resReq: true } }
582
+ ]
583
+ },
584
+ {
585
+ id: 'check-pricing',
586
+ markConcurrentPhase: true,
587
+ requests: [
588
+ { id: 'price', requestOptions: { reqData: { path: '/pricing' }, resReq: true } }
589
+ ],
590
+ // Decision hook receives results from all concurrent phases
591
+ phaseDecisionHook: async ({ concurrentPhaseResults }) => {
592
+ const inventory = concurrentPhaseResults![0].responses[0]?.data;
593
+ const pricing = concurrentPhaseResults![1].responses[0]?.data;
594
+
595
+ if (inventory.available && pricing.inBudget) {
596
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
597
+ }
598
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'out-of-stock' };
599
+ }
600
+ },
601
+ // This phase executes serially after the parallel group
602
+ {
603
+ id: 'process-order',
604
+ requests: [
605
+ { id: 'order', requestOptions: { reqData: { path: '/order' }, resReq: true } }
606
+ ]
607
+ },
608
+ {
609
+ id: 'out-of-stock',
610
+ requests: [
611
+ { id: 'notify', requestOptions: { reqData: { path: '/notify' }, resReq: true } }
612
+ ]
613
+ }
614
+ ];
615
+
616
+ const result = await stableWorkflow(phases, {
617
+ workflowId: 'mixed-execution',
618
+ commonRequestData: { hostname: 'api.example.com' },
619
+ enableNonLinearExecution: true
620
+ });
621
+ ```
622
+
623
+ **Multiple Parallel Groups:**
624
+
625
+ ```typescript
626
+ const phases: STABLE_WORKFLOW_PHASE[] = [
627
+ {
628
+ id: 'authenticate',
629
+ requests: [
630
+ { id: 'auth', requestOptions: { reqData: { path: '/auth' }, resReq: true } }
631
+ ]
632
+ },
633
+ // First parallel group: Data validation
634
+ {
635
+ id: 'validate-user',
636
+ markConcurrentPhase: true,
637
+ requests: [
638
+ { id: 'val-user', requestOptions: { reqData: { path: '/validate/user' }, resReq: true } }
639
+ ]
640
+ },
641
+ {
642
+ id: 'validate-payment',
643
+ markConcurrentPhase: true,
644
+ requests: [
645
+ { id: 'val-pay', requestOptions: { reqData: { path: '/validate/payment' }, resReq: true } }
646
+ ]
647
+ },
648
+ {
649
+ id: 'validate-shipping',
650
+ markConcurrentPhase: true,
651
+ requests: [
652
+ { id: 'val-ship', requestOptions: { reqData: { path: '/validate/shipping' }, resReq: true } }
653
+ ],
654
+ phaseDecisionHook: async ({ concurrentPhaseResults }) => {
655
+ const allValid = concurrentPhaseResults!.every(r => r.success && r.responses[0]?.data?.valid);
656
+ if (!allValid) {
657
+ return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Validation failed' } };
658
+ }
659
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
660
+ }
661
+ },
662
+ // Serial processing phase
663
+ {
664
+ id: 'calculate-total',
665
+ requests: [
666
+ { id: 'calc', requestOptions: { reqData: { path: '/calculate' }, resReq: true } }
667
+ ]
668
+ },
669
+ // Second parallel group: External integrations
670
+ {
671
+ id: 'notify-warehouse',
672
+ markConcurrentPhase: true,
673
+ requests: [
674
+ { id: 'warehouse', requestOptions: { reqData: { path: '/notify/warehouse' }, resReq: true } }
675
+ ]
676
+ },
677
+ {
678
+ id: 'notify-shipping',
679
+ markConcurrentPhase: true,
680
+ requests: [
681
+ { id: 'shipping', requestOptions: { reqData: { path: '/notify/shipping' }, resReq: true } }
682
+ ]
683
+ },
684
+ {
685
+ id: 'update-inventory',
686
+ markConcurrentPhase: true,
687
+ requests: [
688
+ { id: 'inventory', requestOptions: { reqData: { path: '/update/inventory' }, resReq: true } }
689
+ ]
690
+ },
691
+ // Final serial phase
692
+ {
693
+ id: 'finalize',
694
+ requests: [
695
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
696
+ ]
697
+ }
698
+ ];
699
+
700
+ const result = await stableWorkflow(phases, {
701
+ workflowId: 'multi-parallel-workflow',
702
+ commonRequestData: { hostname: 'api.example.com' },
703
+ enableNonLinearExecution: true
704
+ });
705
+
706
+ console.log('Execution order demonstrates mixed serial/parallel execution');
707
+ ```
708
+
709
+ **Decision Making with Concurrent Results:**
710
+
711
+ ```typescript
712
+ const phases: STABLE_WORKFLOW_PHASE[] = [
713
+ {
714
+ id: 'api-check-1',
715
+ markConcurrentPhase: true,
716
+ requests: [
717
+ { id: 'api1', requestOptions: { reqData: { path: '/health/api1' }, resReq: true } }
718
+ ]
719
+ },
720
+ {
721
+ id: 'api-check-2',
722
+ markConcurrentPhase: true,
723
+ requests: [
724
+ { id: 'api2', requestOptions: { reqData: { path: '/health/api2' }, resReq: true } }
725
+ ]
726
+ },
727
+ {
728
+ id: 'api-check-3',
729
+ markConcurrentPhase: true,
730
+ requests: [
731
+ { id: 'api3', requestOptions: { reqData: { path: '/health/api3' }, resReq: true } }
732
+ ],
733
+ phaseDecisionHook: async ({ concurrentPhaseResults, sharedBuffer }) => {
734
+ // Aggregate results from all parallel phases
735
+ const healthScores = concurrentPhaseResults!.map(result =>
736
+ result.responses[0]?.data?.score || 0
737
+ );
738
+
739
+ const averageScore = healthScores.reduce((a, b) => a + b, 0) / healthScores.length;
740
+ sharedBuffer!.healthScore = averageScore;
741
+
742
+ if (averageScore > 0.8) {
743
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'optimal-path' };
744
+ } else if (averageScore > 0.5) {
745
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to degraded-path
746
+ } else {
747
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'fallback-path' };
748
+ }
749
+ }
750
+ },
751
+ {
752
+ id: 'degraded-path',
753
+ requests: [
754
+ { id: 'degraded', requestOptions: { reqData: { path: '/degraded' }, resReq: true } }
755
+ ]
756
+ },
757
+ {
758
+ id: 'optimal-path',
759
+ requests: [
760
+ { id: 'optimal', requestOptions: { reqData: { path: '/optimal' }, resReq: true } }
761
+ ]
762
+ },
763
+ {
764
+ id: 'fallback-path',
765
+ requests: [
766
+ { id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
767
+ ]
768
+ }
769
+ ];
770
+
771
+ const sharedBuffer = {};
772
+ const result = await stableWorkflow(phases, {
773
+ workflowId: 'adaptive-routing',
774
+ commonRequestData: { hostname: 'api.example.com' },
775
+ enableNonLinearExecution: true,
776
+ sharedBuffer
777
+ });
778
+
779
+ console.log('Average health score:', sharedBuffer.healthScore);
780
+ ```
781
+
782
+ **Error Handling in Parallel Groups:**
783
+
784
+ ```typescript
785
+ const phases: STABLE_WORKFLOW_PHASE[] = [
786
+ {
787
+ id: 'critical-check',
788
+ markConcurrentPhase: true,
789
+ requests: [
790
+ {
791
+ id: 'check1',
792
+ requestOptions: {
793
+ reqData: { path: '/critical/check1' },
794
+ resReq: true,
795
+ attempts: 3
796
+ }
797
+ }
798
+ ]
799
+ },
800
+ {
801
+ id: 'optional-check',
802
+ markConcurrentPhase: true,
803
+ requests: [
804
+ {
805
+ id: 'check2',
806
+ requestOptions: {
807
+ reqData: { path: '/optional/check2' },
808
+ resReq: true,
809
+ attempts: 1,
810
+ finalErrorAnalyzer: async () => true // Suppress errors
811
+ }
812
+ }
813
+ ],
814
+ phaseDecisionHook: async ({ concurrentPhaseResults }) => {
815
+ // Check if critical phase succeeded
816
+ const criticalSuccess = concurrentPhaseResults![0].success;
817
+
818
+ if (!criticalSuccess) {
819
+ return {
820
+ action: PHASE_DECISION_ACTIONS.TERMINATE,
821
+ metadata: { reason: 'Critical check failed' }
822
+ };
823
+ }
824
+
825
+ // Continue even if optional check failed
826
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
827
+ }
828
+ },
829
+ {
830
+ id: 'process',
831
+ requests: [
832
+ { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
833
+ ]
834
+ }
835
+ ];
836
+
837
+ const result = await stableWorkflow(phases, {
838
+ workflowId: 'resilient-parallel',
839
+ commonRequestData: { hostname: 'api.example.com' },
840
+ enableNonLinearExecution: true,
841
+ stopOnFirstPhaseError: false // Continue even with phase errors
842
+ });
843
+ ```
844
+
845
+ **Key Points:**
846
+ - Only **consecutive phases** with `markConcurrentPhase: true` execute in parallel
847
+ - The **last phase** in a concurrent group can have a `phaseDecisionHook` that receives `concurrentPhaseResults`
848
+ - Parallel groups are separated by phases **without** `markConcurrentPhase` (or phases with it set to false)
849
+ - All decision actions work with parallel groups except `REPLAY` (not supported for concurrent groups)
850
+ - Error handling follows normal workflow rules - use `stopOnFirstPhaseError` to control behavior
851
+
852
+ #### Configuration Options
853
+
854
+ **Workflow Options:**
855
+ - `enableNonLinearExecution`: Enable non-linear workflow (required)
856
+ - `maxWorkflowIterations`: Maximum total iterations (default: 1000)
857
+ - `handlePhaseDecision`: Called when phase makes a decision
858
+ - `stopOnFirstPhaseError`: Stop on phase failure (default: false)
859
+
860
+ **Phase Options:**
861
+ - `phaseDecisionHook`: Function returning `PhaseExecutionDecision`
862
+ - `allowReplay`: Allow phase replay (default: false)
863
+ - `allowSkip`: Allow phase skip (default: false)
864
+ - `maxReplayCount`: Maximum replays (default: Infinity)
865
+
866
+ **Decision Hook Parameters:**
867
+ ```typescript
868
+ interface PhaseDecisionHookOptions {
869
+ workflowId: string;
870
+ phaseResult: STABLE_WORKFLOW_PHASE_RESULT;
871
+ phaseId: string;
872
+ phaseIndex: number;
873
+ executionHistory: PhaseExecutionRecord[];
874
+ sharedBuffer?: Record<string, any>;
875
+ params?: any;
876
+ }
877
+ ```
878
+
879
+ **Decision Object:**
880
+ ```typescript
881
+ interface PhaseExecutionDecision {
882
+ action: PHASE_DECISION_ACTIONS;
883
+ targetPhaseId?: string;
884
+ replayCount?: number;
885
+ metadata?: Record<string, any>;
886
+ }
887
+ ```
246
888
 
247
889
  ### Retry Strategies
248
890
 
@@ -952,83 +1594,11 @@ console.log('Products:', result.data.products?.length);
952
1594
  console.log('Orders:', result.data.orders?.length);
953
1595
  ```
954
1596
 
955
- ## Configuration Options
956
-
957
- ### Request Data Configuration
958
-
959
- ```typescript
960
- interface REQUEST_DATA<RequestDataType> {
961
- hostname: string;
962
- protocol?: 'http' | 'https'; // default: 'https'
963
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; // default: 'GET'
964
- path?: `/${string}`;
965
- port?: number; // default: 443
966
- headers?: Record<string, any>;
967
- body?: RequestDataType;
968
- query?: Record<string, any>;
969
- timeout?: number; // default: 15000ms
970
- signal?: AbortSignal;
971
- }
972
- ```
973
-
974
- ### Retry Configuration
975
-
976
- ```typescript
977
- interface RetryConfig {
978
- attempts?: number; // default: 1
979
- wait?: number; // default: 1000ms
980
- maxAllowedWait?: number; // default: 60000ms
981
- retryStrategy?: 'fixed' | 'linear' | 'exponential'; // default: 'fixed'
982
- performAllAttempts?: boolean; // default: false
983
- }
984
- ```
985
-
986
- ### Circuit Breaker Configuration
987
-
988
- ```typescript
989
- interface CircuitBreakerConfig {
990
- failureThresholdPercentage: number; // 0-100
991
- minimumRequests: number;
992
- recoveryTimeoutMs: number;
993
- trackIndividualAttempts?: boolean; // default: false
994
- }
995
- ```
996
-
997
- ### Rate Limit Configuration
998
-
999
- ```typescript
1000
- interface RateLimitConfig {
1001
- maxRequests: number;
1002
- windowMs: number;
1003
- }
1004
- ```
1005
-
1006
- ### Cache Configuration
1007
-
1008
- ```typescript
1009
- interface CacheConfig {
1010
- enabled: boolean;
1011
- ttl?: number; // milliseconds, default: 300000 (5 minutes)
1012
- }
1013
- ```
1014
-
1015
- ### Pre-Execution Configuration
1016
-
1017
- ```typescript
1018
- interface RequestPreExecutionOptions {
1019
- preExecutionHook: (options: PreExecutionHookOptions) => any | Promise<any>;
1020
- preExecutionHookParams?: any;
1021
- applyPreExecutionConfigOverride?: boolean; // default: false
1022
- continueOnPreExecutionHookFailure?: boolean; // default: false
1023
- }
1024
- ```
1025
-
1026
1597
  ## License
1027
1598
 
1028
1599
  MIT © Manish Varma
1029
1600
 
1030
1601
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
1031
-
1032
1602
  ---
1033
1603
 
1034
1604
  **Made with ❤️ for developers integrating with unreliable APIs**