@emmvish/stable-request 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  It ensures that **every request attempt**, whether it succeeds or fails, can be:
6
6
 
7
+ - Sent reliably
7
8
  - Observed
8
9
  - Analyzed
9
10
  - Retried intelligently
@@ -17,178 +18,12 @@ All without crashing your application or hiding context behind opaque errors.
17
18
  > If you’ve ever logged `error.message` and thought
18
19
  > **“This tells me absolutely nothing”** — this library is for you.
19
20
 
20
- In addition, it enables **content-aware retries**, **hierarchical configuration**, **batch orchestration**, and **multi-phase workflows** with deep observability — all built on top of standard HTTP calls.
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.
21
22
 
22
23
  All in all, it provides you with the **entire ecosystem** to build **API-integrations based workflows** with **complete flexibility**.
23
24
 
24
25
  ---
25
26
 
26
- ## 📚 Table of Contents
27
- <!-- TOC START -->
28
- - [Why stable-request exists](#why-stable-request-exists)
29
- - [What stable-request gives you](#what-stable-request-gives-you)
30
- - [Core capabilities](#core-capabilities)
31
- - [Scaling beyond single requests](#scaling-beyond-single-requests)
32
- - [Full workflow orchestration](#full-workflow-orchestration)
33
- - [How stable-request is different](#how-stable-request-is-different)
34
- - [Choose your entry point](#choose-your-entry-point)
35
- - [Installation](#installation)
36
- - [Quick Start](#quick-start)
37
- - [Basic Request (No Retries)](#1-basic-request-no-retries)
38
- - [Add Simple Retries](#2-add-simple-retries)
39
- - [Validate Response Content](#3-validate-response-content-content-aware-retries)
40
- - [Monitor Errors](#4-monitor-errors-observability)
41
- - [Monitor Successful Attempts](#5-monitor-successful-attempts)
42
- - [Handle Final Errors Gracefully](#6-handle-final-errors-gracefully)
43
- - [Pass Custom Parameters to Hooks](#7-pass-custom-parameters-to-hooks)
44
- - [Pre-Execution Hook](#8-pre-execution-hook-dynamic-configuration)
45
- - [Intermediate Concepts](#intermediate-concepts)
46
- - [Making POST/PUT/PATCH/DELETE Requests](#making-postputpatchdelete-requests)
47
- - [Query Parameters](#query-parameters)
48
- - [Custom Timeout and Port](#custom-timeout-and-port)
49
- - [Request Cancellation](#request-cancellation)
50
- - [Trial Mode](#trial-mode-testing-your-retry-logic)
51
- - [Batch Processing - Multiple Requests](#batch-processing---multiple-requests)
52
- - [Basic Batch Request](#basic-batch-request)
53
- - [Sequential Execution (With Dependencies)](#sequential-execution-with-dependencies)
54
- - [Shared Configuration (Common Options)](#shared-configuration-common-options)
55
- - [Advanced: Request Grouping](#advanced-request-grouping)
56
- - [Service Tiers](#example-service-tiers)
57
- - [Multi-Region Configuration](#example-multi-region-configuration)
58
- - [Shared Buffer Across Requests](#example-shared-buffer-is-common-across-the-entire-batch-of-requests)
59
- - [Multi-Phase Workflows](#multi-phase-workflows)
60
- - [Basic Workflow](#basic-workflow)
61
- - [Phase Configuration](#phase-configuration)
62
- - [Workflow with Request Groups](#workflow-with-request-groups)
63
- - [Phase Observability Hooks](#phase-observability-hooks)
64
- - [Workflow Buffer](#workflow-buffer)
65
- - [Concurrent Execution of Phases](#concurrent-execution-of-phases)
66
- - [Mixed Execution of Phases](#mixed-execution-of-phases)
67
- - [Real-World Examples](#real-world-examples)
68
- - [Polling for Job Completion](#1-polling-for-job-completion)
69
- - [Database Replication Lag](#2-database-replication-lag)
70
- - [Idempotent Payment Processing](#3-idempotent-payment-processing)
71
- - [Batch User Creation](#4-batch-user-creation-with-error-handling)
72
- - [Health Check Monitoring System](#5-health-check-monitoring-system)
73
- - [Data Pipeline (ETL Workflow)](#6-data-pipeline-etl-workflow)
74
- - [Complete API Reference](#complete-api-reference)
75
- - [`stableRequest`](#stablerequestoptions)
76
- - [`stableApiGateway`](#stableapigatewayrequests-options)
77
- - [`stableWorkflow`](#stableworkflowphases-options)
78
- - [Hooks Reference](#hooks-reference)
79
- - [`preExecutionHook`](#preexecutionhook)
80
- - [`responseAnalyzer`](#responseanalyzer)
81
- - [`handleErrors`](#handleerrors)
82
- - [`handleSuccessfulAttemptData`](#handlesuccessfulattemptdata)
83
- - [`finalErrorAnalyzer`](#finalerroranalyzer)
84
- - [`handlePhaseCompletion`](#handlephasecompletion)
85
- - [`handlePhaseError`](#handlephaseerror)
86
- - [Configuration Hierarchy](#configuration-hierarchy)
87
- - [TypeScript Support](#typescript-support)
88
- - [License](#license)
89
- <!-- TOC END -->
90
-
91
- ---
92
-
93
- ## Why stable-request exists
94
-
95
- Modern systems fail in subtle and dangerous ways:
96
-
97
- - APIs return `200` but the resource isn’t ready
98
- - Databases are eventually consistent
99
- - Downstream services partially fail
100
- - Some requests are critical, others are optional
101
- - Blind retries amplify failures
102
- - Workflows fail midway and leave systems inconsistent
103
-
104
- Most HTTP clients answer only one question:
105
-
106
- > “Did the request fail at the network or HTTP layer?”
107
-
108
- **stable-request answers a different one:**
109
-
110
- > “Is the system state actually correct yet?”
111
-
112
- ---
113
-
114
- ## What stable-request gives you
115
-
116
- ### Core capabilities
117
-
118
- ✅ **Content-aware retries**
119
-
120
- Retry based on response validation, not just status codes
121
-
122
- 🔄 **Deterministic execution semantics**
123
-
124
- Fixed, linear, or exponential retry strategies with hard limits
125
-
126
- 🧠 **Graceful failure handling**
127
-
128
- Suppress non-critical failures without crashing workflows
129
-
130
- 🧪 **Trial mode / chaos testing**
131
-
132
- Simulate failures without depending on real outages
133
-
134
- 📊 **First-class observability hooks**
135
-
136
- Inspect every failed and successful attempt
137
-
138
- ---
139
-
140
- ### Scaling beyond single requests
141
-
142
- 🚀 **Batch execution with shared state (`stableApiGateway`)**
143
-
144
- Run many requests concurrently or sequentially with shared configuration and shared state
145
-
146
- 🎯 **Request groups**
147
-
148
- Apply different reliability rules to critical, standard, and optional services
149
-
150
- 🧱 **Hierarchical configuration**
151
-
152
- Workflow → Phase → Group → Request (predictable overrides)
153
-
154
- ---
155
-
156
- ### Full workflow orchestration
157
-
158
- 🧩 **Multi-phase workflows with shared state (`stableWorkflow`)**
159
-
160
- Model real-world business flows as deterministic, observable execution graphs.
161
-
162
- 🔀 **Mix concurrent and sequential execution**
163
-
164
- Parallelize where safe, serialize where correctness matters.
165
-
166
- 🛑 **Stop early or degrade gracefully**
167
-
168
- Stop execution early or continue based on business criticality.
169
-
170
- 📈 **Phase-level metrics and hooks**
171
-
172
- Track execution time, success rates, and failure boundaries per phase.
173
-
174
- 🧭 **Deterministic, observable execution paths**
175
-
176
- Every decision is explicit, traceable, and reproducible.
177
-
178
- ---
179
-
180
- ## How stable-request is different
181
-
182
- | Traditional HTTP Clients | stable-request |
183
- |--------------------------|---------------|
184
- | Status-code based retries | Content-aware retries |
185
- | Per-request thinking | System-level thinking |
186
- | Fire-and-forget | Deterministic workflows |
187
- | Best-effort retries | Business-aware execution |
188
- | Little observability | Deep, structured hooks |
189
-
190
- ---
191
-
192
27
  ## Choose your entry point
193
28
 
194
29
  | Need | Use |
@@ -197,7 +32,31 @@ Most HTTP clients answer only one question:
197
32
  | Batch or fan-out requests | `stableApiGateway` |
198
33
  | Multi-step orchestration | `stableWorkflow` |
199
34
 
200
- Start small and scale **without changing mental models**.
35
+
36
+ Start small and scale.
37
+
38
+ ---
39
+
40
+ ## 📚 Table of Contents
41
+ <!-- TOC START -->
42
+ - [Installation](#installation)
43
+ - [Core Features](#core-features)
44
+ - [Quick Start](#quick-start)
45
+ - [Advanced Features](#advanced-features)
46
+ - [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)
58
+ - [License](#license)
59
+ <!-- TOC END -->
201
60
 
202
61
  ---
203
62
 
@@ -207,1114 +66,1010 @@ Start small and scale **without changing mental models**.
207
66
  npm install @emmvish/stable-request
208
67
  ```
209
68
 
210
- ## Quick Start
211
-
212
- ### 1. Basic Request (No Retries)
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
213
84
 
214
- ```typescript
215
- import { stableRequest, REQUEST_METHODS } from '@emmvish/stable-request';
216
-
217
- interface PatchRequestBodyParams {
218
- id: number;
219
- updates: {
220
- name?: string;
221
- age?: number;
222
- }
223
- }
224
-
225
- interface ResponseParams {
226
- id: number;
227
- name: string;
228
- age: number;
229
- }
230
-
231
- const getStableResponse = async () => {
232
-
233
- const token = 'my-auth-token';
234
-
235
- const data = await stableRequest<PatchRequestBodyParams, ResponseParams>({
236
- reqData: {
237
- method: REQUEST_METHODS.PATCH,
238
- hostname: 'api.example.com',
239
- path: '/users',
240
- headers: { Authorization: `Bearer ${token}` },
241
- body: { id: 123, updates: { age: 27 } }
242
- },
243
- resReq: true // Return the response data
244
- });
245
-
246
- console.log(data); // { id: 123, name: 'MV', age: 27 }
247
- }
248
-
249
- getStableResponse();
250
- ```
85
+ ## Quick Start
251
86
 
252
- ### 2. Add Simple Retries
87
+ ### Basic Request with Retry
253
88
 
254
89
  ```typescript
255
90
  import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
256
91
 
257
- const getStableResponse = async () => {
258
- const data = await stableRequest({
259
- reqData: {
260
- hostname: 'api.example.com',
261
- path: '/users/123'
262
- },
263
- resReq: true,
264
- attempts: 3, // Retry up to 3 times
265
- wait: 1000, // Wait 1 second between retries
266
- maxAllowedWait: 8000, // Maximum permissible wait time between retries
267
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL // 1s, 2s, 4s, 8s...
268
- });
269
-
270
- console.log(data);
271
- }
272
-
273
- getStableResponse();
274
- ```
275
-
276
- **Retry Strategies:**
277
- - `RETRY_STRATEGIES.FIXED` - Same delay every time (1s, 1s, 1s...)
278
- - `RETRY_STRATEGIES.LINEAR` - Increasing delay (1s, 2s, 3s...)
279
- - `RETRY_STRATEGIES.EXPONENTIAL` - Exponential backoff (1s, 2s, 4s, 8s...)
280
-
281
- ### 3. Validate Response Content (Content-Aware Retries)
282
-
283
- Sometimes an API returns HTTP 200 but the data isn't ready yet. Use `responseAnalyzer`:
284
-
285
- ```typescript
286
92
  const data = await stableRequest({
287
93
  reqData: {
288
94
  hostname: 'api.example.com',
289
- path: '/jobs/456/status'
95
+ path: '/users/123',
96
+ method: 'GET'
290
97
  },
291
98
  resReq: true,
292
- attempts: 10,
293
- wait: 2000,
294
-
295
- // This hook validates the response content
296
- responseAnalyzer: async ({ reqData, data, trialMode, params, commonBuffer }) => {
297
- // Return true if response is valid, false to retry
298
- if (data.status === 'completed') {
299
- return true; // Success! Don't retry
300
- }
301
-
302
- console.log(`Job still processing... (${data.percentComplete}%)`);
303
- return false; // Retry this request
304
- }
99
+ attempts: 3,
100
+ wait: 1000,
101
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
305
102
  });
306
103
 
307
- console.log('Job completed:', data);
104
+ console.log(data);
308
105
  ```
309
106
 
310
- **Hook Signature:**
107
+ ### Batch Requests via API Gateway
108
+
311
109
  ```typescript
312
- responseAnalyzer?: (options: {
313
- reqData: AxiosRequestConfig; // Request configuration
314
- data: ResponseDataType; // Response data from API
315
- trialMode?: TRIAL_MODE_OPTIONS; // Trial mode settings (if enabled)
316
- params?: any; // Custom parameters (via hookParams)
317
- commonBuffer: Record<string, any> // For communication between request hooks
318
- }) => boolean | Promise<boolean>;
319
- ```
110
+ import { stableApiGateway } from '@emmvish/stable-request';
320
111
 
321
- ### 4. Monitor Errors (Observability)
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
+ ];
322
117
 
323
- Track every failed attempt with `handleErrors`:
118
+ const results = await stableApiGateway(requests, {
119
+ commonRequestData: { hostname: 'api.example.com' },
120
+ concurrentExecution: true,
121
+ maxConcurrentRequests: 10
122
+ });
324
123
 
325
- ```typescript
326
- const data = await stableRequest({
327
- reqData: {
328
- hostname: 'api.example.com',
329
- path: '/data'
330
- },
331
- resReq: true,
332
- attempts: 5,
333
- logAllErrors: true, // Enable error logging
334
-
335
- // This hook is called on every failed attempt
336
- handleErrors: async ({ reqData, errorLog, maxSerializableChars, commonBuffer }) => {
337
- // Log to your monitoring service
338
- await monitoring.logError({
339
- url: reqData.url,
340
- attempt: errorLog.attempt, // e.g., "3/5"
341
- error: errorLog.error, // Error message
342
- isRetryable: errorLog.isRetryable, // Can we retry?
343
- type: errorLog.type, // 'HTTP_ERROR' or 'INVALID_CONTENT'
344
- statusCode: errorLog.statusCode, // HTTP status code
345
- timestamp: errorLog.timestamp, // ISO timestamp
346
- executionTime: errorLog.executionTime // ms
347
- });
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);
348
129
  }
349
130
  });
350
131
  ```
351
132
 
352
- **Hook Signature:**
353
- ```typescript
354
- handleErrors?: (options: {
355
- reqData: AxiosRequestConfig; // Request configuration
356
- errorLog: ERROR_LOG; // Detailed error information
357
- maxSerializableChars?: number; // Max chars for stringification
358
- commonBuffer: Record<string, any> // For communication between request hooks
359
- }) => any | Promise<any>;
360
- ```
133
+ ### Multi-Phase Workflow
361
134
 
362
- **ERROR_LOG Structure:**
363
135
  ```typescript
364
- interface ERROR_LOG {
365
- timestamp: string; // ISO timestamp
366
- executionTime: number; // Request duration in ms
367
- statusCode: number; // HTTP status code (0 if network error)
368
- attempt: string; // e.g., "3/5"
369
- error: string; // Error message
370
- type: 'HTTP_ERROR' | 'INVALID_CONTENT';
371
- isRetryable: boolean; // Can this error be retried?
372
- }
373
- ```
136
+ import { stableWorkflow, STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
137
+
138
+ const phases: STABLE_WORKFLOW_PHASE[] = [
139
+ {
140
+ id: 'authentication',
141
+ requests: [
142
+ { id: 'login', requestOptions: { reqData: { path: '/auth/login' }, resReq: true } }
143
+ ]
144
+ },
145
+ {
146
+ id: 'data-fetching',
147
+ concurrentExecution: true,
148
+ requests: [
149
+ { id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
150
+ { id: 'posts', requestOptions: { reqData: { path: '/posts' }, resReq: true } }
151
+ ]
152
+ }
153
+ ];
154
+
155
+ const result = await stableWorkflow(phases, {
156
+ workflowId: 'data-pipeline',
157
+ commonRequestData: { hostname: 'api.example.com' },
158
+ stopOnFirstPhaseError: true,
159
+ logPhaseResults: true
160
+ });
374
161
 
375
- ### 5. Monitor Successful Attempts
162
+ console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
163
+ ```
376
164
 
377
- Track successful requests with `handleSuccessfulAttemptData`:
165
+ ### Non-Linear Workflow with Dynamic Routing
378
166
 
379
167
  ```typescript
380
- const data = await stableRequest({
381
- reqData: {
382
- hostname: 'api.example.com',
383
- path: '/data'
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
384
190
  },
385
- resReq: true,
386
- attempts: 3,
387
- logAllSuccessfulAttempts: true, // Enable success logging
388
-
389
- // This hook is called on every successful attempt
390
- handleSuccessfulAttemptData: async ({ reqData, successfulAttemptData, maxSerializableChars }) => {
391
- // Track metrics
392
- await analytics.track('api_success', {
393
- url: reqData.url,
394
- attempt: successfulAttemptData.attempt, // e.g., "2/3"
395
- duration: successfulAttemptData.executionTime, // ms
396
- statusCode: successfulAttemptData.statusCode, // 200, 201, etc.
397
- timestamp: successfulAttemptData.timestamp
398
- });
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
+ ]
399
208
  }
209
+ ];
210
+
211
+ const result = await stableWorkflow(phases, {
212
+ workflowId: 'dynamic-workflow',
213
+ commonRequestData: { hostname: 'api.example.com' },
214
+ enableNonLinearExecution: true,
215
+ maxWorkflowIterations: 50,
216
+ sharedBuffer: {}
400
217
  });
401
- ```
402
218
 
403
- **Hook Signature:**
404
- ```typescript
405
- handleSuccessfulAttemptData?: (options: {
406
- reqData: AxiosRequestConfig; // Request configuration
407
- successfulAttemptData: SUCCESSFUL_ATTEMPT_DATA; // Success details
408
- maxSerializableChars?: number; // Max chars for stringification
409
- commonBuffer: Record<string, any> // For communication between request hooks
410
- }) => any | Promise<any>;
219
+ console.log('Execution history:', result.executionHistory);
220
+ console.log('Terminated early:', result.terminatedEarly);
411
221
  ```
412
222
 
413
- **SUCCESSFUL_ATTEMPT_DATA Structure:**
414
- ```typescript
415
- interface SUCCESSFUL_ATTEMPT_DATA<ResponseDataType> {
416
- attempt: string; // e.g., "2/3"
417
- timestamp: string; // ISO timestamp
418
- executionTime: number; // Request duration in ms
419
- data: ResponseDataType; // Response data
420
- statusCode: number; // HTTP status code
421
- }
422
- ```
223
+ ## Advanced Features
224
+
225
+ ### Non-Linear Workflows
226
+
227
+ Non-linear workflows enable dynamic phase execution based on runtime decisions, allowing you to build complex orchestrations with conditional branching, polling loops, error recovery, and adaptive routing.
423
228
 
424
- ### 6. Handle Final Errors Gracefully
229
+ #### Phase Decision Actions
425
230
 
426
- Decide what to do when all retries fail using `finalErrorAnalyzer`:
231
+ Each phase can make decisions about workflow execution:
232
+
233
+ - **`continue`**: Proceed to the next sequential phase
234
+ - **`jump`**: Jump to a specific phase by ID
235
+ - **`replay`**: Re-execute the current phase
236
+ - **`skip`**: Skip to a target phase or skip the next phase
237
+ - **`terminate`**: Stop the workflow immediately
238
+
239
+ #### Basic Non-Linear Workflow
427
240
 
428
241
  ```typescript
429
- const data = await stableRequest({
430
- reqData: {
431
- hostname: 'api.example.com',
432
- path: '/optional-feature'
433
- },
434
- resReq: true,
435
- attempts: 3,
436
-
437
- // This hook is called when all retries are exhausted
438
- finalErrorAnalyzer: async ({ reqData, error, trialMode, params }) => {
439
- // Check if this is a non-critical error
440
- if (error.message.includes('404')) {
441
- console.log('Feature not available, continuing without it');
442
- return true; // Suppress error, return false instead of throwing
242
+ import { stableWorkflow, STABLE_WORKFLOW_PHASE, PhaseExecutionDecision, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
243
+
244
+ const phases: STABLE_WORKFLOW_PHASE[] = [
245
+ {
246
+ id: 'validate-input',
247
+ requests: [
248
+ { id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
249
+ ],
250
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
251
+ const isValid = phaseResult.responses[0]?.data?.valid;
252
+
253
+ if (isValid) {
254
+ sharedBuffer.validationPassed = true;
255
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to next phase
256
+ } else {
257
+ return {
258
+ action: PHASE_DECISION_ACTIONS.TERMINATE,
259
+ metadata: { reason: 'Validation failed' }
260
+ };
261
+ }
443
262
  }
444
-
445
- // For critical errors
446
- await alerting.sendAlert('Critical API failure', error);
447
- return false; // Throw the error
263
+ },
264
+ {
265
+ id: 'process-data',
266
+ requests: [
267
+ { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
268
+ ]
448
269
  }
270
+ ];
271
+
272
+ const result = await stableWorkflow(phases, {
273
+ workflowId: 'validation-workflow',
274
+ commonRequestData: { hostname: 'api.example.com' },
275
+ enableNonLinearExecution: true,
276
+ sharedBuffer: {}
449
277
  });
450
278
 
451
- if (data === false) {
452
- console.log('Optional feature unavailable, using default');
279
+ if (result.terminatedEarly) {
280
+ console.log('Workflow terminated:', result.terminationReason);
453
281
  }
454
282
  ```
455
283
 
456
- **Hook Signature:**
457
- ```typescript
458
- finalErrorAnalyzer?: (options: {
459
- reqData: AxiosRequestConfig; // Request configuration
460
- error: any; // The final error object
461
- trialMode?: TRIAL_MODE_OPTIONS; // Trial mode settings (if enabled)
462
- params?: any; // Custom parameters (via hookParams)
463
- commonBuffer: Record<string, any> // For communication between request hooks
464
- }) => boolean | Promise<boolean>;
465
- ```
466
-
467
- **Return value:**
468
- - `true` - Suppress the error, function returns `false` instead of throwing
469
- - `false` - Throw the error
470
-
471
- ### 7. Pass Custom Parameters to Hooks
472
-
473
- You can pass custom data to `responseAnalyzer` and `finalErrorAnalyzer`:
284
+ #### Conditional Branching
474
285
 
475
286
  ```typescript
476
- const expectedVersion = 42;
477
-
478
- const data = await stableRequest({
479
- reqData: {
480
- hostname: 'api.example.com',
481
- path: '/data'
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
+ }
482
305
  },
483
- resReq: true,
484
- attempts: 5,
485
-
486
- // Pass custom parameters
487
- hookParams: {
488
- responseAnalyzerParams: { expectedVersion, minItems: 10 },
489
- finalErrorAnalyzerParams: { alertTeam: true }
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' })
490
312
  },
491
-
492
- responseAnalyzer: async ({ data, params }) => {
493
- // Access custom parameters
494
- return data.version >= params.expectedVersion &&
495
- data.items.length >= params.minItems;
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' })
496
319
  },
497
-
498
- finalErrorAnalyzer: async ({ error, params }) => {
499
- if (params.alertTeam) {
500
- await pagerDuty.alert('API failure', error);
501
- }
502
- return false;
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
+ ];
333
+
334
+ const result = await stableWorkflow(phases, {
335
+ enableNonLinearExecution: true,
336
+ sharedBuffer: {},
337
+ handlePhaseDecision: (decision, phaseResult) => {
338
+ console.log(`Phase ${phaseResult.phaseId} decided: ${decision.action}`);
503
339
  }
504
340
  });
505
341
  ```
506
342
 
507
- ### 8. Pre-Execution Hook (Dynamic Configuration)
508
-
509
- Use `preExecution` to modify request configuration dynamically before execution:
343
+ #### Polling with Replay
510
344
 
511
345
  ```typescript
512
- const outputBuffer: Record<string, any> = {};
513
-
514
- const data = await stableRequest({
515
- reqData: {
516
- hostname: 'api.example.com',
517
- path: '/protected-resource'
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
+ }
518
373
  },
519
- resReq: true,
520
- attempts: 3,
521
-
522
- preExecution: {
523
- // Hook executed before any request attempts
524
- preExecutionHook: async ({ inputParams, commonBuffer }) => {
525
- const token = await authService.getToken(inputParams.userId);
526
- commonBuffer.token = token;
527
- commonBuffer.fetchedAt = new Date().toISOString();
528
- // Return configuration overrides
529
- return {
530
- reqData: {
531
- hostname: 'api.example.com',
532
- path: '/protected-resource',
533
- headers: {
534
- 'Authorization': `Bearer ${token}`
535
- }
536
- },
537
- attempts: 5
538
- };
539
- },
540
- preExecutionHookParams: {
541
- userId: 'user-123',
542
- environment: 'production'
543
- },
544
- applyPreExecutionConfigOverride: true,
545
- continueOnPreExecutionHookFailure: false
374
+ {
375
+ id: 'process-results',
376
+ requests: [
377
+ { id: 'process', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
378
+ ]
546
379
  },
547
- commonBuffer: outputBuffer
548
- });
549
-
550
- console.log('Token used:', outputBuffer.token);
551
- console.log('Fetched at:', outputBuffer.fetchedAt);
552
- ```
380
+ {
381
+ id: 'error-recovery',
382
+ requests: [
383
+ { id: 'recover', requestOptions: { reqData: { path: '/job/retry' }, resReq: true } }
384
+ ]
385
+ }
386
+ ];
553
387
 
554
- **Pre-Execution Options:**
388
+ const result = await stableWorkflow(phases, {
389
+ workflowId: 'polling-workflow',
390
+ commonRequestData: { hostname: 'api.example.com' },
391
+ enableNonLinearExecution: true,
392
+ maxWorkflowIterations: 100
393
+ });
555
394
 
556
- ```typescript
557
- interface PreExecutionOptions {
558
- preExecutionHook: (options: {
559
- inputParams: any; // Custom parameters you provide
560
- }) => any | Promise<any>; // Returns config overrides
561
- preExecutionHookParams?: any; // Custom input parameters
562
- applyPreExecutionConfigOverride?: boolean; // Apply returned overrides (default: false)
563
- continueOnPreExecutionHookFailure?: boolean; // Continue if hook fails (default: false)
564
- }
395
+ console.log('Total iterations:', result.executionHistory.length);
396
+ console.log('Phases executed:', result.completedPhases);
565
397
  ```
566
398
 
567
- ## Intermediate Concepts
568
-
569
- ### Making POST/PUT/PATCH/DELETE Requests
399
+ #### Retry Logic with Replay
570
400
 
571
401
  ```typescript
572
- import { stableRequest, REQUEST_METHODS } from '@emmvish/stable-request';
573
-
574
- const newUser = await stableRequest({
575
- reqData: {
576
- hostname: 'api.example.com',
577
- path: '/users',
578
- method: REQUEST_METHODS.POST,
579
- headers: {
580
- 'Content-Type': 'application/json',
581
- 'Authorization': 'Bearer your-token'
582
- },
583
- body: {
584
- name: 'John Doe',
585
- email: 'john@example.com'
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
+ }
586
438
  }
587
439
  },
588
- resReq: true,
589
- attempts: 3
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
+ ];
453
+
454
+ const result = await stableWorkflow(phases, {
455
+ enableNonLinearExecution: true,
456
+ sharedBuffer: { retryAttempts: 0 },
457
+ logPhaseResults: true
590
458
  });
591
459
  ```
592
460
 
593
- ### Query Parameters
461
+ #### Skip Phases
594
462
 
595
463
  ```typescript
596
- const users = await stableRequest({
597
- reqData: {
598
- hostname: 'api.example.com',
599
- path: '/users',
600
- query: {
601
- page: 1,
602
- limit: 10,
603
- sort: 'createdAt'
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 };
604
480
  }
605
481
  },
606
- resReq: true
482
+ {
483
+ id: 'expensive-computation',
484
+ requests: [
485
+ { id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
486
+ ]
487
+ },
488
+ {
489
+ id: 'save-to-cache',
490
+ requests: [
491
+ { id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
492
+ ]
493
+ },
494
+ {
495
+ id: 'finalize',
496
+ requests: [
497
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
498
+ ]
499
+ }
500
+ ];
501
+
502
+ const result = await stableWorkflow(phases, {
503
+ enableNonLinearExecution: true,
504
+ sharedBuffer: {}
607
505
  });
608
- // Requests: https://api.example.com:443/users?page=1&limit=10&sort=createdAt
609
506
  ```
610
507
 
611
- ### Custom Timeout and Port
508
+ #### Execution History and Tracking
612
509
 
613
510
  ```typescript
614
- const data = await stableRequest({
615
- reqData: {
616
- hostname: 'api.example.com',
617
- path: '/slow-endpoint',
618
- port: 8080,
619
- protocol: 'http',
620
- timeout: 30000 // 30 seconds
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
+ });
621
520
  },
622
- resReq: true,
623
- attempts: 2
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
+ }
624
529
  });
625
- ```
626
530
 
627
- ### Request Cancellation
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);
628
535
 
629
- ```typescript
630
- const controller = new AbortController();
536
+ result.executionHistory.forEach(record => {
537
+ console.log(`- ${record.phaseId} (attempt ${record.executionNumber}): ${record.success ? '✓' : '✗'}`);
538
+ });
539
+ ```
631
540
 
632
- // Cancel after 5 seconds
633
- setTimeout(() => controller.abort(), 5000);
541
+ #### Loop Protection
634
542
 
635
- try {
636
- await stableRequest({
637
- reqData: {
638
- hostname: 'api.example.com',
639
- path: '/data',
640
- signal: controller.signal
641
- },
642
- resReq: true
643
- });
644
- } catch (error) {
645
- if (error.message.includes('cancelled')) {
646
- console.log('Request was cancelled');
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
+ }
647
551
  }
552
+ });
553
+
554
+ if (result.terminatedEarly && result.terminationReason?.includes('iterations')) {
555
+ console.error('Workflow hit iteration limit - possible infinite loop');
648
556
  }
649
557
  ```
650
558
 
651
- ### Trial Mode (Testing Your Retry Logic)
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.
652
562
 
653
- Simulate failures without depending on actual API issues:
563
+ **Basic Mixed Execution:**
654
564
 
655
565
  ```typescript
656
- await stableRequest({
657
- reqData: {
658
- hostname: 'api.example.com',
659
- path: '/data'
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 })
660
575
  },
661
- resReq: true,
662
- attempts: 5,
663
- logAllErrors: true,
664
-
665
- trialMode: {
666
- enabled: true,
667
- reqFailureProbability: 0.3, // 30% chance each request fails
668
- retryFailureProbability: 0.2 // 20% chance error is non-retryable
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
+ ]
669
613
  }
614
+ ];
615
+
616
+ const result = await stableWorkflow(phases, {
617
+ workflowId: 'mixed-execution',
618
+ commonRequestData: { hostname: 'api.example.com' },
619
+ enableNonLinearExecution: true
670
620
  });
671
621
  ```
672
622
 
673
- **Use cases:**
674
- - Test your error handling logic
675
- - Verify monitoring alerts work
676
- - Chaos engineering experiments
677
- - Integration testing
678
-
679
- ## Batch Processing - Multiple Requests
680
-
681
- ### Basic Batch Request
623
+ **Multiple Parallel Groups:**
682
624
 
683
625
  ```typescript
684
- import { stableApiGateway } from '@emmvish/stable-request';
685
-
686
- const requests = [
626
+ const phases: STABLE_WORKFLOW_PHASE[] = [
687
627
  {
688
- id: 'user-1',
689
- requestOptions: {
690
- reqData: { path: '/users/1' },
691
- resReq: true
692
- }
628
+ id: 'authenticate',
629
+ requests: [
630
+ { id: 'auth', requestOptions: { reqData: { path: '/auth' }, resReq: true } }
631
+ ]
693
632
  },
633
+ // First parallel group: Data validation
694
634
  {
695
- id: 'user-2',
696
- requestOptions: {
697
- reqData: { path: '/users/2' },
698
- resReq: true
699
- }
635
+ id: 'validate-user',
636
+ markConcurrentPhase: true,
637
+ requests: [
638
+ { id: 'val-user', requestOptions: { reqData: { path: '/validate/user' }, resReq: true } }
639
+ ]
700
640
  },
701
641
  {
702
- id: 'user-3',
703
- requestOptions: {
704
- reqData: { path: '/users/3' },
705
- resReq: true
642
+ id: 'validate-payment',
643
+ markConcurrentPhase: true,
644
+ requests: [
645
+ { id: 'val-pay', requestOptions: { reqData: { path: '/validate/payment' }, resReq: true } }
646
+ ]
647
+ },
648
+ {
649
+ id: 'validate-shipping',
650
+ markConcurrentPhase: true,
651
+ requests: [
652
+ { id: 'val-ship', requestOptions: { reqData: { path: '/validate/shipping' }, resReq: true } }
653
+ ],
654
+ phaseDecisionHook: async ({ concurrentPhaseResults }) => {
655
+ const allValid = concurrentPhaseResults!.every(r => r.success && r.responses[0]?.data?.valid);
656
+ if (!allValid) {
657
+ return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Validation failed' } };
658
+ }
659
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
706
660
  }
661
+ },
662
+ // Serial processing phase
663
+ {
664
+ id: 'calculate-total',
665
+ requests: [
666
+ { id: 'calc', requestOptions: { reqData: { path: '/calculate' }, resReq: true } }
667
+ ]
668
+ },
669
+ // Second parallel group: External integrations
670
+ {
671
+ id: 'notify-warehouse',
672
+ markConcurrentPhase: true,
673
+ requests: [
674
+ { id: 'warehouse', requestOptions: { reqData: { path: '/notify/warehouse' }, resReq: true } }
675
+ ]
676
+ },
677
+ {
678
+ id: 'notify-shipping',
679
+ markConcurrentPhase: true,
680
+ requests: [
681
+ { id: 'shipping', requestOptions: { reqData: { path: '/notify/shipping' }, resReq: true } }
682
+ ]
683
+ },
684
+ {
685
+ id: 'update-inventory',
686
+ markConcurrentPhase: true,
687
+ requests: [
688
+ { id: 'inventory', requestOptions: { reqData: { path: '/update/inventory' }, resReq: true } }
689
+ ]
690
+ },
691
+ // Final serial phase
692
+ {
693
+ id: 'finalize',
694
+ requests: [
695
+ { id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
696
+ ]
707
697
  }
708
698
  ];
709
699
 
710
- const results = await stableApiGateway(requests, {
711
- // Common options applied to ALL requests
712
- commonRequestData: {
713
- hostname: 'api.example.com'
714
- },
715
- commonAttempts: 3,
716
- commonWait: 1000,
717
- concurrentExecution: true // Run all requests in parallel
700
+ const result = await stableWorkflow(phases, {
701
+ workflowId: 'multi-parallel-workflow',
702
+ commonRequestData: { hostname: 'api.example.com' },
703
+ enableNonLinearExecution: true
718
704
  });
719
705
 
720
- // Process results
721
- results.forEach(result => {
722
- if (result.success) {
723
- console.log(`${result.requestId} succeeded:`, result.data);
724
- } else {
725
- console.error(`${result.requestId} failed:`, result.error);
726
- }
727
- });
706
+ console.log('Execution order demonstrates mixed serial/parallel execution');
728
707
  ```
729
708
 
730
- **Response Format:**
709
+ **Decision Making with Concurrent Results:**
710
+
731
711
  ```typescript
732
- interface API_GATEWAY_RESPONSE<ResponseDataType> {
733
- requestId: string; // The ID you provided
734
- groupId?: string; // Group ID (if request was grouped)
735
- success: boolean; // Did the request succeed?
736
- data?: ResponseDataType; // Response data (if success)
737
- error?: string; // Error message (if failed)
738
- }
712
+ const phases: STABLE_WORKFLOW_PHASE[] = [
713
+ {
714
+ id: 'api-check-1',
715
+ markConcurrentPhase: true,
716
+ requests: [
717
+ { id: 'api1', requestOptions: { reqData: { path: '/health/api1' }, resReq: true } }
718
+ ]
719
+ },
720
+ {
721
+ id: 'api-check-2',
722
+ markConcurrentPhase: true,
723
+ requests: [
724
+ { id: 'api2', requestOptions: { reqData: { path: '/health/api2' }, resReq: true } }
725
+ ]
726
+ },
727
+ {
728
+ id: 'api-check-3',
729
+ markConcurrentPhase: true,
730
+ requests: [
731
+ { id: 'api3', requestOptions: { reqData: { path: '/health/api3' }, resReq: true } }
732
+ ],
733
+ phaseDecisionHook: async ({ concurrentPhaseResults, sharedBuffer }) => {
734
+ // Aggregate results from all parallel phases
735
+ const healthScores = concurrentPhaseResults!.map(result =>
736
+ result.responses[0]?.data?.score || 0
737
+ );
738
+
739
+ const averageScore = healthScores.reduce((a, b) => a + b, 0) / healthScores.length;
740
+ sharedBuffer!.healthScore = averageScore;
741
+
742
+ if (averageScore > 0.8) {
743
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'optimal-path' };
744
+ } else if (averageScore > 0.5) {
745
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to degraded-path
746
+ } else {
747
+ return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'fallback-path' };
748
+ }
749
+ }
750
+ },
751
+ {
752
+ id: 'degraded-path',
753
+ requests: [
754
+ { id: 'degraded', requestOptions: { reqData: { path: '/degraded' }, resReq: true } }
755
+ ]
756
+ },
757
+ {
758
+ id: 'optimal-path',
759
+ requests: [
760
+ { id: 'optimal', requestOptions: { reqData: { path: '/optimal' }, resReq: true } }
761
+ ]
762
+ },
763
+ {
764
+ id: 'fallback-path',
765
+ requests: [
766
+ { id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
767
+ ]
768
+ }
769
+ ];
770
+
771
+ const sharedBuffer = {};
772
+ const result = await stableWorkflow(phases, {
773
+ workflowId: 'adaptive-routing',
774
+ commonRequestData: { hostname: 'api.example.com' },
775
+ enableNonLinearExecution: true,
776
+ sharedBuffer
777
+ });
778
+
779
+ console.log('Average health score:', sharedBuffer.healthScore);
739
780
  ```
740
781
 
741
- ### Sequential Execution (With Dependencies)
782
+ **Error Handling in Parallel Groups:**
742
783
 
743
784
  ```typescript
744
- const steps = [
785
+ const phases: STABLE_WORKFLOW_PHASE[] = [
745
786
  {
746
- id: 'step-1-create',
747
- requestOptions: {
748
- reqData: {
749
- path: '/orders',
750
- method: REQUEST_METHODS.POST,
751
- body: { item: 'Widget' }
752
- },
753
- resReq: true
754
- }
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
+ ]
755
799
  },
756
800
  {
757
- id: 'step-2-process',
758
- requestOptions: {
759
- reqData: {
760
- path: '/orders/123/process',
761
- method: REQUEST_METHODS.POST
762
- },
763
- resReq: true
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 };
764
827
  }
765
828
  },
766
829
  {
767
- id: 'step-3-ship',
768
- requestOptions: {
769
- reqData: { path: '/orders/123/ship' },
770
- resReq: true
771
- }
830
+ id: 'process',
831
+ requests: [
832
+ { id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
833
+ ]
772
834
  }
773
835
  ];
774
836
 
775
- const results = await stableApiGateway(steps, {
776
- concurrentExecution: false, // Run one at a time
777
- stopOnFirstError: true, // Stop if any step fails
778
- commonRequestData: {
779
- hostname: 'api.example.com'
780
- },
781
- commonAttempts: 3
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
782
842
  });
783
-
784
- if (results.every(r => r.success)) {
785
- console.log('Workflow completed successfully');
786
- } else {
787
- const failedStep = results.findIndex(r => !r.success);
788
- console.error(`Workflow failed at step ${failedStep + 1}`);
789
- }
790
843
  ```
791
844
 
792
- ### Shared Configuration (Common Options)
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
793
851
 
794
- Instead of repeating configuration for each request:
852
+ #### Configuration Options
795
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:**
796
867
  ```typescript
797
- const results = await stableApiGateway(
798
- [
799
- { id: 'req-1', requestOptions: { reqData: { path: '/users/1' } } },
800
- { id: 'req-2', requestOptions: { reqData: { path: '/users/2' } } },
801
- { id: 'req-3', requestOptions: { reqData: { path: '/users/3' } } }
802
- ],
803
- {
804
- // Applied to ALL requests
805
- commonRequestData: {
806
- hostname: 'api.example.com',
807
- headers: { 'Authorization': `Bearer ${token}` }
808
- },
809
- commonResReq: true,
810
- commonAttempts: 5,
811
- commonWait: 2000,
812
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
813
- commonLogAllErrors: true,
814
-
815
- // Shared hooks
816
- commonHandleErrors: async ({ reqData, errorLog }) => {
817
- console.log(`Request to ${reqData.url} failed (${errorLog.attempt})`);
818
- },
819
-
820
- commonResponseAnalyzer: async ({ data }) => {
821
- return data?.success === true;
822
- }
823
- }
824
- );
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
+ }
825
877
  ```
826
878
 
827
- ## Advanced: Request Grouping
828
-
829
- Group related requests with different configurations. Configuration priority:
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
+ ```
830
888
 
831
- **Individual Request** > **Group Config** > **Global Common Config**
889
+ ### Retry Strategies
832
890
 
833
- ### Example: Service Tiers
891
+ Control the delay between retry attempts:
834
892
 
835
893
  ```typescript
836
- const results = await stableApiGateway(
837
- [
838
- // Critical services - need high reliability
839
- {
840
- id: 'auth-check',
841
- groupId: 'critical',
842
- requestOptions: {
843
- reqData: { path: '/auth/verify' },
844
- resReq: true
845
- }
846
- },
847
- {
848
- id: 'payment-process',
849
- groupId: 'critical',
850
- requestOptions: {
851
- reqData: { path: '/payments/charge' },
852
- resReq: true,
853
- // Individual override: even MORE attempts for payments
854
- attempts: 15
855
- }
856
- },
857
-
858
- // Analytics - failures are acceptable
859
- {
860
- id: 'track-event',
861
- groupId: 'analytics',
862
- requestOptions: {
863
- reqData: { path: '/analytics/track' },
864
- resReq: true
865
- }
866
- }
867
- ],
868
- {
869
- // Global defaults (lowest priority)
870
- commonRequestData: {
871
- hostname: 'api.example.com'
872
- },
873
- commonAttempts: 2,
874
- commonWait: 500,
875
-
876
- // Define groups with their own configs
877
- requestGroups: [
878
- {
879
- id: 'critical',
880
- commonConfig: {
881
- // Critical services: aggressive retries
882
- commonAttempts: 10,
883
- commonWait: 2000,
884
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
885
-
886
- commonHandleErrors: async ({ errorLog }) => {
887
- // Alert on critical failures
888
- await pagerDuty.alert('Critical service failure', errorLog);
889
- },
890
-
891
- commonResponseAnalyzer: async ({ data }) => {
892
- // Strict validation
893
- return data?.status === 'success' && !data?.errors;
894
- }
895
- }
896
- },
897
- {
898
- id: 'analytics',
899
- commonConfig: {
900
- // Analytics: minimal retries, don't throw on failure
901
- commonAttempts: 1,
902
-
903
- commonFinalErrorAnalyzer: async () => {
904
- return true; // Suppress errors
905
- }
906
- }
907
- }
908
- ]
909
- }
910
- );
894
+ import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
911
895
 
912
- // Analyze by group
913
- const criticalOk = results
914
- .filter(r => r.groupId === 'critical')
915
- .every(r => r.success);
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
+ });
916
903
 
917
- const analyticsCount = results
918
- .filter(r => r.groupId === 'analytics' && r.success)
919
- .length;
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
+ });
920
911
 
921
- console.log('Critical services:', criticalOk ? 'HEALTHY' : 'DEGRADED');
922
- console.log('Analytics events tracked:', analyticsCount);
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
+ });
923
920
  ```
924
921
 
925
- ### Example: Multi-Region Configuration
922
+ ### Circuit Breaker
926
923
 
927
- ```typescript
928
- const results = await stableApiGateway(
929
- [
930
- { id: 'us-data', groupId: 'us-east', requestOptions: { reqData: { path: '/data' }, resReq: true } },
931
- { id: 'eu-data', groupId: 'eu-west', requestOptions: { reqData: { path: '/data' }, resReq: true } },
932
- { id: 'ap-data', groupId: 'ap-southeast', requestOptions: { reqData: { path: '/data' }, resReq: true } }
933
- ],
934
- {
935
- commonAttempts: 3,
936
-
937
- requestGroups: [
938
- {
939
- id: 'us-east',
940
- commonConfig: {
941
- commonRequestData: {
942
- hostname: 'api-us.example.com',
943
- timeout: 5000, // Low latency expected
944
- headers: { 'X-Region': 'us-east-1' }
945
- },
946
- commonAttempts: 3
947
- }
948
- },
949
- {
950
- id: 'eu-west',
951
- commonConfig: {
952
- commonRequestData: {
953
- hostname: 'api-eu.example.com',
954
- timeout: 8000, // Medium latency
955
- headers: { 'X-Region': 'eu-west-1' }
956
- },
957
- commonAttempts: 5
958
- }
959
- },
960
- {
961
- id: 'ap-southeast',
962
- commonConfig: {
963
- commonRequestData: {
964
- hostname: 'api-ap.example.com',
965
- timeout: 12000, // Higher latency expected
966
- headers: { 'X-Region': 'ap-southeast-1' }
967
- },
968
- commonAttempts: 7,
969
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
970
- }
971
- }
972
- ]
973
- }
974
- );
975
- ```
976
-
977
- ### Example: Shared Buffer is common across the entire batch of requests
924
+ Prevent cascading failures by automatically blocking requests when error thresholds are exceeded:
978
925
 
979
926
  ```typescript
980
- const sharedBuffer: Record<string, any> = {};
981
-
982
- const requests = [
983
- {
984
- id: 'a',
985
- requestOptions: {
986
- reqData: { path: '/a' },
987
- resReq: true,
988
- preExecution: {
989
- preExecutionHook: ({ commonBuffer }: any) => {
990
- commonBuffer.fromA = true;
991
- return {};
992
- },
993
- preExecutionHookParams: {},
994
- applyPreExecutionConfigOverride: false,
995
- continueOnPreExecutionHookFailure: false
996
- }
997
- }
998
- },
999
- {
1000
- id: 'b',
1001
- requestOptions: {
1002
- reqData: { path: '/b' },
1003
- resReq: true,
1004
- preExecution: {
1005
- preExecutionHook: ({ commonBuffer }: any) => {
1006
- commonBuffer.fromB = true;
1007
- return {};
1008
- },
1009
- preExecutionHookParams: {},
1010
- applyPreExecutionConfigOverride: false,
1011
- continueOnPreExecutionHookFailure: false
1012
- }
1013
- }
1014
- }
1015
- ] satisfies API_GATEWAY_REQUEST[];
927
+ import { stableApiGateway, CircuitBreakerState } from '@emmvish/stable-request';
1016
928
 
1017
929
  const results = await stableApiGateway(requests, {
1018
- concurrentExecution: true,
1019
930
  commonRequestData: { hostname: 'api.example.com' },
1020
- sharedBuffer
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
+ }
1021
937
  });
1022
938
 
1023
- console.log(sharedBuffer); // { fromA: true, fromB: true }
1024
- ```
939
+ // Circuit breaker can be shared across workflows
940
+ const breaker = new CircuitBreaker({
941
+ failureThresholdPercentage: 50,
942
+ minimumRequests: 10,
943
+ recoveryTimeoutMs: 60000
944
+ });
1025
945
 
946
+ const result = await stableWorkflow(phases, {
947
+ circuitBreaker: breaker,
948
+ // ... other options
949
+ });
1026
950
 
1027
- ## Multi-Phase Workflows
951
+ // Check circuit breaker state
952
+ const state = breaker.getState();
953
+ console.log(`Circuit breaker state: ${state.state}`); // CLOSED, OPEN, or HALF_OPEN
954
+ ```
1028
955
 
1029
- For complex operations that require multiple stages of execution, use `stableWorkflow` to orchestrate phase-based workflows with full control over execution order and error handling.
956
+ ### Rate Limiting
1030
957
 
1031
- ### Basic Workflow
958
+ Control request throughput to prevent overwhelming APIs:
1032
959
 
1033
960
  ```typescript
1034
- import { stableWorkflow } from '@emmvish/stable-request';
961
+ import { stableApiGateway } from '@emmvish/stable-request';
1035
962
 
1036
- const workflow = await stableWorkflow(
1037
- [
1038
- {
1039
- id: 'validation',
1040
- concurrentExecution: true,
1041
- requests: [
1042
- {
1043
- id: 'check-inventory',
1044
- requestOptions: {
1045
- reqData: { path: '/inventory/check' },
1046
- resReq: true
1047
- }
1048
- },
1049
- {
1050
- id: 'validate-payment',
1051
- requestOptions: {
1052
- reqData: { path: '/payment/validate' },
1053
- resReq: true
1054
- }
1055
- }
1056
- ]
1057
- },
1058
- {
1059
- id: 'processing',
1060
- concurrentExecution: false,
1061
- stopOnFirstError: true,
1062
- requests: [
1063
- {
1064
- id: 'charge-payment',
1065
- requestOptions: {
1066
- reqData: { path: '/payment/charge', method: REQUEST_METHODS.POST },
1067
- resReq: true
1068
- }
1069
- },
1070
- {
1071
- id: 'reserve-inventory',
1072
- requestOptions: {
1073
- reqData: { path: '/inventory/reserve', method: REQUEST_METHODS.POST },
1074
- resReq: true
1075
- }
1076
- }
1077
- ]
1078
- }
1079
- ],
1080
- {
1081
- workflowId: 'order-processing-123',
1082
- stopOnFirstPhaseError: true,
1083
- logPhaseResults: true,
1084
- commonRequestData: {
1085
- hostname: 'api.example.com',
1086
- headers: { 'X-Transaction-Id': 'txn-123' }
1087
- },
1088
- commonAttempts: 3,
1089
- commonWait: 1000
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
1090
969
  }
1091
- );
1092
-
1093
- console.log('Workflow completed:', workflow.success);
1094
- console.log(`${workflow.successfulRequests}/${workflow.totalRequests} requests succeeded`);
1095
- console.log(`Completed in ${workflow.executionTime}ms`);
1096
- ```
970
+ });
1097
971
 
1098
- **Workflow Result:**
1099
- ```typescript
1100
- interface STABLE_WORKFLOW_RESULT {
1101
- workflowId: string; // Workflow identifier
1102
- success: boolean; // Did all phases succeed?
1103
- executionTime: number; // Total workflow duration (ms)
1104
- timestamp: string; // ISO timestamp
1105
- totalPhases: number; // Number of phases defined
1106
- completedPhases: number; // Number of phases executed
1107
- totalRequests: number; // Total requests across all phases
1108
- successfulRequests: number; // Successful requests
1109
- failedRequests: number; // Failed requests
1110
- phases: PHASE_RESULT[]; // Detailed results per phase
1111
- error?: string; // Workflow-level error (if any)
1112
- }
972
+ // Rate limiting in workflows
973
+ const result = await stableWorkflow(phases, {
974
+ rateLimit: {
975
+ maxRequests: 5,
976
+ windowMs: 1000
977
+ }
978
+ });
1113
979
  ```
1114
980
 
1115
- ### Phase Configuration
981
+ ### Caching
1116
982
 
1117
- Each phase can have its own execution mode and error handling, and can also work upon the shared buffer:
983
+ Cache responses with TTL to reduce redundant API calls:
1118
984
 
1119
985
  ```typescript
1120
- {
1121
- id: 'phase-name', // Optional: phase identifier
1122
- concurrentExecution?: boolean, // true = parallel, false = sequential
1123
- stopOnFirstError?: boolean, // Stop phase on first request failure
1124
- commonConfig?: { /* phase-level common config */ },
1125
- requests: [/* array of requests */],
1126
- sharedBuffer?: Record<string, any> // State to be shared among all requests in the phase
1127
- }
1128
- ```
1129
-
1130
- **Configuration Priority:**
1131
- Individual Request > Phase Common Config > Workflow Common Config
986
+ import { stableRequest, getGlobalCacheManager } from '@emmvish/stable-request';
1132
987
 
1133
- ### Workflow with Request Groups
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
+ });
1134
997
 
1135
- Combine workflows with request groups for fine-grained control:
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
+ });
1136
1003
 
1137
- ```typescript
1138
- const workflow = await stableWorkflow(
1139
- [
1140
- {
1141
- id: 'critical-validation',
1142
- concurrentExecution: true,
1143
- requests: [
1144
- {
1145
- id: 'auth-check',
1146
- groupId: 'critical',
1147
- requestOptions: {
1148
- reqData: { path: '/auth/verify' },
1149
- resReq: true
1150
- }
1151
- },
1152
- {
1153
- id: 'rate-limit-check',
1154
- groupId: 'critical',
1155
- requestOptions: {
1156
- reqData: { path: '/ratelimit/check' },
1157
- resReq: true
1158
- }
1159
- }
1160
- ]
1161
- },
1162
- {
1163
- id: 'data-processing',
1164
- concurrentExecution: false,
1165
- commonConfig: {
1166
- // Phase-specific overrides
1167
- commonAttempts: 5,
1168
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1169
- },
1170
- requests: [
1171
- {
1172
- id: 'process-data',
1173
- groupId: 'standard',
1174
- requestOptions: {
1175
- reqData: { path: '/data/process', method: REQUEST_METHODS.POST },
1176
- resReq: true
1177
- }
1178
- },
1179
- {
1180
- id: 'store-result',
1181
- groupId: 'standard',
1182
- requestOptions: {
1183
- reqData: { path: '/data/store', method: REQUEST_METHODS.POST },
1184
- resReq: true
1185
- }
1186
- }
1187
- ]
1188
- },
1189
- {
1190
- id: 'notifications',
1191
- concurrentExecution: true,
1192
- requests: [
1193
- {
1194
- id: 'email-notification',
1195
- groupId: 'optional',
1196
- requestOptions: {
1197
- reqData: { path: '/notify/email' },
1198
- resReq: true
1199
- }
1200
- },
1201
- {
1202
- id: 'webhook-notification',
1203
- groupId: 'optional',
1204
- requestOptions: {
1205
- reqData: { path: '/notify/webhook' },
1206
- resReq: true
1207
- }
1208
- }
1209
- ]
1210
- }
1211
- ],
1212
- {
1213
- workflowId: 'data-pipeline-workflow',
1214
- stopOnFirstPhaseError: true,
1215
- logPhaseResults: true,
1216
-
1217
- commonRequestData: {
1218
- hostname: 'api.example.com'
1219
- },
1220
- commonAttempts: 3,
1221
- commonWait: 1000,
1222
-
1223
- // Request groups with different reliability requirements
1224
- requestGroups: [
1225
- {
1226
- id: 'critical',
1227
- commonConfig: {
1228
- commonAttempts: 10,
1229
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1230
- commonWait: 2000,
1231
- commonHandleErrors: async ({ errorLog }) => {
1232
- await alerting.critical('Critical service failure', errorLog);
1233
- }
1234
- }
1235
- },
1236
- {
1237
- id: 'standard',
1238
- commonConfig: {
1239
- commonAttempts: 5,
1240
- commonRetryStrategy: RETRY_STRATEGIES.LINEAR,
1241
- commonWait: 1000
1242
- }
1243
- },
1244
- {
1245
- id: 'optional',
1246
- commonConfig: {
1247
- commonAttempts: 2,
1248
- commonFinalErrorAnalyzer: async () => true // Suppress errors
1249
- }
1250
- }
1251
- ]
1252
- }
1253
- );
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
1254
1009
  ```
1255
1010
 
1256
- ### Phase Observability Hooks
1011
+ ### Pre-Execution Hooks
1257
1012
 
1258
- Monitor workflow execution with phase-level hooks:
1013
+ Transform requests dynamically before execution:
1259
1014
 
1260
1015
  ```typescript
1261
- const workflow = await stableWorkflow(
1262
- [
1263
- // ...phases...
1264
- ],
1265
- {
1266
- workflowId: 'monitored-workflow',
1267
-
1268
- // Called after each phase completes successfully
1269
- handlePhaseCompletion: async ({ workflowId, phaseResult, sharedBuffer }) => {
1270
- console.log(`Phase ${phaseResult.phaseId} completed`);
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();
1271
1027
 
1272
- await analytics.track('workflow_phase_complete', {
1273
- workflowId,
1274
- phaseId: phaseResult.phaseId,
1275
- duration: phaseResult.executionTime,
1276
- successRate: phaseResult.successfulRequests / phaseResult.totalRequests
1277
- });
1278
- },
1279
-
1280
- // Called when a phase fails
1281
- handlePhaseError: async ({ workflowId, phaseResult, error, sharedBuffer }) => {
1282
- console.error(`Phase ${phaseResult.phaseId} failed`);
1028
+ // Store in shared buffer
1029
+ commonBuffer.token = token;
1030
+ commonBuffer.timestamp = Date.now();
1283
1031
 
1284
- await alerting.notify('workflow_phase_failed', {
1285
- workflowId,
1286
- phaseId: phaseResult.phaseId,
1287
- error: error.message,
1288
- failedRequests: phaseResult.failedRequests
1289
- });
1032
+ // Override request configuration
1033
+ return {
1034
+ reqData: {
1035
+ hostname: 'api.example.com',
1036
+ path: '/authenticated-data',
1037
+ headers: { Authorization: `Bearer ${token}` }
1038
+ }
1039
+ };
1290
1040
  },
1291
-
1292
- logPhaseResults: true // Enable console logging
1293
- }
1294
- );
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);
1295
1049
  ```
1296
1050
 
1297
- ### Workflow Buffer
1051
+ ### Shared Buffer
1298
1052
 
1299
- Workflow Buffer is shared by all phases and by extension, all requests contained in every phase of the workflow :
1053
+ Share state across requests in gateways and workflows:
1300
1054
 
1301
1055
  ```typescript
1302
- const workflowBuffer: Record<string, any> = {};
1056
+ import { stableWorkflow } from '@emmvish/stable-request';
1303
1057
 
1304
- const phases = [
1058
+ const sharedBuffer: Record<string, any> = { requestCount: 0 };
1059
+
1060
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1305
1061
  {
1306
- id: 'p1',
1307
- concurrentExecution: false,
1062
+ id: 'phase-1',
1308
1063
  requests: [
1309
1064
  {
1310
- id: 'r1',
1065
+ id: 'req-1',
1311
1066
  requestOptions: {
1312
- reqData: { path: '/p1' },
1067
+ reqData: { path: '/step1' },
1313
1068
  resReq: true,
1314
1069
  preExecution: {
1315
- preExecutionHook: ({ commonBuffer }: any) => {
1316
- commonBuffer.token = 'wf-token';
1317
- commonBuffer.setIn = 'p1';
1070
+ preExecutionHook: ({ commonBuffer }) => {
1071
+ commonBuffer.requestCount++;
1072
+ commonBuffer.phase1Data = 'initialized';
1318
1073
  return {};
1319
1074
  },
1320
1075
  preExecutionHookParams: {},
@@ -1326,17 +1081,18 @@ const phases = [
1326
1081
  ]
1327
1082
  },
1328
1083
  {
1329
- id: 'p2',
1330
- concurrentExecution: false,
1084
+ id: 'phase-2',
1331
1085
  requests: [
1332
1086
  {
1333
- id: 'r2',
1087
+ id: 'req-2',
1334
1088
  requestOptions: {
1335
- reqData: { path: '/p2' },
1089
+ reqData: { path: '/step2' },
1336
1090
  resReq: true,
1337
1091
  preExecution: {
1338
- preExecutionHook: ({ commonBuffer }: any) => {
1339
- commonBuffer.usedIn = 'p2';
1092
+ preExecutionHook: ({ commonBuffer }) => {
1093
+ commonBuffer.requestCount++;
1094
+ // Access data from phase-1
1095
+ console.log('Phase 1 data:', commonBuffer.phase1Data);
1340
1096
  return {};
1341
1097
  },
1342
1098
  preExecutionHookParams: {},
@@ -1347,715 +1103,502 @@ const phases = [
1347
1103
  }
1348
1104
  ]
1349
1105
  }
1350
- ] satisfies STABLE_WORKFLOW_PHASE[];
1106
+ ];
1351
1107
 
1352
1108
  const result = await stableWorkflow(phases, {
1353
- workflowId: 'wf-buffer-demo',
1109
+ workflowId: 'stateful-workflow',
1354
1110
  commonRequestData: { hostname: 'api.example.com' },
1355
- sharedBuffer: workflowBuffer
1111
+ sharedBuffer
1356
1112
  });
1357
1113
 
1358
- console.log(workflowBuffer); // { token: 'wf-token' setIn: 'p1', usedIn: 'p2' }
1114
+ console.log('Total requests processed:', sharedBuffer.requestCount);
1359
1115
  ```
1360
- ### Concurrent Execution of Phases
1116
+
1117
+ ### Request Grouping
1118
+
1119
+ Apply different configurations to request groups:
1361
1120
 
1362
1121
  ```typescript
1363
- const phases = [
1122
+ import { stableApiGateway } from '@emmvish/stable-request';
1123
+
1124
+ const requests = [
1364
1125
  {
1365
- id: 'phase-1',
1366
- requests: [
1367
- { id: 'r1', requestOptions: { reqData: { path: '/p1/r1' }, resReq: true } }
1368
- ]
1126
+ id: 'critical-1',
1127
+ groupId: 'critical',
1128
+ requestOptions: { reqData: { path: '/critical/1' }, resReq: true }
1369
1129
  },
1370
1130
  {
1371
- id: 'phase-2',
1372
- requests: [
1373
- { id: 'r2', requestOptions: { reqData: { path: '/p2/r1' }, resReq: true } }
1374
- ]
1131
+ id: 'critical-2',
1132
+ groupId: 'critical',
1133
+ requestOptions: { reqData: { path: '/critical/2' }, resReq: true }
1375
1134
  },
1376
1135
  {
1377
- id: 'phase-3',
1378
- requests: [
1379
- { id: 'r3', requestOptions: { reqData: { path: '/p3/r1' }, resReq: true } }
1380
- ]
1136
+ id: 'optional-1',
1137
+ groupId: 'optional',
1138
+ requestOptions: { reqData: { path: '/optional/1' }, resReq: true }
1381
1139
  }
1382
- ] satisfies STABLE_WORKFLOW_PHASE[];
1140
+ ];
1383
1141
 
1384
- const result = await stableWorkflow(phases, {
1385
- workflowId: 'wf-concurrent-phases',
1142
+ const results = await stableApiGateway(requests, {
1386
1143
  commonRequestData: { hostname: 'api.example.com' },
1387
1144
  commonAttempts: 1,
1388
- commonWait: 1,
1389
- concurrentPhaseExecution: true
1390
- });
1391
- ```
1392
- ### Mixed Execution of Phases
1393
-
1394
- ```typescript
1395
- const workflow = await stableWorkflow(
1396
- [
1397
- {
1398
- id: 'phase-1-sequential',
1399
- requests: [/* ... */]
1400
- },
1401
- {
1402
- id: 'phase-2-concurrent-start',
1403
- markConcurrentPhase: true, // Will run concurrently with phase-3
1404
- requests: [/* ... */]
1405
- },
1145
+ commonWait: 100,
1146
+ requestGroups: [
1406
1147
  {
1407
- id: 'phase-3-concurrent',
1408
- markConcurrentPhase: true, // Will run concurrently with phase-2
1409
- requests: [/* ... */]
1148
+ id: 'critical',
1149
+ commonConfig: {
1150
+ commonAttempts: 5, // More retries for critical requests
1151
+ commonWait: 2000,
1152
+ commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1153
+ }
1410
1154
  },
1411
1155
  {
1412
- id: 'phase-4-sequential',
1413
- requests: [/* ... */] // Runs after phases 2 & 3 complete
1156
+ id: 'optional',
1157
+ commonConfig: {
1158
+ commonAttempts: 1, // No retries for optional requests
1159
+ commonFinalErrorAnalyzer: async () => true // Suppress errors
1160
+ }
1414
1161
  }
1415
- ],
1416
- {
1417
- workflowId: 'mixed-execution-workflow',
1418
- allowExecutionMixing: true // Enable mixed execution mode
1419
- }
1420
- );
1162
+ ]
1163
+ });
1421
1164
  ```
1422
1165
 
1423
- ## Real-World Examples
1166
+ ### Concurrency Control
1424
1167
 
1425
- ### 1. Polling for Job Completion
1168
+ Limit concurrent request execution:
1426
1169
 
1427
1170
  ```typescript
1428
- const jobResult = await stableRequest({
1429
- reqData: {
1430
- hostname: 'api.example.com',
1431
- path: '/jobs/abc123/status'
1432
- },
1433
- resReq: true,
1434
- attempts: 20, // Poll up to 20 times
1435
- wait: 3000, // Wait 3 seconds between polls
1436
- retryStrategy: RETRY_STRATEGIES.FIXED,
1437
-
1438
- responseAnalyzer: async ({ data }) => {
1439
- if (data.status === 'completed') {
1440
- console.log('Job completed!');
1441
- return true; // Success
1442
- }
1443
-
1444
- if (data.status === 'failed') {
1445
- throw new Error(`Job failed: ${data.error}`);
1446
- }
1447
-
1448
- console.log(`Job ${data.status}... ${data.progress}%`);
1449
- return false; // Keep polling
1450
- },
1451
-
1452
- handleErrors: async ({ errorLog }) => {
1453
- console.log(`Poll attempt ${errorLog.attempt}`);
1454
- }
1171
+ import { stableApiGateway } from '@emmvish/stable-request';
1172
+
1173
+ // Limit to 5 concurrent requests
1174
+ const results = await stableApiGateway(requests, {
1175
+ commonRequestData: { hostname: 'api.example.com' },
1176
+ concurrentExecution: true,
1177
+ maxConcurrentRequests: 5
1455
1178
  });
1456
1179
 
1457
- console.log('Final result:', jobResult);
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
+ ];
1458
1189
  ```
1459
1190
 
1460
- ### 2. Database Replication Lag
1191
+ ### Response Analysis
1192
+
1193
+ Validate response content and trigger retries:
1461
1194
 
1462
1195
  ```typescript
1463
- const expectedVersion = 42;
1196
+ import { stableRequest } from '@emmvish/stable-request';
1464
1197
 
1465
1198
  const data = await stableRequest({
1466
- reqData: {
1467
- hostname: 'replica.db.example.com',
1468
- path: '/records/123'
1469
- },
1199
+ reqData: { hostname: 'api.example.com', path: '/job/status' },
1470
1200
  resReq: true,
1471
1201
  attempts: 10,
1472
- wait: 500,
1473
- retryStrategy: RETRY_STRATEGIES.LINEAR,
1474
-
1475
- hookParams: {
1476
- responseAnalyzerParams: { expectedVersion }
1477
- },
1478
-
1479
- responseAnalyzer: async ({ data, params }) => {
1480
- // Wait until replica catches up
1481
- if (data.version >= params.expectedVersion) {
1482
- return true;
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
1483
1208
  }
1484
-
1485
- console.log(`Replica at version ${data.version}, waiting for ${params.expectedVersion}`);
1486
- return false;
1209
+ return data.status === 'completed';
1487
1210
  }
1488
1211
  });
1212
+
1213
+ console.log('Job completed:', data);
1489
1214
  ```
1490
1215
 
1491
- ### 3. Idempotent Payment Processing
1216
+ ### Error Handling
1217
+
1218
+ Comprehensive error handling with observability hooks:
1492
1219
 
1493
1220
  ```typescript
1494
- const paymentResult = await stableRequest({
1495
- reqData: {
1496
- hostname: 'api.stripe.com',
1497
- path: '/v1/charges',
1498
- method: REQUEST_METHODS.POST,
1499
- headers: {
1500
- 'Authorization': 'Bearer sk_...',
1501
- 'Idempotency-Key': crypto.randomUUID() // Ensure idempotency
1502
- },
1503
- body: {
1504
- amount: 1000,
1505
- currency: 'usd',
1506
- source: 'tok_visa'
1507
- }
1508
- },
1221
+ import { stableRequest } from '@emmvish/stable-request';
1222
+
1223
+ const data = await stableRequest({
1224
+ reqData: { hostname: 'api.example.com', path: '/data' },
1509
1225
  resReq: true,
1510
- attempts: 5,
1511
- wait: 2000,
1512
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1513
-
1226
+ attempts: 3,
1227
+ wait: 1000,
1514
1228
  logAllErrors: true,
1515
- logAllSuccessfulAttempts: true,
1516
-
1517
- handleErrors: async ({ errorLog }) => {
1518
- await paymentLogger.error({
1229
+ handleErrors: ({ reqData, errorLog, params }) => {
1230
+ // Custom error logging
1231
+ console.error('Request failed:', {
1232
+ url: reqData.url,
1519
1233
  attempt: errorLog.attempt,
1234
+ statusCode: errorLog.statusCode,
1520
1235
  error: errorLog.error,
1521
1236
  isRetryable: errorLog.isRetryable
1522
1237
  });
1238
+
1239
+ // Send to monitoring service
1240
+ monitoringService.trackError(errorLog);
1523
1241
  },
1524
-
1525
- responseAnalyzer: async ({ data }) => {
1526
- // Validate payment succeeded
1527
- return data.status === 'succeeded' && data.paid === true;
1242
+ logAllSuccessfulAttempts: true,
1243
+ handleSuccessfulAttemptData: ({ successfulAttemptData }) => {
1244
+ console.log('Request succeeded on attempt:', successfulAttemptData.attempt);
1528
1245
  },
1529
-
1530
- finalErrorAnalyzer: async ({ error }) => {
1531
- // Alert team on payment failure
1532
- await alerting.critical('Payment processing failed', error);
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
1251
+ }
1533
1252
  return false; // Throw error
1534
1253
  }
1535
1254
  });
1536
1255
  ```
1537
1256
 
1538
- ### 4. Batch User Creation with Error Handling
1539
-
1540
- ```typescript
1541
- const users = [
1542
- { name: 'Alice', email: 'alice@example.com' },
1543
- { name: 'Bob', email: 'bob@example.com' },
1544
- { name: 'Charlie', email: 'charlie@example.com' }
1545
- ];
1546
-
1547
- const requests = users.map((user, index) => ({
1548
- id: `user-${index}`,
1549
- requestOptions: {
1550
- reqData: {
1551
- body: user
1552
- },
1553
- resReq: true
1554
- }
1555
- }));
1556
-
1557
- const results = await stableApiGateway(requests, {
1558
- concurrentExecution: true,
1559
-
1560
- commonRequestData: {
1561
- hostname: 'api.example.com',
1562
- path: '/users',
1563
- method: REQUEST_METHODS.POST,
1564
- headers: {
1565
- 'Content-Type': 'application/json'
1566
- }
1567
- },
1568
-
1569
- commonAttempts: 3,
1570
- commonWait: 1000,
1571
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1572
- commonResReq: true,
1573
- commonLogAllErrors: true,
1574
-
1575
- commonHandleErrors: async ({ reqData, errorLog }) => {
1576
- const user = reqData.data;
1577
- console.error(`Failed to create user ${user.name}: ${errorLog.error}`);
1578
- },
1579
-
1580
- commonResponseAnalyzer: async ({ data }) => {
1581
- // Ensure user was created with an ID
1582
- return data?.id && data?.email;
1583
- }
1584
- });
1585
-
1586
- const successful = results.filter(r => r.success);
1587
- const failed = results.filter(r => !r.success);
1588
-
1589
- console.log(`✓ Created ${successful.length} users`);
1590
- console.log(`✗ Failed to create ${failed.length} users`);
1591
-
1592
- failed.forEach(r => {
1593
- console.error(` - ${r.requestId}: ${r.error}`);
1594
- });
1595
- ```
1257
+ ## Advanced Use Cases
1596
1258
 
1597
- ### 5. Health Check Monitoring System
1259
+ ### Use Case 1: Multi-Tenant API with Dynamic Authentication
1598
1260
 
1599
1261
  ```typescript
1600
- const healthChecks = await stableApiGateway(
1601
- [
1602
- // Core services - must be healthy
1603
- { id: 'auth', groupId: 'core', requestOptions: { reqData: { hostname: 'auth.internal', path: '/health' } } },
1604
- { id: 'database', groupId: 'core', requestOptions: { reqData: { hostname: 'db.internal', path: '/health' } } },
1605
- { id: 'api', groupId: 'core', requestOptions: { reqData: { hostname: 'api.internal', path: '/health' } } },
1606
-
1607
- // Optional services
1608
- { id: 'cache', groupId: 'optional', requestOptions: { reqData: { hostname: 'cache.internal', path: '/health' } } },
1609
- { id: 'search', groupId: 'optional', requestOptions: { reqData: { hostname: 'search.internal', path: '/health' } } }
1610
- ],
1611
- {
1612
- commonResReq: true,
1613
- concurrentExecution: true,
1614
-
1615
- requestGroups: [
1616
- {
1617
- id: 'core',
1618
- commonConfig: {
1619
- commonAttempts: 5,
1620
- commonWait: 2000,
1621
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1622
-
1623
- commonResponseAnalyzer: async ({ data }) => {
1624
- // Core services need strict validation
1625
- return data?.status === 'healthy' &&
1626
- data?.uptime > 0 &&
1627
- data?.dependencies?.every(d => d.healthy);
1628
- },
1629
-
1630
- commonHandleErrors: async ({ reqData, errorLog }) => {
1631
- // Alert on core service issues
1632
- await pagerDuty.trigger({
1633
- severity: 'critical',
1634
- service: reqData.baseURL,
1635
- message: errorLog.error
1636
- });
1637
- }
1638
- }
1639
- },
1640
- {
1641
- id: 'optional',
1642
- commonConfig: {
1643
- commonAttempts: 2,
1644
-
1645
- commonResponseAnalyzer: async ({ data }) => {
1646
- // Optional services: basic check
1647
- return data?.status === 'ok';
1648
- },
1649
-
1650
- commonFinalErrorAnalyzer: async ({ reqData, error }) => {
1651
- // Log but don't alert
1652
- console.warn(`Optional service ${reqData.baseURL} unhealthy`);
1653
- return true; // Don't throw
1654
- }
1655
- }
1656
- }
1657
- ]
1658
- }
1659
- );
1660
-
1661
- const report = {
1662
- timestamp: new Date().toISOString(),
1663
- core: healthChecks.filter(r => r.groupId === 'core').every(r => r.success),
1664
- optional: healthChecks.filter(r => r.groupId === 'optional').every(r => r.success),
1665
- overall: healthChecks.every(r => r.success) ? 'HEALTHY' : 'DEGRADED'
1666
- };
1262
+ import { stableWorkflow, RETRY_STRATEGIES } from '@emmvish/stable-request';
1667
1263
 
1668
- console.log('System Health:', report);
1669
- ```
1264
+ interface TenantConfig {
1265
+ tenantId: string;
1266
+ apiKey: string;
1267
+ baseUrl: string;
1268
+ }
1670
1269
 
1671
- ### 6. Data Pipeline (ETL Workflow)
1270
+ async function executeTenantWorkflow(tenantConfig: TenantConfig) {
1271
+ const sharedBuffer: Record<string, any> = {
1272
+ tenantId: tenantConfig.tenantId,
1273
+ authToken: null,
1274
+ processedItems: []
1275
+ };
1672
1276
 
1673
- ```typescript
1674
- const etlWorkflow = await stableWorkflow(
1675
- [
1277
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1676
1278
  {
1677
- id: 'extract',
1678
- concurrentExecution: true,
1679
- commonConfig: {
1680
- commonAttempts: 5,
1681
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1682
- },
1279
+ id: 'authentication',
1683
1280
  requests: [
1684
- { id: 'extract-users', requestOptions: { reqData: { path: '/extract/users' }, resReq: true } },
1685
- { id: 'extract-orders', requestOptions: { reqData: { path: '/extract/orders' }, resReq: true } },
1686
- { id: 'extract-products', requestOptions: { reqData: { path: '/extract/products' }, resReq: true } }
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
+ }
1687
1303
  ]
1688
1304
  },
1689
1305
  {
1690
- id: 'transform',
1691
- concurrentExecution: false,
1692
- stopOnFirstError: true,
1306
+ id: 'data-fetching',
1307
+ concurrentExecution: true,
1308
+ maxConcurrentRequests: 5,
1693
1309
  requests: [
1694
1310
  {
1695
- id: 'clean-data',
1696
- requestOptions: {
1697
- reqData: { path: '/transform/clean', method: REQUEST_METHODS.POST },
1698
- resReq: true
1699
- }
1700
- },
1701
- {
1702
- id: 'enrich-data',
1311
+ id: 'fetch-users',
1703
1312
  requestOptions: {
1704
- reqData: { path: '/transform/enrich', method: REQUEST_METHODS.POST },
1705
- resReq: true
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
+ }
1706
1324
  }
1707
1325
  },
1708
1326
  {
1709
- id: 'validate-data',
1327
+ id: 'fetch-settings',
1710
1328
  requestOptions: {
1711
- reqData: { path: '/transform/validate', method: REQUEST_METHODS.POST },
1329
+ reqData: { path: '/settings' },
1712
1330
  resReq: true,
1713
- responseAnalyzer: async ({ data }) => {
1714
- return data?.validationErrors?.length === 0;
1331
+ preExecution: {
1332
+ preExecutionHook: ({ commonBuffer }) => ({
1333
+ reqData: {
1334
+ path: '/settings',
1335
+ headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
1336
+ }
1337
+ }),
1338
+ applyPreExecutionConfigOverride: true
1715
1339
  }
1716
1340
  }
1717
1341
  }
1718
1342
  ]
1719
1343
  },
1720
1344
  {
1721
- id: 'load',
1345
+ id: 'data-processing',
1722
1346
  concurrentExecution: true,
1723
1347
  requests: [
1724
1348
  {
1725
- id: 'load-warehouse',
1726
- requestOptions: {
1727
- reqData: { path: '/load/warehouse', method: REQUEST_METHODS.POST },
1728
- resReq: true
1729
- }
1730
- },
1731
- {
1732
- id: 'update-indexes',
1733
- requestOptions: {
1734
- reqData: { path: '/load/indexes', method: REQUEST_METHODS.POST },
1735
- resReq: true
1736
- }
1737
- },
1738
- {
1739
- id: 'refresh-cache',
1349
+ id: 'process-users',
1740
1350
  requestOptions: {
1741
- reqData: { path: '/cache/refresh', method: REQUEST_METHODS.POST },
1742
- resReq: true
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
+ }
1743
1376
  }
1744
1377
  }
1745
1378
  ]
1746
1379
  }
1747
- ],
1748
- {
1749
- workflowId: `etl-${new Date().toISOString()}`,
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
+ },
1750
1388
  stopOnFirstPhaseError: true,
1751
1389
  logPhaseResults: true,
1752
-
1753
- commonRequestData: {
1754
- hostname: 'pipeline.example.com'
1390
+ sharedBuffer,
1391
+ circuitBreaker: {
1392
+ failureThresholdPercentage: 40,
1393
+ minimumRequests: 5,
1394
+ recoveryTimeoutMs: 30000
1755
1395
  },
1756
- commonAttempts: 3,
1757
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1758
-
1759
- handlePhaseCompletion: async ({ phaseResult }) => {
1760
- const recordsProcessed = phaseResult.responses
1761
- .filter(r => r.success)
1762
- .reduce((sum, r) => sum + (r.data?.recordCount || 0), 0);
1763
-
1764
- await metrics.gauge('etl.phase.records', recordsProcessed, {
1765
- phase: phaseResult.phaseId
1766
- });
1396
+ rateLimit: {
1397
+ maxRequests: 20,
1398
+ windowMs: 1000
1767
1399
  },
1768
-
1769
- handlePhaseError: async ({ phaseResult, error }) => {
1770
- await pagerDuty.alert('ETL Pipeline Phase Failed', {
1771
- phase: phaseResult.phaseId,
1772
- error: error.message,
1773
- failedRequests: phaseResult.failedRequests
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`
1774
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);
1775
1415
  }
1776
- }
1777
- );
1778
-
1779
- console.log(`ETL Pipeline: ${etlWorkflow.success ? 'SUCCESS' : 'FAILED'}`);
1780
- console.log(`Total time: ${etlWorkflow.executionTime}ms`);
1781
- console.log(`Records processed: ${etlWorkflow.successfulRequests}/${etlWorkflow.totalRequests}`);
1782
- ```
1783
-
1784
- ## Complete API Reference
1785
-
1786
- ### `stableRequest(options)`
1787
-
1788
- | Option | Type | Default | Description |
1789
- |--------|------|---------|-------------|
1790
- | `reqData` | `REQUEST_DATA` | **required** | Request configuration |
1791
- | `resReq` | `boolean` | `false` | Return response data vs. just boolean |
1792
- | `attempts` | `number` | `1` | Max retry attempts |
1793
- | `wait` | `number` | `1000` | Base delay between retries (ms) |
1794
- | `maxAllowedWait` | `number` | `60000` | Maximum permitted wait duration between retries (ms) |
1795
- | `retryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Retry backoff strategy |
1796
- | `performAllAttempts` | `boolean` | `false` | Execute all attempts regardless |
1797
- | `logAllErrors` | `boolean` | `false` | Enable error logging |
1798
- | `logAllSuccessfulAttempts` | `boolean` | `false` | Enable success logging |
1799
- | `maxSerializableChars` | `number` | `1000` | Max chars for logs |
1800
- | `trialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Failure simulation |
1801
- | `hookParams` | `HookParams` | `{}` | Custom parameters for hooks |
1802
- | `preExecution` | `RequestPreExecutionOptions` | `{}` | Executes before actually sending request, can modify request config |
1803
- | `commonBuffer` | `Record<string, any>` | `{}` | For communication between various request hooks |
1804
- | `responseAnalyzer` | `function` | `() => true` | Validate response content |
1805
- | `handleErrors` | `function` | `console.log` | Error handler |
1806
- | `handleSuccessfulAttemptData` | `function` | `console.log` | Success handler |
1807
- | `finalErrorAnalyzer` | `function` | `() => false` | Final error handler |
1808
-
1809
- ### REQUEST_DATA
1810
-
1811
- ```typescript
1812
- interface REQUEST_DATA<RequestDataType = any> {
1813
- hostname: string; // Required
1814
- protocol?: 'http' | 'https'; // Default: 'https'
1815
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; // Default: 'GET'
1816
- path?: `/${string}`; // Default: ''
1817
- port?: number; // Default: 443
1818
- headers?: Record<string, any>; // Default: {}
1819
- body?: RequestDataType; // Request body
1820
- query?: Record<string, any>; // Query parameters
1821
- timeout?: number; // Default: 15000ms
1822
- signal?: AbortSignal; // For cancellation
1823
- }
1824
- ```
1825
-
1826
- ### `stableApiGateway(requests, options)`
1827
-
1828
- | Option | Type | Default | Description |
1829
- |--------|------|---------|-------------|
1830
- | `concurrentExecution` | `boolean` | `true` | Execute requests concurrently or sequentially |
1831
- | `stopOnFirstError` | `boolean` | `false` | Stop execution on first error (sequential only) |
1832
- | `requestGroups` | `RequestGroup[]` | `[]` | Define groups with their own common configurations |
1833
- | `commonAttempts` | `number` | `1` | Default attempts for all requests |
1834
- | `commonPerformAllAttempts` | `boolean` | `false` | Default performAllAttempts for all requests |
1835
- | `commonWait` | `number` | `1000` | Default wait time for all requests |
1836
- | `commonMaxAllowedWait` | `number` | `60000` | Default maximum permitted wait time for all requests |
1837
- | `commonRetryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Default retry strategy for all requests |
1838
- | `commonLogAllErrors` | `boolean` | `false` | Default error logging for all requests |
1839
- | `commonLogAllSuccessfulAttempts` | `boolean` | `false` | Default success logging for all requests |
1840
- | `commonMaxSerializableChars` | `number` | `1000` | Default max chars for serialization |
1841
- | `commonTrialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Default trial mode for all requests |
1842
- | `commonResponseAnalyzer` | `function` | `() => true` | Default response analyzer for all requests |
1843
- | `commonResReq` | `boolean` | `false` | Default resReq for all requests |
1844
- | `commonFinalErrorAnalyzer` | `function` | `() => false` | Default final error analyzer for all requests |
1845
- | `commonHandleErrors` | `function` | console.log | Default error handler for all requests |
1846
- | `commonHandleSuccessfulAttemptData` | `function` | console.log | Default success handler for all requests |
1847
- | `commonRequestData` | `Partial<REQUEST_DATA>` | `{ hostname: '' }` | Common set of request options for each request |
1848
- | `commonHookParams` | `HookParams` | `{ }` | Common options for each request hook |
1849
- | `sharedBuffer` | `Record<string, any>` | `undefined` | For communication between various requests |
1850
-
1851
- ### `stableWorkflow(phases, options)`
1852
-
1853
- Execute a multi-phase workflow with full control over execution order and error handling.
1854
-
1855
- **Phases Array:**
1856
- ```typescript
1857
- interface STABLE_WORKFLOW_PHASE {
1858
- id?: string; // Phase identifier (auto-generated if omitted)
1859
- concurrentExecution?: boolean; // true = parallel, false = sequential (default: true)
1860
- stopOnFirstError?: boolean; // Stop phase on first request failure (default: false)
1861
- commonConfig?: Omit<API_GATEWAY_OPTIONS; 'concurrentExecution' | 'stopOnFirstError' | 'requestGroups'>;
1862
- requests: API_GATEWAY_REQUEST[]; // Array of requests for this phase
1863
- markConcurrentPhase?: boolean; // Allows this phase to be executed concurrently with immediately next phase marked as concurrent
1864
- }
1865
- ```
1866
-
1867
- **Workflow Options:**
1868
-
1869
- | Option | Type | Default | Description |
1870
- |--------|------|---------|-------------|
1871
- | `workflowId` | `string` | `workflow-{timestamp}` | Workflow identifier |
1872
- | `stopOnFirstPhaseError` | `boolean` | `false` | Stop workflow if any phase fails |
1873
- | `logPhaseResults` | `boolean` | `false` | Log phase execution to console |
1874
- | `concurrentPhaseExecution` | `boolean` | `false` | Execute all phases in parallel. Overrides `enableMixedExecution` |
1875
- | `handlePhaseCompletion` | `function` | `undefined` | Hook called after each successful phase |
1876
- | `handlePhaseError` | `function` | `undefined` | Hook called when a phase fails |
1877
- | `maxSerializableChars` | `number` | `1000` | Max chars for serialization in hooks |
1878
- | `workflowHookParams` | `WorkflowHookParams` | {} | Custom set of params passed to hooks |
1879
- | `sharedBuffer` | `Record<string, any>` | `undefined` | Buffer shared by all phases and all requests within them |
1880
- | `enableMixedExecution` | `boolean` | `false` | Enables mixing of sequential and parallel sub-workflows |
1881
- | All `stableApiGateway` options | - | - | Applied as workflow-level defaults |
1882
-
1883
- **STABLE_WORKFLOW_RESULT response:**
1884
- ```typescript
1885
- interface STABLE_WORKFLOW_RESULT {
1886
- workflowId: string;
1887
- success: boolean; // All phases successful?
1888
- executionTime: number; // Total workflow duration (ms)
1889
- timestamp: string; // ISO timestamp
1890
- totalPhases: number;
1891
- completedPhases: number;
1892
- totalRequests: number;
1893
- successfulRequests: number;
1894
- failedRequests: number;
1895
- phases: PHASE_RESULT[]; // Detailed results per phase
1896
- error?: string; // Workflow-level error
1897
- }
1898
- ```
1899
-
1900
- ### Hooks Reference
1901
-
1902
- #### preExecutionHook
1903
-
1904
- **Purpose:** Dynamically configure request before execution
1905
-
1906
- ```typescript
1907
- preExecution: {
1908
- preExecutionHook: async ({ inputParams, commonBuffer }) => {
1909
- // Fetch dynamic data
1910
- const token = await getAuthToken();
1911
-
1912
- // Store in common buffer
1913
- commonBuffer.token = token;
1914
- commonBuffer.timestamp = Date.now();
1915
-
1916
- // Return config overrides
1917
- return {
1918
- reqData: {
1919
- headers: { 'Authorization': `Bearer ${token}` }
1920
- },
1921
- attempts: 5
1922
- };
1923
- },
1924
- preExecutionHookParams: { userId: 'user-123' },
1925
- applyPreExecutionConfigOverride: true,
1926
- continueOnPreExecutionHookFailure: false
1927
- }
1928
- ```
1929
-
1930
- #### responseAnalyzer
1931
-
1932
- **Purpose:** Validate response content, retry even on HTTP 200
1933
-
1934
- ```typescript
1935
- responseAnalyzer: async ({ reqData, data, trialMode, params, commonBuffer }) => {
1936
- // Return true if valid, false to retry
1937
- return data.status === 'ready';
1938
- }
1939
- ```
1940
-
1941
- #### handleErrors
1942
-
1943
- **Purpose:** Monitor and log failed attempts
1944
-
1945
- ```typescript
1946
- handleErrors: async ({ reqData, errorLog, maxSerializableChars, params, commonBuffer }) => {
1947
- await logger.error({
1948
- url: reqData.url,
1949
- attempt: errorLog.attempt,
1950
- error: errorLog.error
1951
1416
  });
1952
- }
1953
- ```
1954
-
1955
- #### handleSuccessfulAttemptData
1956
1417
 
1957
- **Purpose:** Monitor and log successful attempts
1958
-
1959
- ```typescript
1960
- handleSuccessfulAttemptData: async ({ reqData, successfulAttemptData, maxSerializableChars, params, commonBuffer }) => {
1961
- await analytics.track({
1962
- url: reqData.url,
1963
- duration: successfulAttemptData.executionTime
1964
- });
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
+ };
1965
1429
  }
1966
- ```
1967
1430
 
1968
- #### finalErrorAnalyzer
1969
-
1970
- **Purpose:** Handle final error after all retries exhausted
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
+ ];
1971
1436
 
1972
- ```typescript
1973
- finalErrorAnalyzer: async ({ reqData, error, trialMode, params, commonBuffer }) => {
1974
- // Return true to suppress error (return false)
1975
- // Return false to throw error
1976
- if (error.message.includes('404')) {
1977
- return true; // Treat as non-critical
1978
- }
1979
- return false; // Throw
1980
- }
1437
+ const results = await Promise.all(tenants.map(executeTenantWorkflow));
1438
+ results.forEach(result => {
1439
+ console.log(`Tenant ${result.tenantId}:`, result.success ? 'Success' : 'Failed');
1440
+ });
1981
1441
  ```
1982
1442
 
1983
- #### handlePhaseCompletion
1984
-
1985
- **Purpose:** Execute phase-bridging code upon successful completion of a phase
1443
+ ### Use Case 2: Resilient Data Pipeline with Fallback Strategies
1986
1444
 
1987
1445
  ```typescript
1988
- handlePhaseCompletion: async ({ workflowId, phaseResult, maxSerializableChars, params, sharedBuffer }) => {
1989
- await logger.log(phaseResult.phaseId, phaseResult.success);
1990
- }
1991
- ```
1446
+ import { stableApiGateway, RETRY_STRATEGIES, CircuitBreaker } from '@emmvish/stable-request';
1992
1447
 
1993
- #### handlePhaseError
1994
-
1995
- **Purpose:** Execute error handling code if a phase runs into an error
1996
-
1997
- ```typescript
1998
- handlePhaseError: async ({ workflowId, phaseResult, error, maxSerializableChars, params, sharedBuffer }) => {
1999
- await logger.error(error);
1448
+ interface DataSource {
1449
+ id: string;
1450
+ priority: number;
1451
+ endpoint: string;
1452
+ hostname: string;
2000
1453
  }
2001
- ```
2002
-
2003
- ## Configuration Hierarchy
2004
-
2005
- Configuration precedence across orchestration:
2006
-
2007
- - Workflow-level (options.sharedBuffer)
2008
- - Phase-level (commonConfig)
2009
- - Request group (requestGroups[].commonConfig)
2010
- - Individual request options (highest priority)
2011
1454
 
2012
- Buffers are state (not config):
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
+ }
2013
1481
 
2014
- - Request scope: commonBuffer
2015
- - Gateway scope: Gateway's / Phase's sharedBuffer
2016
- - Workflow scope: Workflow's sharedBuffer
1482
+ console.log(`Attempting to fetch from ${source.id}...`);
2017
1483
 
2018
- ## TypeScript Support
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
+ ];
2019
1517
 
2020
- Fully typed with generics:
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
+ });
2021
1542
 
2022
- ```typescript
2023
- interface CreateUserRequest {
2024
- name: string;
2025
- email: string;
2026
- }
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
+ }
2027
1564
 
2028
- interface UserResponse {
2029
- id: string;
2030
- name: string;
2031
- email: string;
2032
- createdAt: string;
1565
+ throw new Error('All data sources failed');
2033
1566
  }
2034
1567
 
2035
- const user = await stableRequest<CreateUserRequest, UserResponse>({
2036
- reqData: {
2037
- hostname: 'api.example.com',
2038
- path: '/users',
2039
- method: REQUEST_METHODS.POST,
2040
- body: {
2041
- name: 'John Doe',
2042
- email: 'john@example.com'
2043
- }
1568
+ // Usage
1569
+ const dataSources: DataSource[] = [
1570
+ {
1571
+ id: 'primary-db',
1572
+ priority: 1,
1573
+ endpoint: '/api/v1',
1574
+ hostname: 'primary.example.com'
2044
1575
  },
2045
- resReq: true
2046
- });
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
+ ];
2047
1589
 
2048
- // user is typed as UserResponse
2049
- console.log(user.id); // TypeScript knows this exists
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);
2050
1595
  ```
2051
1596
 
2052
1597
  ## License
2053
1598
 
2054
1599
  MIT © Manish Varma
2055
1600
 
2056
-
2057
1601
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2058
-
2059
1602
  ---
2060
1603
 
2061
1604
  **Made with ❤️ for developers integrating with unreliable APIs**