@emmvish/stable-request 1.5.3 → 1.6.1

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,9 @@ 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)
47
+ - [Branched Workflows](#branched-workflows)
49
48
  - [Retry Strategies](#retry-strategies)
50
49
  - [Circuit Breaker](#circuit-breaker)
51
50
  - [Rate Limiting](#rate-limiting)
@@ -57,192 +56,1356 @@ Start small and scale
57
56
  - [Response Analysis](#response-analysis)
58
57
  - [Error Handling](#error-handling)
59
58
  - [Advanced Use Cases](#advanced-use-cases)
60
- - [Configuration Options](#configuration-options)
61
59
  - [License](#license)
62
60
  <!-- TOC END -->
63
61
 
64
- ---
62
+ ---
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ npm install @emmvish/stable-request
68
+ ```
69
+
70
+ ## Core Features
71
+
72
+ - ✅ **Configurable Retry Strategies**: Fixed, Linear, and Exponential backoff
73
+ - ✅ **Circuit Breaker**: Prevent cascading failures with automatic circuit breaking
74
+ - ✅ **Rate Limiting**: Control request throughput across single or multiple requests
75
+ - ✅ **Response Caching**: Built-in TTL-based caching with global cache manager
76
+ - ✅ **Batch Processing**: Execute multiple requests concurrently or sequentially via API Gateway
77
+ - ✅ **Multi-Phase Non-Linear Workflows**: Orchestrate complex request workflows with phase dependencies
78
+ - ✅ **Branched Workflows**: Execute parallel or serial branches with conditional routing and decision hooks
79
+ - ✅ **Pre-Execution Hooks**: Transform requests before execution with dynamic configuration
80
+ - ✅ **Shared Buffer**: Share state across requests in workflows and gateways
81
+ - ✅ **Request Grouping**: Apply different configurations to request groups
82
+ - ✅ **Observability Hooks**: Track errors, successful attempts, and phase completions
83
+ - ✅ **Response Analysis**: Validate responses and trigger retries based on content
84
+ - ✅ **Trial Mode**: Test configurations without making real API calls
85
+ - ✅ **TypeScript Support**: Full type safety with generics for request/response data
86
+
87
+ ## Quick Start
88
+
89
+ ### Basic Request with Retry
90
+
91
+ ```typescript
92
+ import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
93
+
94
+ const data = await stableRequest({
95
+ reqData: {
96
+ hostname: 'api.example.com',
97
+ path: '/users/123',
98
+ method: 'GET'
99
+ },
100
+ resReq: true,
101
+ attempts: 3,
102
+ wait: 1000,
103
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
104
+ });
105
+
106
+ console.log(data);
107
+ ```
108
+
109
+ ### Batch Requests via API Gateway
110
+
111
+ ```typescript
112
+ import { stableApiGateway } from '@emmvish/stable-request';
113
+
114
+ const requests = [
115
+ { id: 'user-1', requestOptions: { reqData: { path: '/users/1' }, resReq: true } },
116
+ { id: 'user-2', requestOptions: { reqData: { path: '/users/2' }, resReq: true } },
117
+ { id: 'user-3', requestOptions: { reqData: { path: '/users/3' }, resReq: true } }
118
+ ];
119
+
120
+ const results = await stableApiGateway(requests, {
121
+ commonRequestData: { hostname: 'api.example.com' },
122
+ concurrentExecution: true,
123
+ maxConcurrentRequests: 10
124
+ });
125
+
126
+ results.forEach(result => {
127
+ if (result.success) {
128
+ console.log(`Request ${result.requestId}:`, result.data);
129
+ } else {
130
+ console.error(`Request ${result.requestId} failed:`, result.error);
131
+ }
132
+ });
133
+ ```
134
+
135
+ ### Multi-Phase Workflow
136
+
137
+ ```typescript
138
+ import { stableWorkflow, STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
139
+
140
+ const phases: STABLE_WORKFLOW_PHASE[] = [
141
+ {
142
+ id: 'authentication',
143
+ requests: [
144
+ { id: 'login', requestOptions: { reqData: { path: '/auth/login' }, resReq: true } }
145
+ ]
146
+ },
147
+ {
148
+ id: 'data-fetching',
149
+ concurrentExecution: true,
150
+ requests: [
151
+ { id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
152
+ { id: 'posts', requestOptions: { reqData: { path: '/posts' }, resReq: true } }
153
+ ]
154
+ }
155
+ ];
156
+
157
+ const result = await stableWorkflow(phases, {
158
+ workflowId: 'data-pipeline',
159
+ commonRequestData: { hostname: 'api.example.com' },
160
+ stopOnFirstPhaseError: true,
161
+ logPhaseResults: true
162
+ });
163
+
164
+ console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
165
+ ```
166
+
167
+ ### Non-Linear Workflow with Dynamic Routing
168
+
169
+ ```typescript
170
+ import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
171
+
172
+ const phases: STABLE_WORKFLOW_PHASE[] = [
173
+ {
174
+ id: 'check-status',
175
+ requests: [
176
+ { id: 'status', requestOptions: { reqData: { path: '/status' }, resReq: true } }
177
+ ],
178
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
179
+ const status = phaseResult.responses[0]?.data?.status;
180
+
181
+ if (status === 'completed') {
182
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' };
183
+ } else if (status === 'processing') {
184
+ await new Promise(resolve => setTimeout(resolve, 2000));
185
+ return { action: PHASE_DECISION_ACTIONS.REPLAY }; // Replay this phase
186
+ } else {
187
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-handler' };
188
+ }
189
+ },
190
+ allowReplay: true,
191
+ maxReplayCount: 10
192
+ },
193
+ {
194
+ id: 'process',
195
+ requests: [
196
+ { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
197
+ ]
198
+ },
199
+ {
200
+ id: 'error-handler',
201
+ requests: [
202
+ { id: 'error', requestOptions: { reqData: { path: '/error' }, resReq: true } }
203
+ ]
204
+ },
205
+ {
206
+ id: 'finalize',
207
+ requests: [
208
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
209
+ ]
210
+ }
211
+ ];
212
+
213
+ const result = await stableWorkflow(phases, {
214
+ workflowId: 'dynamic-workflow',
215
+ commonRequestData: { hostname: 'api.example.com' },
216
+ enableNonLinearExecution: true,
217
+ maxWorkflowIterations: 50,
218
+ sharedBuffer: {}
219
+ });
220
+
221
+ console.log('Execution history:', result.executionHistory);
222
+ console.log('Terminated early:', result.terminatedEarly);
223
+ ```
224
+
225
+ ## Advanced Features
226
+
227
+ ### Non-Linear Workflows
228
+
229
+ 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.
230
+
231
+ #### Phase Decision Actions
232
+
233
+ Each phase can make decisions about workflow execution:
234
+
235
+ - **`continue`**: Proceed to the next sequential phase
236
+ - **`jump`**: Jump to a specific phase by ID
237
+ - **`replay`**: Re-execute the current phase
238
+ - **`skip`**: Skip to a target phase or skip the next phase
239
+ - **`terminate`**: Stop the workflow immediately
240
+
241
+ #### Basic Non-Linear Workflow
242
+
243
+ ```typescript
244
+ import { stableWorkflow, STABLE_WORKFLOW_PHASE, PhaseExecutionDecision, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
245
+
246
+ const phases: STABLE_WORKFLOW_PHASE[] = [
247
+ {
248
+ id: 'validate-input',
249
+ requests: [
250
+ { id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
251
+ ],
252
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
253
+ const isValid = phaseResult.responses[0]?.data?.valid;
254
+
255
+ if (isValid) {
256
+ sharedBuffer.validationPassed = true;
257
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to next phase
258
+ } else {
259
+ return {
260
+ action: PHASE_DECISION_ACTIONS.TERMINATE,
261
+ metadata: { reason: 'Validation failed' }
262
+ };
263
+ }
264
+ }
265
+ },
266
+ {
267
+ id: 'process-data',
268
+ requests: [
269
+ { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
270
+ ]
271
+ }
272
+ ];
273
+
274
+ const result = await stableWorkflow(phases, {
275
+ workflowId: 'validation-workflow',
276
+ commonRequestData: { hostname: 'api.example.com' },
277
+ enableNonLinearExecution: true,
278
+ sharedBuffer: {}
279
+ });
280
+
281
+ if (result.terminatedEarly) {
282
+ console.log('Workflow terminated:', result.terminationReason);
283
+ }
284
+ ```
285
+
286
+ #### Conditional Branching
287
+
288
+ ```typescript
289
+ const phases: STABLE_WORKFLOW_PHASE[] = [
290
+ {
291
+ id: 'check-user-type',
292
+ requests: [
293
+ { id: 'user', requestOptions: { reqData: { path: '/user/info' }, resReq: true } }
294
+ ],
295
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
296
+ const userType = phaseResult.responses[0]?.data?.type;
297
+ sharedBuffer.userType = userType;
298
+
299
+ if (userType === 'premium') {
300
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'premium-flow' };
301
+ } else if (userType === 'trial') {
302
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'trial-flow' };
303
+ } else {
304
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'free-flow' };
305
+ }
306
+ }
307
+ },
308
+ {
309
+ id: 'premium-flow',
310
+ requests: [
311
+ { id: 'premium', requestOptions: { reqData: { path: '/premium/data' }, resReq: true } }
312
+ ],
313
+ phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
314
+ },
315
+ {
316
+ id: 'trial-flow',
317
+ requests: [
318
+ { id: 'trial', requestOptions: { reqData: { path: '/trial/data' }, resReq: true } }
319
+ ],
320
+ phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
321
+ },
322
+ {
323
+ id: 'free-flow',
324
+ requests: [
325
+ { id: 'free', requestOptions: { reqData: { path: '/free/data' }, resReq: true } }
326
+ ]
327
+ },
328
+ {
329
+ id: 'finalize',
330
+ requests: [
331
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
332
+ ]
333
+ }
334
+ ];
335
+
336
+ const result = await stableWorkflow(phases, {
337
+ enableNonLinearExecution: true,
338
+ sharedBuffer: {},
339
+ handlePhaseDecision: (decision, phaseResult) => {
340
+ console.log(`Phase ${phaseResult.phaseId} decided: ${decision.action}`);
341
+ }
342
+ });
343
+ ```
344
+
345
+ #### Polling with Replay
346
+
347
+ ```typescript
348
+ const phases: STABLE_WORKFLOW_PHASE[] = [
349
+ {
350
+ id: 'poll-job-status',
351
+ allowReplay: true,
352
+ maxReplayCount: 20,
353
+ requests: [
354
+ { id: 'check', requestOptions: { reqData: { path: '/job/status' }, resReq: true } }
355
+ ],
356
+ phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
357
+ const status = phaseResult.responses[0]?.data?.status;
358
+ const attempts = executionHistory.filter(h => h.phaseId === 'poll-job-status').length;
359
+
360
+ if (status === 'completed') {
361
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
362
+ } else if (status === 'failed') {
363
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-recovery' };
364
+ } else if (attempts < 20) {
365
+ // Still processing, wait and replay
366
+ await new Promise(resolve => setTimeout(resolve, 2000));
367
+ return { action: PHASE_DECISION_ACTIONS.REPLAY };
368
+ } else {
369
+ return {
370
+ action: PHASE_DECISION_ACTIONS.TERMINATE,
371
+ metadata: { reason: 'Job timeout after 20 attempts' }
372
+ };
373
+ }
374
+ }
375
+ },
376
+ {
377
+ id: 'process-results',
378
+ requests: [
379
+ { id: 'process', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
380
+ ]
381
+ },
382
+ {
383
+ id: 'error-recovery',
384
+ requests: [
385
+ { id: 'recover', requestOptions: { reqData: { path: '/job/retry' }, resReq: true } }
386
+ ]
387
+ }
388
+ ];
389
+
390
+ const result = await stableWorkflow(phases, {
391
+ workflowId: 'polling-workflow',
392
+ commonRequestData: { hostname: 'api.example.com' },
393
+ enableNonLinearExecution: true,
394
+ maxWorkflowIterations: 100
395
+ });
396
+
397
+ console.log('Total iterations:', result.executionHistory.length);
398
+ console.log('Phases executed:', result.completedPhases);
399
+ ```
400
+
401
+ #### Retry Logic with Replay
402
+
403
+ ```typescript
404
+ const phases: STABLE_WORKFLOW_PHASE[] = [
405
+ {
406
+ id: 'attempt-operation',
407
+ allowReplay: true,
408
+ maxReplayCount: 3,
409
+ requests: [
410
+ {
411
+ id: 'operation',
412
+ requestOptions: {
413
+ reqData: { path: '/risky-operation', method: 'POST' },
414
+ resReq: true,
415
+ attempts: 1 // No retries at request level
416
+ }
417
+ }
418
+ ],
419
+ phaseDecisionHook: async ({ phaseResult, executionHistory, sharedBuffer }) => {
420
+ const success = phaseResult.responses[0]?.success;
421
+ const attemptCount = executionHistory.filter(h => h.phaseId === 'attempt-operation').length;
422
+
423
+ if (success) {
424
+ sharedBuffer.operationResult = phaseResult.responses[0]?.data;
425
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
426
+ } else if (attemptCount < 3) {
427
+ // Exponential backoff
428
+ const delay = 1000 * Math.pow(2, attemptCount);
429
+ await new Promise(resolve => setTimeout(resolve, delay));
430
+
431
+ sharedBuffer.retryAttempts = attemptCount;
432
+ return { action: PHASE_DECISION_ACTIONS.REPLAY };
433
+ } else {
434
+ return {
435
+ action: PHASE_DECISION_ACTIONS.JUMP,
436
+ targetPhaseId: 'fallback-operation',
437
+ metadata: { reason: 'Max retries exceeded' }
438
+ };
439
+ }
440
+ }
441
+ },
442
+ {
443
+ id: 'primary-flow',
444
+ requests: [
445
+ { id: 'primary', requestOptions: { reqData: { path: '/primary' }, resReq: true } }
446
+ ]
447
+ },
448
+ {
449
+ id: 'fallback-operation',
450
+ requests: [
451
+ { id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
452
+ ]
453
+ }
454
+ ];
455
+
456
+ const result = await stableWorkflow(phases, {
457
+ enableNonLinearExecution: true,
458
+ sharedBuffer: { retryAttempts: 0 },
459
+ logPhaseResults: true
460
+ });
461
+ ```
462
+
463
+ #### Skip Phases
464
+
465
+ ```typescript
466
+ const phases: STABLE_WORKFLOW_PHASE[] = [
467
+ {
468
+ id: 'check-cache',
469
+ allowSkip: true,
470
+ requests: [
471
+ { id: 'cache', requestOptions: { reqData: { path: '/cache/check' }, resReq: true } }
472
+ ],
473
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
474
+ const cached = phaseResult.responses[0]?.data?.cached;
475
+
476
+ if (cached) {
477
+ sharedBuffer.cachedData = phaseResult.responses[0]?.data;
478
+ // Skip expensive-computation and go directly to finalize
479
+ return { action: PHASE_DECISION_ACTIONS.SKIP, targetPhaseId: 'finalize' };
480
+ }
481
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
482
+ }
483
+ },
484
+ {
485
+ id: 'expensive-computation',
486
+ requests: [
487
+ { id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
488
+ ]
489
+ },
490
+ {
491
+ id: 'save-to-cache',
492
+ requests: [
493
+ { id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
494
+ ]
495
+ },
496
+ {
497
+ id: 'finalize',
498
+ requests: [
499
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
500
+ ]
501
+ }
502
+ ];
503
+
504
+ const result = await stableWorkflow(phases, {
505
+ enableNonLinearExecution: true,
506
+ sharedBuffer: {}
507
+ });
508
+ ```
509
+
510
+ #### Execution History and Tracking
511
+
512
+ ```typescript
513
+ const result = await stableWorkflow(phases, {
514
+ workflowId: 'tracked-workflow',
515
+ enableNonLinearExecution: true,
516
+ handlePhaseCompletion: ({ phaseResult, workflowId }) => {
517
+ console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
518
+ executionNumber: phaseResult.executionNumber,
519
+ success: phaseResult.success,
520
+ decision: phaseResult.decision
521
+ });
522
+ },
523
+ handlePhaseDecision: (decision, phaseResult) => {
524
+ console.log(`Decision made:`, {
525
+ phase: phaseResult.phaseId,
526
+ action: decision.action,
527
+ target: decision.targetPhaseId,
528
+ metadata: decision.metadata
529
+ });
530
+ }
531
+ });
532
+
533
+ // Analyze execution history
534
+ console.log('Total phase executions:', result.executionHistory.length);
535
+ console.log('Unique phases executed:', new Set(result.executionHistory.map(h => h.phaseId)).size);
536
+ console.log('Replay count:', result.executionHistory.filter(h => h.decision?.action === 'replay').length);
537
+
538
+ result.executionHistory.forEach(record => {
539
+ console.log(`- ${record.phaseId} (attempt ${record.executionNumber}): ${record.success ? '✓' : '✗'}`);
540
+ });
541
+ ```
542
+
543
+ #### Loop Protection
544
+
545
+ ```typescript
546
+ const result = await stableWorkflow(phases, {
547
+ enableNonLinearExecution: true,
548
+ maxWorkflowIterations: 50, // Prevent infinite loops
549
+ handlePhaseCompletion: ({ phaseResult }) => {
550
+ if (phaseResult.executionNumber && phaseResult.executionNumber > 10) {
551
+ console.warn(`Phase ${phaseResult.phaseId} executed ${phaseResult.executionNumber} times`);
552
+ }
553
+ }
554
+ });
555
+
556
+ if (result.terminatedEarly && result.terminationReason?.includes('iterations')) {
557
+ console.error('Workflow hit iteration limit - possible infinite loop');
558
+ }
559
+ ```
560
+
561
+ ### Branched Workflows
562
+
563
+ Branched workflows enable orchestration of complex business logic by organizing phases into branches that can execute in parallel or serial order. Each branch is a self-contained workflow with its own phases, and branches can make decisions to control execution flow using JUMP, TERMINATE, or CONTINUE actions.
564
+
565
+ #### Why Branched Workflows?
566
+
567
+ - **Organize complex logic**: Group related phases into logical branches
568
+ - **Parallel processing**: Execute independent branches concurrently for better performance
569
+ - **Conditional routing**: Branches can decide whether to continue, jump to other branches, or terminate
570
+ - **Clean architecture**: Separate concerns into distinct branches (validation, processing, error handling)
571
+ - **Shared state**: Branches share a common buffer for state management
572
+
573
+ #### Basic Branched Workflow
574
+
575
+ ```typescript
576
+ import { stableWorkflow, STABLE_WORKFLOW_BRANCH } from '@emmvish/stable-request';
577
+
578
+ const branches: STABLE_WORKFLOW_BRANCH[] = [
579
+ {
580
+ id: 'validation',
581
+ phases: [
582
+ {
583
+ id: 'validate-input',
584
+ requests: [
585
+ { id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
586
+ ]
587
+ }
588
+ ]
589
+ },
590
+ {
591
+ id: 'processing',
592
+ phases: [
593
+ {
594
+ id: 'process-data',
595
+ requests: [
596
+ { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
597
+ ]
598
+ }
599
+ ]
600
+ },
601
+ {
602
+ id: 'finalization',
603
+ phases: [
604
+ {
605
+ id: 'finalize',
606
+ requests: [
607
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
608
+ ]
609
+ }
610
+ ]
611
+ }
612
+ ];
613
+
614
+ const result = await stableWorkflow([], {
615
+ workflowId: 'branched-workflow',
616
+ commonRequestData: { hostname: 'api.example.com' },
617
+ branches,
618
+ executeBranchesConcurrently: false, // Execute branches serially
619
+ sharedBuffer: {}
620
+ });
621
+
622
+ console.log('Branches executed:', result.branches?.length);
623
+ ```
624
+
625
+ #### Parallel vs Serial Branch Execution
626
+
627
+ ```typescript
628
+ // Parallel execution - all branches run concurrently
629
+ const result = await stableWorkflow([], {
630
+ workflowId: 'parallel-branches',
631
+ commonRequestData: { hostname: 'api.example.com' },
632
+ branches: [
633
+ { id: 'fetch-users', phases: [/* ... */] },
634
+ { id: 'fetch-products', phases: [/* ... */] },
635
+ { id: 'fetch-orders', phases: [/* ... */] }
636
+ ],
637
+ executeBranchesConcurrently: true, // Parallel execution
638
+ sharedBuffer: {}
639
+ });
640
+
641
+ // Serial execution - branches run one after another
642
+ const result = await stableWorkflow([], {
643
+ workflowId: 'serial-branches',
644
+ commonRequestData: { hostname: 'api.example.com' },
645
+ branches: [
646
+ { id: 'authenticate', phases: [/* ... */] },
647
+ { id: 'fetch-data', phases: [/* ... */] },
648
+ { id: 'process', phases: [/* ... */] }
649
+ ],
650
+ executeBranchesConcurrently: false, // Serial execution
651
+ sharedBuffer: {}
652
+ });
653
+ ```
654
+
655
+ #### Branch Decision Hooks
656
+
657
+ Each branch can have a decision hook to control workflow execution:
658
+
659
+ ```typescript
660
+ import { BRANCH_DECISION_ACTIONS } from '@emmvish/stable-request';
661
+
662
+ const branches: STABLE_WORKFLOW_BRANCH[] = [
663
+ {
664
+ id: 'validation',
665
+ phases: [
666
+ {
667
+ id: 'validate',
668
+ requests: [
669
+ { id: 'val', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
670
+ ]
671
+ }
672
+ ],
673
+ branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
674
+ const isValid = branchResult.phases[0]?.responses[0]?.data?.valid;
675
+
676
+ if (!isValid) {
677
+ return {
678
+ action: BRANCH_DECISION_ACTIONS.TERMINATE,
679
+ metadata: { reason: 'Validation failed' }
680
+ };
681
+ }
682
+
683
+ sharedBuffer!.validated = true;
684
+ return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
685
+ }
686
+ },
687
+ {
688
+ id: 'processing',
689
+ phases: [/* ... */]
690
+ }
691
+ ];
692
+
693
+ const result = await stableWorkflow([], {
694
+ workflowId: 'validation-workflow',
695
+ commonRequestData: { hostname: 'api.example.com' },
696
+ branches,
697
+ executeBranchesConcurrently: false,
698
+ sharedBuffer: {}
699
+ });
700
+
701
+ if (result.terminatedEarly) {
702
+ console.log('Workflow terminated:', result.terminationReason);
703
+ }
704
+ ```
705
+
706
+ #### JUMP Action - Skip Branches
707
+
708
+ ```typescript
709
+ const branches: STABLE_WORKFLOW_BRANCH[] = [
710
+ {
711
+ id: 'check-cache',
712
+ phases: [
713
+ {
714
+ id: 'cache-check',
715
+ requests: [
716
+ { id: 'check', requestOptions: { reqData: { path: '/cache/check' }, resReq: true } }
717
+ ]
718
+ }
719
+ ],
720
+ branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
721
+ const cached = branchResult.phases[0]?.responses[0]?.data?.cached;
722
+
723
+ if (cached) {
724
+ sharedBuffer!.cachedData = branchResult.phases[0]?.responses[0]?.data;
725
+ // Skip expensive computation, jump directly to finalize
726
+ return {
727
+ action: BRANCH_DECISION_ACTIONS.JUMP,
728
+ targetBranchId: 'finalize'
729
+ };
730
+ }
731
+
732
+ return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
733
+ }
734
+ },
735
+ {
736
+ id: 'expensive-computation',
737
+ phases: [
738
+ {
739
+ id: 'compute',
740
+ requests: [
741
+ { id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
742
+ ]
743
+ }
744
+ ]
745
+ },
746
+ {
747
+ id: 'save-cache',
748
+ phases: [
749
+ {
750
+ id: 'save',
751
+ requests: [
752
+ { id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
753
+ ]
754
+ }
755
+ ]
756
+ },
757
+ {
758
+ id: 'finalize',
759
+ phases: [
760
+ {
761
+ id: 'final',
762
+ requests: [
763
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
764
+ ]
765
+ }
766
+ ]
767
+ }
768
+ ];
769
+
770
+ const result = await stableWorkflow([], {
771
+ workflowId: 'cache-optimization',
772
+ commonRequestData: { hostname: 'api.example.com' },
773
+ branches,
774
+ executeBranchesConcurrently: false,
775
+ sharedBuffer: {}
776
+ });
777
+
778
+ // If cache hit: check-cache → finalize (skips expensive-computation and save-cache)
779
+ // If cache miss: check-cache → expensive-computation → save-cache → finalize
780
+ ```
781
+
782
+ #### Conditional Branching
783
+
784
+ ```typescript
785
+ const branches: STABLE_WORKFLOW_BRANCH[] = [
786
+ {
787
+ id: 'check-user-type',
788
+ phases: [
789
+ {
790
+ id: 'user-info',
791
+ requests: [
792
+ { id: 'user', requestOptions: { reqData: { path: '/user/info' }, resReq: true } }
793
+ ]
794
+ }
795
+ ],
796
+ branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
797
+ const userType = branchResult.phases[0]?.responses[0]?.data?.type;
798
+ sharedBuffer!.userType = userType;
799
+
800
+ if (userType === 'premium') {
801
+ return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'premium-flow' };
802
+ } else if (userType === 'trial') {
803
+ return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'trial-flow' };
804
+ }
805
+
806
+ return { action: BRANCH_DECISION_ACTIONS.CONTINUE }; // free-flow
807
+ }
808
+ },
809
+ {
810
+ id: 'free-flow',
811
+ phases: [
812
+ {
813
+ id: 'free-data',
814
+ requests: [
815
+ { id: 'free', requestOptions: { reqData: { path: '/free/data' }, resReq: true } }
816
+ ]
817
+ }
818
+ ],
819
+ branchDecisionHook: async () => {
820
+ return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'finalize' };
821
+ }
822
+ },
823
+ {
824
+ id: 'trial-flow',
825
+ phases: [
826
+ {
827
+ id: 'trial-data',
828
+ requests: [
829
+ { id: 'trial', requestOptions: { reqData: { path: '/trial/data' }, resReq: true } }
830
+ ]
831
+ }
832
+ ],
833
+ branchDecisionHook: async () => {
834
+ return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'finalize' };
835
+ }
836
+ },
837
+ {
838
+ id: 'premium-flow',
839
+ phases: [
840
+ {
841
+ id: 'premium-data',
842
+ requests: [
843
+ { id: 'premium', requestOptions: { reqData: { path: '/premium/data' }, resReq: true } }
844
+ ]
845
+ }
846
+ ],
847
+ branchDecisionHook: async () => {
848
+ return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'finalize' };
849
+ }
850
+ },
851
+ {
852
+ id: 'finalize',
853
+ phases: [
854
+ {
855
+ id: 'final',
856
+ requests: [
857
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
858
+ ]
859
+ }
860
+ ]
861
+ }
862
+ ];
863
+
864
+ const result = await stableWorkflow([], {
865
+ workflowId: 'user-type-routing',
866
+ commonRequestData: { hostname: 'api.example.com' },
867
+ branches,
868
+ executeBranchesConcurrently: false,
869
+ sharedBuffer: {}
870
+ });
871
+ ```
872
+
873
+ #### Retry Logic Within Branches
874
+
875
+ ```typescript
876
+ const branches: STABLE_WORKFLOW_BRANCH[] = [
877
+ {
878
+ id: 'retry-branch',
879
+ phases: [
880
+ {
881
+ id: 'retry-phase',
882
+ commonConfig: {
883
+ commonAttempts: 5,
884
+ commonWait: 100,
885
+ commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
886
+ },
887
+ requests: [
888
+ {
889
+ id: 'retry-req',
890
+ requestOptions: {
891
+ reqData: { path: '/unstable-endpoint' },
892
+ resReq: true
893
+ }
894
+ }
895
+ ]
896
+ }
897
+ ]
898
+ }
899
+ ];
900
+
901
+ const result = await stableWorkflow([], {
902
+ workflowId: 'retry-workflow',
903
+ commonRequestData: { hostname: 'api.example.com' },
904
+ branches,
905
+ executeBranchesConcurrently: false
906
+ });
907
+ ```
908
+
909
+ #### Error Handling in Branches
910
+
911
+ ```typescript
912
+ const branches: STABLE_WORKFLOW_BRANCH[] = [
913
+ {
914
+ id: 'risky-operation',
915
+ phases: [
916
+ {
917
+ id: 'operation',
918
+ requests: [
919
+ {
920
+ id: 'op',
921
+ requestOptions: {
922
+ reqData: { path: '/risky' },
923
+ resReq: true,
924
+ attempts: 3
925
+ }
926
+ }
927
+ ]
928
+ }
929
+ ],
930
+ branchDecisionHook: async ({ branchResult }) => {
931
+ if (!branchResult.success) {
932
+ return {
933
+ action: BRANCH_DECISION_ACTIONS.JUMP,
934
+ targetBranchId: 'error-handler'
935
+ };
936
+ }
937
+ return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'success-handler' };
938
+ }
939
+ },
940
+ {
941
+ id: 'success-handler',
942
+ phases: [
943
+ {
944
+ id: 'success',
945
+ requests: [
946
+ { id: 'success', requestOptions: { reqData: { path: '/success' }, resReq: true } }
947
+ ]
948
+ }
949
+ ],
950
+ branchDecisionHook: async () => {
951
+ return { action: BRANCH_DECISION_ACTIONS.TERMINATE };
952
+ }
953
+ },
954
+ {
955
+ id: 'error-handler',
956
+ phases: [
957
+ {
958
+ id: 'error',
959
+ requests: [
960
+ { id: 'error', requestOptions: { reqData: { path: '/error' }, resReq: true } }
961
+ ]
962
+ }
963
+ ]
964
+ }
965
+ ];
966
+
967
+ const result = await stableWorkflow([], {
968
+ workflowId: 'error-handling-workflow',
969
+ commonRequestData: { hostname: 'api.example.com' },
970
+ branches,
971
+ executeBranchesConcurrently: false,
972
+ stopOnFirstPhaseError: false // Continue to error handler branch
973
+ });
974
+ ```
65
975
 
66
- ## Installation
976
+ #### Branch Completion Hooks
67
977
 
68
- ```bash
69
- npm install @emmvish/stable-request
978
+ ```typescript
979
+ const result = await stableWorkflow([], {
980
+ workflowId: 'tracked-branches',
981
+ commonRequestData: { hostname: 'api.example.com' },
982
+ branches,
983
+ executeBranchesConcurrently: true,
984
+ handleBranchCompletion: ({ branchResult, workflowId }) => {
985
+ console.log(`[${workflowId}] Branch ${branchResult.branchId} completed:`, {
986
+ success: branchResult.success,
987
+ phases: branchResult.phases.length,
988
+ decision: branchResult.decision?.action
989
+ });
990
+ }
991
+ });
70
992
  ```
71
993
 
72
- ## Core Features
994
+ #### Mixed Parallel and Serial Branches
73
995
 
74
- - ✅ **Configurable Retry Strategies**: Fixed, Linear, and Exponential backoff
75
- - **Circuit Breaker**: Prevent cascading failures with automatic circuit breaking
76
- - ✅ **Rate Limiting**: Control request throughput across single or multiple requests
77
- - ✅ **Response Caching**: Built-in TTL-based caching with global cache manager
78
- - **Batch Processing**: Execute multiple requests concurrently or sequentially via API Gateway
79
- - ✅ **Multi-Phase Workflows**: Orchestrate complex request workflows with phase dependencies
80
- - ✅ **Pre-Execution Hooks**: Transform requests before execution with dynamic configuration
81
- - ✅ **Shared Buffer**: Share state across requests in workflows and gateways
82
- - ✅ **Request Grouping**: Apply different configurations to request groups
83
- - **Observability Hooks**: Track errors, successful attempts, and phase completions
84
- - ✅ **Response Analysis**: Validate responses and trigger retries based on content
85
- - ✅ **Trial Mode**: Test configurations without making real API calls
86
- - ✅ **TypeScript Support**: Full type safety with generics for request/response data
996
+ ```typescript
997
+ const branches: STABLE_WORKFLOW_BRANCH[] = [
998
+ {
999
+ id: 'init',
1000
+ phases: [/* initialization */]
1001
+ },
1002
+ {
1003
+ id: 'parallel-1',
1004
+ markConcurrentBranch: true,
1005
+ phases: [/* independent task 1 */]
1006
+ },
1007
+ {
1008
+ id: 'parallel-2',
1009
+ markConcurrentBranch: true,
1010
+ phases: [/* independent task 2 */]
1011
+ },
1012
+ {
1013
+ id: 'parallel-3',
1014
+ markConcurrentBranch: true,
1015
+ phases: [/* independent task 3 */],
1016
+ branchDecisionHook: async ({ concurrentBranchResults }) => {
1017
+ // All parallel branches completed, make decision
1018
+ const allSuccessful = concurrentBranchResults!.every(b => b.success);
1019
+ if (!allSuccessful) {
1020
+ return { action: BRANCH_DECISION_ACTIONS.TERMINATE };
1021
+ }
1022
+ return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
1023
+ }
1024
+ },
1025
+ {
1026
+ id: 'finalize',
1027
+ phases: [/* finalization */]
1028
+ }
1029
+ ];
87
1030
 
88
- ## Quick Start
1031
+ const result = await stableWorkflow([], {
1032
+ workflowId: 'mixed-execution',
1033
+ commonRequestData: { hostname: 'api.example.com' },
1034
+ branches,
1035
+ executeBranchesConcurrently: false // Base mode is serial
1036
+ });
89
1037
 
90
- ### Basic Request with Retry
1038
+ // Execution: init [parallel-1, parallel-2, parallel-3] → finalize
1039
+ ```
91
1040
 
92
- ```typescript
93
- import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
1041
+ #### Configuration Options
94
1042
 
95
- const data = await stableRequest({
96
- reqData: {
97
- hostname: 'api.example.com',
98
- path: '/users/123',
99
- method: 'GET'
100
- },
101
- resReq: true,
102
- attempts: 3,
103
- wait: 1000,
104
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
105
- });
1043
+ **Workflow Options:**
1044
+ - `branches`: Array of branch definitions
1045
+ - `executeBranchesConcurrently`: Execute all branches in parallel (default: false)
1046
+ - `handleBranchCompletion`: Called when each branch completes
106
1047
 
107
- console.log(data);
1048
+ **Branch Options:**
1049
+ - `id`: Unique branch identifier
1050
+ - `phases`: Array of phases to execute in this branch
1051
+ - `branchDecisionHook`: Function returning `BranchExecutionDecision`
1052
+ - `markConcurrentBranch`: Mark as part of concurrent group (default: false)
1053
+
1054
+ **Branch Decision Actions:**
1055
+ - `CONTINUE`: Proceed to next branch
1056
+ - `JUMP`: Jump to specific branch by ID
1057
+ - `TERMINATE`: Stop workflow execution
1058
+
1059
+ **Decision Hook Parameters:**
1060
+ ```typescript
1061
+ interface BranchDecisionHookOptions {
1062
+ workflowId: string;
1063
+ branchResult: STABLE_WORKFLOW_BRANCH_RESULT;
1064
+ branchId: string;
1065
+ branchIndex: number;
1066
+ sharedBuffer?: Record<string, any>;
1067
+ concurrentBranchResults?: STABLE_WORKFLOW_BRANCH_RESULT[]; // For concurrent groups
1068
+ }
108
1069
  ```
109
1070
 
110
- ### Batch Requests via API Gateway
1071
+ **Decision Object:**
1072
+ ```typescript
1073
+ interface BranchExecutionDecision {
1074
+ action: BRANCH_DECISION_ACTIONS;
1075
+ targetBranchId?: string;
1076
+ metadata?: Record<string, any>;
1077
+ }
1078
+ ```
1079
+
1080
+ #### Mixed Serial and Parallel Execution
1081
+
1082
+ 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.
1083
+
1084
+ **Basic Mixed Execution:**
111
1085
 
112
1086
  ```typescript
113
- import { stableApiGateway } from '@emmvish/stable-request';
1087
+ import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
114
1088
 
115
- const requests = [
116
- { id: 'user-1', requestOptions: { reqData: { path: '/users/1' }, resReq: true } },
117
- { id: 'user-2', requestOptions: { reqData: { path: '/users/2' }, resReq: true } },
118
- { id: 'user-3', requestOptions: { reqData: { path: '/users/3' }, resReq: true } }
1089
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1090
+ {
1091
+ id: 'init',
1092
+ requests: [
1093
+ { id: 'init', requestOptions: { reqData: { path: '/init' }, resReq: true } }
1094
+ ],
1095
+ phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.CONTINUE })
1096
+ },
1097
+ // These two phases execute in parallel
1098
+ {
1099
+ id: 'check-inventory',
1100
+ markConcurrentPhase: true,
1101
+ requests: [
1102
+ { id: 'inv', requestOptions: { reqData: { path: '/inventory' }, resReq: true } }
1103
+ ]
1104
+ },
1105
+ {
1106
+ id: 'check-pricing',
1107
+ markConcurrentPhase: true,
1108
+ requests: [
1109
+ { id: 'price', requestOptions: { reqData: { path: '/pricing' }, resReq: true } }
1110
+ ],
1111
+ // Decision hook receives results from all concurrent phases
1112
+ phaseDecisionHook: async ({ concurrentPhaseResults }) => {
1113
+ const inventory = concurrentPhaseResults![0].responses[0]?.data;
1114
+ const pricing = concurrentPhaseResults![1].responses[0]?.data;
1115
+
1116
+ if (inventory.available && pricing.inBudget) {
1117
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1118
+ }
1119
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'out-of-stock' };
1120
+ }
1121
+ },
1122
+ // This phase executes serially after the parallel group
1123
+ {
1124
+ id: 'process-order',
1125
+ requests: [
1126
+ { id: 'order', requestOptions: { reqData: { path: '/order' }, resReq: true } }
1127
+ ]
1128
+ },
1129
+ {
1130
+ id: 'out-of-stock',
1131
+ requests: [
1132
+ { id: 'notify', requestOptions: { reqData: { path: '/notify' }, resReq: true } }
1133
+ ]
1134
+ }
119
1135
  ];
120
1136
 
121
- const results = await stableApiGateway(requests, {
1137
+ const result = await stableWorkflow(phases, {
1138
+ workflowId: 'mixed-execution',
122
1139
  commonRequestData: { hostname: 'api.example.com' },
123
- concurrentExecution: true,
124
- maxConcurrentRequests: 10
125
- });
126
-
127
- results.forEach(result => {
128
- if (result.success) {
129
- console.log(`Request ${result.requestId}:`, result.data);
130
- } else {
131
- console.error(`Request ${result.requestId} failed:`, result.error);
132
- }
1140
+ enableNonLinearExecution: true
133
1141
  });
134
1142
  ```
135
1143
 
136
- ### Multi-Phase Workflow
1144
+ **Multiple Parallel Groups:**
137
1145
 
138
1146
  ```typescript
139
- import { stableWorkflow, STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
140
-
141
1147
  const phases: STABLE_WORKFLOW_PHASE[] = [
142
1148
  {
143
- id: 'authentication',
1149
+ id: 'authenticate',
144
1150
  requests: [
145
- { id: 'login', requestOptions: { reqData: { path: '/auth/login' }, resReq: true } }
1151
+ { id: 'auth', requestOptions: { reqData: { path: '/auth' }, resReq: true } }
146
1152
  ]
147
1153
  },
1154
+ // First parallel group: Data validation
148
1155
  {
149
- id: 'data-fetching',
150
- concurrentExecution: true,
1156
+ id: 'validate-user',
1157
+ markConcurrentPhase: true,
151
1158
  requests: [
152
- { id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
153
- { id: 'posts', requestOptions: { reqData: { path: '/posts' }, resReq: true } }
1159
+ { id: 'val-user', requestOptions: { reqData: { path: '/validate/user' }, resReq: true } }
1160
+ ]
1161
+ },
1162
+ {
1163
+ id: 'validate-payment',
1164
+ markConcurrentPhase: true,
1165
+ requests: [
1166
+ { id: 'val-pay', requestOptions: { reqData: { path: '/validate/payment' }, resReq: true } }
1167
+ ]
1168
+ },
1169
+ {
1170
+ id: 'validate-shipping',
1171
+ markConcurrentPhase: true,
1172
+ requests: [
1173
+ { id: 'val-ship', requestOptions: { reqData: { path: '/validate/shipping' }, resReq: true } }
1174
+ ],
1175
+ phaseDecisionHook: async ({ concurrentPhaseResults }) => {
1176
+ const allValid = concurrentPhaseResults!.every(r => r.success && r.responses[0]?.data?.valid);
1177
+ if (!allValid) {
1178
+ return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Validation failed' } };
1179
+ }
1180
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1181
+ }
1182
+ },
1183
+ // Serial processing phase
1184
+ {
1185
+ id: 'calculate-total',
1186
+ requests: [
1187
+ { id: 'calc', requestOptions: { reqData: { path: '/calculate' }, resReq: true } }
1188
+ ]
1189
+ },
1190
+ // Second parallel group: External integrations
1191
+ {
1192
+ id: 'notify-warehouse',
1193
+ markConcurrentPhase: true,
1194
+ requests: [
1195
+ { id: 'warehouse', requestOptions: { reqData: { path: '/notify/warehouse' }, resReq: true } }
1196
+ ]
1197
+ },
1198
+ {
1199
+ id: 'notify-shipping',
1200
+ markConcurrentPhase: true,
1201
+ requests: [
1202
+ { id: 'shipping', requestOptions: { reqData: { path: '/notify/shipping' }, resReq: true } }
1203
+ ]
1204
+ },
1205
+ {
1206
+ id: 'update-inventory',
1207
+ markConcurrentPhase: true,
1208
+ requests: [
1209
+ { id: 'inventory', requestOptions: { reqData: { path: '/update/inventory' }, resReq: true } }
1210
+ ]
1211
+ },
1212
+ // Final serial phase
1213
+ {
1214
+ id: 'finalize',
1215
+ requests: [
1216
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
154
1217
  ]
155
1218
  }
156
1219
  ];
157
1220
 
158
1221
  const result = await stableWorkflow(phases, {
159
- workflowId: 'data-pipeline',
1222
+ workflowId: 'multi-parallel-workflow',
160
1223
  commonRequestData: { hostname: 'api.example.com' },
161
- stopOnFirstPhaseError: true,
162
- logPhaseResults: true
1224
+ enableNonLinearExecution: true
163
1225
  });
164
1226
 
165
- console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
1227
+ console.log('Execution order demonstrates mixed serial/parallel execution');
166
1228
  ```
167
1229
 
168
- ## API Reference
169
-
170
- ### stableRequest
1230
+ **Decision Making with Concurrent Results:**
171
1231
 
172
- Execute a single HTTP request with retry logic and observability.
173
-
174
- **Signature:**
175
1232
  ```typescript
176
- async function stableRequest<RequestDataType, ResponseDataType>(
177
- options: STABLE_REQUEST<RequestDataType, ResponseDataType>
178
- ): Promise<ResponseDataType | boolean>
179
- ```
1233
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1234
+ {
1235
+ id: 'api-check-1',
1236
+ markConcurrentPhase: true,
1237
+ requests: [
1238
+ { id: 'api1', requestOptions: { reqData: { path: '/health/api1' }, resReq: true } }
1239
+ ]
1240
+ },
1241
+ {
1242
+ id: 'api-check-2',
1243
+ markConcurrentPhase: true,
1244
+ requests: [
1245
+ { id: 'api2', requestOptions: { reqData: { path: '/health/api2' }, resReq: true } }
1246
+ ]
1247
+ },
1248
+ {
1249
+ id: 'api-check-3',
1250
+ markConcurrentPhase: true,
1251
+ requests: [
1252
+ { id: 'api3', requestOptions: { reqData: { path: '/health/api3' }, resReq: true } }
1253
+ ],
1254
+ phaseDecisionHook: async ({ concurrentPhaseResults, sharedBuffer }) => {
1255
+ // Aggregate results from all parallel phases
1256
+ const healthScores = concurrentPhaseResults!.map(result =>
1257
+ result.responses[0]?.data?.score || 0
1258
+ );
1259
+
1260
+ const averageScore = healthScores.reduce((a, b) => a + b, 0) / healthScores.length;
1261
+ sharedBuffer!.healthScore = averageScore;
1262
+
1263
+ if (averageScore > 0.8) {
1264
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'optimal-path' };
1265
+ } else if (averageScore > 0.5) {
1266
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to degraded-path
1267
+ } else {
1268
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'fallback-path' };
1269
+ }
1270
+ }
1271
+ },
1272
+ {
1273
+ id: 'degraded-path',
1274
+ requests: [
1275
+ { id: 'degraded', requestOptions: { reqData: { path: '/degraded' }, resReq: true } }
1276
+ ]
1277
+ },
1278
+ {
1279
+ id: 'optimal-path',
1280
+ requests: [
1281
+ { id: 'optimal', requestOptions: { reqData: { path: '/optimal' }, resReq: true } }
1282
+ ]
1283
+ },
1284
+ {
1285
+ id: 'fallback-path',
1286
+ requests: [
1287
+ { id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
1288
+ ]
1289
+ }
1290
+ ];
180
1291
 
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
1292
+ const sharedBuffer = {};
1293
+ const result = await stableWorkflow(phases, {
1294
+ workflowId: 'adaptive-routing',
1295
+ commonRequestData: { hostname: 'api.example.com' },
1296
+ enableNonLinearExecution: true,
1297
+ sharedBuffer
1298
+ });
193
1299
 
194
- ### stableApiGateway
1300
+ console.log('Average health score:', sharedBuffer.healthScore);
1301
+ ```
195
1302
 
196
- Execute multiple requests concurrently or sequentially with shared configuration.
1303
+ **Error Handling in Parallel Groups:**
197
1304
 
198
- **Signature:**
199
1305
  ```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>[]>
1306
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1307
+ {
1308
+ id: 'critical-check',
1309
+ markConcurrentPhase: true,
1310
+ requests: [
1311
+ {
1312
+ id: 'check1',
1313
+ requestOptions: {
1314
+ reqData: { path: '/critical/check1' },
1315
+ resReq: true,
1316
+ attempts: 3
1317
+ }
1318
+ }
1319
+ ]
1320
+ },
1321
+ {
1322
+ id: 'optional-check',
1323
+ markConcurrentPhase: true,
1324
+ requests: [
1325
+ {
1326
+ id: 'check2',
1327
+ requestOptions: {
1328
+ reqData: { path: '/optional/check2' },
1329
+ resReq: true,
1330
+ attempts: 1,
1331
+ finalErrorAnalyzer: async () => true // Suppress errors
1332
+ }
1333
+ }
1334
+ ],
1335
+ phaseDecisionHook: async ({ concurrentPhaseResults }) => {
1336
+ // Check if critical phase succeeded
1337
+ const criticalSuccess = concurrentPhaseResults![0].success;
1338
+
1339
+ if (!criticalSuccess) {
1340
+ return {
1341
+ action: PHASE_DECISION_ACTIONS.TERMINATE,
1342
+ metadata: { reason: 'Critical check failed' }
1343
+ };
1344
+ }
1345
+
1346
+ // Continue even if optional check failed
1347
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1348
+ }
1349
+ },
1350
+ {
1351
+ id: 'process',
1352
+ requests: [
1353
+ { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
1354
+ ]
1355
+ }
1356
+ ];
1357
+
1358
+ const result = await stableWorkflow(phases, {
1359
+ workflowId: 'resilient-parallel',
1360
+ commonRequestData: { hostname: 'api.example.com' },
1361
+ enableNonLinearExecution: true,
1362
+ stopOnFirstPhaseError: false // Continue even with phase errors
1363
+ });
204
1364
  ```
205
1365
 
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`)
1366
+ **Key Points:**
1367
+ - Only **consecutive phases** with `markConcurrentPhase: true` execute in parallel
1368
+ - The **last phase** in a concurrent group can have a `phaseDecisionHook` that receives `concurrentPhaseResults`
1369
+ - Parallel groups are separated by phases **without** `markConcurrentPhase` (or phases with it set to false)
1370
+ - All decision actions work with parallel groups except `REPLAY` (not supported for concurrent groups)
1371
+ - Error handling follows normal workflow rules - use `stopOnFirstPhaseError` to control behavior
1372
+
1373
+ #### Configuration Options
215
1374
 
216
- ### stableWorkflow
1375
+ **Workflow Options:**
1376
+ - `enableNonLinearExecution`: Enable non-linear workflow (required)
1377
+ - `maxWorkflowIterations`: Maximum total iterations (default: 1000)
1378
+ - `handlePhaseDecision`: Called when phase makes a decision
1379
+ - `stopOnFirstPhaseError`: Stop on phase failure (default: false)
217
1380
 
218
- Execute multi-phase workflows with sequential or concurrent phase execution.
1381
+ **Phase Options:**
1382
+ - `phaseDecisionHook`: Function returning `PhaseExecutionDecision`
1383
+ - `allowReplay`: Allow phase replay (default: false)
1384
+ - `allowSkip`: Allow phase skip (default: false)
1385
+ - `maxReplayCount`: Maximum replays (default: Infinity)
219
1386
 
220
- **Signature:**
1387
+ **Decision Hook Parameters:**
221
1388
  ```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>>
1389
+ interface PhaseDecisionHookOptions {
1390
+ workflowId: string;
1391
+ phaseResult: STABLE_WORKFLOW_PHASE_RESULT;
1392
+ phaseId: string;
1393
+ phaseIndex: number;
1394
+ executionHistory: PhaseExecutionRecord[];
1395
+ sharedBuffer?: Record<string, any>;
1396
+ params?: any;
1397
+ }
226
1398
  ```
227
1399
 
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
244
-
245
- ## Advanced Features
1400
+ **Decision Object:**
1401
+ ```typescript
1402
+ interface PhaseExecutionDecision {
1403
+ action: PHASE_DECISION_ACTIONS;
1404
+ targetPhaseId?: string;
1405
+ replayCount?: number;
1406
+ metadata?: Record<string, any>;
1407
+ }
1408
+ ```
246
1409
 
247
1410
  ### Retry Strategies
248
1411
 
@@ -952,77 +2115,6 @@ console.log('Products:', result.data.products?.length);
952
2115
  console.log('Orders:', result.data.orders?.length);
953
2116
  ```
954
2117
 
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
2118
  ## License
1027
2119
 
1028
2120
  MIT © Manish Varma