@emmvish/stable-request 1.6.1 → 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,65 +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
19
  - [Branched Workflows](#branched-workflows)
48
- - [Retry Strategies](#retry-strategies)
49
- - [Circuit Breaker](#circuit-breaker)
50
- - [Rate Limiting](#rate-limiting)
51
- - [Caching](#caching)
52
- - [Pre-Execution Hooks](#pre-execution-hooks)
53
- - [Shared Buffer](#shared-buffer)
54
- - [Request Grouping](#request-grouping)
55
- - [Concurrency Control](#concurrency-control)
56
- - [Response Analysis](#response-analysis)
57
- - [Error Handling](#error-handling)
58
- - [Advanced Use Cases](#advanced-use-cases)
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)
59
25
  - [License](#license)
60
- <!-- TOC END -->
61
26
 
62
- ---
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
63
35
 
64
36
  ## Installation
65
37
 
@@ -67,26 +39,9 @@ Start small and scale.
67
39
  npm install @emmvish/stable-request
68
40
  ```
69
41
 
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
42
  ## Quick Start
88
43
 
89
- ### Basic Request with Retry
44
+ ### Single Request with Retry
90
45
 
91
46
  ```typescript
92
47
  import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
@@ -94,7 +49,7 @@ import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
94
49
  const data = await stableRequest({
95
50
  reqData: {
96
51
  hostname: 'api.example.com',
97
- path: '/users/123',
52
+ path: '/users',
98
53
  method: 'GET'
99
54
  },
100
55
  resReq: true,
@@ -102,2025 +57,324 @@ const data = await stableRequest({
102
57
  wait: 1000,
103
58
  retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
104
59
  });
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
60
  ```
134
61
 
135
62
  ### Multi-Phase Workflow
136
63
 
137
64
  ```typescript
138
- import { stableWorkflow, STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
65
+ import { stableWorkflow } from '@emmvish/stable-request';
139
66
 
140
- const phases: STABLE_WORKFLOW_PHASE[] = [
67
+ const result = await stableWorkflow([
141
68
  {
142
- id: 'authentication',
69
+ id: 'auth',
143
70
  requests: [
144
71
  { id: 'login', requestOptions: { reqData: { path: '/auth/login' }, resReq: true } }
145
72
  ]
146
73
  },
147
74
  {
148
- id: 'data-fetching',
75
+ id: 'fetch-data',
149
76
  concurrentExecution: true,
150
77
  requests: [
151
78
  { id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
152
- { id: 'posts', requestOptions: { reqData: { path: '/posts' }, resReq: true } }
79
+ { id: 'orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } }
153
80
  ]
154
81
  }
155
- ];
156
-
157
- const result = await stableWorkflow(phases, {
158
- workflowId: 'data-pipeline',
82
+ ], {
83
+ workflowId: 'user-data-sync',
159
84
  commonRequestData: { hostname: 'api.example.com' },
160
- stopOnFirstPhaseError: true,
161
- logPhaseResults: true
85
+ stopOnFirstPhaseError: true
162
86
  });
163
-
164
- console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
165
87
  ```
166
88
 
167
- ### Non-Linear Workflow with Dynamic Routing
89
+ ## Core Features
168
90
 
169
- ```typescript
170
- import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
91
+ ### Intelligent Retry Strategies
171
92
 
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
- ];
93
+ Automatically retry failed requests with configurable strategies:
212
94
 
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
- });
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
220
99
 
221
- console.log('Execution history:', result.executionHistory);
222
- console.log('Terminated early:', result.terminatedEarly);
223
- ```
100
+ Each request can have individual retry configurations, or inherit from workflow-level defaults.
224
101
 
225
- ## Advanced Features
102
+ ### Circuit Breaker Pattern
226
103
 
227
- ### Non-Linear Workflows
104
+ Prevent cascade failures and system overload with built-in circuit breakers:
228
105
 
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.
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
230
110
 
231
- #### Phase Decision Actions
111
+ ### Response Caching
232
112
 
233
- Each phase can make decisions about workflow execution:
113
+ Reduce redundant API calls with intelligent caching:
234
114
 
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
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
240
119
 
241
- #### Basic Non-Linear Workflow
120
+ ### Rate Limiting and Concurrency Control
242
121
 
243
- ```typescript
244
- import { stableWorkflow, STABLE_WORKFLOW_PHASE, PhaseExecutionDecision, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
122
+ Respect API rate limits and control system load:
245
123
 
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
- ];
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
273
128
 
274
- const result = await stableWorkflow(phases, {
275
- workflowId: 'validation-workflow',
276
- commonRequestData: { hostname: 'api.example.com' },
277
- enableNonLinearExecution: true,
278
- sharedBuffer: {}
279
- });
129
+ ## Workflow Execution Patterns
280
130
 
281
- if (result.terminatedEarly) {
282
- console.log('Workflow terminated:', result.terminationReason);
283
- }
284
- ```
131
+ ### Sequential and Concurrent Phases
285
132
 
286
- #### Conditional Branching
133
+ Control execution order at the phase level:
134
+
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
287
138
 
288
139
  ```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
- ]
140
+ const phases = [
141
+ { id: 'init', requests: [...] }, // Sequential phase
142
+ {
143
+ id: 'parallel-fetch',
144
+ concurrentExecution: true, // Concurrent requests within phase
145
+ requests: [...]
333
146
  }
334
147
  ];
335
148
 
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
- }
149
+ await stableWorkflow(phases, {
150
+ concurrentPhaseExecution: true // Run phases in parallel
342
151
  });
343
152
  ```
344
153
 
345
- #### Polling with Replay
154
+ ### Mixed Execution Mode
346
155
 
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
- ];
156
+ Combine sequential and concurrent phases in a single workflow:
389
157
 
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
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"
402
161
 
403
162
  ```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
- }
163
+ const phases = [
164
+ { id: 'auth', requests: [...] }, // Sequential
165
+ {
166
+ id: 'fetch',
167
+ markConcurrentPhase: true, // Runs concurrently with next phase
168
+ requests: [...]
441
169
  },
442
- {
443
- id: 'primary-flow',
444
- requests: [
445
- { id: 'primary', requestOptions: { reqData: { path: '/primary' }, resReq: true } }
446
- ]
170
+ {
171
+ id: 'more-fetch',
172
+ markConcurrentPhase: true, // Runs concurrently with previous
173
+ requests: [...]
447
174
  },
448
- {
449
- id: 'fallback-operation',
450
- requests: [
451
- { id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
452
- ]
453
- }
175
+ { id: 'process', requests: [...] } // Sequential, waits for above
454
176
  ];
455
177
 
456
- const result = await stableWorkflow(phases, {
457
- enableNonLinearExecution: true,
458
- sharedBuffer: { retryAttempts: 0 },
459
- logPhaseResults: true
178
+ await stableWorkflow(phases, {
179
+ enableMixedExecution: true
460
180
  });
461
181
  ```
462
182
 
463
- #### Skip Phases
183
+ ### Non-Linear Workflows
184
+
185
+ Build dynamic workflows with conditional branching and looping:
186
+
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)
464
192
 
465
193
  ```typescript
466
- const phases: STABLE_WORKFLOW_PHASE[] = [
194
+ const phases = [
467
195
  {
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' };
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' };
480
201
  }
481
202
  return { action: PHASE_DECISION_ACTIONS.CONTINUE };
482
203
  }
483
204
  },
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
- }
205
+ { id: 'retry-logic', requests: [...] },
206
+ { id: 'success', requests: [...] }
502
207
  ];
503
208
 
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 ? '✓' : '✗'}`);
209
+ await stableWorkflow(phases, {
210
+ enableNonLinearExecution: true
540
211
  });
541
212
  ```
542
213
 
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
- ```
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)
560
219
 
561
220
  ### Branched Workflows
562
221
 
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
222
+ Execute multiple independent workflow paths in parallel or sequentially:
572
223
 
573
- #### Basic Branched Workflow
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
574
228
 
575
229
  ```typescript
576
- import { stableWorkflow, STABLE_WORKFLOW_BRANCH } from '@emmvish/stable-request';
577
-
578
- const branches: STABLE_WORKFLOW_BRANCH[] = [
230
+ const branches = [
579
231
  {
580
- id: 'validation',
581
- phases: [
582
- {
583
- id: 'validate-input',
584
- requests: [
585
- { id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
586
- ]
587
- }
588
- ]
232
+ id: 'user-flow',
233
+ markConcurrentBranch: true, // Parallel
234
+ phases: [...]
589
235
  },
590
236
  {
591
- id: 'processing',
592
- phases: [
593
- {
594
- id: 'process-data',
595
- requests: [
596
- { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
597
- ]
598
- }
599
- ]
237
+ id: 'analytics-flow',
238
+ markConcurrentBranch: true, // Parallel
239
+ phases: [...]
600
240
  },
601
241
  {
602
- id: 'finalization',
603
- phases: [
604
- {
605
- id: 'finalize',
606
- requests: [
607
- { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
608
- ]
609
- }
610
- ]
242
+ id: 'cleanup-flow', // Sequential (default)
243
+ phases: [...]
611
244
  }
612
245
  ];
613
246
 
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: {}
247
+ await stableWorkflow([], {
248
+ enableBranchExecution: true,
249
+ branches
620
250
  });
621
-
622
- console.log('Branches executed:', result.branches?.length);
623
251
  ```
624
252
 
625
- #### Parallel vs Serial Branch Execution
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
626
258
 
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
- });
259
+ ## Advanced Capabilities
640
260
 
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: {}
261
+ ### Config Cascading
262
+
263
+ Configuration inheritance across workflow → branch → phase → request levels:
264
+
265
+ ```typescript
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
278
+ },
279
+ phases: [{
280
+ id: 'my-phase',
281
+ commonConfig: {
282
+ // Phase-level config (overrides branch and workflow)
283
+ commonAttempts: 1
284
+ },
285
+ requests: [{
286
+ requestOptions: {
287
+ // Request-level config (highest priority)
288
+ attempts: 10,
289
+ cache: { enabled: false }
290
+ }
291
+ }]
292
+ }]
293
+ }]
652
294
  });
653
295
  ```
654
296
 
655
- #### Branch Decision Hooks
297
+ ### Shared Buffer and Pre-Execution Hooks
656
298
 
657
- Each branch can have a decision hook to control workflow execution:
299
+ Share state and transform requests dynamically:
658
300
 
301
+ **Shared Buffer**: Cross-phase/branch communication
659
302
  ```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
- ];
303
+ const sharedBuffer = { authToken: null };
692
304
 
693
- const result = await stableWorkflow([], {
694
- workflowId: 'validation-workflow',
695
- commonRequestData: { hostname: 'api.example.com' },
696
- branches,
697
- executeBranchesConcurrently: false,
698
- sharedBuffer: {}
305
+ await stableWorkflow(phases, {
306
+ sharedBuffer,
307
+ // Phases can read/write to sharedBuffer via preExecution hooks
699
308
  });
700
-
701
- if (result.terminatedEarly) {
702
- console.log('Workflow terminated:', result.terminationReason);
703
- }
704
309
  ```
705
310
 
706
- #### JUMP Action - Skip Branches
707
-
311
+ **Pre-Execution Hooks**: Modify requests before execution
708
312
  ```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
313
+ {
314
+ requestOptions: {
315
+ preExecution: {
316
+ preExecutionHook: ({ commonBuffer, inputParams }) => {
317
+ // Access buffer, compute values, return config overrides
726
318
  return {
727
- action: BRANCH_DECISION_ACTIONS.JUMP,
728
- targetBranchId: 'finalize'
319
+ reqData: {
320
+ headers: { 'Authorization': `Bearer ${commonBuffer.authToken}` }
321
+ }
729
322
  };
730
- }
731
-
732
- return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
323
+ },
324
+ applyPreExecutionConfigOverride: true
733
325
  }
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
326
  }
768
- ];
327
+ }
328
+ ```
769
329
 
770
- const result = await stableWorkflow([], {
771
- workflowId: 'cache-optimization',
772
- commonRequestData: { hostname: 'api.example.com' },
773
- branches,
774
- executeBranchesConcurrently: false,
775
- sharedBuffer: {}
776
- });
330
+ ### Comprehensive Observability
777
331
 
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
- ```
332
+ Built-in hooks for monitoring, logging, and analysis:
781
333
 
782
- #### Conditional Branching
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
783
339
 
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
- ];
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
863
346
 
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
- ```
347
+ **Execution History**:
348
+ Every workflow result includes detailed execution history with timestamps, decisions, and metadata.
872
349
 
873
- #### Retry Logic Within Branches
350
+ ## API Surface
874
351
 
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
- ```
975
-
976
- #### Branch Completion Hooks
977
-
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
- });
992
- ```
993
-
994
- #### Mixed Parallel and Serial Branches
995
-
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
- ];
1030
-
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
- });
1037
-
1038
- // Execution: init → [parallel-1, parallel-2, parallel-3] → finalize
1039
- ```
1040
-
1041
- #### Configuration Options
1042
-
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
1047
-
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
- }
1069
- ```
1070
-
1071
- **Decision Object:**
1072
- ```typescript
1073
- interface BranchExecutionDecision {
1074
- action: BRANCH_DECISION_ACTIONS;
1075
- targetBranchId?: string;
1076
- metadata?: Record<string, any>;
1077
- }
1078
- ```
352
+ ### Core Functions
1079
353
 
1080
- #### Mixed Serial and Parallel Execution
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
1081
357
 
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.
358
+ ### Utility Exports
1083
359
 
1084
- **Basic Mixed Execution:**
360
+ - **Circuit Breaker**: `CircuitBreaker`, `CircuitBreakerOpenError`
361
+ - **Rate Limiting**: `RateLimiter`
362
+ - **Concurrency**: `ConcurrencyLimiter`
363
+ - **Caching**: `CacheManager`, `getGlobalCacheManager`, `resetGlobalCacheManager`
364
+ - **Execution Utilities**: `executeNonLinearWorkflow`, `executeBranchWorkflow`, `executePhase`
1085
365
 
1086
- ```typescript
1087
- import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
366
+ ### Enums
1088
367
 
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
- }
1135
- ];
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
1136
373
 
1137
- const result = await stableWorkflow(phases, {
1138
- workflowId: 'mixed-execution',
1139
- commonRequestData: { hostname: 'api.example.com' },
1140
- enableNonLinearExecution: true
1141
- });
1142
- ```
374
+ ### TypeScript Types
1143
375
 
1144
- **Multiple Parallel Groups:**
1145
-
1146
- ```typescript
1147
- const phases: STABLE_WORKFLOW_PHASE[] = [
1148
- {
1149
- id: 'authenticate',
1150
- requests: [
1151
- { id: 'auth', requestOptions: { reqData: { path: '/auth' }, resReq: true } }
1152
- ]
1153
- },
1154
- // First parallel group: Data validation
1155
- {
1156
- id: 'validate-user',
1157
- markConcurrentPhase: true,
1158
- requests: [
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 } }
1217
- ]
1218
- }
1219
- ];
1220
-
1221
- const result = await stableWorkflow(phases, {
1222
- workflowId: 'multi-parallel-workflow',
1223
- commonRequestData: { hostname: 'api.example.com' },
1224
- enableNonLinearExecution: true
1225
- });
1226
-
1227
- console.log('Execution order demonstrates mixed serial/parallel execution');
1228
- ```
1229
-
1230
- **Decision Making with Concurrent Results:**
1231
-
1232
- ```typescript
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
- ];
1291
-
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
- });
1299
-
1300
- console.log('Average health score:', sharedBuffer.healthScore);
1301
- ```
1302
-
1303
- **Error Handling in Parallel Groups:**
1304
-
1305
- ```typescript
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
- });
1364
- ```
1365
-
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
1374
-
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)
1380
-
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)
1386
-
1387
- **Decision Hook Parameters:**
1388
- ```typescript
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
- }
1398
- ```
1399
-
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
- ```
1409
-
1410
- ### Retry Strategies
1411
-
1412
- Control the delay between retry attempts:
1413
-
1414
- ```typescript
1415
- import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
1416
-
1417
- // Fixed delay: 1000ms between each retry
1418
- await stableRequest({
1419
- reqData: { hostname: 'api.example.com', path: '/data' },
1420
- attempts: 3,
1421
- wait: 1000,
1422
- retryStrategy: RETRY_STRATEGIES.FIXED
1423
- });
1424
-
1425
- // Linear backoff: 1000ms, 2000ms, 3000ms
1426
- await stableRequest({
1427
- reqData: { hostname: 'api.example.com', path: '/data' },
1428
- attempts: 3,
1429
- wait: 1000,
1430
- retryStrategy: RETRY_STRATEGIES.LINEAR
1431
- });
1432
-
1433
- // Exponential backoff: 1000ms, 2000ms, 4000ms
1434
- await stableRequest({
1435
- reqData: { hostname: 'api.example.com', path: '/data' },
1436
- attempts: 3,
1437
- wait: 1000,
1438
- maxAllowedWait: 10000,
1439
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1440
- });
1441
- ```
1442
-
1443
- ### Circuit Breaker
1444
-
1445
- Prevent cascading failures by automatically blocking requests when error thresholds are exceeded:
1446
-
1447
- ```typescript
1448
- import { stableApiGateway, CircuitBreakerState } from '@emmvish/stable-request';
1449
-
1450
- const results = await stableApiGateway(requests, {
1451
- commonRequestData: { hostname: 'api.example.com' },
1452
- circuitBreaker: {
1453
- failureThresholdPercentage: 50, // Open circuit at 50% failure rate
1454
- minimumRequests: 5, // Need at least 5 requests to calculate
1455
- recoveryTimeoutMs: 30000, // Try recovery after 30 seconds
1456
- trackIndividualAttempts: false // Track per-request success/failure
1457
- }
1458
- });
1459
-
1460
- // Circuit breaker can be shared across workflows
1461
- const breaker = new CircuitBreaker({
1462
- failureThresholdPercentage: 50,
1463
- minimumRequests: 10,
1464
- recoveryTimeoutMs: 60000
1465
- });
1466
-
1467
- const result = await stableWorkflow(phases, {
1468
- circuitBreaker: breaker,
1469
- // ... other options
1470
- });
1471
-
1472
- // Check circuit breaker state
1473
- const state = breaker.getState();
1474
- console.log(`Circuit breaker state: ${state.state}`); // CLOSED, OPEN, or HALF_OPEN
1475
- ```
1476
-
1477
- ### Rate Limiting
1478
-
1479
- Control request throughput to prevent overwhelming APIs:
1480
-
1481
- ```typescript
1482
- import { stableApiGateway } from '@emmvish/stable-request';
1483
-
1484
- const results = await stableApiGateway(requests, {
1485
- commonRequestData: { hostname: 'api.example.com' },
1486
- concurrentExecution: true,
1487
- rateLimit: {
1488
- maxRequests: 10, // Maximum 10 requests
1489
- windowMs: 1000 // Per 1 second window
1490
- }
1491
- });
1492
-
1493
- // Rate limiting in workflows
1494
- const result = await stableWorkflow(phases, {
1495
- rateLimit: {
1496
- maxRequests: 5,
1497
- windowMs: 1000
1498
- }
1499
- });
1500
- ```
1501
-
1502
- ### Caching
1503
-
1504
- Cache responses with TTL to reduce redundant API calls:
1505
-
1506
- ```typescript
1507
- import { stableRequest, getGlobalCacheManager } from '@emmvish/stable-request';
1508
-
1509
- // Enable caching for a request
1510
- const data = await stableRequest({
1511
- reqData: { hostname: 'api.example.com', path: '/users/123' },
1512
- resReq: true,
1513
- cache: {
1514
- enabled: true,
1515
- ttl: 60000 // Cache for 60 seconds
1516
- }
1517
- });
1518
-
1519
- // Use global cache manager across requests
1520
- const results = await stableApiGateway(requests, {
1521
- commonRequestData: { hostname: 'api.example.com' },
1522
- commonCache: { enabled: true, ttl: 300000 } // 5 minutes
1523
- });
1524
-
1525
- // Manage cache manually
1526
- const cacheManager = getGlobalCacheManager();
1527
- const stats = cacheManager.getStats();
1528
- console.log(`Cache size: ${stats.size}, Valid entries: ${stats.validEntries}`);
1529
- cacheManager.clear(); // Clear all cache
1530
- ```
1531
-
1532
- ### Pre-Execution Hooks
1533
-
1534
- Transform requests dynamically before execution:
1535
-
1536
- ```typescript
1537
- import { stableRequest } from '@emmvish/stable-request';
1538
-
1539
- const commonBuffer: Record<string, any> = {};
1540
-
1541
- const data = await stableRequest({
1542
- reqData: { hostname: 'api.example.com', path: '/data' },
1543
- resReq: true,
1544
- preExecution: {
1545
- preExecutionHook: async ({ inputParams, commonBuffer }) => {
1546
- // Fetch authentication token
1547
- const token = await getAuthToken();
1548
-
1549
- // Store in shared buffer
1550
- commonBuffer.token = token;
1551
- commonBuffer.timestamp = Date.now();
1552
-
1553
- // Override request configuration
1554
- return {
1555
- reqData: {
1556
- hostname: 'api.example.com',
1557
- path: '/authenticated-data',
1558
- headers: { Authorization: `Bearer ${token}` }
1559
- }
1560
- };
1561
- },
1562
- preExecutionHookParams: { userId: 'user123' },
1563
- applyPreExecutionConfigOverride: true, // Apply returned config
1564
- continueOnPreExecutionHookFailure: false
1565
- },
1566
- commonBuffer
1567
- });
1568
-
1569
- console.log('Token used:', commonBuffer.token);
1570
- ```
1571
-
1572
- ### Shared Buffer
1573
-
1574
- Share state across requests in gateways and workflows:
1575
-
1576
- ```typescript
1577
- import { stableWorkflow } from '@emmvish/stable-request';
1578
-
1579
- const sharedBuffer: Record<string, any> = { requestCount: 0 };
1580
-
1581
- const phases: STABLE_WORKFLOW_PHASE[] = [
1582
- {
1583
- id: 'phase-1',
1584
- requests: [
1585
- {
1586
- id: 'req-1',
1587
- requestOptions: {
1588
- reqData: { path: '/step1' },
1589
- resReq: true,
1590
- preExecution: {
1591
- preExecutionHook: ({ commonBuffer }) => {
1592
- commonBuffer.requestCount++;
1593
- commonBuffer.phase1Data = 'initialized';
1594
- return {};
1595
- },
1596
- preExecutionHookParams: {},
1597
- applyPreExecutionConfigOverride: false,
1598
- continueOnPreExecutionHookFailure: false
1599
- }
1600
- }
1601
- }
1602
- ]
1603
- },
1604
- {
1605
- id: 'phase-2',
1606
- requests: [
1607
- {
1608
- id: 'req-2',
1609
- requestOptions: {
1610
- reqData: { path: '/step2' },
1611
- resReq: true,
1612
- preExecution: {
1613
- preExecutionHook: ({ commonBuffer }) => {
1614
- commonBuffer.requestCount++;
1615
- // Access data from phase-1
1616
- console.log('Phase 1 data:', commonBuffer.phase1Data);
1617
- return {};
1618
- },
1619
- preExecutionHookParams: {},
1620
- applyPreExecutionConfigOverride: false,
1621
- continueOnPreExecutionHookFailure: false
1622
- }
1623
- }
1624
- }
1625
- ]
1626
- }
1627
- ];
1628
-
1629
- const result = await stableWorkflow(phases, {
1630
- workflowId: 'stateful-workflow',
1631
- commonRequestData: { hostname: 'api.example.com' },
1632
- sharedBuffer
1633
- });
1634
-
1635
- console.log('Total requests processed:', sharedBuffer.requestCount);
1636
- ```
1637
-
1638
- ### Request Grouping
1639
-
1640
- Apply different configurations to request groups:
1641
-
1642
- ```typescript
1643
- import { stableApiGateway } from '@emmvish/stable-request';
1644
-
1645
- const requests = [
1646
- {
1647
- id: 'critical-1',
1648
- groupId: 'critical',
1649
- requestOptions: { reqData: { path: '/critical/1' }, resReq: true }
1650
- },
1651
- {
1652
- id: 'critical-2',
1653
- groupId: 'critical',
1654
- requestOptions: { reqData: { path: '/critical/2' }, resReq: true }
1655
- },
1656
- {
1657
- id: 'optional-1',
1658
- groupId: 'optional',
1659
- requestOptions: { reqData: { path: '/optional/1' }, resReq: true }
1660
- }
1661
- ];
1662
-
1663
- const results = await stableApiGateway(requests, {
1664
- commonRequestData: { hostname: 'api.example.com' },
1665
- commonAttempts: 1,
1666
- commonWait: 100,
1667
- requestGroups: [
1668
- {
1669
- id: 'critical',
1670
- commonConfig: {
1671
- commonAttempts: 5, // More retries for critical requests
1672
- commonWait: 2000,
1673
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1674
- }
1675
- },
1676
- {
1677
- id: 'optional',
1678
- commonConfig: {
1679
- commonAttempts: 1, // No retries for optional requests
1680
- commonFinalErrorAnalyzer: async () => true // Suppress errors
1681
- }
1682
- }
1683
- ]
1684
- });
1685
- ```
1686
-
1687
- ### Concurrency Control
1688
-
1689
- Limit concurrent request execution:
1690
-
1691
- ```typescript
1692
- import { stableApiGateway } from '@emmvish/stable-request';
1693
-
1694
- // Limit to 5 concurrent requests
1695
- const results = await stableApiGateway(requests, {
1696
- commonRequestData: { hostname: 'api.example.com' },
1697
- concurrentExecution: true,
1698
- maxConcurrentRequests: 5
1699
- });
1700
-
1701
- // Phase-level concurrency control
1702
- const phases: STABLE_WORKFLOW_PHASE[] = [
1703
- {
1704
- id: 'limited-phase',
1705
- concurrentExecution: true,
1706
- maxConcurrentRequests: 3,
1707
- requests: [/* ... */]
1708
- }
1709
- ];
1710
- ```
1711
-
1712
- ### Response Analysis
1713
-
1714
- Validate response content and trigger retries:
1715
-
1716
- ```typescript
1717
- import { stableRequest } from '@emmvish/stable-request';
1718
-
1719
- const data = await stableRequest({
1720
- reqData: { hostname: 'api.example.com', path: '/job/status' },
1721
- resReq: true,
1722
- attempts: 10,
1723
- wait: 2000,
1724
- responseAnalyzer: async ({ data, reqData, params }) => {
1725
- // Retry until job is completed
1726
- if (data.status === 'processing') {
1727
- console.log('Job still processing, will retry...');
1728
- return false; // Trigger retry
1729
- }
1730
- return data.status === 'completed';
1731
- }
1732
- });
1733
-
1734
- console.log('Job completed:', data);
1735
- ```
1736
-
1737
- ### Error Handling
1738
-
1739
- Comprehensive error handling with observability hooks:
1740
-
1741
- ```typescript
1742
- import { stableRequest } from '@emmvish/stable-request';
1743
-
1744
- const data = await stableRequest({
1745
- reqData: { hostname: 'api.example.com', path: '/data' },
1746
- resReq: true,
1747
- attempts: 3,
1748
- wait: 1000,
1749
- logAllErrors: true,
1750
- handleErrors: ({ reqData, errorLog, params }) => {
1751
- // Custom error logging
1752
- console.error('Request failed:', {
1753
- url: reqData.url,
1754
- attempt: errorLog.attempt,
1755
- statusCode: errorLog.statusCode,
1756
- error: errorLog.error,
1757
- isRetryable: errorLog.isRetryable
1758
- });
1759
-
1760
- // Send to monitoring service
1761
- monitoringService.trackError(errorLog);
1762
- },
1763
- logAllSuccessfulAttempts: true,
1764
- handleSuccessfulAttemptData: ({ successfulAttemptData }) => {
1765
- console.log('Request succeeded on attempt:', successfulAttemptData.attempt);
1766
- },
1767
- finalErrorAnalyzer: async ({ error, reqData }) => {
1768
- // Gracefully handle specific errors
1769
- if (error.response?.status === 404) {
1770
- console.warn('Resource not found, continuing...');
1771
- return true; // Return false to suppress error
1772
- }
1773
- return false; // Throw error
1774
- }
1775
- });
1776
- ```
1777
-
1778
- ## Advanced Use Cases
1779
-
1780
- ### Use Case 1: Multi-Tenant API with Dynamic Authentication
1781
-
1782
- ```typescript
1783
- import { stableWorkflow, RETRY_STRATEGIES } from '@emmvish/stable-request';
1784
-
1785
- interface TenantConfig {
1786
- tenantId: string;
1787
- apiKey: string;
1788
- baseUrl: string;
1789
- }
1790
-
1791
- async function executeTenantWorkflow(tenantConfig: TenantConfig) {
1792
- const sharedBuffer: Record<string, any> = {
1793
- tenantId: tenantConfig.tenantId,
1794
- authToken: null,
1795
- processedItems: []
1796
- };
1797
-
1798
- const phases: STABLE_WORKFLOW_PHASE[] = [
1799
- {
1800
- id: 'authentication',
1801
- requests: [
1802
- {
1803
- id: 'get-token',
1804
- requestOptions: {
1805
- reqData: {
1806
- path: '/auth/token',
1807
- method: 'POST',
1808
- headers: { 'X-API-Key': tenantConfig.apiKey }
1809
- },
1810
- resReq: true,
1811
- attempts: 3,
1812
- wait: 2000,
1813
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1814
- responseAnalyzer: async ({ data, commonBuffer }) => {
1815
- if (data?.token) {
1816
- commonBuffer.authToken = data.token;
1817
- commonBuffer.tokenExpiry = Date.now() + (data.expiresIn * 1000);
1818
- return true;
1819
- }
1820
- return false;
1821
- }
1822
- }
1823
- }
1824
- ]
1825
- },
1826
- {
1827
- id: 'data-fetching',
1828
- concurrentExecution: true,
1829
- maxConcurrentRequests: 5,
1830
- requests: [
1831
- {
1832
- id: 'fetch-users',
1833
- requestOptions: {
1834
- reqData: { path: '/users' },
1835
- resReq: true,
1836
- preExecution: {
1837
- preExecutionHook: ({ commonBuffer }) => ({
1838
- reqData: {
1839
- path: '/users',
1840
- headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
1841
- }
1842
- }),
1843
- applyPreExecutionConfigOverride: true
1844
- }
1845
- }
1846
- },
1847
- {
1848
- id: 'fetch-settings',
1849
- requestOptions: {
1850
- reqData: { path: '/settings' },
1851
- resReq: true,
1852
- preExecution: {
1853
- preExecutionHook: ({ commonBuffer }) => ({
1854
- reqData: {
1855
- path: '/settings',
1856
- headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
1857
- }
1858
- }),
1859
- applyPreExecutionConfigOverride: true
1860
- }
1861
- }
1862
- }
1863
- ]
1864
- },
1865
- {
1866
- id: 'data-processing',
1867
- concurrentExecution: true,
1868
- requests: [
1869
- {
1870
- id: 'process-users',
1871
- requestOptions: {
1872
- reqData: { path: '/process/users', method: 'POST' },
1873
- resReq: true,
1874
- preExecution: {
1875
- preExecutionHook: ({ commonBuffer }) => {
1876
- const usersPhase = commonBuffer.phases?.find(p => p.phaseId === 'data-fetching');
1877
- const usersData = usersPhase?.responses?.find(r => r.requestId === 'fetch-users')?.data;
1878
-
1879
- return {
1880
- reqData: {
1881
- path: '/process/users',
1882
- method: 'POST',
1883
- headers: { Authorization: `Bearer ${commonBuffer.authToken}` },
1884
- body: { users: usersData }
1885
- }
1886
- };
1887
- },
1888
- applyPreExecutionConfigOverride: true
1889
- },
1890
- responseAnalyzer: async ({ data, commonBuffer }) => {
1891
- if (data?.processed) {
1892
- commonBuffer.processedItems.push(...data.processed);
1893
- return true;
1894
- }
1895
- return false;
1896
- }
1897
- }
1898
- }
1899
- ]
1900
- }
1901
- ];
1902
-
1903
- const result = await stableWorkflow(phases, {
1904
- workflowId: `tenant-${tenantConfig.tenantId}-workflow`,
1905
- commonRequestData: {
1906
- hostname: tenantConfig.baseUrl,
1907
- headers: { 'X-Tenant-ID': tenantConfig.tenantId }
1908
- },
1909
- stopOnFirstPhaseError: true,
1910
- logPhaseResults: true,
1911
- sharedBuffer,
1912
- circuitBreaker: {
1913
- failureThresholdPercentage: 40,
1914
- minimumRequests: 5,
1915
- recoveryTimeoutMs: 30000
1916
- },
1917
- rateLimit: {
1918
- maxRequests: 20,
1919
- windowMs: 1000
1920
- },
1921
- commonCache: {
1922
- enabled: true,
1923
- ttl: 300000 // Cache for 5 minutes
1924
- },
1925
- handlePhaseCompletion: ({ workflowId, phaseResult }) => {
1926
- console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
1927
- success: phaseResult.success,
1928
- successfulRequests: phaseResult.successfulRequests,
1929
- executionTime: `${phaseResult.executionTime}ms`
1930
- });
1931
- },
1932
- handlePhaseError: ({ workflowId, error, phaseResult }) => {
1933
- console.error(`[${workflowId}] Phase ${phaseResult.phaseId} failed:`, error);
1934
- // Send to monitoring
1935
- monitoringService.trackPhaseError(workflowId, phaseResult.phaseId, error);
1936
- }
1937
- });
1938
-
1939
- return {
1940
- success: result.success,
1941
- tenantId: tenantConfig.tenantId,
1942
- processedItems: sharedBuffer.processedItems,
1943
- executionTime: result.executionTime,
1944
- phases: result.phases.map(p => ({
1945
- id: p.phaseId,
1946
- success: p.success,
1947
- requestCount: p.totalRequests
1948
- }))
1949
- };
1950
- }
1951
-
1952
- // Execute workflows for multiple tenants
1953
- const tenants: TenantConfig[] = [
1954
- { tenantId: 'tenant-1', apiKey: 'key1', baseUrl: 'api.tenant1.com' },
1955
- { tenantId: 'tenant-2', apiKey: 'key2', baseUrl: 'api.tenant2.com' }
1956
- ];
1957
-
1958
- const results = await Promise.all(tenants.map(executeTenantWorkflow));
1959
- results.forEach(result => {
1960
- console.log(`Tenant ${result.tenantId}:`, result.success ? 'Success' : 'Failed');
1961
- });
1962
- ```
1963
-
1964
- ### Use Case 2: Resilient Data Pipeline with Fallback Strategies
1965
-
1966
- ```typescript
1967
- import { stableApiGateway, RETRY_STRATEGIES, CircuitBreaker } from '@emmvish/stable-request';
1968
-
1969
- interface DataSource {
1970
- id: string;
1971
- priority: number;
1972
- endpoint: string;
1973
- hostname: string;
1974
- }
1975
-
1976
- async function fetchDataWithFallback(dataSources: DataSource[]) {
1977
- // Sort by priority
1978
- const sortedSources = [...dataSources].sort((a, b) => a.priority - b.priority);
1979
-
1980
- // Create circuit breakers for each source
1981
- const circuitBreakers = new Map(
1982
- sortedSources.map(source => [
1983
- source.id,
1984
- new CircuitBreaker({
1985
- failureThresholdPercentage: 50,
1986
- minimumRequests: 3,
1987
- recoveryTimeoutMs: 60000
1988
- })
1989
- ])
1990
- );
1991
-
1992
- // Try each data source in priority order
1993
- for (const source of sortedSources) {
1994
- const breaker = circuitBreakers.get(source.id)!;
1995
- const breakerState = breaker.getState();
1996
-
1997
- // Skip if circuit is open
1998
- if (breakerState.state === 'OPEN') {
1999
- console.warn(`Circuit breaker open for ${source.id}, skipping...`);
2000
- continue;
2001
- }
2002
-
2003
- console.log(`Attempting to fetch from ${source.id}...`);
2004
-
2005
- try {
2006
- const requests = [
2007
- {
2008
- id: 'users',
2009
- requestOptions: {
2010
- reqData: { path: `${source.endpoint}/users` },
2011
- resReq: true,
2012
- attempts: 3,
2013
- wait: 1000,
2014
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
2015
- }
2016
- },
2017
- {
2018
- id: 'products',
2019
- requestOptions: {
2020
- reqData: { path: `${source.endpoint}/products` },
2021
- resReq: true,
2022
- attempts: 3,
2023
- wait: 1000,
2024
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
2025
- }
2026
- },
2027
- {
2028
- id: 'orders',
2029
- requestOptions: {
2030
- reqData: { path: `${source.endpoint}/orders` },
2031
- resReq: true,
2032
- attempts: 3,
2033
- wait: 1000,
2034
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
2035
- }
2036
- }
2037
- ];
2038
-
2039
- const results = await stableApiGateway(requests, {
2040
- commonRequestData: {
2041
- hostname: source.hostname,
2042
- headers: { 'X-Source-ID': source.id }
2043
- },
2044
- concurrentExecution: true,
2045
- maxConcurrentRequests: 10,
2046
- circuitBreaker: breaker,
2047
- rateLimit: {
2048
- maxRequests: 50,
2049
- windowMs: 1000
2050
- },
2051
- commonCache: {
2052
- enabled: true,
2053
- ttl: 60000
2054
- },
2055
- commonResponseAnalyzer: async ({ data }) => {
2056
- // Validate data structure
2057
- return data && typeof data === 'object' && !data.error;
2058
- },
2059
- commonHandleErrors: ({ errorLog }) => {
2060
- console.error(`Error from ${source.id}:`, errorLog);
2061
- }
2062
- });
2063
-
2064
- // Check if all requests succeeded
2065
- const allSuccessful = results.every(r => r.success);
2066
-
2067
- if (allSuccessful) {
2068
- console.log(`Successfully fetched data from ${source.id}`);
2069
- return {
2070
- source: source.id,
2071
- data: {
2072
- users: results.find(r => r.requestId === 'users')?.data,
2073
- products: results.find(r => r.requestId === 'products')?.data,
2074
- orders: results.find(r => r.requestId === 'orders')?.data
2075
- }
2076
- };
2077
- } else {
2078
- console.warn(`Partial failure from ${source.id}, trying next source...`);
2079
- }
2080
- } catch (error) {
2081
- console.error(`Failed to fetch from ${source.id}:`, error);
2082
- // Continue to next source
2083
- }
2084
- }
2085
-
2086
- throw new Error('All data sources failed');
2087
- }
2088
-
2089
- // Usage
2090
- const dataSources: DataSource[] = [
2091
- {
2092
- id: 'primary-db',
2093
- priority: 1,
2094
- endpoint: '/api/v1',
2095
- hostname: 'primary.example.com'
2096
- },
2097
- {
2098
- id: 'replica-db',
2099
- priority: 2,
2100
- endpoint: '/api/v1',
2101
- hostname: 'replica.example.com'
2102
- },
2103
- {
2104
- id: 'backup-cache',
2105
- priority: 3,
2106
- endpoint: '/cached',
2107
- hostname: 'cache.example.com'
2108
- }
2109
- ];
2110
-
2111
- const result = await fetchDataWithFallback(dataSources);
2112
- console.log('Data fetched from:', result.source);
2113
- console.log('Users:', result.data.users?.length);
2114
- console.log('Products:', result.data.products?.length);
2115
- console.log('Orders:', result.data.orders?.length);
2116
- ```
376
+ Full TypeScript support with 40+ exported types for complete type safety across workflows, requests, configurations, and hooks.
2117
377
 
2118
378
  ## License
2119
379
 
2120
380
  MIT © Manish Varma
2121
-
2122
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2123
-
2124
- ---
2125
-
2126
- **Made with ❤️ for developers integrating with unreliable APIs**