@emmvish/stable-request 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,64 +1,37 @@
1
- # stable-request
1
+ # @emmvish/stable-request
2
2
 
3
- **stable-request** is a TypeScript-first **HTTP workflow execution engine** for real-world distributed systems where HTTP `200 OK` does **not** guarantee business success, and HTTP failures still deserve **structured, actionable responses**.
3
+ A powerful HTTP Workflow Execution Engine for Node.js that transforms unreliable API calls into robust, production-ready workflows with advanced retry mechanisms, circuit breakers, and sophisticated execution patterns.
4
4
 
5
- It ensures that **every request attempt**, whether it succeeds or fails, can be:
5
+ ## Navigation
6
6
 
7
- - Sent reliably
8
- - Observed
9
- - Analyzed
10
- - Retried intelligently
11
- - Suppressed when non-critical
12
- - Escalated when business-critical
13
-
14
- All without crashing your application or hiding context behind opaque errors.
15
-
16
- **stable-request treats failures as data.**
17
-
18
- > If you’ve ever logged `error.message` and thought
19
- > **“This tells me absolutely nothing”** — this library is for you.
20
-
21
- In addition, it enables **reliability** **content-aware retries**, **hierarchical configuration**, **batch orchestration**, and **multi-phase workflows** with deep observability — all built on top of standard HTTP calls.
22
-
23
- All in all, it provides you with the **entire ecosystem** to build **API-integrations based workflows** with **complete flexibility**.
24
-
25
- ---
26
-
27
- ## Choose your entry point
28
-
29
- | Need | Use |
30
- |-----|-----|
31
- | Reliable single API call | `stableRequest` |
32
- | Batch or fan-out requests | `stableApiGateway` |
33
- | Multi-step orchestration | `stableWorkflow` |
34
-
35
-
36
- Start small and scale.
37
-
38
- ---
39
-
40
- ## 📚 Table of Contents
41
- <!-- TOC START -->
7
+ - [Overview](#overview)
42
8
  - [Installation](#installation)
43
- - [Core Features](#core-features)
44
9
  - [Quick Start](#quick-start)
45
- - [Advanced Features](#advanced-features)
10
+ - [Core Features](#core-features)
11
+ - [Intelligent Retry Strategies](#intelligent-retry-strategies)
12
+ - [Circuit Breaker Pattern](#circuit-breaker-pattern)
13
+ - [Response Caching](#response-caching)
14
+ - [Rate Limiting and Concurrency Control](#rate-limiting-and-concurrency-control)
15
+ - [Workflow Execution Patterns](#workflow-execution-patterns)
16
+ - [Sequential and Concurrent Phases](#sequential-and-concurrent-phases)
17
+ - [Mixed Execution Mode](#mixed-execution-mode)
46
18
  - [Non-Linear Workflows](#non-linear-workflows)
47
- - [Retry Strategies](#retry-strategies)
48
- - [Circuit Breaker](#circuit-breaker)
49
- - [Rate Limiting](#rate-limiting)
50
- - [Caching](#caching)
51
- - [Pre-Execution Hooks](#pre-execution-hooks)
52
- - [Shared Buffer](#shared-buffer)
53
- - [Request Grouping](#request-grouping)
54
- - [Concurrency Control](#concurrency-control)
55
- - [Response Analysis](#response-analysis)
56
- - [Error Handling](#error-handling)
57
- - [Advanced Use Cases](#advanced-use-cases)
19
+ - [Branched Workflows](#branched-workflows)
20
+ - [Advanced Capabilities](#advanced-capabilities)
21
+ - [Config Cascading](#config-cascading)
22
+ - [Shared Buffer and Pre-Execution Hooks](#shared-buffer-and-pre-execution-hooks)
23
+ - [Comprehensive Observability](#comprehensive-observability)
24
+ - [API Surface](#api-surface)
58
25
  - [License](#license)
59
- <!-- TOC END -->
60
26
 
61
- ---
27
+ ## Overview
28
+
29
+ `@emmvish/stable-request` is built for applications that need to orchestrate complex, multi-step API interactions with guarantees around reliability, observability, and fault tolerance. Unlike simple HTTP clients, it provides:
30
+
31
+ - **Workflow-First Design**: Organize API calls into phases, branches, and decision trees
32
+ - **Enterprise Resilience**: Built-in circuit breakers, retry strategies, and failure handling
33
+ - **Execution Flexibility**: Sequential, concurrent, mixed, and non-linear execution patterns
34
+ - **Production-Ready Observability**: Detailed hooks for monitoring, logging, and error analysis
62
35
 
63
36
  ## Installation
64
37
 
@@ -66,25 +39,9 @@ Start small and scale.
66
39
  npm install @emmvish/stable-request
67
40
  ```
68
41
 
69
- ## Core Features
70
-
71
- - ✅ **Configurable Retry Strategies**: Fixed, Linear, and Exponential backoff
72
- - ✅ **Circuit Breaker**: Prevent cascading failures with automatic circuit breaking
73
- - ✅ **Rate Limiting**: Control request throughput across single or multiple requests
74
- - ✅ **Response Caching**: Built-in TTL-based caching with global cache manager
75
- - ✅ **Batch Processing**: Execute multiple requests concurrently or sequentially via API Gateway
76
- - ✅ **Multi-Phase Non-Linear Workflows**: Orchestrate complex request workflows with phase dependencies
77
- - ✅ **Pre-Execution Hooks**: Transform requests before execution with dynamic configuration
78
- - ✅ **Shared Buffer**: Share state across requests in workflows and gateways
79
- - ✅ **Request Grouping**: Apply different configurations to request groups
80
- - ✅ **Observability Hooks**: Track errors, successful attempts, and phase completions
81
- - ✅ **Response Analysis**: Validate responses and trigger retries based on content
82
- - ✅ **Trial Mode**: Test configurations without making real API calls
83
- - ✅ **TypeScript Support**: Full type safety with generics for request/response data
84
-
85
42
  ## Quick Start
86
43
 
87
- ### Basic Request with Retry
44
+ ### Single Request with Retry
88
45
 
89
46
  ```typescript
90
47
  import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
@@ -92,7 +49,7 @@ import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
92
49
  const data = await stableRequest({
93
50
  reqData: {
94
51
  hostname: 'api.example.com',
95
- path: '/users/123',
52
+ path: '/users',
96
53
  method: 'GET'
97
54
  },
98
55
  resReq: true,
@@ -100,1505 +57,324 @@ const data = await stableRequest({
100
57
  wait: 1000,
101
58
  retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
102
59
  });
103
-
104
- console.log(data);
105
- ```
106
-
107
- ### Batch Requests via API Gateway
108
-
109
- ```typescript
110
- import { stableApiGateway } from '@emmvish/stable-request';
111
-
112
- const requests = [
113
- { id: 'user-1', requestOptions: { reqData: { path: '/users/1' }, resReq: true } },
114
- { id: 'user-2', requestOptions: { reqData: { path: '/users/2' }, resReq: true } },
115
- { id: 'user-3', requestOptions: { reqData: { path: '/users/3' }, resReq: true } }
116
- ];
117
-
118
- const results = await stableApiGateway(requests, {
119
- commonRequestData: { hostname: 'api.example.com' },
120
- concurrentExecution: true,
121
- maxConcurrentRequests: 10
122
- });
123
-
124
- results.forEach(result => {
125
- if (result.success) {
126
- console.log(`Request ${result.requestId}:`, result.data);
127
- } else {
128
- console.error(`Request ${result.requestId} failed:`, result.error);
129
- }
130
- });
131
60
  ```
132
61
 
133
62
  ### Multi-Phase Workflow
134
63
 
135
64
  ```typescript
136
- import { stableWorkflow, STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
65
+ import { stableWorkflow } from '@emmvish/stable-request';
137
66
 
138
- const phases: STABLE_WORKFLOW_PHASE[] = [
67
+ const result = await stableWorkflow([
139
68
  {
140
- id: 'authentication',
69
+ id: 'auth',
141
70
  requests: [
142
71
  { id: 'login', requestOptions: { reqData: { path: '/auth/login' }, resReq: true } }
143
72
  ]
144
73
  },
145
74
  {
146
- id: 'data-fetching',
75
+ id: 'fetch-data',
147
76
  concurrentExecution: true,
148
77
  requests: [
149
78
  { id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
150
- { id: 'posts', requestOptions: { reqData: { path: '/posts' }, resReq: true } }
79
+ { id: 'orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } }
151
80
  ]
152
81
  }
153
- ];
154
-
155
- const result = await stableWorkflow(phases, {
156
- workflowId: 'data-pipeline',
82
+ ], {
83
+ workflowId: 'user-data-sync',
157
84
  commonRequestData: { hostname: 'api.example.com' },
158
- stopOnFirstPhaseError: true,
159
- logPhaseResults: true
85
+ stopOnFirstPhaseError: true
160
86
  });
161
-
162
- console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
163
87
  ```
164
88
 
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
- ];
89
+ ## Core Features
210
90
 
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
- });
91
+ ### Intelligent Retry Strategies
218
92
 
219
- console.log('Execution history:', result.executionHistory);
220
- console.log('Terminated early:', result.terminatedEarly);
221
- ```
93
+ Automatically retry failed requests with configurable strategies:
222
94
 
223
- ## Advanced Features
95
+ - **Fixed Delay**: Constant wait time between retries
96
+ - **Linear Backoff**: Incrementally increasing delays
97
+ - **Exponential Backoff**: Exponentially growing delays with optional jitter
98
+ - **Fibonacci Backoff**: Delays based on Fibonacci sequence
224
99
 
225
- ### Non-Linear Workflows
100
+ Each request can have individual retry configurations, or inherit from workflow-level defaults.
226
101
 
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.
102
+ ### Circuit Breaker Pattern
228
103
 
229
- #### Phase Decision Actions
104
+ Prevent cascade failures and system overload with built-in circuit breakers:
230
105
 
231
- Each phase can make decisions about workflow execution:
106
+ - **Automatic State Management**: Transitions between Closed → Open → Half-Open states
107
+ - **Configurable Thresholds**: Set failure rates and time windows
108
+ - **Request/Attempt Level Tracking**: Monitor at granular or aggregate levels
109
+ - **Graceful Degradation**: Fail fast when services are down
232
110
 
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
111
+ ### Response Caching
238
112
 
239
- #### Basic Non-Linear Workflow
113
+ Reduce redundant API calls with intelligent caching:
240
114
 
241
- ```typescript
242
- import { stableWorkflow, STABLE_WORKFLOW_PHASE, PhaseExecutionDecision, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
115
+ - **TTL-Based Expiration**: Configure cache lifetime per request
116
+ - **Request Fingerprinting**: Automatic deduplication based on request signature
117
+ - **Workflow-Wide Sharing**: Cache responses across phases and branches
118
+ - **Manual Cache Management**: Programmatic cache inspection and clearing
243
119
 
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
- ];
120
+ ### Rate Limiting and Concurrency Control
271
121
 
272
- const result = await stableWorkflow(phases, {
273
- workflowId: 'validation-workflow',
274
- commonRequestData: { hostname: 'api.example.com' },
275
- enableNonLinearExecution: true,
276
- sharedBuffer: {}
277
- });
122
+ Respect API rate limits and control system load:
278
123
 
279
- if (result.terminatedEarly) {
280
- console.log('Workflow terminated:', result.terminationReason);
281
- }
282
- ```
124
+ - **Token Bucket Rate Limiting**: Smooth out request bursts
125
+ - **Concurrency Limiters**: Cap maximum parallel requests
126
+ - **Per-Phase Configuration**: Different limits for different workflow stages
127
+ - **Automatic Queueing**: Requests wait their turn without failing
283
128
 
284
- #### Conditional Branching
129
+ ## Workflow Execution Patterns
285
130
 
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
- ];
131
+ ### Sequential and Concurrent Phases
333
132
 
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
- ```
133
+ Control execution order at the phase level:
342
134
 
343
- #### Polling with Replay
135
+ - **Sequential Phases**: Execute phases one after another (default)
136
+ - **Concurrent Phases**: Run all phases in parallel
137
+ - **Per-Phase Control**: Each phase can define whether its requests run concurrently or sequentially
344
138
 
345
139
  ```typescript
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
- ]
140
+ const phases = [
141
+ { id: 'init', requests: [...] }, // Sequential phase
142
+ {
143
+ id: 'parallel-fetch',
144
+ concurrentExecution: true, // Concurrent requests within phase
145
+ requests: [...]
385
146
  }
386
147
  ];
387
148
 
388
- const result = await stableWorkflow(phases, {
389
- workflowId: 'polling-workflow',
390
- commonRequestData: { hostname: 'api.example.com' },
391
- enableNonLinearExecution: true,
392
- maxWorkflowIterations: 100
149
+ await stableWorkflow(phases, {
150
+ concurrentPhaseExecution: true // Run phases in parallel
393
151
  });
394
-
395
- console.log('Total iterations:', result.executionHistory.length);
396
- console.log('Phases executed:', result.completedPhases);
397
152
  ```
398
153
 
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
- ];
154
+ ### Mixed Execution Mode
453
155
 
454
- const result = await stableWorkflow(phases, {
455
- enableNonLinearExecution: true,
456
- sharedBuffer: { retryAttempts: 0 },
457
- logPhaseResults: true
458
- });
459
- ```
156
+ Combine sequential and concurrent phases in a single workflow:
460
157
 
461
- #### Skip Phases
158
+ - Mark specific phases as concurrent while others remain sequential
159
+ - Fine-grained control over execution topology
160
+ - Useful for scenarios like: "authenticate first, then fetch data in parallel, then process sequentially"
462
161
 
463
162
  ```typescript
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
- }
163
+ const phases = [
164
+ { id: 'auth', requests: [...] }, // Sequential
165
+ {
166
+ id: 'fetch',
167
+ markConcurrentPhase: true, // Runs concurrently with next phase
168
+ requests: [...]
169
+ },
170
+ {
171
+ id: 'more-fetch',
172
+ markConcurrentPhase: true, // Runs concurrently with previous
173
+ requests: [...]
174
+ },
175
+ { id: 'process', requests: [...] } // Sequential, waits for above
500
176
  ];
501
177
 
502
- const result = await stableWorkflow(phases, {
503
- enableNonLinearExecution: true,
504
- sharedBuffer: {}
505
- });
506
- ```
507
-
508
- #### Execution History and Tracking
509
-
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 ? '✓' : '✗'}`);
178
+ await stableWorkflow(phases, {
179
+ enableMixedExecution: true
538
180
  });
539
181
  ```
540
182
 
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
- ];
183
+ ### Non-Linear Workflows
615
184
 
616
- const result = await stableWorkflow(phases, {
617
- workflowId: 'mixed-execution',
618
- commonRequestData: { hostname: 'api.example.com' },
619
- enableNonLinearExecution: true
620
- });
621
- ```
185
+ Build dynamic workflows with conditional branching and looping:
622
186
 
623
- **Multiple Parallel Groups:**
187
+ - **JUMP**: Skip to a specific phase based on runtime conditions
188
+ - **SKIP**: Skip upcoming phases and jump to a target
189
+ - **REPLAY**: Re-execute the current phase (with limits)
190
+ - **TERMINATE**: Stop the entire workflow early
191
+ - **CONTINUE**: Proceed to the next phase (default)
624
192
 
625
193
  ```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
- },
194
+ const phases = [
648
195
  {
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' } };
196
+ id: 'validate',
197
+ requests: [...],
198
+ phaseDecisionHook: async ({ phaseResult }) => {
199
+ if (phaseResult.responses[0].data.isValid) {
200
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'success' };
658
201
  }
659
202
  return { action: PHASE_DECISION_ACTIONS.CONTINUE };
660
203
  }
661
204
  },
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
- }
205
+ { id: 'retry-logic', requests: [...] },
206
+ { id: 'success', requests: [...] }
698
207
  ];
699
208
 
700
- const result = await stableWorkflow(phases, {
701
- workflowId: 'multi-parallel-workflow',
702
- commonRequestData: { hostname: 'api.example.com' },
703
- enableNonLinearExecution: true
209
+ await stableWorkflow(phases, {
210
+ enableNonLinearExecution: true
704
211
  });
705
-
706
- console.log('Execution order demonstrates mixed serial/parallel execution');
707
212
  ```
708
213
 
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
- ];
214
+ **Decision Hook Context**:
215
+ - Access to current phase results
216
+ - Execution history (replay count, previous phases)
217
+ - Shared buffer for cross-phase state
218
+ - Concurrent phase results (in mixed execution)
770
219
 
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
- });
220
+ ### Branched Workflows
778
221
 
779
- console.log('Average health score:', sharedBuffer.healthScore);
780
- ```
222
+ Execute multiple independent workflow paths in parallel or sequentially:
781
223
 
782
- **Error Handling in Parallel Groups:**
224
+ - **Parallel Branches**: Run branches concurrently (mark with `markConcurrentBranch: true`)
225
+ - **Sequential Branches**: Execute branches one after another
226
+ - **Branch-Level Decisions**: Control workflow from branch hooks
227
+ - **Branch Replay/Termination**: Branches support non-linear execution too
783
228
 
784
229
  ```typescript
785
- const phases: STABLE_WORKFLOW_PHASE[] = [
230
+ const branches = [
786
231
  {
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
- ]
232
+ id: 'user-flow',
233
+ markConcurrentBranch: true, // Parallel
234
+ phases: [...]
799
235
  },
800
236
  {
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
- }
237
+ id: 'analytics-flow',
238
+ markConcurrentBranch: true, // Parallel
239
+ phases: [...]
828
240
  },
829
241
  {
830
- id: 'process',
831
- requests: [
832
- { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
833
- ]
242
+ id: 'cleanup-flow', // Sequential (default)
243
+ phases: [...]
834
244
  }
835
245
  ];
836
246
 
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
- ```
888
-
889
- ### Retry Strategies
890
-
891
- Control the delay between retry attempts:
892
-
893
- ```typescript
894
- import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
895
-
896
- // Fixed delay: 1000ms between each retry
897
- await stableRequest({
898
- reqData: { hostname: 'api.example.com', path: '/data' },
899
- attempts: 3,
900
- wait: 1000,
901
- retryStrategy: RETRY_STRATEGIES.FIXED
902
- });
903
-
904
- // Linear backoff: 1000ms, 2000ms, 3000ms
905
- await stableRequest({
906
- reqData: { hostname: 'api.example.com', path: '/data' },
907
- attempts: 3,
908
- wait: 1000,
909
- retryStrategy: RETRY_STRATEGIES.LINEAR
910
- });
911
-
912
- // Exponential backoff: 1000ms, 2000ms, 4000ms
913
- await stableRequest({
914
- reqData: { hostname: 'api.example.com', path: '/data' },
915
- attempts: 3,
916
- wait: 1000,
917
- maxAllowedWait: 10000,
918
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
919
- });
920
- ```
921
-
922
- ### Circuit Breaker
923
-
924
- Prevent cascading failures by automatically blocking requests when error thresholds are exceeded:
925
-
926
- ```typescript
927
- import { stableApiGateway, CircuitBreakerState } from '@emmvish/stable-request';
928
-
929
- const results = await stableApiGateway(requests, {
930
- commonRequestData: { hostname: 'api.example.com' },
931
- circuitBreaker: {
932
- failureThresholdPercentage: 50, // Open circuit at 50% failure rate
933
- minimumRequests: 5, // Need at least 5 requests to calculate
934
- recoveryTimeoutMs: 30000, // Try recovery after 30 seconds
935
- trackIndividualAttempts: false // Track per-request success/failure
936
- }
937
- });
938
-
939
- // Circuit breaker can be shared across workflows
940
- const breaker = new CircuitBreaker({
941
- failureThresholdPercentage: 50,
942
- minimumRequests: 10,
943
- recoveryTimeoutMs: 60000
944
- });
945
-
946
- const result = await stableWorkflow(phases, {
947
- circuitBreaker: breaker,
948
- // ... other options
949
- });
950
-
951
- // Check circuit breaker state
952
- const state = breaker.getState();
953
- console.log(`Circuit breaker state: ${state.state}`); // CLOSED, OPEN, or HALF_OPEN
954
- ```
955
-
956
- ### Rate Limiting
957
-
958
- Control request throughput to prevent overwhelming APIs:
959
-
960
- ```typescript
961
- import { stableApiGateway } from '@emmvish/stable-request';
962
-
963
- const results = await stableApiGateway(requests, {
964
- commonRequestData: { hostname: 'api.example.com' },
965
- concurrentExecution: true,
966
- rateLimit: {
967
- maxRequests: 10, // Maximum 10 requests
968
- windowMs: 1000 // Per 1 second window
969
- }
970
- });
971
-
972
- // Rate limiting in workflows
973
- const result = await stableWorkflow(phases, {
974
- rateLimit: {
975
- maxRequests: 5,
976
- windowMs: 1000
977
- }
247
+ await stableWorkflow([], {
248
+ enableBranchExecution: true,
249
+ branches
978
250
  });
979
251
  ```
980
252
 
981
- ### Caching
982
-
983
- Cache responses with TTL to reduce redundant API calls:
984
-
985
- ```typescript
986
- import { stableRequest, getGlobalCacheManager } from '@emmvish/stable-request';
987
-
988
- // Enable caching for a request
989
- const data = await stableRequest({
990
- reqData: { hostname: 'api.example.com', path: '/users/123' },
991
- resReq: true,
992
- cache: {
993
- enabled: true,
994
- ttl: 60000 // Cache for 60 seconds
995
- }
996
- });
997
-
998
- // Use global cache manager across requests
999
- const results = await stableApiGateway(requests, {
1000
- commonRequestData: { hostname: 'api.example.com' },
1001
- commonCache: { enabled: true, ttl: 300000 } // 5 minutes
1002
- });
253
+ **Branch Features**:
254
+ - Each branch has its own phase execution
255
+ - Branches share the workflow's `sharedBuffer`
256
+ - Branch decision hooks can terminate the entire workflow
257
+ - Supports all execution patterns (mixed, non-linear) within branches
1003
258
 
1004
- // Manage cache manually
1005
- const cacheManager = getGlobalCacheManager();
1006
- const stats = cacheManager.getStats();
1007
- console.log(`Cache size: ${stats.size}, Valid entries: ${stats.validEntries}`);
1008
- cacheManager.clear(); // Clear all cache
1009
- ```
259
+ ## Advanced Capabilities
1010
260
 
1011
- ### Pre-Execution Hooks
261
+ ### Config Cascading
1012
262
 
1013
- Transform requests dynamically before execution:
263
+ Configuration inheritance across workflow → branch → phase → request levels:
1014
264
 
1015
265
  ```typescript
1016
- import { stableRequest } from '@emmvish/stable-request';
1017
-
1018
- const commonBuffer: Record<string, any> = {};
1019
-
1020
- const data = await stableRequest({
1021
- reqData: { hostname: 'api.example.com', path: '/data' },
1022
- resReq: true,
1023
- preExecution: {
1024
- preExecutionHook: async ({ inputParams, commonBuffer }) => {
1025
- // Fetch authentication token
1026
- const token = await getAuthToken();
1027
-
1028
- // Store in shared buffer
1029
- commonBuffer.token = token;
1030
- commonBuffer.timestamp = Date.now();
1031
-
1032
- // Override request configuration
1033
- return {
1034
- reqData: {
1035
- hostname: 'api.example.com',
1036
- path: '/authenticated-data',
1037
- headers: { Authorization: `Bearer ${token}` }
1038
- }
1039
- };
266
+ await stableWorkflow(phases, {
267
+ // Workflow-level config (lowest priority)
268
+ commonAttempts: 3,
269
+ commonWait: 1000,
270
+ commonCache: { enabled: true, ttl: 60000 },
271
+
272
+ branches: [{
273
+ id: 'my-branch',
274
+ commonConfig: {
275
+ // Branch-level config (overrides workflow)
276
+ commonAttempts: 5,
277
+ commonWait: 500
1040
278
  },
1041
- preExecutionHookParams: { userId: 'user123' },
1042
- applyPreExecutionConfigOverride: true, // Apply returned config
1043
- continueOnPreExecutionHookFailure: false
1044
- },
1045
- commonBuffer
1046
- });
1047
-
1048
- console.log('Token used:', commonBuffer.token);
1049
- ```
1050
-
1051
- ### Shared Buffer
1052
-
1053
- Share state across requests in gateways and workflows:
1054
-
1055
- ```typescript
1056
- import { stableWorkflow } from '@emmvish/stable-request';
1057
-
1058
- const sharedBuffer: Record<string, any> = { requestCount: 0 };
1059
-
1060
- const phases: STABLE_WORKFLOW_PHASE[] = [
1061
- {
1062
- id: 'phase-1',
1063
- requests: [
1064
- {
1065
- id: 'req-1',
1066
- requestOptions: {
1067
- reqData: { path: '/step1' },
1068
- resReq: true,
1069
- preExecution: {
1070
- preExecutionHook: ({ commonBuffer }) => {
1071
- commonBuffer.requestCount++;
1072
- commonBuffer.phase1Data = 'initialized';
1073
- return {};
1074
- },
1075
- preExecutionHookParams: {},
1076
- applyPreExecutionConfigOverride: false,
1077
- continueOnPreExecutionHookFailure: false
1078
- }
1079
- }
1080
- }
1081
- ]
1082
- },
1083
- {
1084
- id: 'phase-2',
1085
- requests: [
1086
- {
1087
- id: 'req-2',
279
+ phases: [{
280
+ id: 'my-phase',
281
+ commonConfig: {
282
+ // Phase-level config (overrides branch and workflow)
283
+ commonAttempts: 1
284
+ },
285
+ requests: [{
1088
286
  requestOptions: {
1089
- reqData: { path: '/step2' },
1090
- resReq: true,
1091
- preExecution: {
1092
- preExecutionHook: ({ commonBuffer }) => {
1093
- commonBuffer.requestCount++;
1094
- // Access data from phase-1
1095
- console.log('Phase 1 data:', commonBuffer.phase1Data);
1096
- return {};
1097
- },
1098
- preExecutionHookParams: {},
1099
- applyPreExecutionConfigOverride: false,
1100
- continueOnPreExecutionHookFailure: false
1101
- }
287
+ // Request-level config (highest priority)
288
+ attempts: 10,
289
+ cache: { enabled: false }
1102
290
  }
1103
- }
1104
- ]
1105
- }
1106
- ];
1107
-
1108
- const result = await stableWorkflow(phases, {
1109
- workflowId: 'stateful-workflow',
1110
- commonRequestData: { hostname: 'api.example.com' },
1111
- sharedBuffer
1112
- });
1113
-
1114
- console.log('Total requests processed:', sharedBuffer.requestCount);
1115
- ```
1116
-
1117
- ### Request Grouping
1118
-
1119
- Apply different configurations to request groups:
1120
-
1121
- ```typescript
1122
- import { stableApiGateway } from '@emmvish/stable-request';
1123
-
1124
- const requests = [
1125
- {
1126
- id: 'critical-1',
1127
- groupId: 'critical',
1128
- requestOptions: { reqData: { path: '/critical/1' }, resReq: true }
1129
- },
1130
- {
1131
- id: 'critical-2',
1132
- groupId: 'critical',
1133
- requestOptions: { reqData: { path: '/critical/2' }, resReq: true }
1134
- },
1135
- {
1136
- id: 'optional-1',
1137
- groupId: 'optional',
1138
- requestOptions: { reqData: { path: '/optional/1' }, resReq: true }
1139
- }
1140
- ];
1141
-
1142
- const results = await stableApiGateway(requests, {
1143
- commonRequestData: { hostname: 'api.example.com' },
1144
- commonAttempts: 1,
1145
- commonWait: 100,
1146
- requestGroups: [
1147
- {
1148
- id: 'critical',
1149
- commonConfig: {
1150
- commonAttempts: 5, // More retries for critical requests
1151
- commonWait: 2000,
1152
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1153
- }
1154
- },
1155
- {
1156
- id: 'optional',
1157
- commonConfig: {
1158
- commonAttempts: 1, // No retries for optional requests
1159
- commonFinalErrorAnalyzer: async () => true // Suppress errors
1160
- }
1161
- }
1162
- ]
291
+ }]
292
+ }]
293
+ }]
1163
294
  });
1164
295
  ```
1165
296
 
1166
- ### Concurrency Control
297
+ ### Shared Buffer and Pre-Execution Hooks
1167
298
 
1168
- Limit concurrent request execution:
299
+ Share state and transform requests dynamically:
1169
300
 
301
+ **Shared Buffer**: Cross-phase/branch communication
1170
302
  ```typescript
1171
- import { stableApiGateway } from '@emmvish/stable-request';
303
+ const sharedBuffer = { authToken: null };
1172
304
 
1173
- // Limit to 5 concurrent requests
1174
- const results = await stableApiGateway(requests, {
1175
- commonRequestData: { hostname: 'api.example.com' },
1176
- concurrentExecution: true,
1177
- maxConcurrentRequests: 5
305
+ await stableWorkflow(phases, {
306
+ sharedBuffer,
307
+ // Phases can read/write to sharedBuffer via preExecution hooks
1178
308
  });
1179
-
1180
- // Phase-level concurrency control
1181
- const phases: STABLE_WORKFLOW_PHASE[] = [
1182
- {
1183
- id: 'limited-phase',
1184
- concurrentExecution: true,
1185
- maxConcurrentRequests: 3,
1186
- requests: [/* ... */]
1187
- }
1188
- ];
1189
309
  ```
1190
310
 
1191
- ### Response Analysis
1192
-
1193
- Validate response content and trigger retries:
1194
-
311
+ **Pre-Execution Hooks**: Modify requests before execution
1195
312
  ```typescript
1196
- import { stableRequest } from '@emmvish/stable-request';
1197
-
1198
- const data = await stableRequest({
1199
- reqData: { hostname: 'api.example.com', path: '/job/status' },
1200
- resReq: true,
1201
- attempts: 10,
1202
- wait: 2000,
1203
- responseAnalyzer: async ({ data, reqData, params }) => {
1204
- // Retry until job is completed
1205
- if (data.status === 'processing') {
1206
- console.log('Job still processing, will retry...');
1207
- return false; // Trigger retry
1208
- }
1209
- return data.status === 'completed';
1210
- }
1211
- });
1212
-
1213
- console.log('Job completed:', data);
1214
- ```
1215
-
1216
- ### Error Handling
1217
-
1218
- Comprehensive error handling with observability hooks:
1219
-
1220
- ```typescript
1221
- import { stableRequest } from '@emmvish/stable-request';
1222
-
1223
- const data = await stableRequest({
1224
- reqData: { hostname: 'api.example.com', path: '/data' },
1225
- resReq: true,
1226
- attempts: 3,
1227
- wait: 1000,
1228
- logAllErrors: true,
1229
- handleErrors: ({ reqData, errorLog, params }) => {
1230
- // Custom error logging
1231
- console.error('Request failed:', {
1232
- url: reqData.url,
1233
- attempt: errorLog.attempt,
1234
- statusCode: errorLog.statusCode,
1235
- error: errorLog.error,
1236
- isRetryable: errorLog.isRetryable
1237
- });
1238
-
1239
- // Send to monitoring service
1240
- monitoringService.trackError(errorLog);
1241
- },
1242
- logAllSuccessfulAttempts: true,
1243
- handleSuccessfulAttemptData: ({ successfulAttemptData }) => {
1244
- console.log('Request succeeded on attempt:', successfulAttemptData.attempt);
1245
- },
1246
- finalErrorAnalyzer: async ({ error, reqData }) => {
1247
- // Gracefully handle specific errors
1248
- if (error.response?.status === 404) {
1249
- console.warn('Resource not found, continuing...');
1250
- return true; // Return false to suppress error
313
+ {
314
+ requestOptions: {
315
+ preExecution: {
316
+ preExecutionHook: ({ commonBuffer, inputParams }) => {
317
+ // Access buffer, compute values, return config overrides
318
+ return {
319
+ reqData: {
320
+ headers: { 'Authorization': `Bearer ${commonBuffer.authToken}` }
321
+ }
322
+ };
323
+ },
324
+ applyPreExecutionConfigOverride: true
1251
325
  }
1252
- return false; // Throw error
1253
326
  }
1254
- });
327
+ }
1255
328
  ```
1256
329
 
1257
- ## Advanced Use Cases
1258
-
1259
- ### Use Case 1: Multi-Tenant API with Dynamic Authentication
330
+ ### Comprehensive Observability
1260
331
 
1261
- ```typescript
1262
- import { stableWorkflow, RETRY_STRATEGIES } from '@emmvish/stable-request';
1263
-
1264
- interface TenantConfig {
1265
- tenantId: string;
1266
- apiKey: string;
1267
- baseUrl: string;
1268
- }
332
+ Built-in hooks for monitoring, logging, and analysis:
1269
333
 
1270
- async function executeTenantWorkflow(tenantConfig: TenantConfig) {
1271
- const sharedBuffer: Record<string, any> = {
1272
- tenantId: tenantConfig.tenantId,
1273
- authToken: null,
1274
- processedItems: []
1275
- };
1276
-
1277
- const phases: STABLE_WORKFLOW_PHASE[] = [
1278
- {
1279
- id: 'authentication',
1280
- requests: [
1281
- {
1282
- id: 'get-token',
1283
- requestOptions: {
1284
- reqData: {
1285
- path: '/auth/token',
1286
- method: 'POST',
1287
- headers: { 'X-API-Key': tenantConfig.apiKey }
1288
- },
1289
- resReq: true,
1290
- attempts: 3,
1291
- wait: 2000,
1292
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1293
- responseAnalyzer: async ({ data, commonBuffer }) => {
1294
- if (data?.token) {
1295
- commonBuffer.authToken = data.token;
1296
- commonBuffer.tokenExpiry = Date.now() + (data.expiresIn * 1000);
1297
- return true;
1298
- }
1299
- return false;
1300
- }
1301
- }
1302
- }
1303
- ]
1304
- },
1305
- {
1306
- id: 'data-fetching',
1307
- concurrentExecution: true,
1308
- maxConcurrentRequests: 5,
1309
- requests: [
1310
- {
1311
- id: 'fetch-users',
1312
- requestOptions: {
1313
- reqData: { path: '/users' },
1314
- resReq: true,
1315
- preExecution: {
1316
- preExecutionHook: ({ commonBuffer }) => ({
1317
- reqData: {
1318
- path: '/users',
1319
- headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
1320
- }
1321
- }),
1322
- applyPreExecutionConfigOverride: true
1323
- }
1324
- }
1325
- },
1326
- {
1327
- id: 'fetch-settings',
1328
- requestOptions: {
1329
- reqData: { path: '/settings' },
1330
- resReq: true,
1331
- preExecution: {
1332
- preExecutionHook: ({ commonBuffer }) => ({
1333
- reqData: {
1334
- path: '/settings',
1335
- headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
1336
- }
1337
- }),
1338
- applyPreExecutionConfigOverride: true
1339
- }
1340
- }
1341
- }
1342
- ]
1343
- },
1344
- {
1345
- id: 'data-processing',
1346
- concurrentExecution: true,
1347
- requests: [
1348
- {
1349
- id: 'process-users',
1350
- requestOptions: {
1351
- reqData: { path: '/process/users', method: 'POST' },
1352
- resReq: true,
1353
- preExecution: {
1354
- preExecutionHook: ({ commonBuffer }) => {
1355
- const usersPhase = commonBuffer.phases?.find(p => p.phaseId === 'data-fetching');
1356
- const usersData = usersPhase?.responses?.find(r => r.requestId === 'fetch-users')?.data;
1357
-
1358
- return {
1359
- reqData: {
1360
- path: '/process/users',
1361
- method: 'POST',
1362
- headers: { Authorization: `Bearer ${commonBuffer.authToken}` },
1363
- body: { users: usersData }
1364
- }
1365
- };
1366
- },
1367
- applyPreExecutionConfigOverride: true
1368
- },
1369
- responseAnalyzer: async ({ data, commonBuffer }) => {
1370
- if (data?.processed) {
1371
- commonBuffer.processedItems.push(...data.processed);
1372
- return true;
1373
- }
1374
- return false;
1375
- }
1376
- }
1377
- }
1378
- ]
1379
- }
1380
- ];
1381
-
1382
- const result = await stableWorkflow(phases, {
1383
- workflowId: `tenant-${tenantConfig.tenantId}-workflow`,
1384
- commonRequestData: {
1385
- hostname: tenantConfig.baseUrl,
1386
- headers: { 'X-Tenant-ID': tenantConfig.tenantId }
1387
- },
1388
- stopOnFirstPhaseError: true,
1389
- logPhaseResults: true,
1390
- sharedBuffer,
1391
- circuitBreaker: {
1392
- failureThresholdPercentage: 40,
1393
- minimumRequests: 5,
1394
- recoveryTimeoutMs: 30000
1395
- },
1396
- rateLimit: {
1397
- maxRequests: 20,
1398
- windowMs: 1000
1399
- },
1400
- commonCache: {
1401
- enabled: true,
1402
- ttl: 300000 // Cache for 5 minutes
1403
- },
1404
- handlePhaseCompletion: ({ workflowId, phaseResult }) => {
1405
- console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
1406
- success: phaseResult.success,
1407
- successfulRequests: phaseResult.successfulRequests,
1408
- executionTime: `${phaseResult.executionTime}ms`
1409
- });
1410
- },
1411
- handlePhaseError: ({ workflowId, error, phaseResult }) => {
1412
- console.error(`[${workflowId}] Phase ${phaseResult.phaseId} failed:`, error);
1413
- // Send to monitoring
1414
- monitoringService.trackPhaseError(workflowId, phaseResult.phaseId, error);
1415
- }
1416
- });
1417
-
1418
- return {
1419
- success: result.success,
1420
- tenantId: tenantConfig.tenantId,
1421
- processedItems: sharedBuffer.processedItems,
1422
- executionTime: result.executionTime,
1423
- phases: result.phases.map(p => ({
1424
- id: p.phaseId,
1425
- success: p.success,
1426
- requestCount: p.totalRequests
1427
- }))
1428
- };
1429
- }
334
+ **Request-Level Hooks**:
335
+ - `responseAnalyzer`: Validate responses, trigger retries based on business logic
336
+ - `handleErrors`: Custom error handling and logging
337
+ - `handleSuccessfulAttemptData`: Log successful attempts
338
+ - `finalErrorAnalyzer`: Analyze final failure after all retries
1430
339
 
1431
- // Execute workflows for multiple tenants
1432
- const tenants: TenantConfig[] = [
1433
- { tenantId: 'tenant-1', apiKey: 'key1', baseUrl: 'api.tenant1.com' },
1434
- { tenantId: 'tenant-2', apiKey: 'key2', baseUrl: 'api.tenant2.com' }
1435
- ];
340
+ **Workflow-Level Hooks**:
341
+ - `handlePhaseCompletion`: React to phase completion
342
+ - `handlePhaseError`: Handle phase-level failures
343
+ - `handlePhaseDecision`: Monitor non-linear execution decisions
344
+ - `handleBranchCompletion`: Track branch execution
345
+ - `handleBranchDecision`: Monitor branch-level decisions
1436
346
 
1437
- const results = await Promise.all(tenants.map(executeTenantWorkflow));
1438
- results.forEach(result => {
1439
- console.log(`Tenant ${result.tenantId}:`, result.success ? 'Success' : 'Failed');
1440
- });
1441
- ```
347
+ **Execution History**:
348
+ Every workflow result includes detailed execution history with timestamps, decisions, and metadata.
1442
349
 
1443
- ### Use Case 2: Resilient Data Pipeline with Fallback Strategies
350
+ ## API Surface
1444
351
 
1445
- ```typescript
1446
- import { stableApiGateway, RETRY_STRATEGIES, CircuitBreaker } from '@emmvish/stable-request';
352
+ ### Core Functions
1447
353
 
1448
- interface DataSource {
1449
- id: string;
1450
- priority: number;
1451
- endpoint: string;
1452
- hostname: string;
1453
- }
354
+ - **`stableRequest`**: Single HTTP request with retry logic
355
+ - **`stableApiGateway`**: Execute multiple requests (concurrent or sequential)
356
+ - **`stableWorkflow`**: Orchestrate multi-phase workflows with advanced patterns
1454
357
 
1455
- async function fetchDataWithFallback(dataSources: DataSource[]) {
1456
- // Sort by priority
1457
- const sortedSources = [...dataSources].sort((a, b) => a.priority - b.priority);
1458
-
1459
- // Create circuit breakers for each source
1460
- const circuitBreakers = new Map(
1461
- sortedSources.map(source => [
1462
- source.id,
1463
- new CircuitBreaker({
1464
- failureThresholdPercentage: 50,
1465
- minimumRequests: 3,
1466
- recoveryTimeoutMs: 60000
1467
- })
1468
- ])
1469
- );
1470
-
1471
- // Try each data source in priority order
1472
- for (const source of sortedSources) {
1473
- const breaker = circuitBreakers.get(source.id)!;
1474
- const breakerState = breaker.getState();
1475
-
1476
- // Skip if circuit is open
1477
- if (breakerState.state === 'OPEN') {
1478
- console.warn(`Circuit breaker open for ${source.id}, skipping...`);
1479
- continue;
1480
- }
358
+ ### Utility Exports
1481
359
 
1482
- console.log(`Attempting to fetch from ${source.id}...`);
1483
-
1484
- try {
1485
- const requests = [
1486
- {
1487
- id: 'users',
1488
- requestOptions: {
1489
- reqData: { path: `${source.endpoint}/users` },
1490
- resReq: true,
1491
- attempts: 3,
1492
- wait: 1000,
1493
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1494
- }
1495
- },
1496
- {
1497
- id: 'products',
1498
- requestOptions: {
1499
- reqData: { path: `${source.endpoint}/products` },
1500
- resReq: true,
1501
- attempts: 3,
1502
- wait: 1000,
1503
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1504
- }
1505
- },
1506
- {
1507
- id: 'orders',
1508
- requestOptions: {
1509
- reqData: { path: `${source.endpoint}/orders` },
1510
- resReq: true,
1511
- attempts: 3,
1512
- wait: 1000,
1513
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1514
- }
1515
- }
1516
- ];
1517
-
1518
- const results = await stableApiGateway(requests, {
1519
- commonRequestData: {
1520
- hostname: source.hostname,
1521
- headers: { 'X-Source-ID': source.id }
1522
- },
1523
- concurrentExecution: true,
1524
- maxConcurrentRequests: 10,
1525
- circuitBreaker: breaker,
1526
- rateLimit: {
1527
- maxRequests: 50,
1528
- windowMs: 1000
1529
- },
1530
- commonCache: {
1531
- enabled: true,
1532
- ttl: 60000
1533
- },
1534
- commonResponseAnalyzer: async ({ data }) => {
1535
- // Validate data structure
1536
- return data && typeof data === 'object' && !data.error;
1537
- },
1538
- commonHandleErrors: ({ errorLog }) => {
1539
- console.error(`Error from ${source.id}:`, errorLog);
1540
- }
1541
- });
360
+ - **Circuit Breaker**: `CircuitBreaker`, `CircuitBreakerOpenError`
361
+ - **Rate Limiting**: `RateLimiter`
362
+ - **Concurrency**: `ConcurrencyLimiter`
363
+ - **Caching**: `CacheManager`, `getGlobalCacheManager`, `resetGlobalCacheManager`
364
+ - **Execution Utilities**: `executeNonLinearWorkflow`, `executeBranchWorkflow`, `executePhase`
1542
365
 
1543
- // Check if all requests succeeded
1544
- const allSuccessful = results.every(r => r.success);
1545
-
1546
- if (allSuccessful) {
1547
- console.log(`Successfully fetched data from ${source.id}`);
1548
- return {
1549
- source: source.id,
1550
- data: {
1551
- users: results.find(r => r.requestId === 'users')?.data,
1552
- products: results.find(r => r.requestId === 'products')?.data,
1553
- orders: results.find(r => r.requestId === 'orders')?.data
1554
- }
1555
- };
1556
- } else {
1557
- console.warn(`Partial failure from ${source.id}, trying next source...`);
1558
- }
1559
- } catch (error) {
1560
- console.error(`Failed to fetch from ${source.id}:`, error);
1561
- // Continue to next source
1562
- }
1563
- }
366
+ ### Enums
1564
367
 
1565
- throw new Error('All data sources failed');
1566
- }
368
+ - `RETRY_STRATEGIES`: Fixed, Linear, Exponential, Fibonacci
369
+ - `REQUEST_METHODS`: GET, POST, PUT, PATCH, DELETE, etc.
370
+ - `PHASE_DECISION_ACTIONS`: CONTINUE, JUMP, SKIP, REPLAY, TERMINATE
371
+ - `VALID_REQUEST_PROTOCOLS`: HTTP, HTTPS
372
+ - `CircuitBreakerState`: CLOSED, OPEN, HALF_OPEN
1567
373
 
1568
- // Usage
1569
- const dataSources: DataSource[] = [
1570
- {
1571
- id: 'primary-db',
1572
- priority: 1,
1573
- endpoint: '/api/v1',
1574
- hostname: 'primary.example.com'
1575
- },
1576
- {
1577
- id: 'replica-db',
1578
- priority: 2,
1579
- endpoint: '/api/v1',
1580
- hostname: 'replica.example.com'
1581
- },
1582
- {
1583
- id: 'backup-cache',
1584
- priority: 3,
1585
- endpoint: '/cached',
1586
- hostname: 'cache.example.com'
1587
- }
1588
- ];
374
+ ### TypeScript Types
1589
375
 
1590
- const result = await fetchDataWithFallback(dataSources);
1591
- console.log('Data fetched from:', result.source);
1592
- console.log('Users:', result.data.users?.length);
1593
- console.log('Products:', result.data.products?.length);
1594
- console.log('Orders:', result.data.orders?.length);
1595
- ```
376
+ Full TypeScript support with 40+ exported types for complete type safety across workflows, requests, configurations, and hooks.
1596
377
 
1597
378
  ## License
1598
379
 
1599
380
  MIT © Manish Varma
1600
-
1601
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
1602
- ---
1603
-
1604
- **Made with ❤️ for developers integrating with unreliable APIs**