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