@emmvish/stable-request 1.1.3 โ†’ 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,20 @@
1
1
  ## stable-request
2
2
 
3
- `stable-request` is a TypeScript-first HTTP reliability toolkit for workflow-driven API integrations, that goes beyond status-code retries by validating response content, handling eventual consistency, coordinating batch workflows with intelligent grouping, and providing deep observability into every request attempt. It is designed for real-world distributed systems where HTTP success does not guarantee business success.
3
+ `stable-request` is a TypeScript-first HTTP reliability toolkit for workflow-driven API integrations, that goes beyond status-code retries by validating response content, handling eventual consistency, coordinating batch workflows with intelligent grouping, and providing deep observability into every request attempt.
4
+
5
+ It is designed for real-world distributed systems where HTTP success (200) does not guarantee business success.
4
6
 
5
7
  ## Why stable-request?
6
8
 
7
9
  Most HTTP client libraries only retry on network failures or specific HTTP status codes. **stable-request** goes further by providing:
8
10
 
9
- - โœ… **Content-aware retries** - Validate response content and retry even on successful HTTP responses
10
- - ๐Ÿš€ **Batch processing with groups** - Execute multiple requests with hierarchical configuration (global โ†’ group โ†’ request)
11
- - ๐ŸŽฏ **Request grouping** - Organize related requests with shared settings and logical boundaries
12
- - ๐Ÿงช **Trial mode** - Simulate failures to test your retry logic without depending on real network instability
13
- - ๐Ÿ“Š **Granular observability** - Monitor every attempt with detailed hooks
14
- - โšก **Multiple retry strategies** - Fixed, linear, or exponential backoff
15
- - ๐Ÿ”ง **Flexible error handling** - Custom error analysis and graceful degradation
11
+ - โœ… **Content-aware Retries** - Validate response content and retry even on successful HTTP responses
12
+ - ๐Ÿš€ **Batch Processing** - Execute multiple requests with hierarchical configuration (global โ†’ group โ†’ request)
13
+ - ๐ŸŽฏ **Request Groups** - Organize related requests with shared settings and logical boundaries
14
+ - ๐Ÿงช **Trial Mode** - Simulate failures to test your retry logic without depending on real network instability
15
+ - ๐Ÿ“Š **Granular Observability** - Monitor every attempt with detailed hooks
16
+ - โšก **Multiple Retry Strategies** - Fixed, linear, or exponential backoff
17
+ - ๐Ÿ”ง **Flexible Error Handling** - Custom error analysis and graceful degradation
16
18
 
17
19
  ## Installation
18
20
 
@@ -22,882 +24,802 @@ npm install @emmvish/stable-request
22
24
 
23
25
  ## Quick Start
24
26
 
25
- ### Single Request
27
+ ### 1. Basic Request (No Retries)
26
28
 
27
29
  ```typescript
28
- import { stableRequest, REQUEST_METHODS, RETRY_STRATEGIES } from '@emmvish/stable-request';
30
+ import { stableRequest } from '@emmvish/stable-request';
29
31
 
30
- // Simple GET request with automatic retries
31
- const response = await stableRequest({
32
+ const data = await stableRequest({
32
33
  reqData: {
33
34
  hostname: 'api.example.com',
34
- path: '/users',
35
- method: REQUEST_METHODS.GET
35
+ path: '/users/123'
36
36
  },
37
- resReq: true,
38
- attempts: 3,
39
- wait: 1000,
40
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
37
+ resReq: true // Return the response data
41
38
  });
39
+
40
+ console.log(data); // { id: 123, name: 'John' }
42
41
  ```
43
42
 
44
- ### Batch Requests
43
+ ### 2. Add Simple Retries
45
44
 
46
45
  ```typescript
47
- import { stableApiGateway } from '@emmvish/stable-request';
46
+ import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
48
47
 
49
- const requests = [
50
- {
51
- id: 'user-1',
52
- requestOptions: {
53
- reqData: { path: '/users/1' },
54
- resReq: true
55
- }
48
+ const data = await stableRequest({
49
+ reqData: {
50
+ hostname: 'api.example.com',
51
+ path: '/users/123'
56
52
  },
57
- {
58
- id: 'user-2',
59
- requestOptions: {
60
- reqData: { path: '/users/2' },
61
- resReq: true
62
- }
63
- }
64
- ];
65
-
66
- const results = await stableApiGateway(requests, {
67
- concurrentExecution: true,
68
- commonAttempts: 3,
69
- commonWait: 1000,
70
- commonRequestData: { hostname: 'api.example.com' }
53
+ resReq: true,
54
+ attempts: 3, // Retry up to 3 times
55
+ wait: 1000, // Wait 1 second between retries
56
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL // 1s, 2s, 4s, 8s...
71
57
  });
72
58
  ```
73
59
 
74
- ### Grouped Requests
60
+ **Retry Strategies:**
61
+ - `RETRY_STRATEGIES.FIXED` - Same delay every time (1s, 1s, 1s...)
62
+ - `RETRY_STRATEGIES.LINEAR` - Increasing delay (1s, 2s, 3s...)
63
+ - `RETRY_STRATEGIES.EXPONENTIAL` - Exponential backoff (1s, 2s, 4s, 8s...)
75
64
 
76
- ```typescript
77
- import { stableApiGateway, RETRY_STRATEGIES } from '@emmvish/stable-request';
65
+ ### 3. Validate Response Content (Content-Aware Retries)
78
66
 
79
- const results = await stableApiGateway(
80
- [
81
- {
82
- id: 'auth-check',
83
- groupId: 'critical-services',
84
- requestOptions: {
85
- reqData: { path: '/auth/verify' },
86
- resReq: true
87
- }
88
- },
89
- {
90
- id: 'analytics-track',
91
- groupId: 'optional-services',
92
- requestOptions: {
93
- reqData: { path: '/analytics/event' },
94
- resReq: true
95
- }
67
+ Sometimes an API returns HTTP 200 but the data isn't ready yet. Use `responseAnalyzer`:
68
+
69
+ ```typescript
70
+ const data = await stableRequest({
71
+ reqData: {
72
+ hostname: 'api.example.com',
73
+ path: '/jobs/456/status'
74
+ },
75
+ resReq: true,
76
+ attempts: 10,
77
+ wait: 2000,
78
+
79
+ // This hook validates the response content
80
+ responseAnalyzer: async ({ reqData, data, trialMode, params }) => {
81
+ // Return true if response is valid, false to retry
82
+ if (data.status === 'completed') {
83
+ return true; // Success! Don't retry
96
84
  }
97
- ],
98
- {
99
- // Global defaults
100
- commonAttempts: 2,
101
- commonRequestData: { hostname: 'api.example.com' },
102
85
 
103
- // Define groups with their own configurations
104
- requestGroups: [
105
- {
106
- id: 'critical-services',
107
- commonConfig: {
108
- commonAttempts: 10,
109
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
110
- }
111
- },
112
- {
113
- id: 'optional-services',
114
- commonConfig: {
115
- commonAttempts: 1,
116
- commonFinalErrorAnalyzer: async () => true // Don't throw on failure
117
- }
118
- }
119
- ]
86
+ console.log(`Job still processing... (${data.percentComplete}%)`);
87
+ return false; // Retry this request
120
88
  }
121
- );
89
+ });
90
+
91
+ console.log('Job completed:', data);
122
92
  ```
123
93
 
124
- ## Core Features
94
+ **Hook Signature:**
95
+ ```typescript
96
+ responseAnalyzer?: (options: {
97
+ reqData: AxiosRequestConfig; // Request configuration
98
+ data: ResponseDataType; // Response data from API
99
+ trialMode?: TRIAL_MODE_OPTIONS; // Trial mode settings (if enabled)
100
+ params?: any; // Custom parameters (via hookParams)
101
+ }) => boolean | Promise<boolean>;
102
+ ```
125
103
 
126
- ### 1. Content-Aware Retries with `stableRequest`
104
+ ### 4. Monitor Errors (Observability)
127
105
 
128
- Unlike traditional retry mechanisms, `stableRequest` validates the **content** of successful responses and retries if needed.
106
+ Track every failed attempt with `handleErrors`:
129
107
 
130
108
  ```typescript
