@emmvish/stable-request 1.0.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +934 -0
  3. package/dist/core/index.d.ts +3 -0
  4. package/dist/core/index.d.ts.map +1 -0
  5. package/dist/core/index.js +3 -0
  6. package/dist/core/index.js.map +1 -0
  7. package/dist/core/send-stable-request.d.ts +3 -0
  8. package/dist/core/send-stable-request.d.ts.map +1 -0
  9. package/dist/core/send-stable-request.js +107 -0
  10. package/dist/core/send-stable-request.js.map +1 -0
  11. package/dist/core/stable-api-gateway.d.ts +3 -0
  12. package/dist/core/stable-api-gateway.d.ts.map +1 -0
  13. package/dist/core/stable-api-gateway.js +30 -0
  14. package/dist/core/stable-api-gateway.js.map +1 -0
  15. package/dist/core.d.ts +3 -0
  16. package/dist/core.d.ts.map +1 -0
  17. package/dist/core.js +101 -0
  18. package/dist/core.js.map +1 -0
  19. package/dist/enums/index.d.ts +28 -0
  20. package/dist/enums/index.d.ts.map +1 -0
  21. package/dist/enums/index.js +33 -0
  22. package/dist/enums/index.js.map +1 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +4 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/test.d.ts +2 -0
  28. package/dist/test.d.ts.map +1 -0
  29. package/dist/test.js +5 -0
  30. package/dist/test.js.map +1 -0
  31. package/dist/tsconfig.tsbuildinfo +1 -0
  32. package/dist/types/index.d.ts +97 -0
  33. package/dist/types/index.d.ts.map +1 -0
  34. package/dist/types/index.js +2 -0
  35. package/dist/types/index.js.map +1 -0
  36. package/dist/utilities/delay.d.ts +2 -0
  37. package/dist/utilities/delay.d.ts.map +1 -0
  38. package/dist/utilities/delay.js +8 -0
  39. package/dist/utilities/delay.js.map +1 -0
  40. package/dist/utilities/execute-concurrently.d.ts +3 -0
  41. package/dist/utilities/execute-concurrently.d.ts.map +1 -0
  42. package/dist/utilities/execute-concurrently.js +36 -0
  43. package/dist/utilities/execute-concurrently.js.map +1 -0
  44. package/dist/utilities/execute-sequentially.d.ts +3 -0
  45. package/dist/utilities/execute-sequentially.d.ts.map +1 -0
  46. package/dist/utilities/execute-sequentially.js +32 -0
  47. package/dist/utilities/execute-sequentially.js.map +1 -0
  48. package/dist/utilities/generate-axios-request-config.d.ts +12 -0
  49. package/dist/utilities/generate-axios-request-config.d.ts.map +1 -0
  50. package/dist/utilities/generate-axios-request-config.js +14 -0
  51. package/dist/utilities/generate-axios-request-config.js.map +1 -0
  52. package/dist/utilities/get-new-delay-time.d.ts +3 -0
  53. package/dist/utilities/get-new-delay-time.d.ts.map +1 -0
  54. package/dist/utilities/get-new-delay-time.js +15 -0
  55. package/dist/utilities/get-new-delay-time.js.map +1 -0
  56. package/dist/utilities/index.d.ts +12 -0
  57. package/dist/utilities/index.d.ts.map +1 -0
  58. package/dist/utilities/index.js +12 -0
  59. package/dist/utilities/index.js.map +1 -0
  60. package/dist/utilities/is-retryable-error.d.ts +4 -0
  61. package/dist/utilities/is-retryable-error.d.ts.map +1 -0
  62. package/dist/utilities/is-retryable-error.js +22 -0
  63. package/dist/utilities/is-retryable-error.js.map +1 -0
  64. package/dist/utilities/prepare-api-request-options.d.ts +3 -0
  65. package/dist/utilities/prepare-api-request-options.d.ts.map +1 -0
  66. package/dist/utilities/prepare-api-request-options.js +19 -0
  67. package/dist/utilities/prepare-api-request-options.js.map +1 -0
  68. package/dist/utilities/req-fn.d.ts +4 -0
  69. package/dist/utilities/req-fn.d.ts.map +1 -0
  70. package/dist/utilities/req-fn.js +67 -0
  71. package/dist/utilities/req-fn.js.map +1 -0
  72. package/dist/utilities/safely-execute-unknown-function.d.ts +2 -0
  73. package/dist/utilities/safely-execute-unknown-function.d.ts.map +1 -0
  74. package/dist/utilities/safely-execute-unknown-function.js +8 -0
  75. package/dist/utilities/safely-execute-unknown-function.js.map +1 -0
  76. package/dist/utilities/safely-execute-unknown-functions.d.ts +2 -0
  77. package/dist/utilities/safely-execute-unknown-functions.d.ts.map +1 -0
  78. package/dist/utilities/safely-execute-unknown-functions.js +8 -0
  79. package/dist/utilities/safely-execute-unknown-functions.js.map +1 -0
  80. package/dist/utilities/safely-stringify.d.ts +4 -0
  81. package/dist/utilities/safely-stringify.d.ts.map +1 -0
  82. package/dist/utilities/safely-stringify.js +13 -0
  83. package/dist/utilities/safely-stringify.js.map +1 -0
  84. package/dist/utilities/validate-trial-mode-probabilities.d.ts +3 -0
  85. package/dist/utilities/validate-trial-mode-probabilities.d.ts.map +1 -0
  86. package/dist/utilities/validate-trial-mode-probabilities.js +13 -0
  87. package/dist/utilities/validate-trial-mode-probabilities.js.map +1 -0
  88. package/package.json +45 -0
