@emmvish/stable-request 1.1.3 โ 1.1.4
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 +752 -913
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/core/send-stable-request.d.ts.map +1 -1
- package/dist/core/send-stable-request.js +23 -5
- package/dist/core/send-stable-request.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +34 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utilities/extract-common-request-config-options.d.ts.map +1 -1
- package/dist/utilities/extract-common-request-config-options.js +1 -0
- package/dist/utilities/extract-common-request-config-options.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
10
|
-
- ๐ **Batch
|
|
11
|
-
- ๐ฏ **Request
|
|
12
|
-
- ๐งช **Trial
|
|
13
|
-
- ๐ **Granular
|
|
14
|
-
- โก **Multiple
|
|
15
|
-
- ๐ง **Flexible
|
|
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
|
-
###
|
|
27
|
+
### 1. Basic Request (No Retries)
|
|
26
28
|
|
|
27
29
|
```typescript
|
|
28
|
-
import { stableRequest
|
|
30
|
+
import { stableRequest } from '@emmvish/stable-request';
|
|
29
31
|
|
|
30
|
-
|
|
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
|
-
###
|
|
43
|
+
### 2. Add Simple Retries
|
|
45
44
|
|
|
46
45
|
```typescript
|
|
47
|
-
import {
|
|
46
|
+
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
48
47
|
|
|
49
|
-
const
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
import { stableApiGateway, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
65
|
+
### 3. Validate Response Content (Content-Aware Retries)
|
|
78
66
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
104
|
+
### 4. Monitor Errors (Observability)
|
|
127
105
|
|
|
128
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
### 5. Monitor Successful Attempts
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
Track successful requests with `handleSuccessfulAttemptData`:
|
|
157
160
|
|
|
158
161
|
```typescript
|
|
159
|
-
|
|
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: '/
|
|
185
|
-
method: REQUEST_METHODS.POST
|
|
165
|
+
path: '/data'
|
|
186
166
|
},
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
204
|
-
|
|
185
|
+
**Hook Signature:**
|
|
205
186
|
```typescript
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
+
### 6. Handle Final Errors Gracefully
|
|
216
206
|
|
|
217
|
-
|
|
207
|
+
Decide what to do when all retries fail using `finalErrorAnalyzer`:
|
|
218
208
|
|
|
219
209
|
```typescript
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
//
|
|
259
|
-
|
|
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
|
-
|
|
292
|
-
|
|
232
|
+
if (data === false) {
|
|
233
|
+
console.log('Optional feature unavailable, using default');
|
|
234
|
+
}
|
|
293
235
|
```
|
|
294
236
|
|
|
295
|
-
**
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
###
|
|
251
|
+
### 7. Pass Custom Parameters to Hooks
|
|
305
252
|
|
|
306
|
-
|
|
253
|
+
You can pass custom data to `responseAnalyzer` and `finalErrorAnalyzer`:
|
|
307
254
|
|
|
308
255
|
```typescript
|
|
309
|
-
|
|
256
|
+
const expectedVersion = 42;
|
|
257
|
+
|
|
258
|
+
const data = await stableRequest({
|
|
310
259
|
reqData: {
|
|
311
260
|
hostname: 'api.example.com',
|
|
312
|
-
path: '/
|
|
261
|
+
path: '/data'
|
|
313
262
|
},
|
|
314
263
|
resReq: true,
|
|
315
264
|
attempts: 5,
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
- Integration testing
|
|
327
|
-
- Chaos engineering
|
|
328
|
-
- Validating monitoring and alerting
|
|
329
|
-
- Testing circuit breaker patterns
|
|
287
|
+
## Intermediate Concepts
|
|
330
288
|
|
|
331
|
-
###
|
|
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,
|
|
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
|
-
|
|
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: '/
|
|
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
|
-
###
|
|
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: '
|
|
419
|
-
path: '/
|
|
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
|
-
###
|
|
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
|
|
334
|
+
const data = await stableRequest({
|
|
435
335
|
reqData: {
|
|
436
336
|
hostname: 'api.example.com',
|
|
437
|
-
path: '/
|
|
337
|
+
path: '/slow-endpoint',
|
|
338
|
+
port: 8080,
|
|
339
|
+
protocol: 'http',
|
|
340
|
+
timeout: 30000 // 30 seconds
|
|
438
341
|
},
|
|
439
342
|
resReq: true,
|
|
440
|
-
attempts:
|
|
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
|
-
###
|
|
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: '/
|
|
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
|
-
|
|
365
|
+
if (error.message.includes('cancelled')) {
|
|
366
|
+
console.log('Request was cancelled');
|
|
367
|
+
}
|
|
478
368
|
}
|
|
479
369
|
```
|
|
480
370
|
|
|
481
|
-
###
|
|
371
|
+
### Trial Mode (Testing Your Retry Logic)
|
|
482
372
|
|
|
483
|
-
|
|
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: '/
|
|
379
|
+
path: '/data'
|
|
490
380
|
},
|
|
381
|
+
resReq: true,
|
|
491
382
|
attempts: 5,
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
393
|
+
**Use cases:**
|
|
394
|
+
- Test your error handling logic
|
|
395
|
+
- Verify monitoring alerts work
|
|
396
|
+
- Chaos engineering experiments
|
|
397
|
+
- Integration testing
|
|
502
398
|
|
|
503
|
-
|
|
399
|
+
## Batch Processing - Multiple Requests
|
|
504
400
|
|
|
505
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
461
|
+
### Sequential Execution (With Dependencies)
|
|
584
462
|
|
|
585
463
|
```typescript
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
512
|
+
### Shared Configuration (Common Options)
|
|
594
513
|
|
|
595
|
-
|
|
514
|
+
Instead of repeating configuration for each request:
|
|
596
515
|
|
|
597
516
|
```typescript
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
##
|
|
547
|
+
## Advanced: Request Grouping
|
|
548
|
+
|
|
549
|
+
Group related requests with different configurations. Configuration priority:
|
|
608
550
|
|
|
609
|
-
|
|
551
|
+
**Individual Request** > **Group Config** > **Global Common Config**
|
|
610
552
|
|
|
611
|
-
|
|
553
|
+
### Example: Service Tiers
|
|
612
554
|
|
|
613
555
|
```typescript
|
|
614
556
|
const results = await stableApiGateway(
|
|
615
557
|
[
|
|
616
|
-
// Critical services
|
|
617
|
-
{
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
|
625
|
-
{
|
|
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
|
-
|
|
641
|
-
|
|
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
|
-
|
|
655
|
-
|
|
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
|
-
|
|
622
|
+
|
|
623
|
+
commonFinalErrorAnalyzer: async () => {
|
|
624
|
+
return true; // Suppress errors
|
|
625
|
+
}
|
|
664
626
|
}
|
|
665
627
|
}
|
|
666
628
|
]
|
|
667
629
|
}
|
|
668
630
|
);
|
|
669
631
|
|
|
670
|
-
// Analyze
|
|
671
|
-
const
|
|
672
|
-
|
|
632
|
+
// Analyze by group
|
|
633
|
+
const criticalOk = results
|
|
634
|
+
.filter(r => r.groupId === 'critical')
|
|
635
|
+
.every(r => r.success);
|
|
673
636
|
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
753
|
-
|
|
754
|
-
Monitor different microservices with service-specific health check configurations.
|
|
645
|
+
### Example: Multi-Region Configuration
|
|
755
646
|
|
|
756
647
|
```typescript
|
|
757
|
-
const
|
|
648
|
+
const results = await stableApiGateway(
|
|
758
649
|
[
|
|
759
|
-
|
|
760
|
-
{ id: '
|
|
761
|
-
{ id: '
|
|
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
|
-
|
|
773
|
-
concurrentExecution: true,
|
|
655
|
+
commonAttempts: 3,
|
|
774
656
|
|
|
775
657
|
requestGroups: [
|
|
776
658
|
{
|
|
777
|
-
id: '
|
|
659
|
+
id: 'us-east',
|
|
778
660
|
commonConfig: {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
787
|
-
await pagerDuty.trigger({ severity: 'critical', message: `Core service down: ${error.error}` });
|
|
788
|
-
}
|
|
666
|
+
commonAttempts: 3
|
|
789
667
|
}
|
|
790
668
|
},
|
|
791
669
|
{
|
|
792
|
-
id: '
|
|
670
|
+
id: 'eu-west',
|
|
793
671
|
commonConfig: {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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: '
|
|
681
|
+
id: 'ap-southeast',
|
|
801
682
|
commonConfig: {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
839
|
-
|
|
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
|
-
|
|
842
|
-
|
|
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
|
-
###
|
|
734
|
+
### 2. Database Replication Lag
|
|
848
735
|
|
|
849
736
|
```typescript
|
|
850
|
-
const
|
|
737
|
+
const expectedVersion = 42;
|
|
738
|
+
|
|
739
|
+
const data = await stableRequest({
|
|
851
740
|
reqData: {
|
|
852
|
-
hostname: '
|
|
853
|
-
path: '/
|
|
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:
|
|
859
|
-
wait:
|
|
860
|
-
retryStrategy: RETRY_STRATEGIES.
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
-
###
|
|
765
|
+
### 3. Idempotent Payment Processing
|
|
872
766
|
|
|
873
767
|
```typescript
|
|
874
|
-
const
|
|
768
|
+
const paymentResult = await stableRequest({
|
|
875
769
|
reqData: {
|
|
876
|
-
hostname: '
|
|
877
|
-
path: '/
|
|
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:
|
|
881
|
-
wait:
|
|
882
|
-
retryStrategy: RETRY_STRATEGIES.
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
###
|
|
812
|
+
### 4. Batch User Creation with Error Handling
|
|
891
813
|
|
|
892
814
|
```typescript
|
|
893
815
|
const users = [
|
|
894
|
-
{ name: '
|
|
895
|
-
{ name: '
|
|
896
|
-
{ name: '
|
|
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: `
|
|
822
|
+
id: `user-${index}`,
|
|
901
823
|
requestOptions: {
|
|
902
824
|
reqData: {
|
|
903
825
|
body: user
|
|
@@ -908,283 +830,238 @@ 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
|
-
|
|
916
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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(
|
|
929
|
-
console.log(
|
|
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
|
-
###
|
|
871
|
+
### 5. Health Check Monitoring System
|
|
933
872
|
|
|
934
873
|
```typescript
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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(
|
|
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
|
-
|
|
945
|
+
## Complete API Reference
|
|
1005
946
|
|
|
1006
|
-
|
|
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
|
-
|
|
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
|
+
| `retryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Retry backoff strategy |
|
|
956
|
+
| `performAllAttempts` | `boolean` | `false` | Execute all attempts regardless |
|
|
957
|
+
| `logAllErrors` | `boolean` | `false` | Enable error logging |
|
|
958
|
+
| `logAllSuccessfulAttempts` | `boolean` | `false` | Enable success logging |
|
|
959
|
+
| `maxSerializableChars` | `number` | `1000` | Max chars for logs |
|
|
960
|
+
| `trialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Failure simulation |
|
|
961
|
+
| `hookParams` | `HookParams` | `{}` | Custom parameters for hooks |
|
|
962
|
+
| `responseAnalyzer` | `function` | `() => true` | Validate response content |
|
|
963
|
+
| `handleErrors` | `function` | `console.log` | Error handler |
|
|
964
|
+
| `handleSuccessfulAttemptData` | `function` | `console.log` | Success handler |
|
|
965
|
+
| `finalErrorAnalyzer` | `function` | `() => false` | Final error handler |
|
|
966
|
+
|
|
967
|
+
### REQUEST_DATA
|
|
1026
968
|
|
|
1027
969
|
```typescript
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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);
|
|
970
|
+
interface REQUEST_DATA<RequestDataType = any> {
|
|
971
|
+
hostname: string; // Required
|
|
972
|
+
protocol?: 'http' | 'https'; // Default: 'https'
|
|
973
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; // Default: 'GET'
|
|
974
|
+
path?: `/${string}`; // Default: ''
|
|
975
|
+
port?: number; // Default: 443
|
|
976
|
+
headers?: Record<string, any>; // Default: {}
|
|
977
|
+
body?: RequestDataType; // Request body
|
|
978
|
+
query?: Record<string, any>; // Query parameters
|
|
979
|
+
timeout?: number; // Default: 15000ms
|
|
980
|
+
signal?: AbortSignal; // For cancellation
|
|
1072
981
|
}
|
|
1073
982
|
```
|
|
1074
983
|
|
|
1075
|
-
|
|
984
|
+
### `stableApiGateway(requests, options)`
|
|
985
|
+
|
|
986
|
+
| Option | Type | Default | Description |
|
|
987
|
+
|--------|------|---------|-------------|
|
|
988
|
+
| `concurrentExecution` | `boolean` | `true` | Execute requests concurrently or sequentially |
|
|
989
|
+
| `stopOnFirstError` | `boolean` | `false` | Stop execution on first error (sequential only) |
|
|
990
|
+
| `requestGroups` | `RequestGroup[]` | `[]` | Define groups with their own common configurations |
|
|
991
|
+
| `commonAttempts` | `number` | `1` | Default attempts for all requests |
|
|
992
|
+
| `commonPerformAllAttempts` | `boolean` | `false` | Default performAllAttempts for all requests |
|
|
993
|
+
| `commonWait` | `number` | `1000` | Default wait time for all requests |
|
|
994
|
+
| `commonRetryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Default retry strategy for all requests |
|
|
995
|
+
| `commonLogAllErrors` | `boolean` | `false` | Default error logging for all requests |
|
|
996
|
+
| `commonLogAllSuccessfulAttempts` | `boolean` | `false` | Default success logging for all requests |
|
|
997
|
+
| `commonMaxSerializableChars` | `number` | `1000` | Default max chars for serialization |
|
|
998
|
+
| `commonTrialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Default trial mode for all requests |
|
|
999
|
+
| `commonResponseAnalyzer` | `function` | `() => true` | Default response analyzer for all requests |
|
|
1000
|
+
| `commonResReq` | `boolean` | `false` | Default resReq for all requests |
|
|
1001
|
+
| `commonFinalErrorAnalyzer` | `function` | `() => false` | Default final error analyzer for all requests |
|
|
1002
|
+
| `commonHandleErrors` | `function` | console.log | Default error handler for all requests |
|
|
1003
|
+
| `commonHandleSuccessfulAttemptData` | `function` | console.log | Default success handler for all requests |
|
|
1004
|
+
| `commonRequestData` | `Partial<REQUEST_DATA>` | `{ hostname: '' }` | Common set of request options for each request |
|
|
1005
|
+
| `commonHookParams` | `HookParams` | `{ }` | Common options for each request hook |
|
|
1076
1006
|
|
|
1077
|
-
###
|
|
1007
|
+
### Hooks Reference
|
|
1078
1008
|
|
|
1079
|
-
|
|
1080
|
-
let failureCount = 0;
|
|
1081
|
-
const CIRCUIT_THRESHOLD = 5;
|
|
1009
|
+
#### responseAnalyzer
|
|
1082
1010
|
|
|
1083
|
-
|
|
1084
|
-
if (failureCount >= CIRCUIT_THRESHOLD) {
|
|
1085
|
-
throw new Error('Circuit breaker open');
|
|
1086
|
-
}
|
|
1011
|
+
**Purpose:** Validate response content, retry even on HTTP 200
|
|
1087
1012
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
-
}
|
|
1013
|
+
```typescript
|
|
1014
|
+
responseAnalyzer: async ({ reqData, data, trialMode, params }) => {
|
|
1015
|
+
// Return true if valid, false to retry
|
|
1016
|
+
return data.status === 'ready';
|
|
1106
1017
|
}
|
|
1107
1018
|
```
|
|
1108
1019
|
|
|
1109
|
-
|
|
1020
|
+
#### handleErrors
|
|
1021
|
+
|
|
1022
|
+
**Purpose:** Monitor and log failed attempts
|
|
1110
1023
|
|
|
1111
1024
|
```typescript
|
|
1112
|
-
|
|
1025
|
+
handleErrors: async ({ reqData, errorLog, maxSerializableChars }) => {
|
|
1026
|
+
await logger.error({
|
|
1027
|
+
url: reqData.url,
|
|
1028
|
+
attempt: errorLog.attempt,
|
|
1029
|
+
error: errorLog.error
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
```
|
|
1113
1033
|
|
|
1114
|
-
|
|
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
|
-
}));
|
|
1034
|
+
#### handleSuccessfulAttemptData
|
|
1129
1035
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
});
|
|
1036
|
+
**Purpose:** Monitor and log successful attempts
|
|
1037
|
+
|
|
1038
|
+
```typescript
|
|
1039
|
+
handleSuccessfulAttemptData: async ({ reqData, successfulAttemptData, maxSerializableChars }) => {
|
|
1040
|
+
await analytics.track({
|
|
1041
|
+
url: reqData.url,
|
|
1042
|
+
duration: successfulAttemptData.executionTime
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1158
1045
|
```
|
|
1159
1046
|
|
|
1160
|
-
|
|
1047
|
+
#### finalErrorAnalyzer
|
|
1048
|
+
|
|
1049
|
+
**Purpose:** Handle final error after all retries exhausted
|
|
1161
1050
|
|
|
1162
1051
|
```typescript
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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;
|
|
1052
|
+
finalErrorAnalyzer: async ({ reqData, error, trialMode, params }) => {
|
|
1053
|
+
// Return true to suppress error (return false)
|
|
1054
|
+
// Return false to throw error
|
|
1055
|
+
if (error.message.includes('404')) {
|
|
1056
|
+
return true; // Treat as non-critical
|
|
1181
1057
|
}
|
|
1182
|
-
|
|
1058
|
+
return false; // Throw
|
|
1059
|
+
}
|
|
1183
1060
|
```
|
|
1184
1061
|
|
|
1185
1062
|
## TypeScript Support
|
|
1186
1063
|
|
|
1187
|
-
|
|
1064
|
+
Fully typed with generics:
|
|
1188
1065
|
|
|
1189
1066
|
```typescript
|
|
1190
1067
|
interface CreateUserRequest {
|
|
@@ -1209,71 +1086,33 @@ const user = await stableRequest<CreateUserRequest, UserResponse>({
|
|
|
1209
1086
|
email: 'john@example.com'
|
|
1210
1087
|
}
|
|
1211
1088
|
},
|
|
1212
|
-
resReq: true
|
|
1213
|
-
attempts: 3
|
|
1089
|
+
resReq: true
|
|
1214
1090
|
});
|
|
1215
1091
|
|
|
1216
|
-
// user is
|
|
1217
|
-
console.log(user.id);
|
|
1092
|
+
// user is typed as UserResponse
|
|
1093
|
+
console.log(user.id); // TypeScript knows this exists
|
|
1218
1094
|
```
|
|
1219
1095
|
|
|
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
1096
|
## Best Practices
|
|
1257
1097
|
|
|
1258
|
-
1. **
|
|
1259
|
-
2. **
|
|
1260
|
-
3. **
|
|
1261
|
-
4. **
|
|
1262
|
-
5. **
|
|
1263
|
-
6. **
|
|
1264
|
-
7. **
|
|
1265
|
-
8. **
|
|
1266
|
-
9. **
|
|
1267
|
-
10. **
|
|
1268
|
-
11. **Group requests by region or service tier** for better monitoring and configuration
|
|
1098
|
+
1. **Start simple** - Use basic retries first, add hooks as needed
|
|
1099
|
+
2. **Use exponential backoff** for rate-limited APIs
|
|
1100
|
+
3. **Validate response content** with `responseAnalyzer` for eventually-consistent systems
|
|
1101
|
+
4. **Monitor everything** with `handleErrors` and `handleSuccessfulAttemptData`
|
|
1102
|
+
5. **Group related requests** by service tier, region, or priority
|
|
1103
|
+
6. **Handle failures gracefully** with `finalErrorAnalyzer` for non-critical features
|
|
1104
|
+
7. **Test with trial mode** before deploying to production
|
|
1105
|
+
8. **Set appropriate timeouts** to prevent hanging requests
|
|
1106
|
+
9. **Use idempotency keys** for payment/financial operations
|
|
1107
|
+
10. **Log contextual information** in your hooks for debugging
|
|
1269
1108
|
|
|
1270
1109
|
## License
|
|
1271
1110
|
|
|
1272
1111
|
MIT ยฉ Manish Varma
|
|
1273
1112
|
|
|
1274
|
-
|
|
1113
|
+
|
|
1275
1114
|
[](https://opensource.org/licenses/MIT)
|
|
1276
1115
|
|
|
1277
1116
|
---
|
|
1278
1117
|
|
|
1279
|
-
**Made with โค๏ธ for developers
|
|
1118
|
+
**Made with โค๏ธ for developers integrating with unreliable APIs**
|