@emmvish/stable-request 1.6.0 → 1.6.2
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 +224 -1448
- package/dist/core/stable-workflow.d.ts.map +1 -1
- package/dist/core/stable-workflow.js +46 -2
- package/dist/core/stable-workflow.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +95 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utilities/execute-branch-workflow.d.ts +3 -0
- package/dist/utilities/execute-branch-workflow.d.ts.map +1 -0
- package/dist/utilities/execute-branch-workflow.js +470 -0
- package/dist/utilities/execute-branch-workflow.js.map +1 -0
- package/dist/utilities/execute-non-linear-workflow.d.ts +2 -10
- package/dist/utilities/execute-non-linear-workflow.d.ts.map +1 -1
- package/dist/utilities/execute-non-linear-workflow.js.map +1 -1
- package/dist/utilities/index.d.ts +1 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +1 -0
- package/dist/utilities/index.js.map +1 -1
- package/package.json +4 -1
- package/dist/utilities/process-phase-result.d.ts +0 -1
- package/dist/utilities/process-phase-result.d.ts.map +0 -1
- package/dist/utilities/process-phase-result.js +0 -2
- package/dist/utilities/process-phase-result.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,64 +1,37 @@
|
|
|
1
|
-
# stable-request
|
|
1
|
+
# @emmvish/stable-request
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A powerful HTTP Workflow Execution Engine for Node.js that transforms unreliable API calls into robust, production-ready workflows with advanced retry mechanisms, circuit breakers, and sophisticated execution patterns.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Navigation
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- Observed
|
|
9
|
-
- Analyzed
|
|
10
|
-
- Retried intelligently
|
|
11
|
-
- Suppressed when non-critical
|
|
12
|
-
- Escalated when business-critical
|
|
13
|
-
|
|
14
|
-
All without crashing your application or hiding context behind opaque errors.
|
|
15
|
-
|
|
16
|
-
**stable-request treats failures as data.**
|
|
17
|
-
|
|
18
|
-
> If you’ve ever logged `error.message` and thought
|
|
19
|
-
> **“This tells me absolutely nothing”** — this library is for you.
|
|
20
|
-
|
|
21
|
-
In addition, it enables **reliability** **content-aware retries**, **hierarchical configuration**, **batch orchestration**, and **multi-phase workflows** with deep observability — all built on top of standard HTTP calls.
|
|
22
|
-
|
|
23
|
-
All in all, it provides you with the **entire ecosystem** to build **API-integrations based workflows** with **complete flexibility**.
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## Choose your entry point
|
|
28
|
-
|
|
29
|
-
| Need | Use |
|
|
30
|
-
|-----|-----|
|
|
31
|
-
| Reliable single API call | `stableRequest` |
|
|
32
|
-
| Batch or fan-out requests | `stableApiGateway` |
|
|
33
|
-
| Multi-step orchestration | `stableWorkflow` |
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Start small and scale.
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## 📚 Table of Contents
|
|
41
|
-
<!-- TOC START -->
|
|
7
|
+
- [Overview](#overview)
|
|
42
8
|
- [Installation](#installation)
|
|
43
|
-
- [Core Features](#core-features)
|
|
44
9
|
- [Quick Start](#quick-start)
|
|
45
|
-
- [
|
|
10
|
+
- [Core Features](#core-features)
|
|
11
|
+
- [Intelligent Retry Strategies](#intelligent-retry-strategies)
|
|
12
|
+
- [Circuit Breaker Pattern](#circuit-breaker-pattern)
|
|
13
|
+
- [Response Caching](#response-caching)
|
|
14
|
+
- [Rate Limiting and Concurrency Control](#rate-limiting-and-concurrency-control)
|
|
15
|
+
- [Workflow Execution Patterns](#workflow-execution-patterns)
|
|
16
|
+
- [Sequential and Concurrent Phases](#sequential-and-concurrent-phases)
|
|
17
|
+
- [Mixed Execution Mode](#mixed-execution-mode)
|
|
46
18
|
- [Non-Linear Workflows](#non-linear-workflows)
|
|
47
|
-
- [
|
|
48
|
-
|
|
49
|
-
- [
|
|
50
|
-
- [
|
|
51
|
-
- [
|
|
52
|
-
|
|
53
|
-
- [Request Grouping](#request-grouping)
|
|
54
|
-
- [Concurrency Control](#concurrency-control)
|
|
55
|
-
- [Response Analysis](#response-analysis)
|
|
56
|
-
- [Error Handling](#error-handling)
|
|
57
|
-
- [Advanced Use Cases](#advanced-use-cases)
|
|
19
|
+
- [Branched Workflows](#branched-workflows)
|
|
20
|
+
- [Advanced Capabilities](#advanced-capabilities)
|
|
21
|
+
- [Config Cascading](#config-cascading)
|
|
22
|
+
- [Shared Buffer and Pre-Execution Hooks](#shared-buffer-and-pre-execution-hooks)
|
|
23
|
+
- [Comprehensive Observability](#comprehensive-observability)
|
|
24
|
+
- [API Surface](#api-surface)
|
|
58
25
|
- [License](#license)
|
|
59
|
-
<!-- TOC END -->
|
|
60
26
|
|
|
61
|
-
|
|
27
|
+
## Overview
|
|
28
|
+
|
|
29
|
+
`@emmvish/stable-request` is built for applications that need to orchestrate complex, multi-step API interactions with guarantees around reliability, observability, and fault tolerance. Unlike simple HTTP clients, it provides:
|
|
30
|
+
|
|
31
|
+
- **Workflow-First Design**: Organize API calls into phases, branches, and decision trees
|
|
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**: Detailed hooks for monitoring, logging, and error analysis
|
|
62
35
|
|
|
63
36
|
## Installation
|
|
64
37
|
|
|
@@ -66,25 +39,9 @@ Start small and scale.
|
|
|
66
39
|
npm install @emmvish/stable-request
|
|
67
40
|
```
|
|
68
41
|
|
|
69
|
-
## Core Features
|
|
70
|
-
|
|
71
|
-
- ✅ **Configurable Retry Strategies**: Fixed, Linear, and Exponential backoff
|
|
72
|
-
- ✅ **Circuit Breaker**: Prevent cascading failures with automatic circuit breaking
|
|
73
|
-
- ✅ **Rate Limiting**: Control request throughput across single or multiple requests
|
|
74
|
-
- ✅ **Response Caching**: Built-in TTL-based caching with global cache manager
|
|
75
|
-
- ✅ **Batch Processing**: Execute multiple requests concurrently or sequentially via API Gateway
|
|
76
|
-
- ✅ **Multi-Phase Non-Linear Workflows**: Orchestrate complex request workflows with phase dependencies
|
|
77
|
-
- ✅ **Pre-Execution Hooks**: Transform requests before execution with dynamic configuration
|
|
78
|
-
- ✅ **Shared Buffer**: Share state across requests in workflows and gateways
|
|
79
|
-
- ✅ **Request Grouping**: Apply different configurations to request groups
|
|
80
|
-
- ✅ **Observability Hooks**: Track errors, successful attempts, and phase completions
|
|
81
|
-
- ✅ **Response Analysis**: Validate responses and trigger retries based on content
|
|
82
|
-
- ✅ **Trial Mode**: Test configurations without making real API calls
|
|
83
|
-
- ✅ **TypeScript Support**: Full type safety with generics for request/response data
|
|
84
|
-
|
|
85
42
|
## Quick Start
|
|
86
43
|
|
|
87
|
-
###
|
|
44
|
+
### Single Request with Retry
|
|
88
45
|
|
|
89
46
|
```typescript
|
|
90
47
|
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
@@ -92,7 +49,7 @@ import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
|
92
49
|
const data = await stableRequest({
|
|
93
50
|
reqData: {
|
|
94
51
|
hostname: 'api.example.com',
|
|
95
|
-
path: '/users
|
|
52
|
+
path: '/users',
|
|
96
53
|
method: 'GET'
|
|
97
54
|
},
|
|
98
55
|
resReq: true,
|
|
@@ -100,1505 +57,324 @@ const data = await stableRequest({
|
|
|
100
57
|
wait: 1000,
|
|
101
58
|
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
102
59
|
});
|
|
103
|
-
|
|
104
|
-
console.log(data);
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### Batch Requests via API Gateway
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
import { stableApiGateway } from '@emmvish/stable-request';
|
|
111
|
-
|
|
112
|
-
const requests = [
|
|
113
|
-
{ id: 'user-1', requestOptions: { reqData: { path: '/users/1' }, resReq: true } },
|
|
114
|
-
{ id: 'user-2', requestOptions: { reqData: { path: '/users/2' }, resReq: true } },
|
|
115
|
-
{ id: 'user-3', requestOptions: { reqData: { path: '/users/3' }, resReq: true } }
|
|
116
|
-
];
|
|
117
|
-
|
|
118
|
-
const results = await stableApiGateway(requests, {
|
|
119
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
120
|
-
concurrentExecution: true,
|
|
121
|
-
maxConcurrentRequests: 10
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
results.forEach(result => {
|
|
125
|
-
if (result.success) {
|
|
126
|
-
console.log(`Request ${result.requestId}:`, result.data);
|
|
127
|
-
} else {
|
|
128
|
-
console.error(`Request ${result.requestId} failed:`, result.error);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
60
|
```
|
|
132
61
|
|
|
133
62
|
### Multi-Phase Workflow
|
|
134
63
|
|
|
135
64
|
```typescript
|
|
136
|
-
import { stableWorkflow
|
|
65
|
+
import { stableWorkflow } from '@emmvish/stable-request';
|
|
137
66
|
|
|
138
|
-
const
|
|
67
|
+
const result = await stableWorkflow([
|
|
139
68
|
{
|
|
140
|
-
id: '
|
|
69
|
+
id: 'auth',
|
|
141
70
|
requests: [
|
|
142
71
|
{ id: 'login', requestOptions: { reqData: { path: '/auth/login' }, resReq: true } }
|
|
143
72
|
]
|
|
144
73
|
},
|
|
145
74
|
{
|
|
146
|
-
id: 'data
|
|
75
|
+
id: 'fetch-data',
|
|
147
76
|
concurrentExecution: true,
|
|
148
77
|
requests: [
|
|
149
78
|
{ id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
|
|
150
|
-
{ id: '
|
|
79
|
+
{ id: 'orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } }
|
|
151
80
|
]
|
|
152
81
|
}
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
const result = await stableWorkflow(phases, {
|
|
156
|
-
workflowId: 'data-pipeline',
|
|
82
|
+
], {
|
|
83
|
+
workflowId: 'user-data-sync',
|
|
157
84
|
commonRequestData: { hostname: 'api.example.com' },
|
|
158
|
-
stopOnFirstPhaseError: true
|
|
159
|
-
logPhaseResults: true
|
|
85
|
+
stopOnFirstPhaseError: true
|
|
160
86
|
});
|
|
161
|
-
|
|
162
|
-
console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
|
|
163
87
|
```
|
|
164
88
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
169
|
-
|
|
170
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
171
|
-
{
|
|
172
|
-
id: 'check-status',
|
|
173
|
-
requests: [
|
|
174
|
-
{ id: 'status', requestOptions: { reqData: { path: '/status' }, resReq: true } }
|
|
175
|
-
],
|
|
176
|
-
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
177
|
-
const status = phaseResult.responses[0]?.data?.status;
|
|
178
|
-
|
|
179
|
-
if (status === 'completed') {
|
|
180
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' };
|
|
181
|
-
} else if (status === 'processing') {
|
|
182
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
183
|
-
return { action: PHASE_DECISION_ACTIONS.REPLAY }; // Replay this phase
|
|
184
|
-
} else {
|
|
185
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-handler' };
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
allowReplay: true,
|
|
189
|
-
maxReplayCount: 10
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
id: 'process',
|
|
193
|
-
requests: [
|
|
194
|
-
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
195
|
-
]
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
id: 'error-handler',
|
|
199
|
-
requests: [
|
|
200
|
-
{ id: 'error', requestOptions: { reqData: { path: '/error' }, resReq: true } }
|
|
201
|
-
]
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
id: 'finalize',
|
|
205
|
-
requests: [
|
|
206
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
207
|
-
]
|
|
208
|
-
}
|
|
209
|
-
];
|
|
89
|
+
## Core Features
|
|
210
90
|
|
|
211
|
-
|
|
212
|
-
workflowId: 'dynamic-workflow',
|
|
213
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
214
|
-
enableNonLinearExecution: true,
|
|
215
|
-
maxWorkflowIterations: 50,
|
|
216
|
-
sharedBuffer: {}
|
|
217
|
-
});
|
|
91
|
+
### Intelligent Retry Strategies
|
|
218
92
|
|
|
219
|
-
|
|
220
|
-
console.log('Terminated early:', result.terminatedEarly);
|
|
221
|
-
```
|
|
93
|
+
Automatically retry failed requests with configurable strategies:
|
|
222
94
|
|
|
223
|
-
|
|
95
|
+
- **Fixed Delay**: Constant wait time between retries
|
|
96
|
+
- **Linear Backoff**: Incrementally increasing delays
|
|
97
|
+
- **Exponential Backoff**: Exponentially growing delays with optional jitter
|
|
98
|
+
- **Fibonacci Backoff**: Delays based on Fibonacci sequence
|
|
224
99
|
|
|
225
|
-
|
|
100
|
+
Each request can have individual retry configurations, or inherit from workflow-level defaults.
|
|
226
101
|
|
|
227
|
-
|
|
102
|
+
### Circuit Breaker Pattern
|
|
228
103
|
|
|
229
|
-
|
|
104
|
+
Prevent cascade failures and system overload with built-in circuit breakers:
|
|
230
105
|
|
|
231
|
-
|
|
106
|
+
- **Automatic State Management**: Transitions between Closed → Open → Half-Open states
|
|
107
|
+
- **Configurable Thresholds**: Set failure rates and time windows
|
|
108
|
+
- **Request/Attempt Level Tracking**: Monitor at granular or aggregate levels
|
|
109
|
+
- **Graceful Degradation**: Fail fast when services are down
|
|
232
110
|
|
|
233
|
-
|
|
234
|
-
- **`jump`**: Jump to a specific phase by ID
|
|
235
|
-
- **`replay`**: Re-execute the current phase
|
|
236
|
-
- **`skip`**: Skip to a target phase or skip the next phase
|
|
237
|
-
- **`terminate`**: Stop the workflow immediately
|
|
111
|
+
### Response Caching
|
|
238
112
|
|
|
239
|
-
|
|
113
|
+
Reduce redundant API calls with intelligent caching:
|
|
240
114
|
|
|
241
|
-
|
|
242
|
-
|
|
115
|
+
- **TTL-Based Expiration**: Configure cache lifetime per request
|
|
116
|
+
- **Request Fingerprinting**: Automatic deduplication based on request signature
|
|
117
|
+
- **Workflow-Wide Sharing**: Cache responses across phases and branches
|
|
118
|
+
- **Manual Cache Management**: Programmatic cache inspection and clearing
|
|
243
119
|
|
|
244
|
-
|
|
245
|
-
{
|
|
246
|
-
id: 'validate-input',
|
|
247
|
-
requests: [
|
|
248
|
-
{ id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
249
|
-
],
|
|
250
|
-
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
251
|
-
const isValid = phaseResult.responses[0]?.data?.valid;
|
|
252
|
-
|
|
253
|
-
if (isValid) {
|
|
254
|
-
sharedBuffer.validationPassed = true;
|
|
255
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to next phase
|
|
256
|
-
} else {
|
|
257
|
-
return {
|
|
258
|
-
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
259
|
-
metadata: { reason: 'Validation failed' }
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
id: 'process-data',
|
|
266
|
-
requests: [
|
|
267
|
-
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
268
|
-
]
|
|
269
|
-
}
|
|
270
|
-
];
|
|
120
|
+
### Rate Limiting and Concurrency Control
|
|
271
121
|
|
|
272
|
-
|
|
273
|
-
workflowId: 'validation-workflow',
|
|
274
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
275
|
-
enableNonLinearExecution: true,
|
|
276
|
-
sharedBuffer: {}
|
|
277
|
-
});
|
|
122
|
+
Respect API rate limits and control system load:
|
|
278
123
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
124
|
+
- **Token Bucket Rate Limiting**: Smooth out request bursts
|
|
125
|
+
- **Concurrency Limiters**: Cap maximum parallel requests
|
|
126
|
+
- **Per-Phase Configuration**: Different limits for different workflow stages
|
|
127
|
+
- **Automatic Queueing**: Requests wait their turn without failing
|
|
283
128
|
|
|
284
|
-
|
|
129
|
+
## Workflow Execution Patterns
|
|
285
130
|
|
|
286
|
-
|
|
287
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
288
|
-
{
|
|
289
|
-
id: 'check-user-type',
|
|
290
|
-
requests: [
|
|
291
|
-
{ id: 'user', requestOptions: { reqData: { path: '/user/info' }, resReq: true } }
|
|
292
|
-
],
|
|
293
|
-
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
294
|
-
const userType = phaseResult.responses[0]?.data?.type;
|
|
295
|
-
sharedBuffer.userType = userType;
|
|
296
|
-
|
|
297
|
-
if (userType === 'premium') {
|
|
298
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'premium-flow' };
|
|
299
|
-
} else if (userType === 'trial') {
|
|
300
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'trial-flow' };
|
|
301
|
-
} else {
|
|
302
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'free-flow' };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
id: 'premium-flow',
|
|
308
|
-
requests: [
|
|
309
|
-
{ id: 'premium', requestOptions: { reqData: { path: '/premium/data' }, resReq: true } }
|
|
310
|
-
],
|
|
311
|
-
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
id: 'trial-flow',
|
|
315
|
-
requests: [
|
|
316
|
-
{ id: 'trial', requestOptions: { reqData: { path: '/trial/data' }, resReq: true } }
|
|
317
|
-
],
|
|
318
|
-
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
id: 'free-flow',
|
|
322
|
-
requests: [
|
|
323
|
-
{ id: 'free', requestOptions: { reqData: { path: '/free/data' }, resReq: true } }
|
|
324
|
-
]
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
id: 'finalize',
|
|
328
|
-
requests: [
|
|
329
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
330
|
-
]
|
|
331
|
-
}
|
|
332
|
-
];
|
|
131
|
+
### Sequential and Concurrent Phases
|
|
333
132
|
|
|
334
|
-
|
|
335
|
-
enableNonLinearExecution: true,
|
|
336
|
-
sharedBuffer: {},
|
|
337
|
-
handlePhaseDecision: (decision, phaseResult) => {
|
|
338
|
-
console.log(`Phase ${phaseResult.phaseId} decided: ${decision.action}`);
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
```
|
|
133
|
+
Control execution order at the phase level:
|
|
342
134
|
|
|
343
|
-
|
|
135
|
+
- **Sequential Phases**: Execute phases one after another (default)
|
|
136
|
+
- **Concurrent Phases**: Run all phases in parallel
|
|
137
|
+
- **Per-Phase Control**: Each phase can define whether its requests run concurrently or sequentially
|
|
344
138
|
|
|
345
139
|
```typescript
|
|
346
|
-
const phases
|
|
347
|
-
{
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
requests: [
|
|
352
|
-
{ id: 'check', requestOptions: { reqData: { path: '/job/status' }, resReq: true } }
|
|
353
|
-
],
|
|
354
|
-
phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
|
|
355
|
-
const status = phaseResult.responses[0]?.data?.status;
|
|
356
|
-
const attempts = executionHistory.filter(h => h.phaseId === 'poll-job-status').length;
|
|
357
|
-
|
|
358
|
-
if (status === 'completed') {
|
|
359
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
360
|
-
} else if (status === 'failed') {
|
|
361
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-recovery' };
|
|
362
|
-
} else if (attempts < 20) {
|
|
363
|
-
// Still processing, wait and replay
|
|
364
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
365
|
-
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
366
|
-
} else {
|
|
367
|
-
return {
|
|
368
|
-
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
369
|
-
metadata: { reason: 'Job timeout after 20 attempts' }
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
id: 'process-results',
|
|
376
|
-
requests: [
|
|
377
|
-
{ id: 'process', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
|
|
378
|
-
]
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
id: 'error-recovery',
|
|
382
|
-
requests: [
|
|
383
|
-
{ id: 'recover', requestOptions: { reqData: { path: '/job/retry' }, resReq: true } }
|
|
384
|
-
]
|
|
140
|
+
const phases = [
|
|
141
|
+
{ id: 'init', requests: [...] }, // Sequential phase
|
|
142
|
+
{
|
|
143
|
+
id: 'parallel-fetch',
|
|
144
|
+
concurrentExecution: true, // Concurrent requests within phase
|
|
145
|
+
requests: [...]
|
|
385
146
|
}
|
|
386
147
|
];
|
|
387
148
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
391
|
-
enableNonLinearExecution: true,
|
|
392
|
-
maxWorkflowIterations: 100
|
|
149
|
+
await stableWorkflow(phases, {
|
|
150
|
+
concurrentPhaseExecution: true // Run phases in parallel
|
|
393
151
|
});
|
|
394
|
-
|
|
395
|
-
console.log('Total iterations:', result.executionHistory.length);
|
|
396
|
-
console.log('Phases executed:', result.completedPhases);
|
|
397
152
|
```
|
|
398
153
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
403
|
-
{
|
|
404
|
-
id: 'attempt-operation',
|
|
405
|
-
allowReplay: true,
|
|
406
|
-
maxReplayCount: 3,
|
|
407
|
-
requests: [
|
|
408
|
-
{
|
|
409
|
-
id: 'operation',
|
|
410
|
-
requestOptions: {
|
|
411
|
-
reqData: { path: '/risky-operation', method: 'POST' },
|
|
412
|
-
resReq: true,
|
|
413
|
-
attempts: 1 // No retries at request level
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
],
|
|
417
|
-
phaseDecisionHook: async ({ phaseResult, executionHistory, sharedBuffer }) => {
|
|
418
|
-
const success = phaseResult.responses[0]?.success;
|
|
419
|
-
const attemptCount = executionHistory.filter(h => h.phaseId === 'attempt-operation').length;
|
|
420
|
-
|
|
421
|
-
if (success) {
|
|
422
|
-
sharedBuffer.operationResult = phaseResult.responses[0]?.data;
|
|
423
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
424
|
-
} else if (attemptCount < 3) {
|
|
425
|
-
// Exponential backoff
|
|
426
|
-
const delay = 1000 * Math.pow(2, attemptCount);
|
|
427
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
428
|
-
|
|
429
|
-
sharedBuffer.retryAttempts = attemptCount;
|
|
430
|
-
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
431
|
-
} else {
|
|
432
|
-
return {
|
|
433
|
-
action: PHASE_DECISION_ACTIONS.JUMP,
|
|
434
|
-
targetPhaseId: 'fallback-operation',
|
|
435
|
-
metadata: { reason: 'Max retries exceeded' }
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
},
|
|
440
|
-
{
|
|
441
|
-
id: 'primary-flow',
|
|
442
|
-
requests: [
|
|
443
|
-
{ id: 'primary', requestOptions: { reqData: { path: '/primary' }, resReq: true } }
|
|
444
|
-
]
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
id: 'fallback-operation',
|
|
448
|
-
requests: [
|
|
449
|
-
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
450
|
-
]
|
|
451
|
-
}
|
|
452
|
-
];
|
|
154
|
+
### Mixed Execution Mode
|
|
453
155
|
|
|
454
|
-
|
|
455
|
-
enableNonLinearExecution: true,
|
|
456
|
-
sharedBuffer: { retryAttempts: 0 },
|
|
457
|
-
logPhaseResults: true
|
|
458
|
-
});
|
|
459
|
-
```
|
|
156
|
+
Combine sequential and concurrent phases in a single workflow:
|
|
460
157
|
|
|
461
|
-
|
|
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"
|
|
462
161
|
|
|
463
162
|
```typescript
|
|
464
|
-
const phases
|
|
465
|
-
{
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return { action: PHASE_DECISION_ACTIONS.SKIP, targetPhaseId: 'finalize' };
|
|
478
|
-
}
|
|
479
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
480
|
-
}
|
|
481
|
-
},
|
|
482
|
-
{
|
|
483
|
-
id: 'expensive-computation',
|
|
484
|
-
requests: [
|
|
485
|
-
{ id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
|
|
486
|
-
]
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
id: 'save-to-cache',
|
|
490
|
-
requests: [
|
|
491
|
-
{ id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
|
|
492
|
-
]
|
|
493
|
-
},
|
|
494
|
-
{
|
|
495
|
-
id: 'finalize',
|
|
496
|
-
requests: [
|
|
497
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
498
|
-
]
|
|
499
|
-
}
|
|
163
|
+
const phases = [
|
|
164
|
+
{ id: 'auth', requests: [...] }, // Sequential
|
|
165
|
+
{
|
|
166
|
+
id: 'fetch',
|
|
167
|
+
markConcurrentPhase: true, // Runs concurrently with next phase
|
|
168
|
+
requests: [...]
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'more-fetch',
|
|
172
|
+
markConcurrentPhase: true, // Runs concurrently with previous
|
|
173
|
+
requests: [...]
|
|
174
|
+
},
|
|
175
|
+
{ id: 'process', requests: [...] } // Sequential, waits for above
|
|
500
176
|
];
|
|
501
177
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
sharedBuffer: {}
|
|
505
|
-
});
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
#### Execution History and Tracking
|
|
509
|
-
|
|
510
|
-
```typescript
|
|
511
|
-
const result = await stableWorkflow(phases, {
|
|
512
|
-
workflowId: 'tracked-workflow',
|
|
513
|
-
enableNonLinearExecution: true,
|
|
514
|
-
handlePhaseCompletion: ({ phaseResult, workflowId }) => {
|
|
515
|
-
console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
|
|
516
|
-
executionNumber: phaseResult.executionNumber,
|
|
517
|
-
success: phaseResult.success,
|
|
518
|
-
decision: phaseResult.decision
|
|
519
|
-
});
|
|
520
|
-
},
|
|
521
|
-
handlePhaseDecision: (decision, phaseResult) => {
|
|
522
|
-
console.log(`Decision made:`, {
|
|
523
|
-
phase: phaseResult.phaseId,
|
|
524
|
-
action: decision.action,
|
|
525
|
-
target: decision.targetPhaseId,
|
|
526
|
-
metadata: decision.metadata
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
// Analyze execution history
|
|
532
|
-
console.log('Total phase executions:', result.executionHistory.length);
|
|
533
|
-
console.log('Unique phases executed:', new Set(result.executionHistory.map(h => h.phaseId)).size);
|
|
534
|
-
console.log('Replay count:', result.executionHistory.filter(h => h.decision?.action === 'replay').length);
|
|
535
|
-
|
|
536
|
-
result.executionHistory.forEach(record => {
|
|
537
|
-
console.log(`- ${record.phaseId} (attempt ${record.executionNumber}): ${record.success ? '✓' : '✗'}`);
|
|
178
|
+
await stableWorkflow(phases, {
|
|
179
|
+
enableMixedExecution: true
|
|
538
180
|
});
|
|
539
181
|
```
|
|
540
182
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
```typescript
|
|
544
|
-
const result = await stableWorkflow(phases, {
|
|
545
|
-
enableNonLinearExecution: true,
|
|
546
|
-
maxWorkflowIterations: 50, // Prevent infinite loops
|
|
547
|
-
handlePhaseCompletion: ({ phaseResult }) => {
|
|
548
|
-
if (phaseResult.executionNumber && phaseResult.executionNumber > 10) {
|
|
549
|
-
console.warn(`Phase ${phaseResult.phaseId} executed ${phaseResult.executionNumber} times`);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
if (result.terminatedEarly && result.terminationReason?.includes('iterations')) {
|
|
555
|
-
console.error('Workflow hit iteration limit - possible infinite loop');
|
|
556
|
-
}
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
#### Mixed Serial and Parallel Execution
|
|
560
|
-
|
|
561
|
-
Non-linear workflows support mixing serial and parallel phase execution. Mark consecutive phases with `markConcurrentPhase: true` to execute them in parallel, while other phases execute serially.
|
|
562
|
-
|
|
563
|
-
**Basic Mixed Execution:**
|
|
564
|
-
|
|
565
|
-
```typescript
|
|
566
|
-
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
567
|
-
|
|
568
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
569
|
-
{
|
|
570
|
-
id: 'init',
|
|
571
|
-
requests: [
|
|
572
|
-
{ id: 'init', requestOptions: { reqData: { path: '/init' }, resReq: true } }
|
|
573
|
-
],
|
|
574
|
-
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.CONTINUE })
|
|
575
|
-
},
|
|
576
|
-
// These two phases execute in parallel
|
|
577
|
-
{
|
|
578
|
-
id: 'check-inventory',
|
|
579
|
-
markConcurrentPhase: true,
|
|
580
|
-
requests: [
|
|
581
|
-
{ id: 'inv', requestOptions: { reqData: { path: '/inventory' }, resReq: true } }
|
|
582
|
-
]
|
|
583
|
-
},
|
|
584
|
-
{
|
|
585
|
-
id: 'check-pricing',
|
|
586
|
-
markConcurrentPhase: true,
|
|
587
|
-
requests: [
|
|
588
|
-
{ id: 'price', requestOptions: { reqData: { path: '/pricing' }, resReq: true } }
|
|
589
|
-
],
|
|
590
|
-
// Decision hook receives results from all concurrent phases
|
|
591
|
-
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
592
|
-
const inventory = concurrentPhaseResults![0].responses[0]?.data;
|
|
593
|
-
const pricing = concurrentPhaseResults![1].responses[0]?.data;
|
|
594
|
-
|
|
595
|
-
if (inventory.available && pricing.inBudget) {
|
|
596
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
597
|
-
}
|
|
598
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'out-of-stock' };
|
|
599
|
-
}
|
|
600
|
-
},
|
|
601
|
-
// This phase executes serially after the parallel group
|
|
602
|
-
{
|
|
603
|
-
id: 'process-order',
|
|
604
|
-
requests: [
|
|
605
|
-
{ id: 'order', requestOptions: { reqData: { path: '/order' }, resReq: true } }
|
|
606
|
-
]
|
|
607
|
-
},
|
|
608
|
-
{
|
|
609
|
-
id: 'out-of-stock',
|
|
610
|
-
requests: [
|
|
611
|
-
{ id: 'notify', requestOptions: { reqData: { path: '/notify' }, resReq: true } }
|
|
612
|
-
]
|
|
613
|
-
}
|
|
614
|
-
];
|
|
183
|
+
### Non-Linear Workflows
|
|
615
184
|
|
|
616
|
-
|
|
617
|
-
workflowId: 'mixed-execution',
|
|
618
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
619
|
-
enableNonLinearExecution: true
|
|
620
|
-
});
|
|
621
|
-
```
|
|
185
|
+
Build dynamic workflows with conditional branching and looping:
|
|
622
186
|
|
|
623
|
-
**
|
|
187
|
+
- **JUMP**: Skip to a specific phase based on runtime conditions
|
|
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)
|
|
624
192
|
|
|
625
193
|
```typescript
|
|
626
|
-
const phases
|
|
627
|
-
{
|
|
628
|
-
id: 'authenticate',
|
|
629
|
-
requests: [
|
|
630
|
-
{ id: 'auth', requestOptions: { reqData: { path: '/auth' }, resReq: true } }
|
|
631
|
-
]
|
|
632
|
-
},
|
|
633
|
-
// First parallel group: Data validation
|
|
634
|
-
{
|
|
635
|
-
id: 'validate-user',
|
|
636
|
-
markConcurrentPhase: true,
|
|
637
|
-
requests: [
|
|
638
|
-
{ id: 'val-user', requestOptions: { reqData: { path: '/validate/user' }, resReq: true } }
|
|
639
|
-
]
|
|
640
|
-
},
|
|
641
|
-
{
|
|
642
|
-
id: 'validate-payment',
|
|
643
|
-
markConcurrentPhase: true,
|
|
644
|
-
requests: [
|
|
645
|
-
{ id: 'val-pay', requestOptions: { reqData: { path: '/validate/payment' }, resReq: true } }
|
|
646
|
-
]
|
|
647
|
-
},
|
|
194
|
+
const phases = [
|
|
648
195
|
{
|
|
649
|
-
id: 'validate
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
655
|
-
const allValid = concurrentPhaseResults!.every(r => r.success && r.responses[0]?.data?.valid);
|
|
656
|
-
if (!allValid) {
|
|
657
|
-
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Validation failed' } };
|
|
196
|
+
id: 'validate',
|
|
197
|
+
requests: [...],
|
|
198
|
+
phaseDecisionHook: async ({ phaseResult }) => {
|
|
199
|
+
if (phaseResult.responses[0].data.isValid) {
|
|
200
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'success' };
|
|
658
201
|
}
|
|
659
202
|
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
660
203
|
}
|
|
661
204
|
},
|
|
662
|
-
|
|
663
|
-
{
|
|
664
|
-
id: 'calculate-total',
|
|
665
|
-
requests: [
|
|
666
|
-
{ id: 'calc', requestOptions: { reqData: { path: '/calculate' }, resReq: true } }
|
|
667
|
-
]
|
|
668
|
-
},
|
|
669
|
-
// Second parallel group: External integrations
|
|
670
|
-
{
|
|
671
|
-
id: 'notify-warehouse',
|
|
672
|
-
markConcurrentPhase: true,
|
|
673
|
-
requests: [
|
|
674
|
-
{ id: 'warehouse', requestOptions: { reqData: { path: '/notify/warehouse' }, resReq: true } }
|
|
675
|
-
]
|
|
676
|
-
},
|
|
677
|
-
{
|
|
678
|
-
id: 'notify-shipping',
|
|
679
|
-
markConcurrentPhase: true,
|
|
680
|
-
requests: [
|
|
681
|
-
{ id: 'shipping', requestOptions: { reqData: { path: '/notify/shipping' }, resReq: true } }
|
|
682
|
-
]
|
|
683
|
-
},
|
|
684
|
-
{
|
|
685
|
-
id: 'update-inventory',
|
|
686
|
-
markConcurrentPhase: true,
|
|
687
|
-
requests: [
|
|
688
|
-
{ id: 'inventory', requestOptions: { reqData: { path: '/update/inventory' }, resReq: true } }
|
|
689
|
-
]
|
|
690
|
-
},
|
|
691
|
-
// Final serial phase
|
|
692
|
-
{
|
|
693
|
-
id: 'finalize',
|
|
694
|
-
requests: [
|
|
695
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
696
|
-
]
|
|
697
|
-
}
|
|
205
|
+
{ id: 'retry-logic', requests: [...] },
|
|
206
|
+
{ id: 'success', requests: [...] }
|
|
698
207
|
];
|
|
699
208
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
703
|
-
enableNonLinearExecution: true
|
|
209
|
+
await stableWorkflow(phases, {
|
|
210
|
+
enableNonLinearExecution: true
|
|
704
211
|
});
|
|
705
|
-
|
|
706
|
-
console.log('Execution order demonstrates mixed serial/parallel execution');
|
|
707
212
|
```
|
|
708
213
|
|
|
709
|
-
**Decision
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
id: 'api-check-1',
|
|
715
|
-
markConcurrentPhase: true,
|
|
716
|
-
requests: [
|
|
717
|
-
{ id: 'api1', requestOptions: { reqData: { path: '/health/api1' }, resReq: true } }
|
|
718
|
-
]
|
|
719
|
-
},
|
|
720
|
-
{
|
|
721
|
-
id: 'api-check-2',
|
|
722
|
-
markConcurrentPhase: true,
|
|
723
|
-
requests: [
|
|
724
|
-
{ id: 'api2', requestOptions: { reqData: { path: '/health/api2' }, resReq: true } }
|
|
725
|
-
]
|
|
726
|
-
},
|
|
727
|
-
{
|
|
728
|
-
id: 'api-check-3',
|
|
729
|
-
markConcurrentPhase: true,
|
|
730
|
-
requests: [
|
|
731
|
-
{ id: 'api3', requestOptions: { reqData: { path: '/health/api3' }, resReq: true } }
|
|
732
|
-
],
|
|
733
|
-
phaseDecisionHook: async ({ concurrentPhaseResults, sharedBuffer }) => {
|
|
734
|
-
// Aggregate results from all parallel phases
|
|
735
|
-
const healthScores = concurrentPhaseResults!.map(result =>
|
|
736
|
-
result.responses[0]?.data?.score || 0
|
|
737
|
-
);
|
|
738
|
-
|
|
739
|
-
const averageScore = healthScores.reduce((a, b) => a + b, 0) / healthScores.length;
|
|
740
|
-
sharedBuffer!.healthScore = averageScore;
|
|
741
|
-
|
|
742
|
-
if (averageScore > 0.8) {
|
|
743
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'optimal-path' };
|
|
744
|
-
} else if (averageScore > 0.5) {
|
|
745
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to degraded-path
|
|
746
|
-
} else {
|
|
747
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'fallback-path' };
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
},
|
|
751
|
-
{
|
|
752
|
-
id: 'degraded-path',
|
|
753
|
-
requests: [
|
|
754
|
-
{ id: 'degraded', requestOptions: { reqData: { path: '/degraded' }, resReq: true } }
|
|
755
|
-
]
|
|
756
|
-
},
|
|
757
|
-
{
|
|
758
|
-
id: 'optimal-path',
|
|
759
|
-
requests: [
|
|
760
|
-
{ id: 'optimal', requestOptions: { reqData: { path: '/optimal' }, resReq: true } }
|
|
761
|
-
]
|
|
762
|
-
},
|
|
763
|
-
{
|
|
764
|
-
id: 'fallback-path',
|
|
765
|
-
requests: [
|
|
766
|
-
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
767
|
-
]
|
|
768
|
-
}
|
|
769
|
-
];
|
|
214
|
+
**Decision Hook Context**:
|
|
215
|
+
- Access to current phase results
|
|
216
|
+
- Execution history (replay count, previous phases)
|
|
217
|
+
- Shared buffer for cross-phase state
|
|
218
|
+
- Concurrent phase results (in mixed execution)
|
|
770
219
|
|
|
771
|
-
|
|
772
|
-
const result = await stableWorkflow(phases, {
|
|
773
|
-
workflowId: 'adaptive-routing',
|
|
774
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
775
|
-
enableNonLinearExecution: true,
|
|
776
|
-
sharedBuffer
|
|
777
|
-
});
|
|
220
|
+
### Branched Workflows
|
|
778
221
|
|
|
779
|
-
|
|
780
|
-
```
|
|
222
|
+
Execute multiple independent workflow paths in parallel or sequentially:
|
|
781
223
|
|
|
782
|
-
**
|
|
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
|
|
783
228
|
|
|
784
229
|
```typescript
|
|
785
|
-
const
|
|
230
|
+
const branches = [
|
|
786
231
|
{
|
|
787
|
-
id: '
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
{
|
|
791
|
-
id: 'check1',
|
|
792
|
-
requestOptions: {
|
|
793
|
-
reqData: { path: '/critical/check1' },
|
|
794
|
-
resReq: true,
|
|
795
|
-
attempts: 3
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
]
|
|
232
|
+
id: 'user-flow',
|
|
233
|
+
markConcurrentBranch: true, // Parallel
|
|
234
|
+
phases: [...]
|
|
799
235
|
},
|
|
800
236
|
{
|
|
801
|
-
id: '
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
{
|
|
805
|
-
id: 'check2',
|
|
806
|
-
requestOptions: {
|
|
807
|
-
reqData: { path: '/optional/check2' },
|
|
808
|
-
resReq: true,
|
|
809
|
-
attempts: 1,
|
|
810
|
-
finalErrorAnalyzer: async () => true // Suppress errors
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
],
|
|
814
|
-
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
815
|
-
// Check if critical phase succeeded
|
|
816
|
-
const criticalSuccess = concurrentPhaseResults![0].success;
|
|
817
|
-
|
|
818
|
-
if (!criticalSuccess) {
|
|
819
|
-
return {
|
|
820
|
-
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
821
|
-
metadata: { reason: 'Critical check failed' }
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Continue even if optional check failed
|
|
826
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
827
|
-
}
|
|
237
|
+
id: 'analytics-flow',
|
|
238
|
+
markConcurrentBranch: true, // Parallel
|
|
239
|
+
phases: [...]
|
|
828
240
|
},
|
|
829
241
|
{
|
|
830
|
-
id: '
|
|
831
|
-
|
|
832
|
-
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
833
|
-
]
|
|
242
|
+
id: 'cleanup-flow', // Sequential (default)
|
|
243
|
+
phases: [...]
|
|
834
244
|
}
|
|
835
245
|
];
|
|
836
246
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
enableNonLinearExecution: true,
|
|
841
|
-
stopOnFirstPhaseError: false // Continue even with phase errors
|
|
842
|
-
});
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
**Key Points:**
|
|
846
|
-
- Only **consecutive phases** with `markConcurrentPhase: true` execute in parallel
|
|
847
|
-
- The **last phase** in a concurrent group can have a `phaseDecisionHook` that receives `concurrentPhaseResults`
|
|
848
|
-
- Parallel groups are separated by phases **without** `markConcurrentPhase` (or phases with it set to false)
|
|
849
|
-
- All decision actions work with parallel groups except `REPLAY` (not supported for concurrent groups)
|
|
850
|
-
- Error handling follows normal workflow rules - use `stopOnFirstPhaseError` to control behavior
|
|
851
|
-
|
|
852
|
-
#### Configuration Options
|
|
853
|
-
|
|
854
|
-
**Workflow Options:**
|
|
855
|
-
- `enableNonLinearExecution`: Enable non-linear workflow (required)
|
|
856
|
-
- `maxWorkflowIterations`: Maximum total iterations (default: 1000)
|
|
857
|
-
- `handlePhaseDecision`: Called when phase makes a decision
|
|
858
|
-
- `stopOnFirstPhaseError`: Stop on phase failure (default: false)
|
|
859
|
-
|
|
860
|
-
**Phase Options:**
|
|
861
|
-
- `phaseDecisionHook`: Function returning `PhaseExecutionDecision`
|
|
862
|
-
- `allowReplay`: Allow phase replay (default: false)
|
|
863
|
-
- `allowSkip`: Allow phase skip (default: false)
|
|
864
|
-
- `maxReplayCount`: Maximum replays (default: Infinity)
|
|
865
|
-
|
|
866
|
-
**Decision Hook Parameters:**
|
|
867
|
-
```typescript
|
|
868
|
-
interface PhaseDecisionHookOptions {
|
|
869
|
-
workflowId: string;
|
|
870
|
-
phaseResult: STABLE_WORKFLOW_PHASE_RESULT;
|
|
871
|
-
phaseId: string;
|
|
872
|
-
phaseIndex: number;
|
|
873
|
-
executionHistory: PhaseExecutionRecord[];
|
|
874
|
-
sharedBuffer?: Record<string, any>;
|
|
875
|
-
params?: any;
|
|
876
|
-
}
|
|
877
|
-
```
|
|
878
|
-
|
|
879
|
-
**Decision Object:**
|
|
880
|
-
```typescript
|
|
881
|
-
interface PhaseExecutionDecision {
|
|
882
|
-
action: PHASE_DECISION_ACTIONS;
|
|
883
|
-
targetPhaseId?: string;
|
|
884
|
-
replayCount?: number;
|
|
885
|
-
metadata?: Record<string, any>;
|
|
886
|
-
}
|
|
887
|
-
```
|
|
888
|
-
|
|
889
|
-
### Retry Strategies
|
|
890
|
-
|
|
891
|
-
Control the delay between retry attempts:
|
|
892
|
-
|
|
893
|
-
```typescript
|
|
894
|
-
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
895
|
-
|
|
896
|
-
// Fixed delay: 1000ms between each retry
|
|
897
|
-
await stableRequest({
|
|
898
|
-
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
899
|
-
attempts: 3,
|
|
900
|
-
wait: 1000,
|
|
901
|
-
retryStrategy: RETRY_STRATEGIES.FIXED
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
// Linear backoff: 1000ms, 2000ms, 3000ms
|
|
905
|
-
await stableRequest({
|
|
906
|
-
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
907
|
-
attempts: 3,
|
|
908
|
-
wait: 1000,
|
|
909
|
-
retryStrategy: RETRY_STRATEGIES.LINEAR
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
// Exponential backoff: 1000ms, 2000ms, 4000ms
|
|
913
|
-
await stableRequest({
|
|
914
|
-
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
915
|
-
attempts: 3,
|
|
916
|
-
wait: 1000,
|
|
917
|
-
maxAllowedWait: 10000,
|
|
918
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
919
|
-
});
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
### Circuit Breaker
|
|
923
|
-
|
|
924
|
-
Prevent cascading failures by automatically blocking requests when error thresholds are exceeded:
|
|
925
|
-
|
|
926
|
-
```typescript
|
|
927
|
-
import { stableApiGateway, CircuitBreakerState } from '@emmvish/stable-request';
|
|
928
|
-
|
|
929
|
-
const results = await stableApiGateway(requests, {
|
|
930
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
931
|
-
circuitBreaker: {
|
|
932
|
-
failureThresholdPercentage: 50, // Open circuit at 50% failure rate
|
|
933
|
-
minimumRequests: 5, // Need at least 5 requests to calculate
|
|
934
|
-
recoveryTimeoutMs: 30000, // Try recovery after 30 seconds
|
|
935
|
-
trackIndividualAttempts: false // Track per-request success/failure
|
|
936
|
-
}
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
// Circuit breaker can be shared across workflows
|
|
940
|
-
const breaker = new CircuitBreaker({
|
|
941
|
-
failureThresholdPercentage: 50,
|
|
942
|
-
minimumRequests: 10,
|
|
943
|
-
recoveryTimeoutMs: 60000
|
|
944
|
-
});
|
|
945
|
-
|
|
946
|
-
const result = await stableWorkflow(phases, {
|
|
947
|
-
circuitBreaker: breaker,
|
|
948
|
-
// ... other options
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
// Check circuit breaker state
|
|
952
|
-
const state = breaker.getState();
|
|
953
|
-
console.log(`Circuit breaker state: ${state.state}`); // CLOSED, OPEN, or HALF_OPEN
|
|
954
|
-
```
|
|
955
|
-
|
|
956
|
-
### Rate Limiting
|
|
957
|
-
|
|
958
|
-
Control request throughput to prevent overwhelming APIs:
|
|
959
|
-
|
|
960
|
-
```typescript
|
|
961
|
-
import { stableApiGateway } from '@emmvish/stable-request';
|
|
962
|
-
|
|
963
|
-
const results = await stableApiGateway(requests, {
|
|
964
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
965
|
-
concurrentExecution: true,
|
|
966
|
-
rateLimit: {
|
|
967
|
-
maxRequests: 10, // Maximum 10 requests
|
|
968
|
-
windowMs: 1000 // Per 1 second window
|
|
969
|
-
}
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
// Rate limiting in workflows
|
|
973
|
-
const result = await stableWorkflow(phases, {
|
|
974
|
-
rateLimit: {
|
|
975
|
-
maxRequests: 5,
|
|
976
|
-
windowMs: 1000
|
|
977
|
-
}
|
|
247
|
+
await stableWorkflow([], {
|
|
248
|
+
enableBranchExecution: true,
|
|
249
|
+
branches
|
|
978
250
|
});
|
|
979
251
|
```
|
|
980
252
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
import { stableRequest, getGlobalCacheManager } from '@emmvish/stable-request';
|
|
987
|
-
|
|
988
|
-
// Enable caching for a request
|
|
989
|
-
const data = await stableRequest({
|
|
990
|
-
reqData: { hostname: 'api.example.com', path: '/users/123' },
|
|
991
|
-
resReq: true,
|
|
992
|
-
cache: {
|
|
993
|
-
enabled: true,
|
|
994
|
-
ttl: 60000 // Cache for 60 seconds
|
|
995
|
-
}
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
// Use global cache manager across requests
|
|
999
|
-
const results = await stableApiGateway(requests, {
|
|
1000
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1001
|
-
commonCache: { enabled: true, ttl: 300000 } // 5 minutes
|
|
1002
|
-
});
|
|
253
|
+
**Branch Features**:
|
|
254
|
+
- Each branch has its own phase execution
|
|
255
|
+
- Branches share the workflow's `sharedBuffer`
|
|
256
|
+
- Branch decision hooks can terminate the entire workflow
|
|
257
|
+
- Supports all execution patterns (mixed, non-linear) within branches
|
|
1003
258
|
|
|
1004
|
-
|
|
1005
|
-
const cacheManager = getGlobalCacheManager();
|
|
1006
|
-
const stats = cacheManager.getStats();
|
|
1007
|
-
console.log(`Cache size: ${stats.size}, Valid entries: ${stats.validEntries}`);
|
|
1008
|
-
cacheManager.clear(); // Clear all cache
|
|
1009
|
-
```
|
|
259
|
+
## Advanced Capabilities
|
|
1010
260
|
|
|
1011
|
-
###
|
|
261
|
+
### Config Cascading
|
|
1012
262
|
|
|
1013
|
-
|
|
263
|
+
Configuration inheritance across workflow → branch → phase → request levels:
|
|
1014
264
|
|
|
1015
265
|
```typescript
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
//
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
// Store in shared buffer
|
|
1029
|
-
commonBuffer.token = token;
|
|
1030
|
-
commonBuffer.timestamp = Date.now();
|
|
1031
|
-
|
|
1032
|
-
// Override request configuration
|
|
1033
|
-
return {
|
|
1034
|
-
reqData: {
|
|
1035
|
-
hostname: 'api.example.com',
|
|
1036
|
-
path: '/authenticated-data',
|
|
1037
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
1038
|
-
}
|
|
1039
|
-
};
|
|
266
|
+
await stableWorkflow(phases, {
|
|
267
|
+
// Workflow-level config (lowest priority)
|
|
268
|
+
commonAttempts: 3,
|
|
269
|
+
commonWait: 1000,
|
|
270
|
+
commonCache: { enabled: true, ttl: 60000 },
|
|
271
|
+
|
|
272
|
+
branches: [{
|
|
273
|
+
id: 'my-branch',
|
|
274
|
+
commonConfig: {
|
|
275
|
+
// Branch-level config (overrides workflow)
|
|
276
|
+
commonAttempts: 5,
|
|
277
|
+
commonWait: 500
|
|
1040
278
|
},
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
console.log('Token used:', commonBuffer.token);
|
|
1049
|
-
```
|
|
1050
|
-
|
|
1051
|
-
### Shared Buffer
|
|
1052
|
-
|
|
1053
|
-
Share state across requests in gateways and workflows:
|
|
1054
|
-
|
|
1055
|
-
```typescript
|
|
1056
|
-
import { stableWorkflow } from '@emmvish/stable-request';
|
|
1057
|
-
|
|
1058
|
-
const sharedBuffer: Record<string, any> = { requestCount: 0 };
|
|
1059
|
-
|
|
1060
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1061
|
-
{
|
|
1062
|
-
id: 'phase-1',
|
|
1063
|
-
requests: [
|
|
1064
|
-
{
|
|
1065
|
-
id: 'req-1',
|
|
1066
|
-
requestOptions: {
|
|
1067
|
-
reqData: { path: '/step1' },
|
|
1068
|
-
resReq: true,
|
|
1069
|
-
preExecution: {
|
|
1070
|
-
preExecutionHook: ({ commonBuffer }) => {
|
|
1071
|
-
commonBuffer.requestCount++;
|
|
1072
|
-
commonBuffer.phase1Data = 'initialized';
|
|
1073
|
-
return {};
|
|
1074
|
-
},
|
|
1075
|
-
preExecutionHookParams: {},
|
|
1076
|
-
applyPreExecutionConfigOverride: false,
|
|
1077
|
-
continueOnPreExecutionHookFailure: false
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
]
|
|
1082
|
-
},
|
|
1083
|
-
{
|
|
1084
|
-
id: 'phase-2',
|
|
1085
|
-
requests: [
|
|
1086
|
-
{
|
|
1087
|
-
id: 'req-2',
|
|
279
|
+
phases: [{
|
|
280
|
+
id: 'my-phase',
|
|
281
|
+
commonConfig: {
|
|
282
|
+
// Phase-level config (overrides branch and workflow)
|
|
283
|
+
commonAttempts: 1
|
|
284
|
+
},
|
|
285
|
+
requests: [{
|
|
1088
286
|
requestOptions: {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
preExecutionHook: ({ commonBuffer }) => {
|
|
1093
|
-
commonBuffer.requestCount++;
|
|
1094
|
-
// Access data from phase-1
|
|
1095
|
-
console.log('Phase 1 data:', commonBuffer.phase1Data);
|
|
1096
|
-
return {};
|
|
1097
|
-
},
|
|
1098
|
-
preExecutionHookParams: {},
|
|
1099
|
-
applyPreExecutionConfigOverride: false,
|
|
1100
|
-
continueOnPreExecutionHookFailure: false
|
|
1101
|
-
}
|
|
287
|
+
// Request-level config (highest priority)
|
|
288
|
+
attempts: 10,
|
|
289
|
+
cache: { enabled: false }
|
|
1102
290
|
}
|
|
1103
|
-
}
|
|
1104
|
-
]
|
|
1105
|
-
}
|
|
1106
|
-
];
|
|
1107
|
-
|
|
1108
|
-
const result = await stableWorkflow(phases, {
|
|
1109
|
-
workflowId: 'stateful-workflow',
|
|
1110
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1111
|
-
sharedBuffer
|
|
1112
|
-
});
|
|
1113
|
-
|
|
1114
|
-
console.log('Total requests processed:', sharedBuffer.requestCount);
|
|
1115
|
-
```
|
|
1116
|
-
|
|
1117
|
-
### Request Grouping
|
|
1118
|
-
|
|
1119
|
-
Apply different configurations to request groups:
|
|
1120
|
-
|
|
1121
|
-
```typescript
|
|
1122
|
-
import { stableApiGateway } from '@emmvish/stable-request';
|
|
1123
|
-
|
|
1124
|
-
const requests = [
|
|
1125
|
-
{
|
|
1126
|
-
id: 'critical-1',
|
|
1127
|
-
groupId: 'critical',
|
|
1128
|
-
requestOptions: { reqData: { path: '/critical/1' }, resReq: true }
|
|
1129
|
-
},
|
|
1130
|
-
{
|
|
1131
|
-
id: 'critical-2',
|
|
1132
|
-
groupId: 'critical',
|
|
1133
|
-
requestOptions: { reqData: { path: '/critical/2' }, resReq: true }
|
|
1134
|
-
},
|
|
1135
|
-
{
|
|
1136
|
-
id: 'optional-1',
|
|
1137
|
-
groupId: 'optional',
|
|
1138
|
-
requestOptions: { reqData: { path: '/optional/1' }, resReq: true }
|
|
1139
|
-
}
|
|
1140
|
-
];
|
|
1141
|
-
|
|
1142
|
-
const results = await stableApiGateway(requests, {
|
|
1143
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1144
|
-
commonAttempts: 1,
|
|
1145
|
-
commonWait: 100,
|
|
1146
|
-
requestGroups: [
|
|
1147
|
-
{
|
|
1148
|
-
id: 'critical',
|
|
1149
|
-
commonConfig: {
|
|
1150
|
-
commonAttempts: 5, // More retries for critical requests
|
|
1151
|
-
commonWait: 2000,
|
|
1152
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1153
|
-
}
|
|
1154
|
-
},
|
|
1155
|
-
{
|
|
1156
|
-
id: 'optional',
|
|
1157
|
-
commonConfig: {
|
|
1158
|
-
commonAttempts: 1, // No retries for optional requests
|
|
1159
|
-
commonFinalErrorAnalyzer: async () => true // Suppress errors
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
]
|
|
291
|
+
}]
|
|
292
|
+
}]
|
|
293
|
+
}]
|
|
1163
294
|
});
|
|
1164
295
|
```
|
|
1165
296
|
|
|
1166
|
-
###
|
|
297
|
+
### Shared Buffer and Pre-Execution Hooks
|
|
1167
298
|
|
|
1168
|
-
|
|
299
|
+
Share state and transform requests dynamically:
|
|
1169
300
|
|
|
301
|
+
**Shared Buffer**: Cross-phase/branch communication
|
|
1170
302
|
```typescript
|
|
1171
|
-
|
|
303
|
+
const sharedBuffer = { authToken: null };
|
|
1172
304
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
concurrentExecution: true,
|
|
1177
|
-
maxConcurrentRequests: 5
|
|
305
|
+
await stableWorkflow(phases, {
|
|
306
|
+
sharedBuffer,
|
|
307
|
+
// Phases can read/write to sharedBuffer via preExecution hooks
|
|
1178
308
|
});
|
|
1179
|
-
|
|
1180
|
-
// Phase-level concurrency control
|
|
1181
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1182
|
-
{
|
|
1183
|
-
id: 'limited-phase',
|
|
1184
|
-
concurrentExecution: true,
|
|
1185
|
-
maxConcurrentRequests: 3,
|
|
1186
|
-
requests: [/* ... */]
|
|
1187
|
-
}
|
|
1188
|
-
];
|
|
1189
309
|
```
|
|
1190
310
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
Validate response content and trigger retries:
|
|
1194
|
-
|
|
311
|
+
**Pre-Execution Hooks**: Modify requests before execution
|
|
1195
312
|
```typescript
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1209
|
-
return data.status === 'completed';
|
|
1210
|
-
}
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
|
-
console.log('Job completed:', data);
|
|
1214
|
-
```
|
|
1215
|
-
|
|
1216
|
-
### Error Handling
|
|
1217
|
-
|
|
1218
|
-
Comprehensive error handling with observability hooks:
|
|
1219
|
-
|
|
1220
|
-
```typescript
|
|
1221
|
-
import { stableRequest } from '@emmvish/stable-request';
|
|
1222
|
-
|
|
1223
|
-
const data = await stableRequest({
|
|
1224
|
-
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
1225
|
-
resReq: true,
|
|
1226
|
-
attempts: 3,
|
|
1227
|
-
wait: 1000,
|
|
1228
|
-
logAllErrors: true,
|
|
1229
|
-
handleErrors: ({ reqData, errorLog, params }) => {
|
|
1230
|
-
// Custom error logging
|
|
1231
|
-
console.error('Request failed:', {
|
|
1232
|
-
url: reqData.url,
|
|
1233
|
-
attempt: errorLog.attempt,
|
|
1234
|
-
statusCode: errorLog.statusCode,
|
|
1235
|
-
error: errorLog.error,
|
|
1236
|
-
isRetryable: errorLog.isRetryable
|
|
1237
|
-
});
|
|
1238
|
-
|
|
1239
|
-
// Send to monitoring service
|
|
1240
|
-
monitoringService.trackError(errorLog);
|
|
1241
|
-
},
|
|
1242
|
-
logAllSuccessfulAttempts: true,
|
|
1243
|
-
handleSuccessfulAttemptData: ({ successfulAttemptData }) => {
|
|
1244
|
-
console.log('Request succeeded on attempt:', successfulAttemptData.attempt);
|
|
1245
|
-
},
|
|
1246
|
-
finalErrorAnalyzer: async ({ error, reqData }) => {
|
|
1247
|
-
// Gracefully handle specific errors
|
|
1248
|
-
if (error.response?.status === 404) {
|
|
1249
|
-
console.warn('Resource not found, continuing...');
|
|
1250
|
-
return true; // Return false to suppress error
|
|
313
|
+
{
|
|
314
|
+
requestOptions: {
|
|
315
|
+
preExecution: {
|
|
316
|
+
preExecutionHook: ({ commonBuffer, inputParams }) => {
|
|
317
|
+
// Access buffer, compute values, return config overrides
|
|
318
|
+
return {
|
|
319
|
+
reqData: {
|
|
320
|
+
headers: { 'Authorization': `Bearer ${commonBuffer.authToken}` }
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
applyPreExecutionConfigOverride: true
|
|
1251
325
|
}
|
|
1252
|
-
return false; // Throw error
|
|
1253
326
|
}
|
|
1254
|
-
}
|
|
327
|
+
}
|
|
1255
328
|
```
|
|
1256
329
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
### Use Case 1: Multi-Tenant API with Dynamic Authentication
|
|
330
|
+
### Comprehensive Observability
|
|
1260
331
|
|
|
1261
|
-
|
|
1262
|
-
import { stableWorkflow, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
1263
|
-
|
|
1264
|
-
interface TenantConfig {
|
|
1265
|
-
tenantId: string;
|
|
1266
|
-
apiKey: string;
|
|
1267
|
-
baseUrl: string;
|
|
1268
|
-
}
|
|
332
|
+
Built-in hooks for monitoring, logging, and analysis:
|
|
1269
333
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
};
|
|
1276
|
-
|
|
1277
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1278
|
-
{
|
|
1279
|
-
id: 'authentication',
|
|
1280
|
-
requests: [
|
|
1281
|
-
{
|
|
1282
|
-
id: 'get-token',
|
|
1283
|
-
requestOptions: {
|
|
1284
|
-
reqData: {
|
|
1285
|
-
path: '/auth/token',
|
|
1286
|
-
method: 'POST',
|
|
1287
|
-
headers: { 'X-API-Key': tenantConfig.apiKey }
|
|
1288
|
-
},
|
|
1289
|
-
resReq: true,
|
|
1290
|
-
attempts: 3,
|
|
1291
|
-
wait: 2000,
|
|
1292
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
1293
|
-
responseAnalyzer: async ({ data, commonBuffer }) => {
|
|
1294
|
-
if (data?.token) {
|
|
1295
|
-
commonBuffer.authToken = data.token;
|
|
1296
|
-
commonBuffer.tokenExpiry = Date.now() + (data.expiresIn * 1000);
|
|
1297
|
-
return true;
|
|
1298
|
-
}
|
|
1299
|
-
return false;
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
]
|
|
1304
|
-
},
|
|
1305
|
-
{
|
|
1306
|
-
id: 'data-fetching',
|
|
1307
|
-
concurrentExecution: true,
|
|
1308
|
-
maxConcurrentRequests: 5,
|
|
1309
|
-
requests: [
|
|
1310
|
-
{
|
|
1311
|
-
id: 'fetch-users',
|
|
1312
|
-
requestOptions: {
|
|
1313
|
-
reqData: { path: '/users' },
|
|
1314
|
-
resReq: true,
|
|
1315
|
-
preExecution: {
|
|
1316
|
-
preExecutionHook: ({ commonBuffer }) => ({
|
|
1317
|
-
reqData: {
|
|
1318
|
-
path: '/users',
|
|
1319
|
-
headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
|
|
1320
|
-
}
|
|
1321
|
-
}),
|
|
1322
|
-
applyPreExecutionConfigOverride: true
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
},
|
|
1326
|
-
{
|
|
1327
|
-
id: 'fetch-settings',
|
|
1328
|
-
requestOptions: {
|
|
1329
|
-
reqData: { path: '/settings' },
|
|
1330
|
-
resReq: true,
|
|
1331
|
-
preExecution: {
|
|
1332
|
-
preExecutionHook: ({ commonBuffer }) => ({
|
|
1333
|
-
reqData: {
|
|
1334
|
-
path: '/settings',
|
|
1335
|
-
headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
|
|
1336
|
-
}
|
|
1337
|
-
}),
|
|
1338
|
-
applyPreExecutionConfigOverride: true
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
]
|
|
1343
|
-
},
|
|
1344
|
-
{
|
|
1345
|
-
id: 'data-processing',
|
|
1346
|
-
concurrentExecution: true,
|
|
1347
|
-
requests: [
|
|
1348
|
-
{
|
|
1349
|
-
id: 'process-users',
|
|
1350
|
-
requestOptions: {
|
|
1351
|
-
reqData: { path: '/process/users', method: 'POST' },
|
|
1352
|
-
resReq: true,
|
|
1353
|
-
preExecution: {
|
|
1354
|
-
preExecutionHook: ({ commonBuffer }) => {
|
|
1355
|
-
const usersPhase = commonBuffer.phases?.find(p => p.phaseId === 'data-fetching');
|
|
1356
|
-
const usersData = usersPhase?.responses?.find(r => r.requestId === 'fetch-users')?.data;
|
|
1357
|
-
|
|
1358
|
-
return {
|
|
1359
|
-
reqData: {
|
|
1360
|
-
path: '/process/users',
|
|
1361
|
-
method: 'POST',
|
|
1362
|
-
headers: { Authorization: `Bearer ${commonBuffer.authToken}` },
|
|
1363
|
-
body: { users: usersData }
|
|
1364
|
-
}
|
|
1365
|
-
};
|
|
1366
|
-
},
|
|
1367
|
-
applyPreExecutionConfigOverride: true
|
|
1368
|
-
},
|
|
1369
|
-
responseAnalyzer: async ({ data, commonBuffer }) => {
|
|
1370
|
-
if (data?.processed) {
|
|
1371
|
-
commonBuffer.processedItems.push(...data.processed);
|
|
1372
|
-
return true;
|
|
1373
|
-
}
|
|
1374
|
-
return false;
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
]
|
|
1379
|
-
}
|
|
1380
|
-
];
|
|
1381
|
-
|
|
1382
|
-
const result = await stableWorkflow(phases, {
|
|
1383
|
-
workflowId: `tenant-${tenantConfig.tenantId}-workflow`,
|
|
1384
|
-
commonRequestData: {
|
|
1385
|
-
hostname: tenantConfig.baseUrl,
|
|
1386
|
-
headers: { 'X-Tenant-ID': tenantConfig.tenantId }
|
|
1387
|
-
},
|
|
1388
|
-
stopOnFirstPhaseError: true,
|
|
1389
|
-
logPhaseResults: true,
|
|
1390
|
-
sharedBuffer,
|
|
1391
|
-
circuitBreaker: {
|
|
1392
|
-
failureThresholdPercentage: 40,
|
|
1393
|
-
minimumRequests: 5,
|
|
1394
|
-
recoveryTimeoutMs: 30000
|
|
1395
|
-
},
|
|
1396
|
-
rateLimit: {
|
|
1397
|
-
maxRequests: 20,
|
|
1398
|
-
windowMs: 1000
|
|
1399
|
-
},
|
|
1400
|
-
commonCache: {
|
|
1401
|
-
enabled: true,
|
|
1402
|
-
ttl: 300000 // Cache for 5 minutes
|
|
1403
|
-
},
|
|
1404
|
-
handlePhaseCompletion: ({ workflowId, phaseResult }) => {
|
|
1405
|
-
console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
|
|
1406
|
-
success: phaseResult.success,
|
|
1407
|
-
successfulRequests: phaseResult.successfulRequests,
|
|
1408
|
-
executionTime: `${phaseResult.executionTime}ms`
|
|
1409
|
-
});
|
|
1410
|
-
},
|
|
1411
|
-
handlePhaseError: ({ workflowId, error, phaseResult }) => {
|
|
1412
|
-
console.error(`[${workflowId}] Phase ${phaseResult.phaseId} failed:`, error);
|
|
1413
|
-
// Send to monitoring
|
|
1414
|
-
monitoringService.trackPhaseError(workflowId, phaseResult.phaseId, error);
|
|
1415
|
-
}
|
|
1416
|
-
});
|
|
1417
|
-
|
|
1418
|
-
return {
|
|
1419
|
-
success: result.success,
|
|
1420
|
-
tenantId: tenantConfig.tenantId,
|
|
1421
|
-
processedItems: sharedBuffer.processedItems,
|
|
1422
|
-
executionTime: result.executionTime,
|
|
1423
|
-
phases: result.phases.map(p => ({
|
|
1424
|
-
id: p.phaseId,
|
|
1425
|
-
success: p.success,
|
|
1426
|
-
requestCount: p.totalRequests
|
|
1427
|
-
}))
|
|
1428
|
-
};
|
|
1429
|
-
}
|
|
334
|
+
**Request-Level Hooks**:
|
|
335
|
+
- `responseAnalyzer`: Validate responses, trigger retries based on business logic
|
|
336
|
+
- `handleErrors`: Custom error handling and logging
|
|
337
|
+
- `handleSuccessfulAttemptData`: Log successful attempts
|
|
338
|
+
- `finalErrorAnalyzer`: Analyze final failure after all retries
|
|
1430
339
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
340
|
+
**Workflow-Level Hooks**:
|
|
341
|
+
- `handlePhaseCompletion`: React to phase completion
|
|
342
|
+
- `handlePhaseError`: Handle phase-level failures
|
|
343
|
+
- `handlePhaseDecision`: Monitor non-linear execution decisions
|
|
344
|
+
- `handleBranchCompletion`: Track branch execution
|
|
345
|
+
- `handleBranchDecision`: Monitor branch-level decisions
|
|
1436
346
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
console.log(`Tenant ${result.tenantId}:`, result.success ? 'Success' : 'Failed');
|
|
1440
|
-
});
|
|
1441
|
-
```
|
|
347
|
+
**Execution History**:
|
|
348
|
+
Every workflow result includes detailed execution history with timestamps, decisions, and metadata.
|
|
1442
349
|
|
|
1443
|
-
|
|
350
|
+
## API Surface
|
|
1444
351
|
|
|
1445
|
-
|
|
1446
|
-
import { stableApiGateway, RETRY_STRATEGIES, CircuitBreaker } from '@emmvish/stable-request';
|
|
352
|
+
### Core Functions
|
|
1447
353
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
endpoint: string;
|
|
1452
|
-
hostname: string;
|
|
1453
|
-
}
|
|
354
|
+
- **`stableRequest`**: Single HTTP request with retry logic
|
|
355
|
+
- **`stableApiGateway`**: Execute multiple requests (concurrent or sequential)
|
|
356
|
+
- **`stableWorkflow`**: Orchestrate multi-phase workflows with advanced patterns
|
|
1454
357
|
|
|
1455
|
-
|
|
1456
|
-
// Sort by priority
|
|
1457
|
-
const sortedSources = [...dataSources].sort((a, b) => a.priority - b.priority);
|
|
1458
|
-
|
|
1459
|
-
// Create circuit breakers for each source
|
|
1460
|
-
const circuitBreakers = new Map(
|
|
1461
|
-
sortedSources.map(source => [
|
|
1462
|
-
source.id,
|
|
1463
|
-
new CircuitBreaker({
|
|
1464
|
-
failureThresholdPercentage: 50,
|
|
1465
|
-
minimumRequests: 3,
|
|
1466
|
-
recoveryTimeoutMs: 60000
|
|
1467
|
-
})
|
|
1468
|
-
])
|
|
1469
|
-
);
|
|
1470
|
-
|
|
1471
|
-
// Try each data source in priority order
|
|
1472
|
-
for (const source of sortedSources) {
|
|
1473
|
-
const breaker = circuitBreakers.get(source.id)!;
|
|
1474
|
-
const breakerState = breaker.getState();
|
|
1475
|
-
|
|
1476
|
-
// Skip if circuit is open
|
|
1477
|
-
if (breakerState.state === 'OPEN') {
|
|
1478
|
-
console.warn(`Circuit breaker open for ${source.id}, skipping...`);
|
|
1479
|
-
continue;
|
|
1480
|
-
}
|
|
358
|
+
### Utility Exports
|
|
1481
359
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
id: 'users',
|
|
1488
|
-
requestOptions: {
|
|
1489
|
-
reqData: { path: `${source.endpoint}/users` },
|
|
1490
|
-
resReq: true,
|
|
1491
|
-
attempts: 3,
|
|
1492
|
-
wait: 1000,
|
|
1493
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1494
|
-
}
|
|
1495
|
-
},
|
|
1496
|
-
{
|
|
1497
|
-
id: 'products',
|
|
1498
|
-
requestOptions: {
|
|
1499
|
-
reqData: { path: `${source.endpoint}/products` },
|
|
1500
|
-
resReq: true,
|
|
1501
|
-
attempts: 3,
|
|
1502
|
-
wait: 1000,
|
|
1503
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1504
|
-
}
|
|
1505
|
-
},
|
|
1506
|
-
{
|
|
1507
|
-
id: 'orders',
|
|
1508
|
-
requestOptions: {
|
|
1509
|
-
reqData: { path: `${source.endpoint}/orders` },
|
|
1510
|
-
resReq: true,
|
|
1511
|
-
attempts: 3,
|
|
1512
|
-
wait: 1000,
|
|
1513
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
];
|
|
1517
|
-
|
|
1518
|
-
const results = await stableApiGateway(requests, {
|
|
1519
|
-
commonRequestData: {
|
|
1520
|
-
hostname: source.hostname,
|
|
1521
|
-
headers: { 'X-Source-ID': source.id }
|
|
1522
|
-
},
|
|
1523
|
-
concurrentExecution: true,
|
|
1524
|
-
maxConcurrentRequests: 10,
|
|
1525
|
-
circuitBreaker: breaker,
|
|
1526
|
-
rateLimit: {
|
|
1527
|
-
maxRequests: 50,
|
|
1528
|
-
windowMs: 1000
|
|
1529
|
-
},
|
|
1530
|
-
commonCache: {
|
|
1531
|
-
enabled: true,
|
|
1532
|
-
ttl: 60000
|
|
1533
|
-
},
|
|
1534
|
-
commonResponseAnalyzer: async ({ data }) => {
|
|
1535
|
-
// Validate data structure
|
|
1536
|
-
return data && typeof data === 'object' && !data.error;
|
|
1537
|
-
},
|
|
1538
|
-
commonHandleErrors: ({ errorLog }) => {
|
|
1539
|
-
console.error(`Error from ${source.id}:`, errorLog);
|
|
1540
|
-
}
|
|
1541
|
-
});
|
|
360
|
+
- **Circuit Breaker**: `CircuitBreaker`, `CircuitBreakerOpenError`
|
|
361
|
+
- **Rate Limiting**: `RateLimiter`
|
|
362
|
+
- **Concurrency**: `ConcurrencyLimiter`
|
|
363
|
+
- **Caching**: `CacheManager`, `getGlobalCacheManager`, `resetGlobalCacheManager`
|
|
364
|
+
- **Execution Utilities**: `executeNonLinearWorkflow`, `executeBranchWorkflow`, `executePhase`
|
|
1542
365
|
|
|
1543
|
-
|
|
1544
|
-
const allSuccessful = results.every(r => r.success);
|
|
1545
|
-
|
|
1546
|
-
if (allSuccessful) {
|
|
1547
|
-
console.log(`Successfully fetched data from ${source.id}`);
|
|
1548
|
-
return {
|
|
1549
|
-
source: source.id,
|
|
1550
|
-
data: {
|
|
1551
|
-
users: results.find(r => r.requestId === 'users')?.data,
|
|
1552
|
-
products: results.find(r => r.requestId === 'products')?.data,
|
|
1553
|
-
orders: results.find(r => r.requestId === 'orders')?.data
|
|
1554
|
-
}
|
|
1555
|
-
};
|
|
1556
|
-
} else {
|
|
1557
|
-
console.warn(`Partial failure from ${source.id}, trying next source...`);
|
|
1558
|
-
}
|
|
1559
|
-
} catch (error) {
|
|
1560
|
-
console.error(`Failed to fetch from ${source.id}:`, error);
|
|
1561
|
-
// Continue to next source
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
366
|
+
### Enums
|
|
1564
367
|
|
|
1565
|
-
|
|
1566
|
-
|
|
368
|
+
- `RETRY_STRATEGIES`: Fixed, Linear, Exponential, Fibonacci
|
|
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
|
|
1567
373
|
|
|
1568
|
-
|
|
1569
|
-
const dataSources: DataSource[] = [
|
|
1570
|
-
{
|
|
1571
|
-
id: 'primary-db',
|
|
1572
|
-
priority: 1,
|
|
1573
|
-
endpoint: '/api/v1',
|
|
1574
|
-
hostname: 'primary.example.com'
|
|
1575
|
-
},
|
|
1576
|
-
{
|
|
1577
|
-
id: 'replica-db',
|
|
1578
|
-
priority: 2,
|
|
1579
|
-
endpoint: '/api/v1',
|
|
1580
|
-
hostname: 'replica.example.com'
|
|
1581
|
-
},
|
|
1582
|
-
{
|
|
1583
|
-
id: 'backup-cache',
|
|
1584
|
-
priority: 3,
|
|
1585
|
-
endpoint: '/cached',
|
|
1586
|
-
hostname: 'cache.example.com'
|
|
1587
|
-
}
|
|
1588
|
-
];
|
|
374
|
+
### TypeScript Types
|
|
1589
375
|
|
|
1590
|
-
|
|
1591
|
-
console.log('Data fetched from:', result.source);
|
|
1592
|
-
console.log('Users:', result.data.users?.length);
|
|
1593
|
-
console.log('Products:', result.data.products?.length);
|
|
1594
|
-
console.log('Orders:', result.data.orders?.length);
|
|
1595
|
-
```
|
|
376
|
+
Full TypeScript support with 40+ exported types for complete type safety across workflows, requests, configurations, and hooks.
|
|
1596
377
|
|
|
1597
378
|
## License
|
|
1598
379
|
|
|
1599
380
|
MIT © Manish Varma
|
|
1600
|
-
|
|
1601
|
-
[](https://opensource.org/licenses/MIT)
|
|
1602
|
-
---
|
|
1603
|
-
|
|
1604
|
-
**Made with ❤️ for developers integrating with unreliable APIs**
|