@emmvish/stable-request 1.6.2 → 1.6.3
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 +942 -152
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# @emmvish/stable-request
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A production-grade HTTP Workflow Execution Engine for Node.js that transforms unreliable API calls into resilient, observable, and sophisticated multi-phase workflows with intelligent retry strategies, circuit breakers, and advanced execution patterns.
|
|
4
4
|
|
|
5
5
|
## Navigation
|
|
6
6
|
|
|
7
7
|
- [Overview](#overview)
|
|
8
|
+
- [Why stable-request?](#why-stable-request)
|
|
8
9
|
- [Installation](#installation)
|
|
9
10
|
- [Quick Start](#quick-start)
|
|
11
|
+
- [Single Request with Retry](#single-request-with-retry)
|
|
12
|
+
- [Batch Requests (API Gateway)](#batch-requests-api-gateway)
|
|
13
|
+
- [Multi-Phase Workflow](#multi-phase-workflow)
|
|
10
14
|
- [Core Features](#core-features)
|
|
11
15
|
- [Intelligent Retry Strategies](#intelligent-retry-strategies)
|
|
12
16
|
- [Circuit Breaker Pattern](#circuit-breaker-pattern)
|
|
@@ -19,19 +23,35 @@ A powerful HTTP Workflow Execution Engine for Node.js that transforms unreliable
|
|
|
19
23
|
- [Branched Workflows](#branched-workflows)
|
|
20
24
|
- [Advanced Capabilities](#advanced-capabilities)
|
|
21
25
|
- [Config Cascading](#config-cascading)
|
|
26
|
+
- [Request Grouping](#request-grouping)
|
|
22
27
|
- [Shared Buffer and Pre-Execution Hooks](#shared-buffer-and-pre-execution-hooks)
|
|
23
28
|
- [Comprehensive Observability](#comprehensive-observability)
|
|
24
|
-
- [
|
|
29
|
+
- [Trial Mode](#trial-mode)
|
|
30
|
+
- [Common Use Cases](#common-use-cases)
|
|
25
31
|
- [License](#license)
|
|
26
32
|
|
|
27
33
|
## Overview
|
|
28
34
|
|
|
29
|
-
`@emmvish/stable-request` is
|
|
35
|
+
`@emmvish/stable-request` is engineered for applications requiring robust orchestration of complex, multi-step API interactions with enterprise-grade reliability, observability, and fault tolerance. It goes far beyond simple HTTP clients by providing:
|
|
30
36
|
|
|
31
|
-
- **Workflow-First
|
|
32
|
-
- **Enterprise Resilience**: Built-in circuit breakers, retry strategies, and failure handling
|
|
33
|
-
- **Execution Flexibility**: Sequential, concurrent, mixed, and non-linear execution patterns
|
|
34
|
-
- **Production-Ready Observability**:
|
|
37
|
+
- **Workflow-First Architecture**: Organize API calls into phases, branches, and decision trees with full control over execution order
|
|
38
|
+
- **Enterprise Resilience**: Built-in circuit breakers, configurable retry strategies, and sophisticated failure handling
|
|
39
|
+
- **Execution Flexibility**: Sequential, concurrent, mixed, and non-linear execution patterns to match your business logic
|
|
40
|
+
- **Production-Ready Observability**: Comprehensive hooks for monitoring, logging, error analysis, and execution history tracking
|
|
41
|
+
- **Performance Optimization**: Response caching, rate limiting, and concurrency control to maximize efficiency
|
|
42
|
+
- **Type Safety**: Full TypeScript support with 40+ exported types
|
|
43
|
+
|
|
44
|
+
## Why stable-request?
|
|
45
|
+
|
|
46
|
+
Modern applications often need to:
|
|
47
|
+
- **Orchestrate complex API workflows** with dependencies between steps
|
|
48
|
+
- **Handle unreliable APIs** with intelligent retry and fallback mechanisms
|
|
49
|
+
- **Prevent cascade failures** when downstream services fail
|
|
50
|
+
- **Optimize performance** by caching responses and controlling request rates
|
|
51
|
+
- **Monitor and debug** complex request flows in production
|
|
52
|
+
- **Implement conditional logic** based on API responses (branching, looping)
|
|
53
|
+
|
|
54
|
+
`@emmvish/stable-request` solves all these challenges with a unified, type-safe API that scales from simple requests to sophisticated multi-phase workflows.
|
|
35
55
|
|
|
36
56
|
## Installation
|
|
37
57
|
|
|
@@ -39,223 +59,619 @@ A powerful HTTP Workflow Execution Engine for Node.js that transforms unreliable
|
|
|
39
59
|
npm install @emmvish/stable-request
|
|
40
60
|
```
|
|
41
61
|
|
|
62
|
+
**Requirements**: Node.js 14+ (ES Modules)
|
|
63
|
+
|
|
64
|
+
**Dependencies**: Built on [Axios](https://axios-http.com/) for HTTP requests
|
|
65
|
+
|
|
42
66
|
## Quick Start
|
|
43
67
|
|
|
44
68
|
### Single Request with Retry
|
|
45
69
|
|
|
70
|
+
Execute a single HTTP request with automatic retry on failure:
|
|
71
|
+
|
|
46
72
|
```typescript
|
|
47
|
-
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
73
|
+
import { stableRequest, RETRY_STRATEGIES, REQUEST_METHODS } from '@emmvish/stable-request';
|
|
48
74
|
|
|
49
|
-
const
|
|
75
|
+
const userData = await stableRequest({
|
|
50
76
|
reqData: {
|
|
51
77
|
hostname: 'api.example.com',
|
|
52
|
-
path: '/users',
|
|
53
|
-
method:
|
|
78
|
+
path: '/users/123',
|
|
79
|
+
method: REQUEST_METHODS.GET,
|
|
80
|
+
headers: { 'Authorization': 'Bearer token' }
|
|
54
81
|
},
|
|
55
|
-
resReq: true,
|
|
56
|
-
attempts: 3,
|
|
57
|
-
wait: 1000,
|
|
58
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
82
|
+
resReq: true, // Return response data
|
|
83
|
+
attempts: 3, // Retry up to 3 times
|
|
84
|
+
wait: 1000, // 1 second between retries
|
|
85
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
86
|
+
logAllErrors: true // Log all failed attempts
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
console.log(userData); // { id: 123, name: 'John' }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Batch Requests (API Gateway)
|
|
93
|
+
|
|
94
|
+
Execute multiple requests concurrently or sequentially:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { stableApiGateway } from '@emmvish/stable-request';
|
|
98
|
+
|
|
99
|
+
const requests = [
|
|
100
|
+
{
|
|
101
|
+
id: 'users',
|
|
102
|
+
requestOptions: {
|
|
103
|
+
reqData: { path: '/users' },
|
|
104
|
+
resReq: true
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'orders',
|
|
109
|
+
requestOptions: {
|
|
110
|
+
reqData: { path: '/orders' },
|
|
111
|
+
resReq: true
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'products',
|
|
116
|
+
requestOptions: {
|
|
117
|
+
reqData: { path: '/products' },
|
|
118
|
+
resReq: true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const results = await stableApiGateway(requests, {
|
|
124
|
+
concurrentExecution: true, // Execute in parallel
|
|
125
|
+
commonRequestData: {
|
|
126
|
+
hostname: 'api.example.com',
|
|
127
|
+
headers: { 'X-API-Key': 'secret' }
|
|
128
|
+
},
|
|
129
|
+
commonAttempts: 2, // Retry each request twice
|
|
130
|
+
commonWait: 500
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
results.forEach(result => {
|
|
134
|
+
console.log(`${result.id}:`, result.data);
|
|
59
135
|
});
|
|
60
136
|
```
|
|
61
137
|
|
|
62
138
|
### Multi-Phase Workflow
|
|
63
139
|
|
|
140
|
+
Orchestrate complex workflows with multiple phases:
|
|
141
|
+
|
|
64
142
|
```typescript
|
|
65
|
-
import { stableWorkflow } from '@emmvish/stable-request';
|
|
143
|
+
import { stableWorkflow, PHASE_DECISION_ACTIONS, REQUEST_METHODS } from '@emmvish/stable-request';
|
|
66
144
|
|
|
67
|
-
const
|
|
145
|
+
const phases = [
|
|
68
146
|
{
|
|
69
|
-
id: '
|
|
147
|
+
id: 'authentication',
|
|
70
148
|
requests: [
|
|
71
|
-
{
|
|
149
|
+
{
|
|
150
|
+
id: 'login',
|
|
151
|
+
requestOptions: {
|
|
152
|
+
reqData: {
|
|
153
|
+
path: '/auth/login',
|
|
154
|
+
method: REQUEST_METHODS.POST,
|
|
155
|
+
body: { username: 'user', password: 'pass' }
|
|
156
|
+
},
|
|
157
|
+
resReq: true
|
|
158
|
+
}
|
|
159
|
+
}
|
|
72
160
|
]
|
|
73
161
|
},
|
|
74
162
|
{
|
|
75
163
|
id: 'fetch-data',
|
|
76
|
-
concurrentExecution: true,
|
|
164
|
+
concurrentExecution: true, // Execute requests in parallel
|
|
77
165
|
requests: [
|
|
78
|
-
{ id: '
|
|
79
|
-
{ id: 'orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } }
|
|
166
|
+
{ id: 'user-profile', requestOptions: { reqData: { path: '/profile' }, resReq: true } },
|
|
167
|
+
{ id: 'user-orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } },
|
|
168
|
+
{ id: 'user-settings', requestOptions: { reqData: { path: '/settings' }, resReq: true } }
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'process-data',
|
|
173
|
+
requests: [
|
|
174
|
+
{
|
|
175
|
+
id: 'update-analytics',
|
|
176
|
+
requestOptions: {
|
|
177
|
+
reqData: { path: '/analytics', method: REQUEST_METHODS.POST },
|
|
178
|
+
resReq: false
|
|
179
|
+
}
|
|
180
|
+
}
|
|
80
181
|
]
|
|
81
182
|
}
|
|
82
|
-
]
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const result = await stableWorkflow(phases, {
|
|
83
186
|
workflowId: 'user-data-sync',
|
|
84
187
|
commonRequestData: { hostname: 'api.example.com' },
|
|
85
|
-
|
|
188
|
+
commonAttempts: 3,
|
|
189
|
+
stopOnFirstPhaseError: true, // Stop if any phase fails
|
|
190
|
+
logPhaseResults: true // Log each phase completion
|
|
86
191
|
});
|
|
192
|
+
|
|
193
|
+
console.log(`Workflow completed: ${result.success}`);
|
|
194
|
+
console.log(`Total requests: ${result.totalRequests}`);
|
|
195
|
+
console.log(`Successful: ${result.successfulRequests}`);
|
|
196
|
+
console.log(`Failed: ${result.failedRequests}`);
|
|
197
|
+
console.log(`Execution time: ${result.executionTime}ms`);
|
|
87
198
|
```
|
|
88
199
|
|
|
89
200
|
## Core Features
|
|
90
201
|
|
|
91
202
|
### Intelligent Retry Strategies
|
|
92
203
|
|
|
93
|
-
Automatically retry failed requests with
|
|
204
|
+
Automatically retry failed requests with sophisticated backoff strategies:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
208
|
+
|
|
209
|
+
// Fixed delay: constant wait time
|
|
210
|
+
await stableRequest({
|
|
211
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
212
|
+
attempts: 5,
|
|
213
|
+
wait: 1000, // 1 second between each retry
|
|
214
|
+
retryStrategy: RETRY_STRATEGIES.FIXED
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Linear backoff: incrementally increasing delays
|
|
218
|
+
await stableRequest({
|
|
219
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
220
|
+
attempts: 5,
|
|
221
|
+
wait: 1000, // 1s, 2s, 3s, 4s, 5s
|
|
222
|
+
retryStrategy: RETRY_STRATEGIES.LINEAR
|
|
223
|
+
});
|
|
94
224
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
225
|
+
// Exponential backoff: exponentially growing delays
|
|
226
|
+
await stableRequest({
|
|
227
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
228
|
+
attempts: 5,
|
|
229
|
+
wait: 1000, // 1s, 2s, 4s, 8s, 16s
|
|
230
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
231
|
+
});
|
|
232
|
+
```
|
|
99
233
|
|
|
100
|
-
|
|
234
|
+
**Features**:
|
|
235
|
+
- Automatic retry on 5xx errors and network failures
|
|
236
|
+
- No retry on 4xx client errors (configurable)
|
|
237
|
+
- Maximum allowed wait time to prevent excessive delays
|
|
238
|
+
- Per-request or workflow-level configuration
|
|
239
|
+
|
|
240
|
+
**Custom Response Validation**:
|
|
241
|
+
```typescript
|
|
242
|
+
await stableRequest({
|
|
243
|
+
reqData: { hostname: 'api.example.com', path: '/job/status' },
|
|
244
|
+
resReq: true,
|
|
245
|
+
attempts: 10,
|
|
246
|
+
wait: 2000,
|
|
247
|
+
responseAnalyzer: async ({ data }) => {
|
|
248
|
+
// Retry until job is complete
|
|
249
|
+
return data.status === 'completed';
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
```
|
|
101
253
|
|
|
102
254
|
### Circuit Breaker Pattern
|
|
103
255
|
|
|
104
256
|
Prevent cascade failures and system overload with built-in circuit breakers:
|
|
105
257
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
258
|
+
```typescript
|
|
259
|
+
import { stableRequest, CircuitBreakerState } from '@emmvish/stable-request';
|
|
260
|
+
|
|
261
|
+
await stableRequest({
|
|
262
|
+
reqData: { hostname: 'unreliable-api.example.com', path: '/data' },
|
|
263
|
+
attempts: 3,
|
|
264
|
+
circuitBreaker: {
|
|
265
|
+
failureThreshold: 5, // Open after 5 failures
|
|
266
|
+
successThreshold: 2, // Close after 2 successes in half-open
|
|
267
|
+
timeout: 60000, // Wait 60s before trying again (half-open)
|
|
268
|
+
trackIndividualAttempts: false // Track at request level (not attempt level)
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Circuit Breaker States**:
|
|
274
|
+
- **CLOSED**: Normal operation, requests flow through
|
|
275
|
+
- **OPEN**: Too many failures, requests blocked immediately
|
|
276
|
+
- **HALF_OPEN**: Testing if service recovered, limited requests allowed
|
|
277
|
+
|
|
278
|
+
**Workflow-Level Circuit Breakers**:
|
|
279
|
+
```typescript
|
|
280
|
+
import { CircuitBreaker } from '@emmvish/stable-request';
|
|
281
|
+
|
|
282
|
+
const sharedBreaker = new CircuitBreaker({
|
|
283
|
+
failureThreshold: 10,
|
|
284
|
+
successThreshold: 3,
|
|
285
|
+
timeout: 120000
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await stableWorkflow(phases, {
|
|
289
|
+
circuitBreaker: sharedBreaker, // Shared across all requests
|
|
290
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Check circuit breaker state
|
|
294
|
+
console.log(sharedBreaker.getState());
|
|
295
|
+
// { state: 'CLOSED', failures: 0, successes: 0, ... }
|
|
296
|
+
```
|
|
110
297
|
|
|
111
298
|
### Response Caching
|
|
112
299
|
|
|
113
|
-
Reduce redundant API calls with intelligent caching:
|
|
300
|
+
Reduce redundant API calls and improve performance with intelligent caching:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
await stableRequest({
|
|
304
|
+
reqData: { hostname: 'api.example.com', path: '/static-data' },
|
|
305
|
+
resReq: true,
|
|
306
|
+
cache: {
|
|
307
|
+
enabled: true,
|
|
308
|
+
ttl: 300000, // Cache for 5 minutes
|
|
309
|
+
key: 'custom-cache-key' // Optional: custom cache key
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Subsequent identical requests within 5 minutes will use cached response
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Global Cache Management**:
|
|
317
|
+
```typescript
|
|
318
|
+
import { getGlobalCacheManager, resetGlobalCacheManager } from '@emmvish/stable-request';
|
|
319
|
+
|
|
320
|
+
const cacheManager = getGlobalCacheManager();
|
|
321
|
+
|
|
322
|
+
// Inspect cache statistics
|
|
323
|
+
const stats = cacheManager.getStats();
|
|
324
|
+
console.log(stats);
|
|
325
|
+
// { size: 42, validEntries: 38, expiredEntries: 4 }
|
|
326
|
+
|
|
327
|
+
// Clear all cached responses
|
|
328
|
+
cacheManager.clearAll();
|
|
114
329
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
330
|
+
// Or reset the global cache instance
|
|
331
|
+
resetGlobalCacheManager();
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Cache Features**:
|
|
335
|
+
- Automatic request fingerprinting (method, URL, headers, body)
|
|
336
|
+
- TTL-based expiration
|
|
337
|
+
- Workflow-wide sharing across phases and branches
|
|
338
|
+
- Manual cache inspection and clearing
|
|
339
|
+
- Per-request cache configuration
|
|
119
340
|
|
|
120
341
|
### Rate Limiting and Concurrency Control
|
|
121
342
|
|
|
122
343
|
Respect API rate limits and control system load:
|
|
123
344
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
345
|
+
```typescript
|
|
346
|
+
await stableWorkflow(phases, {
|
|
347
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
348
|
+
|
|
349
|
+
// Rate limiting (token bucket algorithm)
|
|
350
|
+
rateLimit: {
|
|
351
|
+
maxRequests: 100, // 100 requests
|
|
352
|
+
timeWindow: 60000 // per 60 seconds
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
// Concurrency limiting
|
|
356
|
+
maxConcurrentRequests: 5 // Max 5 parallel requests
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Per-Phase Configuration**:
|
|
361
|
+
```typescript
|
|
362
|
+
const phases = [
|
|
363
|
+
{
|
|
364
|
+
id: 'bulk-import',
|
|
365
|
+
maxConcurrentRequests: 10, // Override workflow limit
|
|
366
|
+
rateLimit: {
|
|
367
|
+
maxRequests: 50,
|
|
368
|
+
timeWindow: 10000
|
|
369
|
+
},
|
|
370
|
+
requests: [...]
|
|
371
|
+
}
|
|
372
|
+
];
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Standalone Rate Limiter**:
|
|
376
|
+
```typescript
|
|
377
|
+
import { RateLimiter } from '@emmvish/stable-request';
|
|
378
|
+
|
|
379
|
+
const limiter = new RateLimiter({
|
|
380
|
+
maxRequests: 1000,
|
|
381
|
+
timeWindow: 3600000 // 1000 requests per hour
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await limiter.acquire(); // Waits if limit exceeded
|
|
385
|
+
// Make request
|
|
386
|
+
```
|
|
128
387
|
|
|
129
388
|
## Workflow Execution Patterns
|
|
130
389
|
|
|
131
390
|
### Sequential and Concurrent Phases
|
|
132
391
|
|
|
133
|
-
Control execution order at the phase level:
|
|
392
|
+
Control execution order at the phase and request level:
|
|
393
|
+
|
|
394
|
+
**Sequential Phases (Default)**:
|
|
395
|
+
```typescript
|
|
396
|
+
const phases = [
|
|
397
|
+
{ id: 'step-1', requests: [...] }, // Executes first
|
|
398
|
+
{ id: 'step-2', requests: [...] }, // Then this
|
|
399
|
+
{ id: 'step-3', requests: [...] } // Finally this
|
|
400
|
+
];
|
|
134
401
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
402
|
+
await stableWorkflow(phases, {
|
|
403
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
404
|
+
});
|
|
405
|
+
```
|
|
138
406
|
|
|
407
|
+
**Concurrent Phases**:
|
|
139
408
|
```typescript
|
|
140
409
|
const phases = [
|
|
141
|
-
{ id: 'init', requests: [...] },
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
|
|
410
|
+
{ id: 'init', requests: [...] },
|
|
411
|
+
{ id: 'parallel-1', requests: [...] },
|
|
412
|
+
{ id: 'parallel-2', requests: [...] }
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
await stableWorkflow(phases, {
|
|
416
|
+
concurrentPhaseExecution: true, // All phases run in parallel
|
|
417
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Concurrent Requests Within Phase**:
|
|
422
|
+
```typescript
|
|
423
|
+
const phases = [
|
|
424
|
+
{
|
|
425
|
+
id: 'data-fetch',
|
|
426
|
+
concurrentExecution: true, // Requests run in parallel
|
|
427
|
+
requests: [
|
|
428
|
+
{ id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
|
|
429
|
+
{ id: 'products', requestOptions: { reqData: { path: '/products' }, resReq: true } },
|
|
430
|
+
{ id: 'orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } }
|
|
431
|
+
]
|
|
432
|
+
}
|
|
433
|
+
];
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Stop on First Error**:
|
|
437
|
+
```typescript
|
|
438
|
+
const phases = [
|
|
439
|
+
{
|
|
440
|
+
id: 'critical-phase',
|
|
441
|
+
stopOnFirstError: true, // Stop phase if any request fails
|
|
145
442
|
requests: [...]
|
|
146
443
|
}
|
|
147
444
|
];
|
|
148
445
|
|
|
149
|
-
await stableWorkflow(phases, {
|
|
150
|
-
|
|
446
|
+
await stableWorkflow(phases, {
|
|
447
|
+
stopOnFirstPhaseError: true, // Stop workflow if any phase fails
|
|
448
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
151
449
|
});
|
|
152
450
|
```
|
|
153
451
|
|
|
154
452
|
### Mixed Execution Mode
|
|
155
453
|
|
|
156
|
-
Combine sequential and concurrent phases
|
|
157
|
-
|
|
158
|
-
- Mark specific phases as concurrent while others remain sequential
|
|
159
|
-
- Fine-grained control over execution topology
|
|
160
|
-
- Useful for scenarios like: "authenticate first, then fetch data in parallel, then process sequentially"
|
|
454
|
+
Combine sequential and concurrent phases for fine-grained control:
|
|
161
455
|
|
|
162
456
|
```typescript
|
|
163
457
|
const phases = [
|
|
164
|
-
{ id: 'auth', requests: [...] }, // Sequential
|
|
165
458
|
{
|
|
166
|
-
id: '
|
|
167
|
-
|
|
168
|
-
requests: [...]
|
|
459
|
+
id: 'authenticate',
|
|
460
|
+
requests: [{ id: 'login', requestOptions: {...} }]
|
|
169
461
|
},
|
|
170
462
|
{
|
|
171
|
-
id: '
|
|
172
|
-
markConcurrentPhase: true,
|
|
173
|
-
requests: [...]
|
|
463
|
+
id: 'fetch-user-data',
|
|
464
|
+
markConcurrentPhase: true, // This phase runs concurrently...
|
|
465
|
+
requests: [{ id: 'profile', requestOptions: {...} }]
|
|
174
466
|
},
|
|
175
|
-
{
|
|
467
|
+
{
|
|
468
|
+
id: 'fetch-orders',
|
|
469
|
+
markConcurrentPhase: true, // ...with this phase
|
|
470
|
+
requests: [{ id: 'orders', requestOptions: {...} }]
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
id: 'process-results', // This waits for above to complete
|
|
474
|
+
requests: [{ id: 'analytics', requestOptions: {...} }]
|
|
475
|
+
}
|
|
176
476
|
];
|
|
177
477
|
|
|
178
|
-
await stableWorkflow(phases, {
|
|
179
|
-
enableMixedExecution: true
|
|
478
|
+
await stableWorkflow(phases, {
|
|
479
|
+
enableMixedExecution: true, // Enable mixed execution mode
|
|
480
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
180
481
|
});
|
|
181
482
|
```
|
|
182
483
|
|
|
183
|
-
|
|
484
|
+
**Use Case**: Authenticate first (sequential), then fetch multiple data sources in parallel (concurrent), then process results (sequential).
|
|
184
485
|
|
|
185
|
-
|
|
486
|
+
### Non-Linear Workflows
|
|
186
487
|
|
|
187
|
-
|
|
188
|
-
- **SKIP**: Skip upcoming phases and jump to a target
|
|
189
|
-
- **REPLAY**: Re-execute the current phase (with limits)
|
|
190
|
-
- **TERMINATE**: Stop the entire workflow early
|
|
191
|
-
- **CONTINUE**: Proceed to the next phase (default)
|
|
488
|
+
Build dynamic workflows with conditional branching, looping, and early termination:
|
|
192
489
|
|
|
193
490
|
```typescript
|
|
491
|
+
import { PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
492
|
+
|
|
194
493
|
const phases = [
|
|
195
494
|
{
|
|
196
|
-
id: 'validate',
|
|
197
|
-
requests: [
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
495
|
+
id: 'validate-user',
|
|
496
|
+
requests: [
|
|
497
|
+
{ id: 'check', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
498
|
+
],
|
|
499
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
500
|
+
const isValid = phaseResult.responses[0]?.data?.isValid;
|
|
501
|
+
|
|
502
|
+
if (isValid) {
|
|
503
|
+
// Jump directly to success phase
|
|
504
|
+
return {
|
|
505
|
+
action: PHASE_DECISION_ACTIONS.JUMP,
|
|
506
|
+
targetPhaseId: 'success-flow'
|
|
507
|
+
};
|
|
508
|
+
} else {
|
|
509
|
+
// Continue to retry logic
|
|
510
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
201
511
|
}
|
|
202
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
203
512
|
}
|
|
204
513
|
},
|
|
205
|
-
{
|
|
206
|
-
|
|
514
|
+
{
|
|
515
|
+
id: 'retry-validation',
|
|
516
|
+
allowReplay: true,
|
|
517
|
+
maxReplayCount: 3,
|
|
518
|
+
requests: [
|
|
519
|
+
{ id: 'retry', requestOptions: { reqData: { path: '/retry-validate' }, resReq: true } }
|
|
520
|
+
],
|
|
521
|
+
phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
|
|
522
|
+
const replayCount = executionHistory.filter(
|
|
523
|
+
h => h.phaseId === 'retry-validation'
|
|
524
|
+
).length;
|
|
525
|
+
|
|
526
|
+
const success = phaseResult.responses[0]?.data?.success;
|
|
527
|
+
|
|
528
|
+
if (success) {
|
|
529
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'success-flow' };
|
|
530
|
+
} else if (replayCount < 3) {
|
|
531
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
532
|
+
} else {
|
|
533
|
+
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Max retries exceeded' } };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
id: 'success-flow',
|
|
539
|
+
requests: [
|
|
540
|
+
{ id: 'finalize', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
541
|
+
]
|
|
542
|
+
}
|
|
207
543
|
];
|
|
208
544
|
|
|
209
|
-
await stableWorkflow(phases, {
|
|
210
|
-
enableNonLinearExecution: true
|
|
545
|
+
const result = await stableWorkflow(phases, {
|
|
546
|
+
enableNonLinearExecution: true, // Enable non-linear execution
|
|
547
|
+
workflowId: 'adaptive-validation',
|
|
548
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
211
549
|
});
|
|
550
|
+
|
|
551
|
+
console.log(result.executionHistory);
|
|
552
|
+
// Array of execution records showing which phases ran and why
|
|
212
553
|
```
|
|
213
554
|
|
|
555
|
+
**Phase Decision Actions**:
|
|
556
|
+
- **CONTINUE**: Proceed to next sequential phase (default)
|
|
557
|
+
- **JUMP**: Skip to a specific phase by ID
|
|
558
|
+
- **SKIP**: Skip upcoming phases until a target phase (or end)
|
|
559
|
+
- **REPLAY**: Re-execute the current phase (requires `allowReplay: true`)
|
|
560
|
+
- **TERMINATE**: Stop the entire workflow immediately
|
|
561
|
+
|
|
214
562
|
**Decision Hook Context**:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
563
|
+
```typescript
|
|
564
|
+
phaseDecisionHook: async ({
|
|
565
|
+
phaseResult, // Current phase execution result
|
|
566
|
+
executionHistory, // Array of all executed phases
|
|
567
|
+
sharedBuffer, // Cross-phase shared state
|
|
568
|
+
concurrentResults // Results from concurrent phases (mixed execution)
|
|
569
|
+
}) => {
|
|
570
|
+
// Your decision logic
|
|
571
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Replay Limits**:
|
|
576
|
+
```typescript
|
|
577
|
+
{
|
|
578
|
+
id: 'polling-phase',
|
|
579
|
+
allowReplay: true,
|
|
580
|
+
maxReplayCount: 10, // Maximum 10 replays
|
|
581
|
+
requests: [...],
|
|
582
|
+
phaseDecisionHook: async ({ phaseResult }) => {
|
|
583
|
+
if (phaseResult.responses[0]?.data?.ready) {
|
|
584
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
585
|
+
}
|
|
586
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
```
|
|
219
590
|
|
|
220
591
|
### Branched Workflows
|
|
221
592
|
|
|
222
593
|
Execute multiple independent workflow paths in parallel or sequentially:
|
|
223
594
|
|
|
224
|
-
- **Parallel Branches**: Run branches concurrently (mark with `markConcurrentBranch: true`)
|
|
225
|
-
- **Sequential Branches**: Execute branches one after another
|
|
226
|
-
- **Branch-Level Decisions**: Control workflow from branch hooks
|
|
227
|
-
- **Branch Replay/Termination**: Branches support non-linear execution too
|
|
228
|
-
|
|
229
595
|
```typescript
|
|
230
596
|
const branches = [
|
|
231
597
|
{
|
|
232
598
|
id: 'user-flow',
|
|
233
|
-
markConcurrentBranch: true,
|
|
234
|
-
phases: [
|
|
599
|
+
markConcurrentBranch: true, // Execute in parallel
|
|
600
|
+
phases: [
|
|
601
|
+
{ id: 'fetch-user', requests: [...] },
|
|
602
|
+
{ id: 'update-user', requests: [...] }
|
|
603
|
+
]
|
|
235
604
|
},
|
|
236
605
|
{
|
|
237
606
|
id: 'analytics-flow',
|
|
238
|
-
markConcurrentBranch: true,
|
|
239
|
-
phases: [
|
|
607
|
+
markConcurrentBranch: true, // Execute in parallel
|
|
608
|
+
phases: [
|
|
609
|
+
{ id: 'log-event', requests: [...] },
|
|
610
|
+
{ id: 'update-metrics', requests: [...] }
|
|
611
|
+
]
|
|
240
612
|
},
|
|
241
613
|
{
|
|
242
|
-
id: 'cleanup-flow',
|
|
243
|
-
phases: [
|
|
614
|
+
id: 'cleanup-flow', // Sequential (waits for above)
|
|
615
|
+
phases: [
|
|
616
|
+
{ id: 'clear-cache', requests: [...] },
|
|
617
|
+
{ id: 'notify', requests: [...] }
|
|
618
|
+
]
|
|
244
619
|
}
|
|
245
620
|
];
|
|
246
621
|
|
|
247
|
-
await stableWorkflow([], {
|
|
622
|
+
const result = await stableWorkflow([], { // Empty phases array
|
|
248
623
|
enableBranchExecution: true,
|
|
249
|
-
branches
|
|
624
|
+
branches,
|
|
625
|
+
workflowId: 'multi-branch-workflow',
|
|
626
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
250
627
|
});
|
|
628
|
+
|
|
629
|
+
console.log(result.branches); // Branch execution results
|
|
630
|
+
console.log(result.branchExecutionHistory); // Branch-level execution history
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
**Branch-Level Configuration**:
|
|
634
|
+
```typescript
|
|
635
|
+
const branches = [
|
|
636
|
+
{
|
|
637
|
+
id: 'high-priority-branch',
|
|
638
|
+
markConcurrentBranch: false,
|
|
639
|
+
commonConfig: { // Branch-level config overrides
|
|
640
|
+
commonAttempts: 5,
|
|
641
|
+
commonWait: 2000,
|
|
642
|
+
commonCache: { enabled: true, ttl: 120000 }
|
|
643
|
+
},
|
|
644
|
+
phases: [...]
|
|
645
|
+
}
|
|
646
|
+
];
|
|
251
647
|
```
|
|
252
648
|
|
|
253
649
|
**Branch Features**:
|
|
254
|
-
- Each branch has
|
|
650
|
+
- Each branch has independent phase execution
|
|
255
651
|
- Branches share the workflow's `sharedBuffer`
|
|
256
652
|
- Branch decision hooks can terminate the entire workflow
|
|
257
653
|
- Supports all execution patterns (mixed, non-linear) within branches
|
|
258
654
|
|
|
655
|
+
**Branch Decision Hooks**:
|
|
656
|
+
```typescript
|
|
657
|
+
const branches = [
|
|
658
|
+
{
|
|
659
|
+
id: 'conditional-branch',
|
|
660
|
+
branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
|
|
661
|
+
if (branchResult.failedRequests > 0) {
|
|
662
|
+
return {
|
|
663
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
664
|
+
terminateWorkflow: true, // Terminate entire workflow
|
|
665
|
+
metadata: { reason: 'Critical branch failed' }
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
669
|
+
},
|
|
670
|
+
phases: [...]
|
|
671
|
+
}
|
|
672
|
+
];
|
|
673
|
+
```
|
|
674
|
+
|
|
259
675
|
## Advanced Capabilities
|
|
260
676
|
|
|
261
677
|
### Config Cascading
|
|
@@ -267,7 +683,12 @@ await stableWorkflow(phases, {
|
|
|
267
683
|
// Workflow-level config (lowest priority)
|
|
268
684
|
commonAttempts: 3,
|
|
269
685
|
commonWait: 1000,
|
|
686
|
+
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
270
687
|
commonCache: { enabled: true, ttl: 60000 },
|
|
688
|
+
commonRequestData: {
|
|
689
|
+
hostname: 'api.example.com',
|
|
690
|
+
headers: { 'X-API-Version': 'v2' }
|
|
691
|
+
},
|
|
271
692
|
|
|
272
693
|
branches: [{
|
|
273
694
|
id: 'my-branch',
|
|
@@ -280,13 +701,17 @@ await stableWorkflow(phases, {
|
|
|
280
701
|
id: 'my-phase',
|
|
281
702
|
commonConfig: {
|
|
282
703
|
// Phase-level config (overrides branch and workflow)
|
|
283
|
-
commonAttempts: 1
|
|
704
|
+
commonAttempts: 1,
|
|
705
|
+
commonCache: { enabled: false }
|
|
284
706
|
},
|
|
285
707
|
requests: [{
|
|
708
|
+
id: 'my-request',
|
|
286
709
|
requestOptions: {
|
|
287
710
|
// Request-level config (highest priority)
|
|
711
|
+
reqData: { path: '/critical' },
|
|
288
712
|
attempts: 10,
|
|
289
|
-
|
|
713
|
+
wait: 100,
|
|
714
|
+
cache: { enabled: true, ttl: 300000 }
|
|
290
715
|
}
|
|
291
716
|
}]
|
|
292
717
|
}]
|
|
@@ -294,86 +719,451 @@ await stableWorkflow(phases, {
|
|
|
294
719
|
});
|
|
295
720
|
```
|
|
296
721
|
|
|
722
|
+
**Priority**: Request > Phase > Branch > Workflow
|
|
723
|
+
|
|
724
|
+
### Request Grouping
|
|
725
|
+
|
|
726
|
+
Define reusable configurations for groups of related requests:
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
const requests = [
|
|
730
|
+
{
|
|
731
|
+
id: 'critical-1',
|
|
732
|
+
groupId: 'critical',
|
|
733
|
+
requestOptions: { reqData: { path: '/critical/1' }, resReq: true }
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
id: 'critical-2',
|
|
737
|
+
groupId: 'critical',
|
|
738
|
+
requestOptions: { reqData: { path: '/critical/2' }, resReq: true }
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
id: 'optional-1',
|
|
742
|
+
groupId: 'optional',
|
|
743
|
+
requestOptions: { reqData: { path: '/optional/1' }, resReq: false }
|
|
744
|
+
}
|
|
745
|
+
];
|
|
746
|
+
|
|
747
|
+
await stableApiGateway(requests, {
|
|
748
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
749
|
+
commonAttempts: 1, // Default: 1 attempt
|
|
750
|
+
|
|
751
|
+
requestGroups: [
|
|
752
|
+
{
|
|
753
|
+
groupId: 'critical',
|
|
754
|
+
commonAttempts: 5, // Critical requests: 5 attempts
|
|
755
|
+
commonWait: 2000,
|
|
756
|
+
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
757
|
+
commonFinalErrorAnalyzer: async () => false // Never suppress errors
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
groupId: 'optional',
|
|
761
|
+
commonAttempts: 2, // Optional requests: 2 attempts
|
|
762
|
+
commonWait: 500,
|
|
763
|
+
commonFinalErrorAnalyzer: async () => true // Suppress errors (return false)
|
|
764
|
+
}
|
|
765
|
+
]
|
|
766
|
+
});
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Use Cases**:
|
|
770
|
+
- Different retry strategies for critical vs. optional requests
|
|
771
|
+
- Separate error handling for different request types
|
|
772
|
+
- Grouped logging and monitoring
|
|
773
|
+
|
|
297
774
|
### Shared Buffer and Pre-Execution Hooks
|
|
298
775
|
|
|
299
|
-
Share state and transform requests
|
|
776
|
+
Share state across phases/branches and dynamically transform requests:
|
|
300
777
|
|
|
301
|
-
**Shared Buffer**:
|
|
778
|
+
**Shared Buffer**:
|
|
302
779
|
```typescript
|
|
303
|
-
const sharedBuffer = {
|
|
780
|
+
const sharedBuffer = {
|
|
781
|
+
authToken: null,
|
|
782
|
+
userId: null,
|
|
783
|
+
metrics: []
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const phases = [
|
|
787
|
+
{
|
|
788
|
+
id: 'auth',
|
|
789
|
+
requests: [{
|
|
790
|
+
id: 'login',
|
|
791
|
+
requestOptions: {
|
|
792
|
+
reqData: { path: '/login', method: REQUEST_METHODS.POST },
|
|
793
|
+
resReq: true,
|
|
794
|
+
preExecution: {
|
|
795
|
+
preExecutionHook: ({ commonBuffer }) => {
|
|
796
|
+
// Write to buffer after response
|
|
797
|
+
return {};
|
|
798
|
+
},
|
|
799
|
+
preExecutionHookParams: {},
|
|
800
|
+
applyPreExecutionConfigOverride: false,
|
|
801
|
+
continueOnPreExecutionHookFailure: false
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}]
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
id: 'fetch-data',
|
|
808
|
+
requests: [{
|
|
809
|
+
id: 'profile',
|
|
810
|
+
requestOptions: {
|
|
811
|
+
reqData: { path: '/profile' },
|
|
812
|
+
resReq: true,
|
|
813
|
+
preExecution: {
|
|
814
|
+
preExecutionHook: ({ commonBuffer }) => {
|
|
815
|
+
// Use token from buffer
|
|
816
|
+
return {
|
|
817
|
+
reqData: {
|
|
818
|
+
headers: {
|
|
819
|
+
'Authorization': `Bearer ${commonBuffer.authToken}`
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
},
|
|
824
|
+
applyPreExecutionConfigOverride: true // Apply returned config
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}]
|
|
828
|
+
}
|
|
829
|
+
];
|
|
304
830
|
|
|
305
831
|
await stableWorkflow(phases, {
|
|
306
832
|
sharedBuffer,
|
|
307
|
-
|
|
833
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
308
834
|
});
|
|
835
|
+
|
|
836
|
+
console.log(sharedBuffer); // Updated with data from workflow
|
|
309
837
|
```
|
|
310
838
|
|
|
311
|
-
**Pre-Execution
|
|
839
|
+
**Pre-Execution Hook Use Cases**:
|
|
840
|
+
- Dynamic header injection (auth tokens, correlation IDs)
|
|
841
|
+
- Request payload transformation based on previous responses
|
|
842
|
+
- Conditional request configuration (skip, modify, enhance)
|
|
843
|
+
- Cross-phase state management
|
|
844
|
+
|
|
845
|
+
**Hook Failure Handling**:
|
|
312
846
|
```typescript
|
|
313
847
|
{
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
},
|
|
324
|
-
applyPreExecutionConfigOverride: true
|
|
325
|
-
}
|
|
848
|
+
preExecution: {
|
|
849
|
+
preExecutionHook: async ({ commonBuffer, inputParams }) => {
|
|
850
|
+
// May throw error
|
|
851
|
+
const token = await fetchTokenFromExternalSource();
|
|
852
|
+
return { reqData: { headers: { 'Authorization': token } } };
|
|
853
|
+
},
|
|
854
|
+
continueOnPreExecutionHookFailure: true // Continue even if hook fails
|
|
326
855
|
}
|
|
327
856
|
}
|
|
328
857
|
```
|
|
329
858
|
|
|
330
859
|
### Comprehensive Observability
|
|
331
860
|
|
|
332
|
-
Built-in hooks for monitoring, logging, and analysis:
|
|
861
|
+
Built-in hooks for monitoring, logging, and analysis at every level:
|
|
333
862
|
|
|
334
863
|
**Request-Level Hooks**:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
864
|
+
```typescript
|
|
865
|
+
await stableRequest({
|
|
866
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
867
|
+
resReq: true,
|
|
868
|
+
attempts: 3,
|
|
869
|
+
|
|
870
|
+
// Validate response content
|
|
871
|
+
responseAnalyzer: async ({ data, reqData, params }) => {
|
|
872
|
+
console.log('Analyzing response:', data);
|
|
873
|
+
return data.status === 'success'; // false = retry
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
// Custom error handling
|
|
877
|
+
handleErrors: async ({ errorLog, reqData, commonBuffer }) => {
|
|
878
|
+
console.error('Request failed:', errorLog);
|
|
879
|
+
await sendToMonitoring(errorLog);
|
|
880
|
+
},
|
|
881
|
+
|
|
882
|
+
// Log successful attempts
|
|
883
|
+
handleSuccessfulAttemptData: async ({ successfulAttemptData, reqData }) => {
|
|
884
|
+
console.log('Request succeeded:', successfulAttemptData);
|
|
885
|
+
},
|
|
886
|
+
|
|
887
|
+
// Analyze final error after all retries
|
|
888
|
+
finalErrorAnalyzer: async ({ error, reqData }) => {
|
|
889
|
+
console.error('All retries exhausted:', error);
|
|
890
|
+
return error.message.includes('404'); // true = return false instead of throw
|
|
891
|
+
},
|
|
892
|
+
|
|
893
|
+
// Pass custom parameters to hooks
|
|
894
|
+
hookParams: {
|
|
895
|
+
responseAnalyzerParams: { expectedFormat: 'json' },
|
|
896
|
+
handleErrorsParams: { alertChannel: 'slack' }
|
|
897
|
+
},
|
|
898
|
+
|
|
899
|
+
logAllErrors: true,
|
|
900
|
+
logAllSuccessfulAttempts: true
|
|
901
|
+
});
|
|
902
|
+
```
|
|
339
903
|
|
|
340
904
|
**Workflow-Level Hooks**:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
905
|
+
```typescript
|
|
906
|
+
await stableWorkflow(phases, {
|
|
907
|
+
workflowId: 'monitored-workflow',
|
|
908
|
+
|
|
909
|
+
// Called after each phase completes
|
|
910
|
+
handlePhaseCompletion: async ({ workflowId, phaseResult, params }) => {
|
|
911
|
+
console.log(`Phase ${phaseResult.phaseId} completed`);
|
|
912
|
+
console.log(`Requests: ${phaseResult.totalRequests}`);
|
|
913
|
+
console.log(`Success: ${phaseResult.successfulRequests}`);
|
|
914
|
+
console.log(`Failed: ${phaseResult.failedRequests}`);
|
|
915
|
+
await sendMetrics(phaseResult);
|
|
916
|
+
},
|
|
917
|
+
|
|
918
|
+
// Called when a phase fails
|
|
919
|
+
handlePhaseError: async ({ workflowId, error, phaseResult }) => {
|
|
920
|
+
console.error(`Phase ${phaseResult.phaseId} failed:`, error);
|
|
921
|
+
await alertOnCall(error);
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
// Monitor non-linear execution decisions
|
|
925
|
+
handlePhaseDecision: async ({ workflowId, decision, phaseResult }) => {
|
|
926
|
+
console.log(`Phase decision: ${decision.action}`);
|
|
927
|
+
if (decision.targetPhaseId) {
|
|
928
|
+
console.log(`Target: ${decision.targetPhaseId}`);
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
|
|
932
|
+
// Monitor branch completion
|
|
933
|
+
handleBranchCompletion: async ({ workflowId, branchResult }) => {
|
|
934
|
+
console.log(`Branch ${branchResult.branchId} completed`);
|
|
935
|
+
},
|
|
936
|
+
|
|
937
|
+
// Monitor branch decisions
|
|
938
|
+
handleBranchDecision: async ({ workflowId, decision, branchResult }) => {
|
|
939
|
+
console.log(`Branch decision: ${decision.action}`);
|
|
940
|
+
},
|
|
941
|
+
|
|
942
|
+
// Pass parameters to workflow hooks
|
|
943
|
+
workflowHookParams: {
|
|
944
|
+
handlePhaseCompletionParams: { environment: 'production' },
|
|
945
|
+
handlePhaseErrorParams: { severity: 'high' }
|
|
946
|
+
},
|
|
947
|
+
|
|
948
|
+
logPhaseResults: true,
|
|
949
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
950
|
+
});
|
|
951
|
+
```
|
|
346
952
|
|
|
347
953
|
**Execution History**:
|
|
348
|
-
|
|
954
|
+
```typescript
|
|
955
|
+
const result = await stableWorkflow(phases, {
|
|
956
|
+
enableNonLinearExecution: true,
|
|
957
|
+
workflowId: 'tracked-workflow',
|
|
958
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// Detailed execution history
|
|
962
|
+
result.executionHistory.forEach(record => {
|
|
963
|
+
console.log({
|
|
964
|
+
phaseId: record.phaseId,
|
|
965
|
+
executionNumber: record.executionNumber,
|
|
966
|
+
decision: record.decision,
|
|
967
|
+
timestamp: record.timestamp,
|
|
968
|
+
metadata: record.metadata
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// Branch execution history
|
|
973
|
+
result.branchExecutionHistory?.forEach(record => {
|
|
974
|
+
console.log({
|
|
975
|
+
branchId: record.branchId,
|
|
976
|
+
action: record.action,
|
|
977
|
+
timestamp: record.timestamp
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
### Trial Mode
|
|
983
|
+
|
|
984
|
+
Test and debug workflows without making real API calls:
|
|
985
|
+
|
|
986
|
+
```typescript
|
|
987
|
+
await stableRequest({
|
|
988
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
989
|
+
resReq: true,
|
|
990
|
+
attempts: 3,
|
|
991
|
+
trialMode: {
|
|
992
|
+
enabled: true,
|
|
993
|
+
successProbability: 0.5, // 50% chance of success
|
|
994
|
+
retryableProbability: 0.8, // 80% of failures are retryable
|
|
995
|
+
latencyRange: { min: 100, max: 500 } // Simulated latency: 100-500ms
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
**Use Cases**:
|
|
1001
|
+
- Test retry logic without hitting APIs
|
|
1002
|
+
- Simulate failure scenarios
|
|
1003
|
+
- Load testing with controlled failure rates
|
|
1004
|
+
- Development without backend dependencies
|
|
1005
|
+
|
|
1006
|
+
## Common Use Cases
|
|
1007
|
+
|
|
1008
|
+
### Multi-Step Data Synchronization
|
|
1009
|
+
|
|
1010
|
+
```typescript
|
|
1011
|
+
const syncPhases = [
|
|
1012
|
+
{
|
|
1013
|
+
id: 'fetch-source-data',
|
|
1014
|
+
concurrentExecution: true,
|
|
1015
|
+
requests: [
|
|
1016
|
+
{ id: 'users', requestOptions: { reqData: { path: '/source/users' }, resReq: true } },
|
|
1017
|
+
{ id: 'orders', requestOptions: { reqData: { path: '/source/orders' }, resReq: true } }
|
|
1018
|
+
]
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
id: 'transform-data',
|
|
1022
|
+
requests: [
|
|
1023
|
+
{
|
|
1024
|
+
id: 'transform',
|
|
1025
|
+
requestOptions: {
|
|
1026
|
+
reqData: { path: '/transform', method: REQUEST_METHODS.POST },
|
|
1027
|
+
resReq: true
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
]
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
id: 'upload-to-destination',
|
|
1034
|
+
concurrentExecution: true,
|
|
1035
|
+
requests: [
|
|
1036
|
+
{ id: 'upload-users', requestOptions: { reqData: { path: '/dest/users', method: REQUEST_METHODS.POST }, resReq: false } },
|
|
1037
|
+
{ id: 'upload-orders', requestOptions: { reqData: { path: '/dest/orders', method: REQUEST_METHODS.POST }, resReq: false } }
|
|
1038
|
+
]
|
|
1039
|
+
}
|
|
1040
|
+
];
|
|
349
1041
|
|
|
350
|
-
|
|
1042
|
+
await stableWorkflow(syncPhases, {
|
|
1043
|
+
workflowId: 'data-sync',
|
|
1044
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
1045
|
+
commonAttempts: 3,
|
|
1046
|
+
stopOnFirstPhaseError: true,
|
|
1047
|
+
logPhaseResults: true
|
|
1048
|
+
});
|
|
1049
|
+
```
|
|
351
1050
|
|
|
352
|
-
###
|
|
1051
|
+
### API Gateway with Fallbacks
|
|
353
1052
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
1053
|
+
```typescript
|
|
1054
|
+
const requests = [
|
|
1055
|
+
{
|
|
1056
|
+
id: 'primary-service',
|
|
1057
|
+
groupId: 'critical',
|
|
1058
|
+
requestOptions: {
|
|
1059
|
+
reqData: { hostname: 'primary.api.com', path: '/data' },
|
|
1060
|
+
resReq: true,
|
|
1061
|
+
finalErrorAnalyzer: async ({ error }) => {
|
|
1062
|
+
// If primary fails, mark as handled (don't throw)
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
id: 'fallback-service',
|
|
1069
|
+
groupId: 'fallback',
|
|
1070
|
+
requestOptions: {
|
|
1071
|
+
reqData: { hostname: 'backup.api.com', path: '/data' },
|
|
1072
|
+
resReq: true
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
];
|
|
357
1076
|
|
|
358
|
-
|
|
1077
|
+
const results = await stableApiGateway(requests, {
|
|
1078
|
+
concurrentExecution: false, // Sequential: try fallback only if primary fails
|
|
1079
|
+
requestGroups: [
|
|
1080
|
+
{ groupId: 'critical', commonAttempts: 3 },
|
|
1081
|
+
{ groupId: 'fallback', commonAttempts: 1 }
|
|
1082
|
+
]
|
|
1083
|
+
});
|
|
1084
|
+
```
|
|
359
1085
|
|
|
360
|
-
|
|
361
|
-
- **Rate Limiting**: `RateLimiter`
|
|
362
|
-
- **Concurrency**: `ConcurrencyLimiter`
|
|
363
|
-
- **Caching**: `CacheManager`, `getGlobalCacheManager`, `resetGlobalCacheManager`
|
|
364
|
-
- **Execution Utilities**: `executeNonLinearWorkflow`, `executeBranchWorkflow`, `executePhase`
|
|
1086
|
+
### Polling with Conditional Termination
|
|
365
1087
|
|
|
366
|
-
|
|
1088
|
+
```typescript
|
|
1089
|
+
const pollingPhases = [
|
|
1090
|
+
{
|
|
1091
|
+
id: 'poll-job-status',
|
|
1092
|
+
allowReplay: true,
|
|
1093
|
+
maxReplayCount: 20,
|
|
1094
|
+
requests: [
|
|
1095
|
+
{
|
|
1096
|
+
id: 'status-check',
|
|
1097
|
+
requestOptions: {
|
|
1098
|
+
reqData: { path: '/job/status' },
|
|
1099
|
+
resReq: true
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
],
|
|
1103
|
+
phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
|
|
1104
|
+
const status = phaseResult.responses[0]?.data?.status;
|
|
1105
|
+
const attempts = executionHistory.filter(h => h.phaseId === 'poll-job-status').length;
|
|
1106
|
+
|
|
1107
|
+
if (status === 'completed') {
|
|
1108
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
1109
|
+
} else if (status === 'failed') {
|
|
1110
|
+
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Job failed' } };
|
|
1111
|
+
} else if (attempts < 20) {
|
|
1112
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
1113
|
+
} else {
|
|
1114
|
+
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Timeout' } };
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
id: 'process-results',
|
|
1120
|
+
requests: [
|
|
1121
|
+
{ id: 'fetch-results', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
|
|
1122
|
+
]
|
|
1123
|
+
}
|
|
1124
|
+
];
|
|
1125
|
+
|
|
1126
|
+
await stableWorkflow(pollingPhases, {
|
|
1127
|
+
enableNonLinearExecution: true,
|
|
1128
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
1129
|
+
commonWait: 5000 // 5 second wait between polls
|
|
1130
|
+
});
|
|
1131
|
+
```
|
|
367
1132
|
|
|
368
|
-
|
|
369
|
-
- `REQUEST_METHODS`: GET, POST, PUT, PATCH, DELETE, etc.
|
|
370
|
-
- `PHASE_DECISION_ACTIONS`: CONTINUE, JUMP, SKIP, REPLAY, TERMINATE
|
|
371
|
-
- `VALID_REQUEST_PROTOCOLS`: HTTP, HTTPS
|
|
372
|
-
- `CircuitBreakerState`: CLOSED, OPEN, HALF_OPEN
|
|
1133
|
+
### Webhook Retry with Circuit Breaker
|
|
373
1134
|
|
|
374
|
-
|
|
1135
|
+
```typescript
|
|
1136
|
+
import { CircuitBreaker, REQUEST_METHODS } from '@emmvish/stable-request';
|
|
1137
|
+
|
|
1138
|
+
const webhookBreaker = new CircuitBreaker({
|
|
1139
|
+
failureThreshold: 3,
|
|
1140
|
+
successThreshold: 2,
|
|
1141
|
+
timeout: 30000
|
|
1142
|
+
});
|
|
375
1143
|
|
|
376
|
-
|
|
1144
|
+
async function sendWebhook(eventData: any) {
|
|
1145
|
+
try {
|
|
1146
|
+
await stableRequest({
|
|
1147
|
+
reqData: {
|
|
1148
|
+
hostname: 'webhook.example.com',
|
|
1149
|
+
path: '/events',
|
|
1150
|
+
method: REQUEST_METHODS.POST,
|
|
1151
|
+
body: eventData
|
|
1152
|
+
},
|
|
1153
|
+
attempts: 5,
|
|
1154
|
+
wait: 1000,
|
|
1155
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
1156
|
+
circuitBreaker: webhookBreaker,
|
|
1157
|
+
handleErrors: async ({ errorLog }) => {
|
|
1158
|
+
console.error('Webhook delivery failed:', errorLog);
|
|
1159
|
+
await queueForRetry(eventData);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
console.error('Webhook permanently failed:', error);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
```
|
|
377
1167
|
|
|
378
1168
|
## License
|
|
379
1169
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emmvish/stable-request",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "stable-request is a TypeScript-first HTTP workflow execution engine for real-world distributed systems — where HTTP 200 OK does not guarantee business success, and HTTP failures still deserve structured, actionable responses.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|