131
- await stableRequest({
109
+ const data = await stableRequest({
132
110
  reqData: {
133
111
  hostname: 'api.example.com',
134
- path: '/data',
112
+ path: '/data'
135
113
  },
136
114
  resReq: true,
137
115
  attempts: 5,
138
- wait: 2000,
139
- // Retry even on HTTP 200 if data is invalid
140
- responseAnalyzer: async (reqConfig, data) => {
141
- return data?.status === 'ready' && data?.items?.length > 0;
116
+ logAllErrors: true, // Enable error logging
117
+
118
+ // This hook is called on every failed attempt
119
+ handleErrors: async ({ reqData, errorLog, maxSerializableChars }) => {
120
+ // Log to your monitoring service
121
+ await monitoring.logError({
122
+ url: reqData.url,
123
+ attempt: errorLog.attempt, // e.g., "3/5"
124
+ error: errorLog.error, // Error message
125
+ isRetryable: errorLog.isRetryable, // Can we retry?
126
+ type: errorLog.type, // 'HTTP_ERROR' or 'INVALID_CONTENT'
127
+ statusCode: errorLog.statusCode, // HTTP status code
128
+ timestamp: errorLog.timestamp, // ISO timestamp
129
+ executionTime: errorLog.executionTime // ms
130
+ });
142
131
  }
143
132
  });
144
133
  ```
145
134
 
146
- **Use Cases:**
147
- - Wait for async processing to complete
148
- - Ensure data quality before proceeding
149
- - Handle eventually-consistent systems
150
- - Validate complex business rules in responses
135
+ **Hook Signature:**
136
+ ```typescript
137
+ handleErrors?: (options: {
138
+ reqData: AxiosRequestConfig; // Request configuration
139
+ errorLog: ERROR_LOG; // Detailed error information
140
+ maxSerializableChars?: number; // Max chars for stringification
141
+ }) => any | Promise<any>;
142
+ ```
151
143
 
152
- ### 2. Batch Processing with `stableApiGateway`
144
+ **ERROR_LOG Structure:**
145
+ ```typescript
146
+ interface ERROR_LOG {
147
+ timestamp: string; // ISO timestamp
148
+ executionTime: number; // Request duration in ms
149
+ statusCode: number; // HTTP status code (0 if network error)
150
+ attempt: string; // e.g., "3/5"
151
+ error: string; // Error message
152
+ type: 'HTTP_ERROR' | 'INVALID_CONTENT';
153
+ isRetryable: boolean; // Can this error be retried?
154
+ }
155
+ ```
153
156
 
154
- Process multiple requests efficiently with shared configuration and execution strategies.
157
+ ### 5. Monitor Successful Attempts
155
158
 
156
- #### Concurrent Execution
159
+ Track successful requests with `handleSuccessfulAttemptData`:
157
160
 
158
161
  ```typescript
159
- import { stableApiGateway, RETRY_STRATEGIES, REQUEST_METHODS } from '@emmvish/stable-request';
160
-
161
- const requests = [
162
- {
163
- id: 'create-user-1',
164
- requestOptions: {
165
- reqData: {
166
- body: { name: 'John Doe', email: 'john@example.com' }
167
- }
168
- }
169
- },
170
- {
171
- id: 'create-user-2',
172
- requestOptions: {
173
- reqData: {
174
- body: { name: 'Jane Smith', email: 'jane@example.com' }
175
- }
176
- }
177
- }
178
- ];
179
-
180
- const results = await stableApiGateway(requests, {
181
- concurrentExecution: true,
182
- commonRequestData: {
162
+ const data = await stableRequest({
163
+ reqData: {
183
164
  hostname: 'api.example.com',
184
- path: '/users',
185
- method: REQUEST_METHODS.POST
165
+ path: '/data'
186
166
  },
187
- commonResReq: true,
188
- commonAttempts: 3,
189
- commonWait: 1000,
190
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
191
- });
192
-
193
- // Process results
194
- results.forEach(result => {
195
- if (result.success) {
196
- console.log(`${result.requestId} succeeded:`, result.data);
197
- } else {
198
- console.error(`${result.requestId} failed:`, result.error);
167
+ resReq: true,
168
+ attempts: 3,
169
+ logAllSuccessfulAttempts: true, // Enable success logging
170
+
171
+ // This hook is called on every successful attempt
172
+ handleSuccessfulAttemptData: async ({ reqData, successfulAttemptData, maxSerializableChars }) => {
173
+ // Track metrics
174
+ await analytics.track('api_success', {
175
+ url: reqData.url,
176
+ attempt: successfulAttemptData.attempt, // e.g., "2/3"
177
+ duration: successfulAttemptData.executionTime, // ms
178
+ statusCode: successfulAttemptData.statusCode, // 200, 201, etc.
179
+ timestamp: successfulAttemptData.timestamp
180
+ });
199
181
  }
200
182
  });
201
183
  ```
202
184
 
203
- #### Sequential Execution
204
-
185
+ **Hook Signature:**
205
186
  ```typescript
206
- const results = await stableApiGateway(requests, {
207
- concurrentExecution: false,
208
- stopOnFirstError: true,
209
- commonAttempts: 3
210
- });
187
+ handleSuccessfulAttemptData?: (options: {
188
+ reqData: AxiosRequestConfig; // Request configuration
189
+ successfulAttemptData: SUCCESSFUL_ATTEMPT_DATA; // Success details
190
+ maxSerializableChars?: number; // Max chars for stringification
191
+ }) => any | Promise<any>;
211
192
  ```
212
193
 
213
- ### 3. Request Grouping - Hierarchical Configuration
194
+ **SUCCESSFUL_ATTEMPT_DATA Structure:**
195
+ ```typescript
196
+ interface SUCCESSFUL_ATTEMPT_DATA<ResponseDataType> {
197
+ attempt: string; // e.g., "2/3"
198
+ timestamp: string; // ISO timestamp
199
+ executionTime: number; // Request duration in ms
200
+ data: ResponseDataType; // Response data
201
+ statusCode: number; // HTTP status code
202
+ }
203
+ ```
214
204
 
215
- Organize requests into logical groups with their own configuration. The configuration priority is:
205
+ ### 6. Handle Final Errors Gracefully
216
206
 
217
- **Individual Request Options** (highest) โ†’ **Group Common Options** (middle) โ†’ **Global Common Options** (lowest)
207
+ Decide what to do when all retries fail using `finalErrorAnalyzer`:
218
208
 
219
209
  ```typescript
220
- const results = await stableApiGateway(
221
- [
222
- {
223
- id: 'payment-stripe',
224
- groupId: 'payment-providers',
225
- requestOptions: {
226
- reqData: { path: '/stripe/charge' },
227
- resReq: true,
228
- // Individual override: even more attempts for Stripe
229
- attempts: 15
230
- }
231
- },
232
- {
233
- id: 'payment-paypal',
234
- groupId: 'payment-providers',
235
- requestOptions: {
236
- reqData: { path: '/paypal/charge' },
237
- resReq: true
238
- }
239
- },
240
- {
241
- id: 'analytics-event',
242
- groupId: 'analytics',
243
- requestOptions: {
244
- reqData: { path: '/track' },
245
- resReq: true
246
- }
210
+ const data = await stableRequest({
211
+ reqData: {
212
+ hostname: 'api.example.com',
213
+ path: '/optional-feature'
214
+ },
215
+ resReq: true,
216
+ attempts: 3,
217
+
218
+ // This hook is called when all retries are exhausted
219
+ finalErrorAnalyzer: async ({ reqData, error, trialMode, params }) => {
220
+ // Check if this is a non-critical error
221
+ if (error.message.includes('404')) {
222
+ console.log('Feature not available, continuing without it');
223
+ return true; // Suppress error, return false instead of throwing
247
224
  }
248
- ],
249
- {
250
- // Global configuration - applies to all ungrouped requests
251
- commonAttempts: 2,
252
- commonWait: 500,
253
- commonRequestData: {
254
- hostname: 'api.example.com',
255
- method: REQUEST_METHODS.POST
256
- },
257
225
 
258
- // Group-specific configurations
259
- requestGroups: [
260
- {
261
- id: 'payment-providers',
262
- commonConfig: {
263
- // Payment group: aggressive retries
264
- commonAttempts: 10,
265
- commonWait: 2000,
266
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
267
- commonRequestData: {
268
- headers: { 'X-Idempotency-Key': crypto.randomUUID() }
269
- },
270
- commonHandleErrors: async (reqData, error) => {
271
- await alertPagerDuty('Payment failure', error);
272
- }
273
- }
274
- },
275
- {
276
- id: 'analytics',
277
- commonConfig: {
278
- // Analytics group: minimal retries, failures acceptable
279
- commonAttempts: 1,
280
- commonFinalErrorAnalyzer: async () => true // Don't throw
281
- }
282
- }
283
- ]
226
+ // For critical errors
227
+ await alerting.sendAlert('Critical API failure', error);
228
+ return false; // Throw the error
284
229
  }
285
- );
286
-
287
- // Filter results by group
288
- const paymentResults = results.filter(r => r.groupId === 'payment-providers');
289
- const analyticsResults = results.filter(r => r.groupId === 'analytics');
230
+ });
290
231
 
291
- console.log('Payment success rate:',
292
- paymentResults.filter(r => r.success).length / paymentResults.length);
232
+ if (data === false) {
233
+ console.log('Optional feature unavailable, using default');
234
+ }
293
235
  ```
294
236
 
295
- **Key Benefits of Request Grouping:**
237
+ **Hook Signature:**
238
+ ```typescript
239
+ finalErrorAnalyzer?: (options: {
240
+ reqData: AxiosRequestConfig; // Request configuration
241
+ error: any; // The final error object
242
+ trialMode?: TRIAL_MODE_OPTIONS; // Trial mode settings (if enabled)
243
+ params?: any; // Custom parameters (via hookParams)
244
+ }) => boolean | Promise<boolean>;
245
+ ```
296
246
 
297
- 1. **Service Tiering** - Different retry strategies for critical vs. optional services
298
- 2. **Regional Configuration** - Customize timeouts and retries per geographic region
299
- 3. **Priority Management** - Handle high-priority requests more aggressively
300
- 4. **Organized Configuration** - Group related requests with shared settings
301
- 5. **Simplified Maintenance** - Update group config instead of individual requests
302
- 6. **Better Monitoring** - Track metrics and failures by logical groups
247
+ **Return value:**
248
+ - `true` - Suppress the error, function returns `false` instead of throwing
249
+ - `false` - Throw the error
303
250
 
304
- ### 4. Trial Mode - Test Your Retry Logic
251
+ ### 7. Pass Custom Parameters to Hooks
305
252
 
306
- Simulate request and retry failures with configurable probabilities.
253
+ You can pass custom data to `responseAnalyzer` and `finalErrorAnalyzer`:
307
254
 
308
255
  ```typescript
309
- await stableRequest({
256
+ const expectedVersion = 42;
257
+
258
+ const data = await stableRequest({
310
259
  reqData: {
311
260
  hostname: 'api.example.com',
312
- path: '/test',
261
+ path: '/data'
313
262
  },
314
263
  resReq: true,
315
264
  attempts: 5,
316
- trialMode: {
317
- enabled: true,
318
- reqFailureProbability: 0.3, // 30% chance each request fails
319
- retryFailureProbability: 0.2 // 20% chance retry is marked non-retryable
265
+
266
+ // Pass custom parameters
267
+ hookParams: {
268
+ responseAnalyzerParams: { expectedVersion, minItems: 10 },
269
+ finalErrorAnalyzerParams: { alertTeam: true }
270
+ },
271
+
272
+ responseAnalyzer: async ({ data, params }) => {
273
+ // Access custom parameters
274
+ return data.version >= params.expectedVersion &&
275
+ data.items.length >= params.minItems;
320
276
  },
321
- logAllErrors: true
277
+
278
+ finalErrorAnalyzer: async ({ error, params }) => {
279
+ if (params.alertTeam) {
280
+ await pagerDuty.alert('API failure', error);
281
+ }
282
+ return false;
283
+ }
322
284
  });
323
285
  ```
324
286
 
325
- **Use Cases:**
326
- - Integration testing
327
- - Chaos engineering
328
- - Validating monitoring and alerting
329
- - Testing circuit breaker patterns
287
+ ## Intermediate Concepts
330
288
 
331
- ### 5. Multiple Retry Strategies
332
-
333
- Choose the backoff strategy that fits your use case.
289
+ ### Making POST/PUT/PATCH Requests
334
290
 
335
291
  ```typescript
336
- import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
337
-
338
- // Fixed delay: 1s, 1s, 1s, 1s...
339
- await stableRequest({
340
- reqData: { hostname: 'api.example.com', path: '/data' },
341
- resReq: true,
342
- attempts: 5,
343
- wait: 1000,
344
- retryStrategy: RETRY_STRATEGIES.FIXED
345
- });
292
+ import { stableRequest, REQUEST_METHODS } from '@emmvish/stable-request';
346
293
 
347
- // Linear backoff: 1s, 2s, 3s, 4s...
348
- await stableRequest({
349
- reqData: { hostname: 'api.example.com', path: '/data' },
350
- resReq: true,
351
- attempts: 5,
352
- wait: 1000,
353
- retryStrategy: RETRY_STRATEGIES.LINEAR
354
- });
355
-
356
- // Exponential backoff: 1s, 2s, 4s, 8s...
357
- await stableRequest({
358
- reqData: { hostname: 'api.example.com', path: '/data' },
359
- resReq: true,
360
- attempts: 5,
361
- wait: 1000,
362
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
363
- });
364
- ```
365
-
366
- ### 6. Comprehensive Observability
367
-
368
- Monitor every request attempt with detailed logging hooks.
369
-
370
- ```typescript
371
- await stableRequest({
294
+ const newUser = await stableRequest({
372
295
  reqData: {
373
296
  hostname: 'api.example.com',
374
- path: '/critical-endpoint',
297
+ path: '/users',
298
+ method: REQUEST_METHODS.POST,
299
+ headers: {
300
+ 'Content-Type': 'application/json',
301
+ 'Authorization': 'Bearer your-token'
302
+ },
303
+ body: {
304
+ name: 'John Doe',
305
+ email: 'john@example.com'
306
+ }
375
307
  },
376
308
  resReq: true,
377
- attempts: 3,
378
- logAllErrors: true,
379
- handleErrors: async (reqConfig, errorLog) => {
380
- // Custom error handling - send to monitoring service
381
- await monitoringService.logError({
382
- endpoint: reqConfig.url,
383
- attempt: errorLog.attempt,
384
- error: errorLog.error,
385
- isRetryable: errorLog.isRetryable,
386
- type: errorLog.type, // 'HTTP_ERROR' or 'INVALID_CONTENT'
387
- timestamp: errorLog.timestamp,
388
- executionTime: errorLog.executionTime,
389
- statusCode: errorLog.statusCode
390
- });
391
- },
392
- logAllSuccessfulAttempts: true,
393
- handleSuccessfulAttemptData: async (reqConfig, successData) => {
394
- // Track successful attempts
395
- analytics.track('request_success', {
396
- endpoint: reqConfig.url,
397
- attempt: successData.attempt,
398
- executionTime: successData.executionTime,
399
- statusCode: successData.statusCode
400
- });
401
- }
309
+ attempts: 3
402
310
  });
403
311
  ```
404
312
 
405
- ### 7. Smart Retry Logic
406
-
407
- Automatically retries on common transient errors:
408
-
409
- - HTTP 5xx (Server Errors)
410
- - HTTP 408 (Request Timeout)
411
- - HTTP 429 (Too Many Requests)
412
- - HTTP 409 (Conflict)
413
- - Network errors: `ECONNRESET`, `ETIMEDOUT`, `ECONNREFUSED`, `ENOTFOUND`, `EAI_AGAIN`
313
+ ### Query Parameters
414
314
 
415
315
  ```typescript
416
- await stableRequest({
316
+ const users = await stableRequest({
417
317
  reqData: {
418
- hostname: 'unreliable-api.com',
419
- path: '/data',
318
+ hostname: 'api.example.com',
319
+ path: '/users',
320
+ query: {
321
+ page: 1,
322
+ limit: 10,
323
+ sort: 'createdAt'
324
+ }
420
325
  },
421
- resReq: true,
422
- attempts: 5,
423
- wait: 2000,
424
- retryStrategy: RETRY_STRATEGIES.LINEAR
425
- // Automatically retries on transient failures
326
+ resReq: true
426
327
  });
328
+ // Requests: https://api.example.com:443/users?page=1&limit=10&sort=createdAt
427
329
  ```
428
330
 
429
- ### 8. Final Error Analysis
430
-
431
- Decide whether to throw or return false based on error analysis.
331
+ ### Custom Timeout and Port
432
332
 
433
333
  ```typescript
434
- const result = await stableRequest({
334
+ const data = await stableRequest({
435
335
  reqData: {
436
336
  hostname: 'api.example.com',
437
- path: '/optional-data',
337
+ path: '/slow-endpoint',
338
+ port: 8080,
339
+ protocol: 'http',
340
+ timeout: 30000 // 30 seconds
438
341
  },
439
342
  resReq: true,
440
- attempts: 3,
441
- finalErrorAnalyzer: async (reqConfig, error) => {
442
- // Return true to suppress error and return false instead of throwing
443
- if (error.message.includes('404')) {
444
- console.log('Resource not found, treating as non-critical');
445
- return true; // Don't throw, return false
446
- }
447
- return false; // Throw the error
448
- }
343
+ attempts: 2
449
344
  });
450
-
451
- // result will be false if finalErrorAnalyzer returned true
452
- if (result === false) {
453
- console.log('Request failed but was handled gracefully');
454
- }
455
345
  ```
456
346
 
457
- ### 9. Request Cancellation Support
458
-
459
- Support for AbortController to cancel requests.
347
+ ### Request Cancellation
460
348
 
461
349
  ```typescript
462
350
  const controller = new AbortController();
463
351
 
352
+ // Cancel after 5 seconds
464
353
  setTimeout(() => controller.abort(), 5000);
465
354
 
466
355
  try {
467
356
  await stableRequest({
468
357
  reqData: {
469
358
  hostname: 'api.example.com',
470
- path: '/slow-endpoint',
359
+ path: '/data',
471
360
  signal: controller.signal
472
361
  },
473
- resReq: true,
474
- attempts: 3
362
+ resReq: true
475
363
  });
476
364
  } catch (error) {
477
- // Request was cancelled
365
+ if (error.message.includes('cancelled')) {
366
+ console.log('Request was cancelled');
367
+ }
478
368
  }
479
369
  ```
480
370
 
481
- ### 10. Perform All Attempts Mode
371
+ ### Trial Mode (Testing Your Retry Logic)
482
372
 
483
- Execute all retry attempts regardless of success, useful for warm-up scenarios.
373
+ Simulate failures without depending on actual API issues:
484
374
 
485
375
  ```typescript
486
376
  await stableRequest({
487
377
  reqData: {
488
378
  hostname: 'api.example.com',
489
- path: '/cache-warmup',
379
+ path: '/data'
490
380
  },
381
+ resReq: true,
491
382
  attempts: 5,
492
- performAllAttempts: true, // Always performs all 5 attempts
493
- wait: 1000
383
+ logAllErrors: true,
384
+
385
+ trialMode: {
386
+ enabled: true,
387
+ reqFailureProbability: 0.3, // 30% chance each request fails
388
+ retryFailureProbability: 0.2 // 20% chance error is non-retryable
389
+ }
494
390
  });
495
391
  ```
496
392
 
497
- ## API Reference
498
-
499
- ### `stableRequest<RequestDataType, ResponseDataType>(options)`
500
-
501
- Execute a single HTTP request with retry logic.
393
+ **Use cases:**
394
+ - Test your error handling logic
395
+ - Verify monitoring alerts work
396
+ - Chaos engineering experiments
397
+ - Integration testing
502
398
 
503
- #### Configuration Options
399
+ ## Batch Processing - Multiple Requests
504
400
 
505
- | Option | Type | Default | Description |
506
- |--------|------|---------|-------------|
507
- | `reqData` | `REQUEST_DATA` | **required** | Request configuration (hostname, path, method, etc.) |
508
- | `resReq` | `boolean` | `false` | Return response data instead of just success boolean |
509
- | `attempts` | `number` | `1` | Maximum number of retry attempts |
510
- | `wait` | `number` | `1000` | Base delay in milliseconds between retries |
511
- | `retryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Retry strategy: `'fixed'`, `'linear'`, or `'exponential'` |
512
- | `responseAnalyzer` | `function` | `() => true` | Validates response content, return false to retry |
513
- | `performAllAttempts` | `boolean` | `false` | Execute all attempts regardless of success |
514
- | `logAllErrors` | `boolean` | `false` | Enable error logging for all failed attempts |
515
- | `handleErrors` | `function` | console.log | Custom error handler |
516
- | `logAllSuccessfulAttempts` | `boolean` | `false` | Log all successful attempts |
517
- | `handleSuccessfulAttemptData` | `function` | console.log | Custom success handler |
518
- | `maxSerializableChars` | `number` | `1000` | Max characters for serialized logs |
519
- | `finalErrorAnalyzer` | `function` | `() => false` | Analyze final error, return true to suppress throwing |
520
- | `trialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Simulate failures for testing |
521
-
522
- #### Request Data Configuration
401
+ ### Basic Batch Request
523
402
 
524
403
  ```typescript
525
- interface REQUEST_DATA<RequestDataType = any> {
526
- hostname: string; // Required
527
- protocol?: 'http' | 'https'; // Default: 'https'
528
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; // Default: 'GET'
529
- path?: `/${string}`; // Default: ''
530
- port?: number; // Default: 443
531
- headers?: Record<string, any>; // Default: {}
532
- body?: RequestDataType; // Request body
533
- query?: Record<string, any>; // Query parameters
534
- timeout?: number; // Default: 15000ms
535
- signal?: AbortSignal; // For request cancellation
536
- }
537
- ```
538
-
539
- ### `stableApiGateway<RequestDataType, ResponseDataType>(requests, options)`
540
-
541
- Execute multiple HTTP requests with shared configuration and optional grouping.
404
+ import { stableApiGateway } from '@emmvish/stable-request';
542
405
 
543
- #### Gateway Configuration Options
406
+ const requests = [
407
+ {
408
+ id: 'user-1',
409
+ requestOptions: {
410
+ reqData: { path: '/users/1' },
411
+ resReq: true
412
+ }
413
+ },
414
+ {
415
+ id: 'user-2',
416
+ requestOptions: {
417
+ reqData: { path: '/users/2' },
418
+ resReq: true
419
+ }
420
+ },
421
+ {
422
+ id: 'user-3',
423
+ requestOptions: {
424
+ reqData: { path: '/users/3' },
425
+ resReq: true
426
+ }
427
+ }
428
+ ];
544
429
 
545
- | Option | Type | Default | Description |
546
- |--------|------|---------|-------------|
547
- | `concurrentExecution` | `boolean` | `true` | Execute requests concurrently or sequentially |
548
- | `stopOnFirstError` | `boolean` | `false` | Stop execution on first error (sequential only) |
549
- | `requestGroups` | `RequestGroup[]` | `[]` | Define groups with their own common configurations |
550
- | `commonAttempts` | `number` | `1` | Default attempts for all requests |
551
- | `commonPerformAllAttempts` | `boolean` | `false` | Default performAllAttempts for all requests |
552
- | `commonWait` | `number` | `1000` | Default wait time for all requests |
553
- | `commonRetryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Default retry strategy for all requests |
554
- | `commonLogAllErrors` | `boolean` | `false` | Default error logging for all requests |
555
- | `commonLogAllSuccessfulAttempts` | `boolean` | `false` | Default success logging for all requests |
556
- | `commonMaxSerializableChars` | `number` | `1000` | Default max chars for serialization |
557
- | `commonTrialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Default trial mode for all requests |
558
- | `commonResponseAnalyzer` | `function` | `() => true` | Default response analyzer for all requests |
559
- | `commonResReq` | `boolean` | `false` | Default resReq for all requests |
560
- | `commonFinalErrorAnalyzer` | `function` | `() => false` | Default final error analyzer for all requests |
561
- | `commonHandleErrors` | `function` | console.log | Default error handler for all requests |
562
- | `commonHandleSuccessfulAttemptData` | `function` | console.log | Default success handler for all requests |
563
- | `commonRequestData` | `Partial<REQUEST_DATA>` | `{ hostname: '' }` | Common set of request options for each request |
430
+ const results = await stableApiGateway(requests, {
431
+ // Common options applied to ALL requests
432
+ commonRequestData: {
433
+ hostname: 'api.example.com'
434
+ },
435
+ commonAttempts: 3,
436
+ commonWait: 1000,
437
+ concurrentExecution: true // Run all requests in parallel
438
+ });
564
439
 
565
- #### Request Group Configuration
440
+ // Process results
441
+ results.forEach(result => {
442
+ if (result.success) {
443
+ console.log(`${result.requestId} succeeded:`, result.data);
444
+ } else {
445
+ console.error(`${result.requestId} failed:`, result.error);
446
+ }
447
+ });
448
+ ```
566
449
 
450
+ **Response Format:**
567
451
  ```typescript
568
- interface RequestGroup {
569
- id: string; // Unique group identifier
570
- commonConfig?: {
571
- // Any common* option can be specified here
572
- commonAttempts?: number;
573
- commonWait?: number;
574
- commonRetryStrategy?: RETRY_STRATEGY_TYPES;
575
- commonRequestData?: Partial<REQUEST_DATA>;
576
- commonResponseAnalyzer?: function;
577
- commonHandleErrors?: function;
578
- // ... all other common* options
579
- };
452
+ interface API_GATEWAY_RESPONSE<ResponseDataType> {
453
+ requestId: string; // The ID you provided
454
+ groupId?: string; // Group ID (if request was grouped)
455
+ success: boolean; // Did the request succeed?
456
+ data?: ResponseDataType; // Response data (if success)
457
+ error?: string; // Error message (if failed)
580
458
  }
581
459
  ```
582
460
 
583
- #### Request Format
461
+ ### Sequential Execution (With Dependencies)
584
462
 
585
463
  ```typescript
586
- interface API_GATEWAY_REQUEST<RequestDataType, ResponseDataType> {
587
- id: string; // Unique identifier for the request
588
- groupId?: string; // Optional group identifier
589
- requestOptions: API_GATEWAY_REQUEST_OPTIONS_TYPE<RequestDataType, ResponseDataType>;
464
+ const steps = [
465
+ {
466
+ id: 'step-1-create',
467
+ requestOptions: {
468
+ reqData: {
469
+ path: '/orders',
470
+ method: REQUEST_METHODS.POST,
471
+ body: { item: 'Widget' }
472
+ },
473
+ resReq: true
474
+ }
475
+ },
476
+ {
477
+ id: 'step-2-process',
478
+ requestOptions: {
479
+ reqData: {
480
+ path: '/orders/123/process',
481
+ method: REQUEST_METHODS.POST
482
+ },
483
+ resReq: true
484
+ }
485
+ },
486
+ {
487
+ id: 'step-3-ship',
488
+ requestOptions: {
489
+ reqData: { path: '/orders/123/ship' },
490
+ resReq: true
491
+ }
492
+ }
493
+ ];
494
+
495
+ const results = await stableApiGateway(steps, {
496
+ concurrentExecution: false, // Run one at a time
497
+ stopOnFirstError: true, // Stop if any step fails
498
+ commonRequestData: {
499
+ hostname: 'api.example.com'
500
+ },
501
+ commonAttempts: 3
502
+ });
503
+
504
+ if (results.every(r => r.success)) {
505
+ console.log('Workflow completed successfully');
506
+ } else {
507
+ const failedStep = results.findIndex(r => !r.success);
508
+ console.error(`Workflow failed at step ${failedStep + 1}`);
590
509
  }
591
510
  ```
592
511
 
593
- **Configuration Priority:** Individual request options override group options, which override global common options.
512
+ ### Shared Configuration (Common Options)
594
513
 
595
- #### Response Format
514
+ Instead of repeating configuration for each request:
596
515
 
597
516
  ```typescript
598
- interface API_GATEWAY_RESPONSE<ResponseDataType> {
599
- requestId: string; // Request identifier
600
- groupId?: string; // Group identifier (if request was grouped)
601
- success: boolean; // Whether the request succeeded
602
- data?: ResponseDataType; // Response data (if success is true)
603
- error?: string; // Error message (if success is false)
604
- }
517
+ const results = await stableApiGateway(
518
+ [
519
+ { id: 'req-1', requestOptions: { reqData: { path: '/users/1' } } },
520
+ { id: 'req-2', requestOptions: { reqData: { path: '/users/2' } } },
521
+ { id: 'req-3', requestOptions: { reqData: { path: '/users/3' } } }
522
+ ],
523
+ {
524
+ // Applied to ALL requests
525
+ commonRequestData: {
526
+ hostname: 'api.example.com',
527
+ headers: { 'Authorization': `Bearer ${token}` }
528
+ },
529
+ commonResReq: true,
530
+ commonAttempts: 5,
531
+ commonWait: 2000,
532
+ commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
533
+ commonLogAllErrors: true,
534
+
535
+ // Shared hooks
536
+ commonHandleErrors: async ({ reqData, errorLog }) => {
537
+ console.log(`Request to ${reqData.url} failed (${errorLog.attempt})`);
538
+ },
539
+
540
+ commonResponseAnalyzer: async ({ data }) => {
541
+ return data?.success === true;
542
+ }
543
+ }
544
+ );
605
545
  ```
606
546
 
607
- ## Real-World Use Cases
547
+ ## Advanced: Request Grouping
548
+
549
+ Group related requests with different configurations. Configuration priority:
608
550
 
609
- ### 1. Environment-Based Service Tiers with Groups
551
+ **Individual Request** > **Group Config** > **Global Common Config**
610
552
 
611
- Organize API calls by criticality with different retry strategies per tier.
553
+ ### Example: Service Tiers
612
554
 
613
555
  ```typescript
614
556
  const results = await stableApiGateway(
615
557
  [
616
- // Critical services
617
- { id: 'auth-validate', groupId: 'critical', requestOptions: { reqData: { path: '/auth/validate' }, resReq: true } },
618
- { id: 'db-primary', groupId: 'critical', requestOptions: { reqData: { path: '/db/health' }, resReq: true } },
619
-
620
- // Payment services
621
- { id: 'stripe-charge', groupId: 'payments', requestOptions: { reqData: { path: '/stripe/charge', body: { amount: 1000 } }, resReq: true } },
622
- { id: 'paypal-charge', groupId: 'payments', requestOptions: { reqData: { path: '/paypal/charge', body: { amount: 1000 } }, resReq: true } },
558
+ // Critical services - need high reliability
559
+ {
560
+ id: 'auth-check',
561
+ groupId: 'critical',
562
+ requestOptions: {
563
+ reqData: { path: '/auth/verify' },
564
+ resReq: true
565
+ }
566
+ },
567
+ {
568
+ id: 'payment-process',
569
+ groupId: 'critical',
570
+ requestOptions: {
571
+ reqData: { path: '/payments/charge' },
572
+ resReq: true,
573
+ // Individual override: even MORE attempts for payments
574
+ attempts: 15
575
+ }
576
+ },
623
577
 
624
- // Analytics services
625
- { id: 'track-event', groupId: 'analytics', requestOptions: { reqData: { path: '/track' }, resReq: true } }
578
+ // Analytics - failures are acceptable
579
+ {
580
+ id: 'track-event',
581
+ groupId: 'analytics',
582
+ requestOptions: {
583
+ reqData: { path: '/analytics/track' },
584
+ resReq: true
585
+ }
586
+ }
626
587
  ],
627
588
  {
628
- // Global defaults
589
+ // Global defaults (lowest priority)
590
+ commonRequestData: {
591
+ hostname: 'api.example.com'
592
+ },
629
593
  commonAttempts: 2,
630
594
  commonWait: 500,
631
- commonRequestData: { hostname: 'api.example.com', method: REQUEST_METHODS.POST },
632
595
 
596
+ // Define groups with their own configs
633
597
  requestGroups: [
634
598
  {
635
599
  id: 'critical',
636
600
  commonConfig: {
601
+ // Critical services: aggressive retries
637
602
  commonAttempts: 10,
638
603
  commonWait: 2000,
639
604
  commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
640
- commonHandleErrors: async (reqData, error) => {
641
- await pagerDuty.alert('CRITICAL', error);
642
- }
643
- }
644
- },
645
- {
646
- id: 'payments',
647
- commonConfig: {
648
- commonAttempts: 5,
649
- commonWait: 1500,
650
- commonRetryStrategy: RETRY_STRATEGIES.LINEAR,
651
- commonRequestData: {
652
- headers: { 'X-Idempotency-Key': crypto.randomUUID() }
605
+
606
+ commonHandleErrors: async ({ errorLog }) => {
607
+ // Alert on critical failures
608
+ await pagerDuty.alert('Critical service failure', errorLog);
653
609
  },
654
- commonResponseAnalyzer: async (reqData, data) => {
655
- return data?.status === 'succeeded' && data?.transactionId;
610
+
611
+ commonResponseAnalyzer: async ({ data }) => {
612
+ // Strict validation
613
+ return data?.status === 'success' && !data?.errors;
656
614
  }
657
615
  }
658
616
  },
659
617
  {
660
618
  id: 'analytics',
661
619
  commonConfig: {
620
+ // Analytics: minimal retries, don't throw on failure
662
621
  commonAttempts: 1,
663
- commonFinalErrorAnalyzer: async () => true // Don't throw
622
+
623
+ commonFinalErrorAnalyzer: async () => {
624
+ return true; // Suppress errors
625
+ }
664
626
  }
665
627
  }
666
628
  ]
667
629
  }
668
630
  );
669
631
 
670
- // Analyze results by group
671
- const criticalHealth = results.filter(r => r.groupId === 'critical').every(r => r.success);
672
- const paymentSuccess = results.filter(r => r.groupId === 'payments').filter(r => r.success).length;
632
+ // Analyze by group
633
+ const criticalOk = results
634
+ .filter(r => r.groupId === 'critical')
635
+ .every(r => r.success);
673
636
 
674
- if (!criticalHealth) {
675
- console.error('CRITICAL SERVICES DEGRADED');
676
- }
677
- console.log(`Payments: ${paymentSuccess} successful`);
678
- ```
637
+ const analyticsCount = results
638
+ .filter(r => r.groupId === 'analytics' && r.success)
639
+ .length;
679
640
 
680
- ### 2. Multi-Region API Deployment with Groups
681
-
682
- Handle requests to different geographic regions with region-specific configurations.
683
-
684
- ```typescript
685
- const results = await stableApiGateway(
686
- [
687
- { id: 'us-user-profile', groupId: 'us-east', requestOptions: { reqData: { path: '/users/profile' }, resReq: true } },
688
- { id: 'us-orders', groupId: 'us-east', requestOptions: { reqData: { path: '/orders' }, resReq: true } },
689
-
690
- { id: 'eu-user-profile', groupId: 'eu-west', requestOptions: { reqData: { path: '/users/profile' }, resReq: true } },
691
- { id: 'eu-orders', groupId: 'eu-west', requestOptions: { reqData: { path: '/orders' }, resReq: true } },
692
-
693
- { id: 'ap-user-profile', groupId: 'ap-southeast', requestOptions: { reqData: { path: '/users/profile' }, resReq: true } },
694
- { id: 'ap-orders', groupId: 'ap-southeast', requestOptions: { reqData: { path: '/orders' }, resReq: true } }
695
- ],
696
- {
697
- commonAttempts: 3,
698
- commonWait: 1000,
699
-
700
- requestGroups: [
701
- {
702
- id: 'us-east',
703
- commonConfig: {
704
- commonRequestData: {
705
- hostname: 'api-us-east.example.com',
706
- headers: { 'X-Region': 'us-east-1' },
707
- timeout: 5000 // Lower latency expected
708
- },
709
- commonAttempts: 3,
710
- commonRetryStrategy: RETRY_STRATEGIES.LINEAR
711
- }
712
- },
713
- {
714
- id: 'eu-west',
715
- commonConfig: {
716
- commonRequestData: {
717
- hostname: 'api-eu-west.example.com',
718
- headers: { 'X-Region': 'eu-west-1' },
719
- timeout: 8000
720
- },
721
- commonAttempts: 5,
722
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
723
- }
724
- },
725
- {
726
- id: 'ap-southeast',
727
- commonConfig: {
728
- commonRequestData: {
729
- hostname: 'api-ap-southeast.example.com',
730
- headers: { 'X-Region': 'ap-southeast-1' },
731
- timeout: 10000 // Higher latency expected
732
- },
733
- commonAttempts: 7,
734
- commonWait: 1500,
735
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
736
- }
737
- }
738
- ]
739
- }
740
- );
741
-
742
- // Regional performance analysis
743
- const regionPerformance = results.reduce((acc, result) => {
744
- if (!acc[result.groupId!]) acc[result.groupId!] = { success: 0, failed: 0 };
745
- result.success ? acc[result.groupId!].success++ : acc[result.groupId!].failed++;
746
- return acc;
747
- }, {} as Record<string, { success: number; failed: number }>);
748
-
749
- console.log('Regional performance:', regionPerformance);
641
+ console.log('Critical services:', criticalOk ? 'HEALTHY' : 'DEGRADED');
642
+ console.log('Analytics events tracked:', analyticsCount);
750
643
  ```
751
644
 
752
- ### 3. Microservices Health Monitoring with Groups
753
-
754
- Monitor different microservices with service-specific health check configurations.
645
+ ### Example: Multi-Region Configuration
755
646
 
756
647
  ```typescript
757
- const healthChecks = await stableApiGateway(
648
+ const results = await stableApiGateway(
758
649
  [
759
- // Core services
760
- { id: 'auth-health', groupId: 'core', requestOptions: { reqData: { hostname: 'auth.internal.example.com', path: '/health' } } },
761
- { id: 'user-health', groupId: 'core', requestOptions: { reqData: { hostname: 'users.internal.example.com', path: '/health' } } },
762
- { id: 'order-health', groupId: 'core', requestOptions: { reqData: { hostname: 'orders.internal.example.com', path: '/health' } } },
763
-
764
- // Auxiliary services
765
- { id: 'cache-health', groupId: 'auxiliary', requestOptions: { reqData: { hostname: 'cache.internal.example.com', path: '/health' } } },
766
- { id: 'search-health', groupId: 'auxiliary', requestOptions: { reqData: { hostname: 'search.internal.example.com', path: '/health' } } },
767
-
768
- // Third-party
769
- { id: 'stripe-health', groupId: 'third-party', requestOptions: { reqData: { hostname: 'api.stripe.com', path: '/v1/health' } } }
650
+ { id: 'us-data', groupId: 'us-east', requestOptions: { reqData: { path: '/data' }, resReq: true } },
651
+ { id: 'eu-data', groupId: 'eu-west', requestOptions: { reqData: { path: '/data' }, resReq: true } },
652
+ { id: 'ap-data', groupId: 'ap-southeast', requestOptions: { reqData: { path: '/data' }, resReq: true } }
770
653
  ],
771
654
  {
772
- commonResReq: true,
773
- concurrentExecution: true,
655
+ commonAttempts: 3,
774
656
 
775
657
  requestGroups: [
776
658
  {
777
- id: 'core',
659
+ id: 'us-east',
778
660
  commonConfig: {
779
- commonAttempts: 5,
780
- commonWait: 2000,
781
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
782
- commonResponseAnalyzer: async (reqData, data) => {
783
- return data?.status === 'healthy' &&
784
- data?.dependencies?.every(d => d.status === 'healthy');
661
+ commonRequestData: {
662
+ hostname: 'api-us.example.com',
663
+ timeout: 5000, // Low latency expected
664
+ headers: { 'X-Region': 'us-east-1' }
785
665
  },
786
- commonHandleErrors: async (reqData, error) => {
787
- await pagerDuty.trigger({ severity: 'critical', message: `Core service down: ${error.error}` });
788
- }
666
+ commonAttempts: 3
789
667
  }
790
668
  },
791
669
  {
792
- id: 'auxiliary',
670
+ id: 'eu-west',
793
671
  commonConfig: {
794
- commonAttempts: 2,
795
- commonResponseAnalyzer: async (reqData, data) => data?.status === 'ok',
796
- commonFinalErrorAnalyzer: async () => true // Don't fail on auxiliary issues
672
+ commonRequestData: {
673
+ hostname: 'api-eu.example.com',
674
+ timeout: 8000, // Medium latency
675
+ headers: { 'X-Region': 'eu-west-1' }
676
+ },
677
+ commonAttempts: 5
797
678
  }
798
679
  },
799
680
  {
800
- id: 'third-party',
681
+ id: 'ap-southeast',
801
682
  commonConfig: {
802
- commonAttempts: 3,
803
- commonWait: 3000,
804
- commonRequestData: { timeout: 15000 },
805
- commonHandleErrors: async (reqData, error) => {
806
- logger.warn('Third-party health check failed', { error });
683
+ commonRequestData: {
684
+ hostname: 'api-ap.example.com',
685
+ timeout: 12000, // Higher latency expected
686
+ headers: { 'X-Region': 'ap-southeast-1' }
807
687
  },
808
- commonFinalErrorAnalyzer: async () => true
688
+ commonAttempts: 7,
689
+ commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
809
690
  }
810
691
  }
811
692
  ]
812
693
  }
813
694
  );
814
-
815
- const healthReport = {
816
- timestamp: new Date().toISOString(),
817
- core: healthChecks.filter(r => r.groupId === 'core').every(r => r.success),
818
- auxiliary: healthChecks.filter(r => r.groupId === 'auxiliary').every(r => r.success),
819
- thirdParty: healthChecks.filter(r => r.groupId === 'third-party').every(r => r.success),
820
- overall: healthChecks.every(r => r.success) ? 'HEALTHY' : 'DEGRADED'
821
- };
822
-
823
- console.log('Health Report:', healthReport);
824
695
  ```
825
696
 
826
- ### 4. Polling for Async Job Completion
697
+ ## Real-World Examples
698
+
699
+ ### 1. Polling for Job Completion
827
700
 
828
701
  ```typescript
829
702
  const jobResult = await stableRequest({
830
703
  reqData: {
831
704
  hostname: 'api.example.com',
832
- path: '/jobs/123/status',
705
+ path: '/jobs/abc123/status'
833
706
  },
834
707
  resReq: true,
835
- attempts: 20,
836
- wait: 3000,
708
+ attempts: 20, // Poll up to 20 times
709
+ wait: 3000, // Wait 3 seconds between polls
837
710
  retryStrategy: RETRY_STRATEGIES.FIXED,
838
- responseAnalyzer: async (reqConfig, data) => {
839
- return data.status === 'completed';
711
+
712
+ responseAnalyzer: async ({ data }) => {
713
+ if (data.status === 'completed') {
714
+ console.log('Job completed!');
715
+ return true; // Success
716
+ }
717
+
718
+ if (data.status === 'failed') {
719
+ throw new Error(`Job failed: ${data.error}`);
720
+ }
721
+
722
+ console.log(`Job ${data.status}... ${data.progress}%`);
723
+ return false; // Keep polling
840
724
  },
841
- handleErrors: async (reqConfig, error) => {
842
- console.log(`Job not ready yet (attempt ${error.attempt})`);
725
+
726
+ handleErrors: async ({ errorLog }) => {
727
+ console.log(`Poll attempt ${errorLog.attempt}`);
843
728
  }
844
729
  });
730
+
731
+ console.log('Final result:', jobResult);
845
732
  ```
846
733
 
847
- ### 5. Resilient External API Integration
734
+ ### 2. Database Replication Lag
848
735
 
849
736
  ```typescript
850
- const weatherData = await stableRequest({
737
+ const expectedVersion = 42;
738
+
739
+ const data = await stableRequest({
851
740
  reqData: {
852
- hostname: 'api.weather.com',
853
- path: '/current',
854
- query: { city: 'London' },
855
- headers: { 'Authorization': `Bearer ${token}` }
741
+ hostname: 'replica.db.example.com',
742
+ path: '/records/123'
856
743
  },
857
744
  resReq: true,
858
- attempts: 5,
859
- wait: 2000,
860
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
861
- logAllErrors: true,
862
- handleErrors: async (reqConfig, error) => {
863
- logger.warn('Weather API retry', {
864
- attempt: error.attempt,
865
- isRetryable: error.isRetryable
866
- });
745
+ attempts: 10,
746
+ wait: 500,
747
+ retryStrategy: RETRY_STRATEGIES.LINEAR,
748
+
749
+ hookParams: {
750
+ responseAnalyzerParams: { expectedVersion }
751
+ },
752
+
753
+ responseAnalyzer: async ({ data, params }) => {
754
+ // Wait until replica catches up
755
+ if (data.version >= params.expectedVersion) {
756
+ return true;
757
+ }
758
+
759
+ console.log(`Replica at version ${data.version}, waiting for ${params.expectedVersion}`);
760
+ return false;
867
761
  }
868
762
  });
869
763
  ```
870
764
 
871
- ### 6. Database Replication Consistency Check
765
+ ### 3. Idempotent Payment Processing
872
766
 
873
767
  ```typescript
874
- const consistentData = await stableRequest({
768
+ const paymentResult = await stableRequest({
875
769
  reqData: {
876
- hostname: 'replica.db.example.com',
877
- path: '/records/456',
770
+ hostname: 'api.stripe.com',
771
+ path: '/v1/charges',
772
+ method: REQUEST_METHODS.POST,
773
+ headers: {
774
+ 'Authorization': 'Bearer sk_...',
775
+ 'Idempotency-Key': crypto.randomUUID() // Ensure idempotency
776
+ },
777
+ body: {
778
+ amount: 1000,
779
+ currency: 'usd',
780
+ source: 'tok_visa'
781
+ }
878
782
  },
879
783
  resReq: true,
880
- attempts: 10,
881
- wait: 500,
882
- retryStrategy: RETRY_STRATEGIES.LINEAR,
883
- responseAnalyzer: async (reqConfig, data) => {
884
- // Wait until replica has the latest version
885
- return data.version >= expectedVersion;
784
+ attempts: 5,
785
+ wait: 2000,
786
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
787
+
788
+ logAllErrors: true,
789
+ logAllSuccessfulAttempts: true,
790
+
791
+ handleErrors: async ({ errorLog }) => {
792
+ await paymentLogger.error({
793
+ attempt: errorLog.attempt,
794
+ error: errorLog.error,
795
+ isRetryable: errorLog.isRetryable
796
+ });
797
+ },
798
+
799
+ responseAnalyzer: async ({ data }) => {
800
+ // Validate payment succeeded
801
+ return data.status === 'succeeded' && data.paid === true;
802
+ },
803
+
804
+ finalErrorAnalyzer: async ({ error }) => {
805
+ // Alert team on payment failure
806
+ await alerting.critical('Payment processing failed', error);
807
+ return false; // Throw error
886
808
  }
887
809
  });
888
810
  ```
889
811
 
890
- ### 7. Batch User Creation
812
+ ### 4. Batch User Creation with Error Handling
891
813
 
892
814
  ```typescript
893
815
  const users = [
894
- { name: 'John Doe', email: 'john@example.com' },
895
- { name: 'Jane Smith', email: 'jane@example.com' },
896
- { name: 'Bob Johnson', email: 'bob@example.com' }
816
+ { name: 'Alice', email: 'alice@example.com' },
817
+ { name: 'Bob', email: 'bob@example.com' },
818
+ { name: 'Charlie', email: 'charlie@example.com' }
897
819
  ];
898
820
 
899
821
  const requests = users.map((user, index) => ({
900
- id: `create-user-${index}`,
822
+ id: `user-${index}`,
901
823
  requestOptions: {
902
824
  reqData: {
903
825
  body: user
@@ -908,283 +830,240 @@ const requests = users.map((user, index) => ({
908
830
 
909
831
  const results = await stableApiGateway(requests, {
910
832
  concurrentExecution: true,
833
+
834
+ commonRequestData: {
835
+ hostname: 'api.example.com',
836
+ path: '/users',
837
+ method: REQUEST_METHODS.POST,
838
+ headers: {
839
+ 'Content-Type': 'application/json'
840
+ }
841
+ },
842
+
911
843
  commonAttempts: 3,
912
844
  commonWait: 1000,
913
845
  commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
846
+ commonResReq: true,
914
847
  commonLogAllErrors: true,
915
- commonHandleErrors: async (reqConfig, error) => {
916
- console.log(`Failed to create user: ${error.error}`);
848
+
849
+ commonHandleErrors: async ({ reqData, errorLog }) => {
850
+ const user = reqData.data;
851
+ console.error(`Failed to create user ${user.name}: ${errorLog.error}`);
917
852
  },
918
- commonRequestData: {
919
- hostname: 'api.example.com',
920
- path: '/users',
921
- method: REQUEST_METHODS.POST
853
+
854
+ commonResponseAnalyzer: async ({ data }) => {
855
+ // Ensure user was created with an ID
856
+ return data?.id && data?.email;
922
857
  }
923
858
  });
924
859
 
925
860
  const successful = results.filter(r => r.success);
926
861
  const failed = results.filter(r => !r.success);
927
862
 
928
- console.log(`Created ${successful.length} users`);
929
- console.log(`Failed to create ${failed.length} users`);
863
+ console.log(`โœ“ Created ${successful.length} users`);
864
+ console.log(`โœ— Failed to create ${failed.length} users`);
865
+
866
+ failed.forEach(r => {
867
+ console.error(` - ${r.requestId}: ${r.error}`);
868
+ });
930
869
  ```
931
870
 
932
- ### 8. Batch Data Processing with Tiered Priority Groups
871
+ ### 5. Health Check Monitoring System
933
872
 
934
873
  ```typescript
935
- const dataItems = [
936
- { id: 1, data: 'critical-transaction', priority: 'high' },
937
- { id: 2, data: 'user-action', priority: 'medium' },
938
- { id: 3, data: 'analytics-event', priority: 'low' }
939
- ];
940
-
941
- const requests = dataItems.map(item => ({
942
- id: `item-${item.id}`,
943
- groupId: item.priority === 'high' ? 'high-priority' :
944
- item.priority === 'medium' ? 'medium-priority' : 'low-priority',
945
- requestOptions: {
946
- reqData: { body: item },
947
- resReq: true
948
- }
949
- }));
950
-
951
- const results = await stableApiGateway(requests, {
952
- concurrentExecution: true,
953
- commonRequestData: {
954
- hostname: 'processing.example.com',
955
- path: '/process',
956
- method: REQUEST_METHODS.POST
957
- },
958
-
959
- requestGroups: [
960
- {
961
- id: 'high-priority',
962
- commonConfig: {
963
- commonAttempts: 10,
964
- commonWait: 2000,
965
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
966
- commonRequestData: { headers: { 'X-Priority': 'high' } },
967
- commonResponseAnalyzer: async (reqData, data) => {
968
- return data?.processed && !data?.errors?.length && data?.validationStatus === 'passed';
874
+ const healthChecks = await stableApiGateway(
875
+ [
876
+ // Core services - must be healthy
877
+ { id: 'auth', groupId: 'core', requestOptions: { reqData: { hostname: 'auth.internal', path: '/health' } } },
878
+ { id: 'database', groupId: 'core', requestOptions: { reqData: { hostname: 'db.internal', path: '/health' } } },
879
+ { id: 'api', groupId: 'core', requestOptions: { reqData: { hostname: 'api.internal', path: '/health' } } },
880
+
881
+ // Optional services
882
+ { id: 'cache', groupId: 'optional', requestOptions: { reqData: { hostname: 'cache.internal', path: '/health' } } },
883
+ { id: 'search', groupId: 'optional', requestOptions: { reqData: { hostname: 'search.internal', path: '/health' } } }
884
+ ],
885
+ {
886
+ commonResReq: true,
887
+ concurrentExecution: true,
888
+
889
+ requestGroups: [
890
+ {
891
+ id: 'core',
892
+ commonConfig: {
893
+ commonAttempts: 5,
894
+ commonWait: 2000,
895
+ commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
896
+
897
+ commonResponseAnalyzer: async ({ data }) => {
898
+ // Core services need strict validation
899
+ return data?.status === 'healthy' &&
900
+ data?.uptime > 0 &&
901
+ data?.dependencies?.every(d => d.healthy);
902
+ },
903
+
904
+ commonHandleErrors: async ({ reqData, errorLog }) => {
905
+ // Alert on core service issues
906
+ await pagerDuty.trigger({
907
+ severity: 'critical',
908
+ service: reqData.baseURL,
909
+ message: errorLog.error
910
+ });
911
+ }
912
+ }
913
+ },
914
+ {
915
+ id: 'optional',
916
+ commonConfig: {
917
+ commonAttempts: 2,
918
+
919
+ commonResponseAnalyzer: async ({ data }) => {
920
+ // Optional services: basic check
921
+ return data?.status === 'ok';
922
+ },
923
+
924
+ commonFinalErrorAnalyzer: async ({ reqData, error }) => {
925
+ // Log but don't alert
926
+ console.warn(`Optional service ${reqData.baseURL} unhealthy`);
927
+ return true; // Don't throw
928
+ }
969
929
  }
970
930
  }
971
- },
972
- {
973
- id: 'medium-priority',
974
- commonConfig: {
975
- commonAttempts: 5,
976
- commonWait: 1000,
977
- commonRetryStrategy: RETRY_STRATEGIES.LINEAR,
978
- commonRequestData: { headers: { 'X-Priority': 'medium' } }
979
- }
980
- },
981
- {
982
- id: 'low-priority',
983
- commonConfig: {
984
- commonAttempts: 2,
985
- commonRequestData: { headers: { 'X-Priority': 'low' } },
986
- commonFinalErrorAnalyzer: async () => true // Accept failures
987
- }
988
- }
989
- ]
990
- });
931
+ ]
932
+ }
933
+ );
991
934
 
992
- // Report by priority
993
935
  const report = {
994
- high: results.filter(r => r.groupId === 'high-priority'),
995
- medium: results.filter(r => r.groupId === 'medium-priority'),
996
- low: results.filter(r => r.groupId === 'low-priority')
936
+ timestamp: new Date().toISOString(),
937
+ core: healthChecks.filter(r => r.groupId === 'core').every(r => r.success),
938
+ optional: healthChecks.filter(r => r.groupId === 'optional').every(r => r.success),
939
+ overall: healthChecks.every(r => r.success) ? 'HEALTHY' : 'DEGRADED'
997
940
  };
998
941
 
999
- console.log(`High: ${report.high.filter(r => r.success).length}/${report.high.length}`);
1000
- console.log(`Medium: ${report.medium.filter(r => r.success).length}/${report.medium.length}`);
1001
- console.log(`Low: ${report.low.filter(r => r.success).length}/${report.low.length}`);
942
+ console.log('System Health:', report);
1002
943
  ```
1003
944
 
1004
- ### 9. Rate-Limited API with Backoff
945
+ ## Complete API Reference
1005
946
 
1006
- ```typescript
1007
- const searchResults = await stableRequest({
1008
- reqData: {
1009
- hostname: 'api.ratelimited-service.com',
1010
- path: '/search',
1011
- query: { q: 'nodejs' }
1012
- },
1013
- resReq: true,
1014
- attempts: 10,
1015
- wait: 1000,
1016
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1017
- handleErrors: async (reqConfig, error) => {
1018
- if (error.type === 'HTTP_ERROR' && error.error.includes('429')) {
1019
- console.log('Rate limited, backing off...');
1020
- }
1021
- }
1022
- });
1023
- ```
947
+ ### `stableRequest(options)`
1024
948
 
1025
- ### 10. Sequential Workflow with Dependencies
949
+ | Option | Type | Default | Description |
950
+ |--------|------|---------|-------------|
951
+ | `reqData` | `REQUEST_DATA` | **required** | Request configuration |
952
+ | `resReq` | `boolean` | `false` | Return response data vs. just boolean |
953
+ | `attempts` | `number` | `1` | Max retry attempts |
954
+ | `wait` | `number` | `1000` | Base delay between retries (ms) |
955
+ | `maxAllowedWait` | `number` | `60000` | Maximum permitted wait duration between retries (ms) |
956
+ | `retryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Retry backoff strategy |
957
+ | `performAllAttempts` | `boolean` | `false` | Execute all attempts regardless |
958
+ | `logAllErrors` | `boolean` | `false` | Enable error logging |
959
+ | `logAllSuccessfulAttempts` | `boolean` | `false` | Enable success logging |
960
+ | `maxSerializableChars` | `number` | `1000` | Max chars for logs |
961
+ | `trialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Failure simulation |
962
+ | `hookParams` | `HookParams` | `{}` | Custom parameters for hooks |
963
+ | `responseAnalyzer` | `function` | `() => true` | Validate response content |
964
+ | `handleErrors` | `function` | `console.log` | Error handler |
965
+ | `handleSuccessfulAttemptData` | `function` | `console.log` | Success handler |
966
+ | `finalErrorAnalyzer` | `function` | `() => false` | Final error handler |
967
+
968
+ ### REQUEST_DATA
1026
969
 
1027
970
  ```typescript
1028
- const workflowSteps = [
1029
- {
1030
- id: 'step-1-init',
1031
- requestOptions: {
1032
- reqData: { path: '/init' },
1033
- resReq: true
1034
- }
1035
- },
1036
- {
1037
- id: 'step-2-process',
1038
- requestOptions: {
1039
- reqData: { path: '/process' },
1040
- resReq: true,
1041
- responseAnalyzer: async (reqConfig, data) => {
1042
- return data.status === 'completed';
1043
- }
1044
- }
1045
- },
1046
- {
1047
- id: 'step-3-finalize',
1048
- requestOptions: {
1049
- reqData: { path: '/finalize' },
1050
- resReq: true
1051
- }
1052
- }
1053
- ];
1054
-
1055
- const results = await stableApiGateway(workflowSteps, {
1056
- concurrentExecution: false,
1057
- stopOnFirstError: true,
1058
- commonRequestData: {
1059
- hostname: 'workflow.example.com',
1060
- method: REQUEST_METHODS.POST,
1061
- body: { workflowId: 'wf-123' }
1062
- },
1063
- commonAttempts: 5,
1064
- commonWait: 2000,
1065
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1066
- });
1067
-
1068
- if (results.every(r => r.success)) {
1069
- console.log('Workflow completed successfully');
1070
- } else {
1071
- console.error('Workflow failed at step:', results.findIndex(r => !r.success) + 1);
971
+ interface REQUEST_DATA<RequestDataType = any> {
972
+ hostname: string; // Required
973
+ protocol?: 'http' | 'https'; // Default: 'https'
974
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; // Default: 'GET'
975
+ path?: `/${string}`; // Default: ''
976
+ port?: number; // Default: 443
977
+ headers?: Record<string, any>; // Default: {}
978
+ body?: RequestDataType; // Request body
979
+ query?: Record<string, any>; // Query parameters
980
+ timeout?: number; // Default: 15000ms
981
+ signal?: AbortSignal; // For cancellation
1072
982
  }
1073
983
  ```
1074
984
 
1075
- ## Advanced Patterns
985
+ ### `stableApiGateway(requests, options)`
986
+
987
+ | Option | Type | Default | Description |
988
+ |--------|------|---------|-------------|
989
+ | `concurrentExecution` | `boolean` | `true` | Execute requests concurrently or sequentially |
990
+ | `stopOnFirstError` | `boolean` | `false` | Stop execution on first error (sequential only) |
991
+ | `requestGroups` | `RequestGroup[]` | `[]` | Define groups with their own common configurations |
992
+ | `commonAttempts` | `number` | `1` | Default attempts for all requests |
993
+ | `commonPerformAllAttempts` | `boolean` | `false` | Default performAllAttempts for all requests |
994
+ | `commonWait` | `number` | `1000` | Default wait time for all requests |
995
+ | `commonMaxAllowedWait` | `number` | `60000` | Default maximum permitted wait time for all requests |
996
+ | `commonRetryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Default retry strategy for all requests |
997
+ | `commonLogAllErrors` | `boolean` | `false` | Default error logging for all requests |
998
+ | `commonLogAllSuccessfulAttempts` | `boolean` | `false` | Default success logging for all requests |
999
+ | `commonMaxSerializableChars` | `number` | `1000` | Default max chars for serialization |
1000
+ | `commonTrialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Default trial mode for all requests |
1001
+ | `commonResponseAnalyzer` | `function` | `() => true` | Default response analyzer for all requests |
1002
+ | `commonResReq` | `boolean` | `false` | Default resReq for all requests |
1003
+ | `commonFinalErrorAnalyzer` | `function` | `() => false` | Default final error analyzer for all requests |
1004
+ | `commonHandleErrors` | `function` | console.log | Default error handler for all requests |
1005
+ | `commonHandleSuccessfulAttemptData` | `function` | console.log | Default success handler for all requests |
1006
+ | `commonRequestData` | `Partial<REQUEST_DATA>` | `{ hostname: '' }` | Common set of request options for each request |
1007
+ | `commonHookParams` | `HookParams` | `{ }` | Common options for each request hook |
1076
1008
 
1077
- ### Circuit Breaker Pattern
1009
+ ### Hooks Reference
1078
1010
 
1079
- ```typescript
1080
- let failureCount = 0;
1081
- const CIRCUIT_THRESHOLD = 5;
1011
+ #### responseAnalyzer
1082
1012
 
1083
- async function resilientRequest(endpoint: string) {
1084
- if (failureCount >= CIRCUIT_THRESHOLD) {
1085
- throw new Error('Circuit breaker open');
1086
- }
1013
+ **Purpose:** Validate response content, retry even on HTTP 200
1087
1014
 
1088
- try {
1089
- const result = await stableRequest({
1090
- reqData: { hostname: 'api.example.com', path: endpoint },
1091
- resReq: true,
1092
- attempts: 3,
1093
- handleErrors: async () => {
1094
- failureCount++;
1095
- }
1096
- });
1097
- failureCount = 0;
1098
- return result;
1099
- } catch (error) {
1100
- if (failureCount >= CIRCUIT_THRESHOLD) {
1101
- console.log('Circuit breaker activated');
1102
- setTimeout(() => { failureCount = 0; }, 60000);
1103
- }
1104
- throw error;
1105
- }
1015
+ ```typescript
1016
+ responseAnalyzer: async ({ reqData, data, trialMode, params }) => {
1017
+ // Return true if valid, false to retry
1018
+ return data.status === 'ready';
1106
1019
  }
1107
1020
  ```
1108
1021
 
1109
- ### Dynamic Request Configuration with Groups
1022
+ #### handleErrors
1023
+
1024
+ **Purpose:** Monitor and log failed attempts
1110
1025
 
1111
1026
  ```typescript
1112
- const endpoints = await getEndpointsFromConfig();
1027
+ handleErrors: async ({ reqData, errorLog, maxSerializableChars }) => {
1028
+ await logger.error({
1029
+ url: reqData.url,
1030
+ attempt: errorLog.attempt,
1031
+ error: errorLog.error
1032
+ });
1033
+ }
1034
+ ```
1113
1035
 
1114
- const requests = endpoints.map(endpoint => ({
1115
- id: endpoint.id,
1116
- groupId: endpoint.tier, // 'critical', 'standard', or 'optional'
1117
- requestOptions: {
1118
- reqData: {
1119
- hostname: endpoint.hostname,
1120
- path: endpoint.path,
1121
- method: endpoint.method,
1122
- ...(endpoint.auth && {
1123
- headers: { Authorization: `Bearer ${endpoint.auth}` }
1124
- })
1125
- },
1126
- resReq: true
1127
- }
1128
- }));
1036
+ #### handleSuccessfulAttemptData
1129
1037
 
1130
- const results = await stableApiGateway(requests, {
1131
- concurrentExecution: true,
1132
- commonWait: 1000,
1133
-
1134
- requestGroups: [
1135
- {
1136
- id: 'critical',
1137
- commonConfig: {
1138
- commonAttempts: 10,
1139
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
1140
- }
1141
- },
1142
- {
1143
- id: 'standard',
1144
- commonConfig: {
1145
- commonAttempts: 5,
1146
- commonRetryStrategy: RETRY_STRATEGIES.LINEAR
1147
- }
1148
- },
1149
- {
1150
- id: 'optional',
1151
- commonConfig: {
1152
- commonAttempts: 2,
1153
- commonFinalErrorAnalyzer: async () => true
1154
- }
1155
- }
1156
- ]
1157
- });
1038
+ **Purpose:** Monitor and log successful attempts
1039
+
1040
+ ```typescript
1041
+ handleSuccessfulAttemptData: async ({ reqData, successfulAttemptData, maxSerializableChars }) => {
1042
+ await analytics.track({
1043
+ url: reqData.url,
1044
+ duration: successfulAttemptData.executionTime
1045
+ });
1046
+ }
1158
1047
  ```
1159
1048
 
1160
- ### Conditional Retry Based on Response
1049
+ #### finalErrorAnalyzer
1050
+
1051
+ **Purpose:** Handle final error after all retries exhausted
1161
1052
 
1162
1053
  ```typescript
1163
- await stableRequest({
1164
- reqData: {
1165
- hostname: 'api.example.com',
1166
- path: '/data',
1167
- },
1168
- resReq: true,
1169
- attempts: 5,
1170
- responseAnalyzer: async (reqConfig, data) => {
1171
- if (!data.complete) {
1172
- console.log('Data incomplete, retrying...');
1173
- return false;
1174
- }
1175
-
1176
- if (data.error) {
1177
- throw new Error('Invalid data, cannot retry');
1178
- }
1179
-
1180
- return true;
1054
+ finalErrorAnalyzer: async ({ reqData, error, trialMode, params }) => {
1055
+ // Return true to suppress error (return false)
1056
+ // Return false to throw error
1057
+ if (error.message.includes('404')) {
1058
+ return true; // Treat as non-critical
1181
1059
  }
1182
- });
1060
+ return false; // Throw
1061
+ }
1183
1062
  ```
1184
1063
 
1185
1064
  ## TypeScript Support
1186
1065
 
1187
- Full TypeScript support with generic types for request and response data:
1066
+ Fully typed with generics:
1188
1067
 
1189
1068
  ```typescript
1190
1069
  interface CreateUserRequest {
@@ -1209,71 +1088,33 @@ const user = await stableRequest<CreateUserRequest, UserResponse>({
1209
1088
  email: 'john@example.com'
1210
1089
  }
1211
1090
  },
1212
- resReq: true,
1213
- attempts: 3
1091
+ resReq: true
1214
1092
  });
1215
1093
 
1216
- // user is fully typed as UserResponse
1217
- console.log(user.id);
1094
+ // user is typed as UserResponse
1095
+ console.log(user.id); // TypeScript knows this exists
1218
1096
  ```
1219
1097
 
1220
- ## Comparison with Similar Libraries
1221
-
1222
- ### vs. axios-retry
1223
-
1224
- | Feature | stable-request | axios-retry |
1225
- |---------|----------------|-------------|
1226
- | **Content validation** | โœ… Full support with `responseAnalyzer` | โŒ Only HTTP status codes |
1227
- | **Batch processing** | โœ… Built-in `stableApiGateway` | โŒ Manual implementation needed |
1228
- | **Request grouping** | โœ… Hierarchical configuration | โŒ No grouping support |
1229
- | **Trial mode** | โœ… Built-in failure simulation | โŒ No testing utilities |
1230
- | **Retry strategies** | โœ… Fixed, Linear, Exponential | โœ… Exponential only |
1231
- | **Observability** | โœ… Granular hooks for every attempt | โš ๏ธ Limited |
1232
- | **Final error analysis** | โœ… Custom error handling | โŒ No |
1233
-
1234
- ### vs. got
1235
-
1236
- | Feature | stable-request | got |
1237
- |---------|----------------|-----|
1238
- | **Built on Axios** | โœ… Leverages Axios ecosystem | โŒ Standalone client |
1239
- | **Content validation** | โœ… Response analyzer | โŒ Only HTTP errors |
1240
- | **Batch processing** | โœ… Built-in gateway with grouping | โŒ Manual implementation |
1241
- | **Request grouping** | โœ… Multi-tier configuration | โŒ No grouping |
1242
- | **Trial mode** | โœ… Simulation for testing | โŒ No |
1243
- | **Retry strategies** | โœ… 3 configurable strategies | โœ… Exponential with jitter |
1244
-
1245
- ### vs. p-retry + axios
1246
-
1247
- | Feature | stable-request | p-retry + axios |
1248
- |---------|----------------|-----------------|
1249
- | **All-in-one** | โœ… Single package | โŒ Requires multiple packages |
1250
- | **HTTP-aware** | โœ… Built for HTTP | โŒ Generic retry wrapper |
1251
- | **Content validation** | โœ… Built-in | โŒ Manual implementation |
1252
- | **Batch processing** | โœ… Built-in with groups | โŒ Manual implementation |
1253
- | **Request grouping** | โœ… Native support | โŒ No grouping |
1254
- | **Observability** | โœ… Request-specific hooks | โš ๏ธ Generic callbacks |
1255
-
1256
1098
  ## Best Practices
1257
1099
 
1258
- 1. **Use exponential backoff for rate-limited APIs** to avoid overwhelming the server
1259
- 2. **Organize related requests into groups** for easier configuration management
1260
- 3. **Set reasonable timeout values** in `reqData.timeout` to prevent hanging requests
1261
- 4. **Implement responseAnalyzer** for APIs that return 200 OK with error details in the body
1262
- 5. **Use concurrent execution** in `stableApiGateway` for independent requests
1263
- 6. **Use sequential execution** when requests have dependencies or need to maintain order
1264
- 7. **Leverage request groups** to differentiate between critical and optional services
1265
- 8. **Use finalErrorAnalyzer** for graceful degradation in non-critical paths
1266
- 9. **Enable logging in development** with `logAllErrors` and `logAllSuccessfulAttempts`
1267
- 10. **Use Trial Mode** to test your error handling without relying on actual failures
1268
- 11. **Group requests by region or service tier** for better monitoring and configuration
1100
+ 1. **Start simple** - Use basic retries first, add hooks as needed
1101
+ 2. **Use exponential backoff** for rate-limited APIs
1102
+ 3. **Validate response content** with `responseAnalyzer` for eventually-consistent systems
1103
+ 4. **Monitor everything** with `handleErrors` and `handleSuccessfulAttemptData`
1104
+ 5. **Group related requests** by service tier, region, or priority
1105
+ 6. **Handle failures gracefully** with `finalErrorAnalyzer` for non-critical features
1106
+ 7. **Test with trial mode** before deploying to production
1107
+ 8. **Set appropriate timeouts** to prevent hanging requests
1108
+ 9. **Use idempotency keys** for payment/financial operations
1109
+ 10. **Log contextual information** in your hooks for debugging
1269
1110
 
1270
1111
  ## License
1271
1112
 
1272
1113
  MIT ยฉ Manish Varma
1273
1114
 
1274
- [![npm version](https://img.shields.io/npm/v/stable-request.svg)](https://www.npmjs.com/package/%40emmvish%2Fstable-request)
1115
+
1275
1116
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
1276
1117
 
1277
1118
  ---
1278
1119
 
1279
- **Made with โค๏ธ for developers who are sick of integrating apps with unreliable APIs**
1120
+ **Made with โค๏ธ for developers integrating with unreliable APIs**