package/README.md ADDED
@@ -0,0 +1,934 @@
1
+ ## stable-request
2
+
3
+ A robust HTTP request wrapper built on top of Axios with intelligent retry strategies, content validation, batch processing, and comprehensive observability features.
4
+
5
+ ## Why stable-request?
6
+
7
+ Most HTTP client libraries only retry on network failures or specific HTTP status codes. **stable-request** goes further by providing:
8
+
9
+ - ✅ **Content-aware retries** - Validate response content and retry even on successful HTTP responses
10
+ - 🚀 **Batch processing** - Execute multiple requests concurrently or sequentially with shared configuration
11
+ - 🧪 **Trial mode** - Simulate failures to test your retry logic without depending on real network instability
12
+ - 📊 **Granular observability** - Monitor every attempt with detailed hooks
13
+ - ⚡ **Multiple retry strategies** - Fixed, linear, or exponential backoff
14
+ - 🎯 **Flexible error handling** - Custom error analysis and graceful degradation
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @emmvish/stable-request
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ### Single Request
25
+
26
+ ```typescript
27
+ import { stableRequest, REQUEST_METHODS, RETRY_STRATEGIES } from '@emmvish/stable-request';
28
+
29
+ // Simple GET request with automatic retries
30
+ const response = await stableRequest({
31
+ reqData: {
32
+ hostname: 'api.example.com',
33
+ path: '/users',
34
+ method: REQUEST_METHODS.GET
35
+ },
36
+ resReq: true,
37
+ attempts: 3,
38
+ wait: 1000,
39
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
40
+ });
41
+ ```
42
+
43
+ ### Batch Requests
44
+
45
+ ```typescript
46
+ import { stableApiGateway } from '@emmvish/stable-request';
47
+
48
+ const requests = [
49
+ {
50
+ id: 'user-1',
51
+ requestOptions: {
52
+ reqData: { hostname: 'api.example.com', path: '/users/1' },
53
+ resReq: true
54
+ }
55
+ },
56
+ {
57
+ id: 'user-2',
58
+ requestOptions: {
59
+ reqData: { hostname: 'api.example.com', path: '/users/2' },
60
+ resReq: true
61
+ }
62
+ }
63
+ ];
64
+
65
+ const results = await stableApiGateway(requests, {
66
+ concurrentExecution: true,
67
+ commonAttempts: 3,
68
+ commonWait: 1000
69
+ });
70
+ ```
71
+
72
+ ## Core Features
73
+
74
+ ### 1. Content-Aware Retries with `stableRequest`
75
+
76
+ Unlike traditional retry mechanisms, `stableRequest` validates the **content** of successful responses and retries if needed.
77
+
78
+ ```typescript
79
+ await stableRequest({
80
+ reqData: {
81
+ hostname: 'api.example.com',
82
+ path: '/data',
83
+ },
84
+ resReq: true,
85
+ attempts: 5,
86
+ wait: 2000,
87
+ // Retry even on HTTP 200 if data is invalid
88
+ responseAnalyzer: async (reqConfig, data) => {
89
+ return data?.status === 'ready' && data?.items?.length > 0;
90
+ }
91
+ });
92
+ ```
93
+
94
+ **Use Cases:**
95
+ - Wait for async processing to complete
96
+ - Ensure data quality before proceeding
97
+ - Handle eventually-consistent systems
98
+ - Validate complex business rules in responses
99
+
100
+ ### 2. Batch Processing with `stableApiGateway`
101
+
102
+ Process multiple requests efficiently with shared configuration and execution strategies.
103
+
104
+ #### Concurrent Execution
105
+
106
+ ```typescript
107
+ import { stableApiGateway, RETRY_STRATEGIES, REQUEST_METHODS } from '@emmvish/stable-request';
108
+
109
+ const requests = [
110
+ {
111
+ id: 'create-user-1',
112
+ requestOptions: {
113
+ reqData: {
114
+ hostname: 'api.example.com',
115
+ path: '/users',
116
+ method: REQUEST_METHODS.POST,
117
+ body: { name: 'John Doe', email: 'john@example.com' }
118
+ }
119
+ }
120
+ },
121
+ {
122
+ id: 'create-user-2',
123
+ requestOptions: {
124
+ reqData: {
125
+ hostname: 'api.example.com',
126
+ path: '/users',
127
+ method: REQUEST_METHODS.POST,
128
+ body: { name: 'Jane Smith', email: 'jane@example.com' }
129
+ }
130
+ }
131
+ }
132
+ ];
133
+
134
+ const results = await stableApiGateway(requests, {
135
+ concurrentExecution: true,
136
+ commonResReq: true,
137
+ commonAttempts: 3,
138
+ commonWait: 1000,
139
+ commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
140
+ });
141
+
142
+ // Process results
143
+ results.forEach(result => {
144
+ if (result.success) {
145
+ console.log(`${result.id} succeeded:`, result.data);
146
+ } else {
147
+ console.error(`${result.id} failed:`, result.error);
148
+ }
149
+ });
150
+ ```
151
+
152
+ #### Sequential Execution
153
+
154
+ ```typescript
155
+ const results = await stableApiGateway(requests, {
156
+ concurrentExecution: false,
157
+ stopOnFirstError: true,
158
+ commonAttempts: 3
159
+ });
160
+ ```
161
+
162
+ ### 3. Trial Mode - Test Your Retry Logic
163
+
164
+ Simulate request and retry failures with configurable probabilities.
165
+
166
+ ```typescript
167
+ await stableRequest({
168
+ reqData: {
169
+ hostname: 'api.example.com',
170
+ path: '/test',
171
+ },
172
+ resReq: true,
173
+ attempts: 5,
174
+ trialMode: {
175
+ enabled: true,
176
+ reqFailureProbability: 0.3, // 30% chance each request fails
177
+ retryFailureProbability: 0.2 // 20% chance retry is marked non-retryable
178
+ },
179
+ logAllErrors: true
180
+ });
181
+ ```
182
+
183
+ **Use Cases:**
184
+ - Integration testing
185
+ - Chaos engineering
186
+ - Validating monitoring and alerting
187
+ - Testing circuit breaker patterns
188
+
189
+ ### 4. Multiple Retry Strategies
190
+
191
+ Choose the backoff strategy that fits your use case.
192
+
193
+ ```typescript
194
+ import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
195
+
196
+ // Fixed delay: 1s, 1s, 1s, 1s...
197
+ await stableRequest({
198
+ reqData: { hostname: 'api.example.com', path: '/data' },
199
+ resReq: true,
200
+ attempts: 5,
201
+ wait: 1000,
202
+ retryStrategy: RETRY_STRATEGIES.FIXED
203
+ });
204
+
205
+ // Linear backoff: 1s, 2s, 3s, 4s...
206
+ await stableRequest({
207
+ reqData: { hostname: 'api.example.com', path: '/data' },
208
+ resReq: true,
209
+ attempts: 5,
210
+ wait: 1000,
211
+ retryStrategy: RETRY_STRATEGIES.LINEAR
212
+ });
213
+
214
+ // Exponential backoff: 1s, 2s, 4s, 8s...
215
+ await stableRequest({
216
+ reqData: { hostname: 'api.example.com', path: '/data' },
217
+ resReq: true,
218
+ attempts: 5,
219
+ wait: 1000,
220
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
221
+ });
222
+ ```
223
+
224
+ ### 5. Comprehensive Observability
225
+
226
+ Monitor every request attempt with detailed logging hooks.
227
+
228
+ ```typescript
229
+ await stableRequest({
230
+ reqData: {
231
+ hostname: 'api.example.com',
232
+ path: '/critical-endpoint',
233
+ },
234
+ resReq: true,
235
+ attempts: 3,
236
+ logAllErrors: true,
237
+ handleErrors: async (reqConfig, errorLog) => {
238
+ // Custom error handling - send to monitoring service
239
+ await monitoringService.logError({
240
+ endpoint: reqConfig.url,
241
+ attempt: errorLog.attempt,
242
+ error: errorLog.error,
243
+ isRetryable: errorLog.isRetryable,
244
+ type: errorLog.type, // 'HTTP_ERROR' or 'INVALID_CONTENT'
245
+ timestamp: errorLog.timestamp,
246
+ executionTime: errorLog.executionTime,
247
+ statusCode: errorLog.statusCode
248
+ });
249
+ },
250
+ logAllSuccessfulAttempts: true,
251
+ handleSuccessfulAttemptData: async (reqConfig, successData) => {
252
+ // Track successful attempts
253
+ analytics.track('request_success', {
254
+ endpoint: reqConfig.url,
255
+ attempt: successData.attempt,
256
+ executionTime: successData.executionTime,
257
+ statusCode: successData.statusCode
258
+ });
259
+ }
260
+ });
261
+ ```
262
+
263
+ ### 6. Smart Retry Logic
264
+
265
+ Automatically retries on common transient errors:
266
+
267
+ - HTTP 5xx (Server Errors)
268
+ - HTTP 408 (Request Timeout)
269
+ - HTTP 429 (Too Many Requests)
270
+ - HTTP 409 (Conflict)
271
+ - Network errors: `ECONNRESET`, `ETIMEDOUT`, `ECONNREFUSED`, `ENOTFOUND`, `EAI_AGAIN`
272
+
273
+ ```typescript
274
+ await stableRequest({
275
+ reqData: {
276
+ hostname: 'unreliable-api.com',
277
+ path: '/data',
278
+ },
279
+ resReq: true,
280
+ attempts: 5,
281
+ wait: 2000,
282
+ retryStrategy: RETRY_STRATEGIES.LINEAR
283
+ // Automatically retries on transient failures
284
+ });
285
+ ```
286
+
287
+ ### 7. Final Error Analysis
288
+
289
+ Decide whether to throw or return false based on error analysis.
290
+
291
+ ```typescript
292
+ const result = await stableRequest({
293
+ reqData: {
294
+ hostname: 'api.example.com',
295
+ path: '/optional-data',
296
+ },
297
+ resReq: true,
298
+ attempts: 3,
299
+ finalErrorAnalyzer: async (reqConfig, error) => {
300
+ // Return true to suppress error and return false instead of throwing
301
+ if (error.message.includes('404')) {
302
+ console.log('Resource not found, treating as non-critical');
303
+ return true; // Don't throw, return false
304
+ }
305
+ return false; // Throw the error
306
+ }
307
+ });
308
+
309
+ // result will be false if finalErrorAnalyzer returned true
310
+ if (result === false) {
311
+ console.log('Request failed but was handled gracefully');
312
+ }
313
+ ```
314
+
315
+ ### 8. Request Cancellation Support
316
+
317
+ Support for AbortController to cancel requests.
318
+
319
+ ```typescript
320
+ const controller = new AbortController();
321
+
322
+ setTimeout(() => controller.abort(), 5000);
323
+
324
+ try {
325
+ await stableRequest({
326
+ reqData: {
327
+ hostname: 'api.example.com',
328
+ path: '/slow-endpoint',
329
+ signal: controller.signal
330
+ },
331
+ resReq: true,
332
+ attempts: 3
333
+ });
334
+ } catch (error) {
335
+ // Request was cancelled
336
+ }
337
+ ```
338
+
339
+ ### 9. Perform All Attempts Mode
340
+
341
+ Execute all retry attempts regardless of success, useful for warm-up scenarios.
342
+
343
+ ```typescript
344
+ await stableRequest({
345
+ reqData: {
346
+ hostname: 'api.example.com',
347
+ path: '/cache-warmup',
348
+ },
349
+ attempts: 5,
350
+ performAllAttempts: true, // Always performs all 5 attempts
351
+ wait: 1000
352
+ });
353
+ ```
354
+
355
+ ## API Reference
356
+
357
+ ### `stableRequest<RequestDataType, ResponseDataType>(options)`
358
+
359
+ Execute a single HTTP request with retry logic.
360
+
361
+ #### Configuration Options
362
+
363
+ | Option | Type | Default | Description |
364
+ |--------|------|---------|-------------|
365
+ | `reqData` | `REQUEST_DATA` | **required** | Request configuration (hostname, path, method, etc.) |
366
+ | `resReq` | `boolean` | `false` | Return response data instead of just success boolean |
367
+ | `attempts` | `number` | `1` | Maximum number of retry attempts |
368
+ | `wait` | `number` | `1000` | Base delay in milliseconds between retries |
369
+ | `retryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Retry strategy: `'fixed'`, `'linear'`, or `'exponential'` |
370
+ | `responseAnalyzer` | `function` | `() => true` | Validates response content, return false to retry |
371
+ | `performAllAttempts` | `boolean` | `false` | Execute all attempts regardless of success |
372
+ | `logAllErrors` | `boolean` | `false` | Enable error logging for all failed attempts |
373
+ | `handleErrors` | `function` | console.log | Custom error handler |
374
+ | `logAllSuccessfulAttempts` | `boolean` | `false` | Log all successful attempts |
375
+ | `handleSuccessfulAttemptData` | `function` | console.log | Custom success handler |
376
+ | `maxSerializableChars` | `number` | `1000` | Max characters for serialized logs |
377
+ | `finalErrorAnalyzer` | `function` | `() => false` | Analyze final error, return true to suppress throwing |
378
+ | `trialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Simulate failures for testing |
379
+
380
+ #### Request Data Configuration
381
+
382
+ ```typescript
383
+ interface REQUEST_DATA<RequestDataType = any> {
384
+ hostname: string; // Required
385
+ protocol?: 'http' | 'https'; // Default: 'https'
386
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; // Default: 'GET'
387
+ path?: `/${string}`; // Default: ''
388
+ port?: number; // Default: 443
389
+ headers?: Record<string, any>; // Default: {}
390
+ body?: RequestDataType; // Request body
391
+ query?: Record<string, any>; // Query parameters
392
+ timeout?: number; // Default: 15000ms
393
+ signal?: AbortSignal; // For request cancellation
394
+ }
395
+ ```
396
+
397
+ ### `stableApiGateway<RequestDataType, ResponseDataType>(requests, options)`
398
+
399
+ Execute multiple HTTP requests with shared configuration.
400
+
401
+ #### Gateway Configuration Options
402
+
403
+ | Option | Type | Default | Description |
404
+ |--------|------|---------|-------------|
405
+ | `concurrentExecution` | `boolean` | `true` | Execute requests concurrently or sequentially |
406
+ | `stopOnFirstError` | `boolean` | `false` | Stop execution on first error (sequential only) |
407
+ | `commonAttempts` | `number` | `1` | Default attempts for all requests |
408
+ | `commonPerformAllAttempts` | `boolean` | `false` | Default performAllAttempts for all requests |
409
+ | `commonWait` | `number` | `1000` | Default wait time for all requests |
410
+ | `commonRetryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Default retry strategy for all requests |
411
+ | `commonLogAllErrors` | `boolean` | `false` | Default error logging for all requests |
412
+ | `commonLogAllSuccessfulAttempts` | `boolean` | `false` | Default success logging for all requests |
413
+ | `commonMaxSerializableChars` | `number` | `1000` | Default max chars for serialization |
414
+ | `commonTrialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Default trial mode for all requests |
415
+ | `commonResponseAnalyzer` | `function` | `() => true` | Default response analyzer for all requests |
416
+ | `commonResReq` | `boolean` | `false` | Default resReq for all requests |
417
+ | `commonFinalErrorAnalyzer` | `function` | `() => false` | Default final error analyzer for all requests |
418
+ | `commonHandleErrors` | `function` | console.log | Default error handler for all requests |
419
+ | `commonHandleSuccessfulAttemptData` | `function` | console.log | Default success handler for all requests |
420
+
421
+ #### Request Format
422
+
423
+ ```typescript
424
+ interface API_GATEWAY_REQUEST<RequestDataType, ResponseDataType> {
425
+ id: string; // Unique identifier for the request
426
+ requestOptions: STABLE_REQUEST<RequestDataType, ResponseDataType>;
427
+ }
428
+ ```
429
+
430
+ **Note:** Individual request options override common options. If a specific option is not provided in `requestOptions`, the corresponding `common*` option is used.
431
+
432
+ #### Response Format
433
+
434
+ ```typescript
435
+ interface API_GATEWAY_RESPONSE<ResponseDataType> {
436
+ id: string; // Request identifier
437
+ success: boolean; // Whether the request succeeded
438
+ data?: ResponseDataType; // Response data (if success is true)
439
+ error?: string; // Error message (if success is false)
440
+ }
441
+ ```
442
+
443
+ ## Real-World Use Cases
444
+
445
+ ### 1. Polling for Async Job Completion
446
+
447
+ ```typescript
448
+ const jobResult = await stableRequest({
449
+ reqData: {
450
+ hostname: 'api.example.com',
451
+ path: '/jobs/123/status',
452
+ },
453
+ resReq: true,
454
+ attempts: 20,
455
+ wait: 3000,
456
+ retryStrategy: RETRY_STRATEGIES.FIXED,
457
+ responseAnalyzer: async (reqConfig, data) => {
458
+ return data.status === 'completed';
459
+ },
460
+ handleErrors: async (reqConfig, error) => {
461
+ console.log(`Job not ready yet (attempt ${error.attempt})`);
462
+ }
463
+ });
464
+ ```
465
+
466
+ ### 2. Resilient External API Integration
467
+
468
+ ```typescript
469
+ const weatherData = await stableRequest({
470
+ reqData: {
471
+ hostname: 'api.weather.com',
472
+ path: '/current',
473
+ query: { city: 'London' },
474
+ headers: { 'Authorization': `Bearer ${token}` }
475
+ },
476
+ resReq: true,
477
+ attempts: 5,
478
+ wait: 2000,
479
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
480
+ logAllErrors: true,
481
+ handleErrors: async (reqConfig, error) => {
482
+ logger.warn('Weather API retry', {
483
+ attempt: error.attempt,
484
+ isRetryable: error.isRetryable
485
+ });
486
+ }
487
+ });
488
+ ```
489
+
490
+ ### 3. Database Replication Consistency Check
491
+
492
+ ```typescript
493
+ const consistentData = await stableRequest({
494
+ reqData: {
495
+ hostname: 'replica.db.example.com',
496
+ path: '/records/456',
497
+ },
498
+ resReq: true,
499
+ attempts: 10,
500
+ wait: 500,
501
+ retryStrategy: RETRY_STRATEGIES.LINEAR,
502
+ responseAnalyzer: async (reqConfig, data) => {
503
+ // Wait until replica has the latest version
504
+ return data.version >= expectedVersion;
505
+ }
506
+ });
507
+ ```
508
+
509
+ ### 4. Batch User Creation
510
+
511
+ ```typescript
512
+ const users = [
513
+ { name: 'John Doe', email: 'john@example.com' },
514
+ { name: 'Jane Smith', email: 'jane@example.com' },
515
+ { name: 'Bob Johnson', email: 'bob@example.com' }
516
+ ];
517
+
518
+ const requests = users.map((user, index) => ({
519
+ id: `create-user-${index}`,
520
+ requestOptions: {
521
+ reqData: {
522
+ hostname: 'api.example.com',
523
+ path: '/users',
524
+ method: REQUEST_METHODS.POST,
525
+ body: user
526
+ },
527
+ resReq: true
528
+ }
529
+ }));
530
+
531
+ const results = await stableApiGateway(requests, {
532
+ concurrentExecution: true,
533
+ commonAttempts: 3,
534
+ commonWait: 1000,
535
+ commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
536
+ commonLogAllErrors: true,
537
+ commonHandleErrors: async (reqConfig, error) => {
538
+ console.log(`Failed to create user: ${error.error}`);
539
+ }
540
+ });
541
+
542
+ const successful = results.filter(r => r.success);
543
+ const failed = results.filter(r => !r.success);
544
+
545
+ console.log(`Created ${successful.length} users`);
546
+ console.log(`Failed to create ${failed.length} users`);
547
+ ```
548
+
549
+ ### 5. Rate-Limited API with Backoff
550
+
551
+ ```typescript
552
+ const searchResults = await stableRequest({
553
+ reqData: {
554
+ hostname: 'api.ratelimited-service.com',
555
+ path: '/search',
556
+ query: { q: 'nodejs' }
557
+ },
558
+ resReq: true,
559
+ attempts: 10,
560
+ wait: 1000,
561
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL, // Exponential backoff for rate limits
562
+ handleErrors: async (reqConfig, error) => {
563
+ if (error.type === 'HTTP_ERROR' && error.error.includes('429')) {
564
+ console.log('Rate limited, backing off...');
565
+ }
566
+ }
567
+ });
568
+ ```
569
+
570
+ ### 6. Microservices Health Check
571
+
572
+ ```typescript
573
+ const services = ['auth', 'users', 'orders', 'payments'];
574
+
575
+ const healthChecks = services.map(service => ({
576
+ id: `health-${service}`,
577
+ requestOptions: {
578
+ reqData: {
579
+ hostname: `${service}.internal.example.com`,
580
+ path: '/health'
581
+ },
582
+ resReq: true,
583
+ attempts: 3,
584
+ wait: 500
585
+ }
586
+ }));
587
+
588
+ const results = await stableApiGateway(healthChecks, {
589
+ concurrentExecution: true,
590
+ commonRetryStrategy: RETRY_STRATEGIES.LINEAR
591
+ });
592
+
593
+ const healthStatus = results.reduce((acc, result) => {
594
+ const serviceName = result.id.replace('health-', '');
595
+ acc[serviceName] = result.success ? 'healthy' : 'unhealthy';
596
+ return acc;
597
+ }, {});
598
+
599
+ console.log('Service health status:', healthStatus);
600
+ ```
601
+
602
+ ### 7. Payment Processing with Idempotency
603
+
604
+ ```typescript
605
+ const payment = await stableRequest({
606
+ reqData: {
607
+ hostname: 'payment-gateway.com',
608
+ path: '/charge',
609
+ method: REQUEST_METHODS.POST,
610
+ headers: { 'Idempotency-Key': uniqueId },
611
+ body: { amount: 1000, currency: 'USD' }
612
+ },
613
+ resReq: true,
614
+ attempts: 3,
615
+ wait: 1000,
616
+ responseAnalyzer: async (reqConfig, data) => {
617
+ return data.status === 'succeeded';
618
+ },
619
+ finalErrorAnalyzer: async (reqConfig, error) => {
620
+ // Check if payment actually went through despite error
621
+ const status = await checkPaymentStatus(uniqueId);
622
+ return status === 'succeeded';
623
+ }
624
+ });
625
+ ```
626
+
627
+ ### 8. Bulk Data Migration
628
+
629
+ ```typescript
630
+ const records = await fetchLegacyRecords();
631
+
632
+ const migrationRequests = records.map((record, index) => ({
633
+ id: `migrate-${record.id}`,
634
+ requestOptions: {
635
+ reqData: {
636
+ hostname: 'new-system.example.com',
637
+ path: '/import',
638
+ method: REQUEST_METHODS.POST,
639
+ body: record
640
+ },
641
+ resReq: true,
642
+ // Individual retry config for critical records
643
+ ...(record.critical && {
644
+ attempts: 5,
645
+ wait: 2000,
646
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
647
+ })
648
+ }
649
+ }));
650
+
651
+ const results = await stableApiGateway(migrationRequests, {
652
+ concurrentExecution: true,
653
+ commonAttempts: 3,
654
+ commonWait: 1000,
655
+ commonRetryStrategy: RETRY_STRATEGIES.LINEAR,
656
+ commonHandleErrors: async (reqConfig, error) => {
657
+ await logMigrationError(reqConfig.data.id, error);
658
+ }
659
+ });
660
+ ```
661
+
662
+ ### 9. Multi-Source Data Aggregation
663
+
664
+ ```typescript
665
+ const sources = [
666
+ { id: 'source-1', hostname: 'api1.example.com', path: '/data' },
667
+ { id: 'source-2', hostname: 'api2.example.com', path: '/info' },
668
+ { id: 'source-3', hostname: 'api3.example.com', path: '/stats' }
669
+ ];
670
+
671
+ const requests = sources.map(source => ({
672
+ id: source.id,
673
+ requestOptions: {
674
+ reqData: {
675
+ hostname: source.hostname,
676
+ path: source.path
677
+ },
678
+ resReq: true,
679
+ attempts: 3,
680
+ finalErrorAnalyzer: async () => true // Don't fail if one source is down
681
+ }
682
+ }));
683
+
684
+ const results = await stableApiGateway(requests, {
685
+ concurrentExecution: true,
686
+ commonWait: 1000,
687
+ commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
688
+ });
689
+
690
+ const aggregatedData = results
691
+ .filter(r => r.success)
692
+ .map(r => r.data);
693
+
694
+ console.log(`Collected data from ${aggregatedData.length}/${sources.length} sources`);
695
+ ```
696
+
697
+ ### 10. Sequential Workflow with Dependencies
698
+
699
+ ```typescript
700
+ const workflowSteps = [
701
+ {
702
+ id: 'step-1-init',
703
+ requestOptions: {
704
+ reqData: {
705
+ hostname: 'workflow.example.com',
706
+ path: '/init',
707
+ method: REQUEST_METHODS.POST,
708
+ body: { workflowId: 'wf-123' }
709
+ },
710
+ resReq: true
711
+ }
712
+ },
713
+ {
714
+ id: 'step-2-process',
715
+ requestOptions: {
716
+ reqData: {
717
+ hostname: 'workflow.example.com',
718
+ path: '/process',
719
+ method: REQUEST_METHODS.POST,
720
+ body: { workflowId: 'wf-123' }
721
+ },
722
+ resReq: true,
723
+ responseAnalyzer: async (reqConfig, data) => {
724
+ return data.status === 'completed';
725
+ }
726
+ }
727
+ },
728
+ {
729
+ id: 'step-3-finalize',
730
+ requestOptions: {
731
+ reqData: {
732
+ hostname: 'workflow.example.com',
733
+ path: '/finalize',
734
+ method: REQUEST_METHODS.POST,
735
+ body: { workflowId: 'wf-123' }
736
+ },
737
+ resReq: true
738
+ }
739
+ }
740
+ ];
741
+
742
+ const results = await stableApiGateway(workflowSteps, {
743
+ concurrentExecution: false, // Execute sequentially
744
+ stopOnFirstError: true, // Stop if any step fails
745
+ commonAttempts: 5,
746
+ commonWait: 2000,
747
+ commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
748
+ });
749
+
750
+ if (results.every(r => r.success)) {
751
+ console.log('Workflow completed successfully');
752
+ } else {
753
+ console.error('Workflow failed at step:', results.findIndex(r => !r.success) + 1);
754
+ }
755
+ ```
756
+
757
+ ## Advanced Patterns
758
+
759
+ ### Circuit Breaker Pattern
760
+
761
+ ```typescript
762
+ let failureCount = 0;
763
+ const CIRCUIT_THRESHOLD = 5;
764
+
765
+ async function resilientRequest(endpoint: string) {
766
+ if (failureCount >= CIRCUIT_THRESHOLD) {
767
+ throw new Error('Circuit breaker open');
768
+ }
769
+
770
+ try {
771
+ const result = await stableRequest({
772
+ reqData: { hostname: 'api.example.com', path: endpoint },
773
+ resReq: true,
774
+ attempts: 3,
775
+ handleErrors: async () => {
776
+ failureCount++;
777
+ }
778
+ });
779
+ failureCount = 0; // Reset on success
780
+ return result;
781
+ } catch (error) {
782
+ if (failureCount >= CIRCUIT_THRESHOLD) {
783
+ console.log('Circuit breaker activated');
784
+ setTimeout(() => { failureCount = 0; }, 60000); // Reset after 1 minute
785
+ }
786
+ throw error;
787
+ }
788
+ }
789
+ ```
790
+
791
+ ### Dynamic Request Configuration
792
+
793
+ ```typescript
794
+ const endpoints = await getEndpointsFromConfig();
795
+
796
+ const requests = endpoints.map(endpoint => ({
797
+ id: endpoint.id,
798
+ requestOptions: {
799
+ reqData: {
800
+ hostname: endpoint.hostname,
801
+ path: endpoint.path,
802
+ method: endpoint.method,
803
+ ...(endpoint.auth && {
804
+ headers: { Authorization: `Bearer ${endpoint.auth}` }
805
+ })
806
+ },
807
+ resReq: true,
808
+ attempts: endpoint.critical ? 5 : 3,
809
+ retryStrategy: endpoint.critical ? RETRY_STRATEGIES.EXPONENTIAL : RETRY_STRATEGIES.FIXED
810
+ }
811
+ }));
812
+
813
+ const results = await stableApiGateway(requests, {
814
+ concurrentExecution: true,
815
+ commonWait: 1000
816
+ });
817
+ ```
818
+
819
+ ### Conditional Retry Based on Response
820
+
821
+ ```typescript
822
+ await stableRequest({
823
+ reqData: {
824
+ hostname: 'api.example.com',
825
+ path: '/data',
826
+ },
827
+ resReq: true,
828
+ attempts: 5,
829
+ responseAnalyzer: async (reqConfig, data) => {
830
+ // Only retry if data is incomplete
831
+ if (!data.complete) {
832
+ console.log('Data incomplete, retrying...');
833
+ return false;
834
+ }
835
+
836
+ // Don't retry if data is invalid (different from incomplete)
837
+ if (data.error) {
838
+ throw new Error('Invalid data, cannot retry');
839
+ }
840
+
841
+ return true;
842
+ }
843
+ });
844
+ ```
845
+
846
+ ## TypeScript Support
847
+
848
+ Full TypeScript support with generic types for request and response data:
849
+
850
+ ```typescript
851
+ interface CreateUserRequest {
852
+ name: string;
853
+ email: string;
854
+ }
855
+
856
+ interface UserResponse {
857
+ id: string;
858
+ name: string;
859
+ email: string;
860
+ createdAt: string;
861
+ }
862
+
863
+ const user = await stableRequest<CreateUserRequest, UserResponse>({
864
+ reqData: {
865
+ hostname: 'api.example.com',
866
+ path: '/users',
867
+ method: REQUEST_METHODS.POST,
868
+ body: {
869
+ name: 'John Doe',
870
+ email: 'john@example.com'
871
+ }
872
+ },
873
+ resReq: true,
874
+ attempts: 3
875
+ });
876
+
877
+ // user is fully typed as UserResponse
878
+ console.log(user.id);
879
+ ```
880
+
881
+ ## Comparison with Similar Libraries
882
+
883
+ ### vs. axios-retry
884
+
885
+ | Feature | stable-request | axios-retry |
886
+ |---------|----------------|-------------|
887
+ | **Content validation** | ✅ Full support with `responseAnalyzer` | ❌ Only HTTP status codes |
888
+ | **Batch processing** | ✅ Built-in `stableApiGateway` | ❌ Manual implementation needed |
889
+ | **Trial mode** | ✅ Built-in failure simulation | ❌ No testing utilities |
890
+ | **Retry strategies** | ✅ Fixed, Linear, Exponential | ✅ Exponential only |
891
+ | **Observability** | ✅ Granular hooks for every attempt | ⚠️ Limited |
892
+ | **Final error analysis** | ✅ Custom error handling | ❌ No |
893
+
894
+ ### vs. got
895
+
896
+ | Feature | stable-request | got |
897
+ |---------|----------------|-----|
898
+ | **Built on Axios** | ✅ Leverages Axios ecosystem | ❌ Standalone client |
899
+ | **Content validation** | ✅ Response analyzer | ❌ Only HTTP errors |
900
+ | **Batch processing** | ✅ Built-in gateway | ❌ Manual implementation |
901
+ | **Trial mode** | ✅ Simulation for testing | ❌ No |
902
+ | **Retry strategies** | ✅ 3 configurable strategies | ✅ Exponential with jitter |
903
+
904
+ ### vs. p-retry + axios
905
+
906
+ | Feature | stable-request | p-retry + axios |
907
+ |---------|----------------|-----------------|
908
+ | **All-in-one** | ✅ Single package | ❌ Requires multiple packages |
909
+ | **HTTP-aware** | ✅ Built for HTTP | ❌ Generic retry wrapper |
910
+ | **Content validation** | ✅ Built-in | ❌ Manual implementation |
911
+ | **Batch processing** | ✅ Built-in | ❌ Manual implementation |
912
+ | **Observability** | ✅ Request-specific hooks | ⚠️ Generic callbacks |
913
+
914
+ ## Best Practices
915
+
916
+ 1. **Use exponential backoff for rate-limited APIs** to avoid overwhelming the server
917
+ 2. **Set reasonable timeout values** in `reqData.timeout` to prevent hanging requests
918
+ 3. **Implement responseAnalyzer** for APIs that return 200 OK with error details in the body
919
+ 4. **Use concurrent execution** in `stableApiGateway` for independent requests
920
+ 5. **Use sequential execution** when requests have dependencies or need to maintain order
921
+ 6. **Leverage finalErrorAnalyzer** for graceful degradation in non-critical paths
922
+ 7. **Enable logging in development** with `logAllErrors` and `logAllSuccessfulAttempts`
923
+ 8. **Use Trial Mode** to test your error handling without relying on actual failures
924
+
925
+ ## License
926
+
927
+ MIT © Manish Varma
928
+
929
+ [![npm version](https://img.shields.io/npm/v/stable-request.svg)](https://www.npmjs.com/package/@emmvish/stable-request)
930
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
931
+
932
+ ---
933
+
934
+ **Made with ❤️ for developers who are sick of integrating apps with unreliable APIs**