@emmvish/stable-request 1.6.1 → 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 +228 -1974
- package/dist/core/stable-workflow.d.ts.map +1 -1
- package/dist/core/stable-workflow.js +3 -1
- package/dist/core/stable-workflow.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +27 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utilities/execute-branch-workflow.d.ts.map +1 -1
- package/dist/utilities/execute-branch-workflow.js +385 -191
- package/dist/utilities/execute-branch-workflow.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,65 +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
19
|
- [Branched Workflows](#branched-workflows)
|
|
48
|
-
|
|
49
|
-
- [
|
|
50
|
-
- [
|
|
51
|
-
- [
|
|
52
|
-
|
|
53
|
-
- [Shared Buffer](#shared-buffer)
|
|
54
|
-
- [Request Grouping](#request-grouping)
|
|
55
|
-
- [Concurrency Control](#concurrency-control)
|
|
56
|
-
- [Response Analysis](#response-analysis)
|
|
57
|
-
- [Error Handling](#error-handling)
|
|
58
|
-
- [Advanced Use Cases](#advanced-use-cases)
|
|
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)
|
|
59
25
|
- [License](#license)
|
|
60
|
-
<!-- TOC END -->
|
|
61
26
|
|
|
62
|
-
|
|
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
|
|
63
35
|
|
|
64
36
|
## Installation
|
|
65
37
|
|
|
@@ -67,26 +39,9 @@ Start small and scale.
|
|
|
67
39
|
npm install @emmvish/stable-request
|
|
68
40
|
```
|
|
69
41
|
|
|
70
|
-
## Core Features
|
|
71
|
-
|
|
72
|
-
- ✅ **Configurable Retry Strategies**: Fixed, Linear, and Exponential backoff
|
|
73
|
-
- ✅ **Circuit Breaker**: Prevent cascading failures with automatic circuit breaking
|
|
74
|
-
- ✅ **Rate Limiting**: Control request throughput across single or multiple requests
|
|
75
|
-
- ✅ **Response Caching**: Built-in TTL-based caching with global cache manager
|
|
76
|
-
- ✅ **Batch Processing**: Execute multiple requests concurrently or sequentially via API Gateway
|
|
77
|
-
- ✅ **Multi-Phase Non-Linear Workflows**: Orchestrate complex request workflows with phase dependencies
|
|
78
|
-
- ✅ **Branched Workflows**: Execute parallel or serial branches with conditional routing and decision hooks
|
|
79
|
-
- ✅ **Pre-Execution Hooks**: Transform requests before execution with dynamic configuration
|
|
80
|
-
- ✅ **Shared Buffer**: Share state across requests in workflows and gateways
|
|
81
|
-
- ✅ **Request Grouping**: Apply different configurations to request groups
|
|
82
|
-
- ✅ **Observability Hooks**: Track errors, successful attempts, and phase completions
|
|
83
|
-
- ✅ **Response Analysis**: Validate responses and trigger retries based on content
|
|
84
|
-
- ✅ **Trial Mode**: Test configurations without making real API calls
|
|
85
|
-
- ✅ **TypeScript Support**: Full type safety with generics for request/response data
|
|
86
|
-
|
|
87
42
|
## Quick Start
|
|
88
43
|
|
|
89
|
-
###
|
|
44
|
+
### Single Request with Retry
|
|
90
45
|
|
|
91
46
|
```typescript
|
|
92
47
|
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
@@ -94,7 +49,7 @@ import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
|
94
49
|
const data = await stableRequest({
|
|
95
50
|
reqData: {
|
|
96
51
|
hostname: 'api.example.com',
|
|
97
|
-
path: '/users
|
|
52
|
+
path: '/users',
|
|
98
53
|
method: 'GET'
|
|
99
54
|
},
|
|
100
55
|
resReq: true,
|
|
@@ -102,2025 +57,324 @@ const data = await stableRequest({
|
|
|
102
57
|
wait: 1000,
|
|
103
58
|
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
104
59
|
});
|
|
105
|
-
|
|
106
|
-
console.log(data);
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### Batch Requests via API Gateway
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
import { stableApiGateway } from '@emmvish/stable-request';
|
|
113
|
-
|
|
114
|
-
const requests = [
|
|
115
|
-
{ id: 'user-1', requestOptions: { reqData: { path: '/users/1' }, resReq: true } },
|
|
116
|
-
{ id: 'user-2', requestOptions: { reqData: { path: '/users/2' }, resReq: true } },
|
|
117
|
-
{ id: 'user-3', requestOptions: { reqData: { path: '/users/3' }, resReq: true } }
|
|
118
|
-
];
|
|
119
|
-
|
|
120
|
-
const results = await stableApiGateway(requests, {
|
|
121
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
122
|
-
concurrentExecution: true,
|
|
123
|
-
maxConcurrentRequests: 10
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
results.forEach(result => {
|
|
127
|
-
if (result.success) {
|
|
128
|
-
console.log(`Request ${result.requestId}:`, result.data);
|
|
129
|
-
} else {
|
|
130
|
-
console.error(`Request ${result.requestId} failed:`, result.error);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
60
|
```
|
|
134
61
|
|
|
135
62
|
### Multi-Phase Workflow
|
|
136
63
|
|
|
137
64
|
```typescript
|
|
138
|
-
import { stableWorkflow
|
|
65
|
+
import { stableWorkflow } from '@emmvish/stable-request';
|
|
139
66
|
|
|
140
|
-
const
|
|
67
|
+
const result = await stableWorkflow([
|
|
141
68
|
{
|
|
142
|
-
id: '
|
|
69
|
+
id: 'auth',
|
|
143
70
|
requests: [
|
|
144
71
|
{ id: 'login', requestOptions: { reqData: { path: '/auth/login' }, resReq: true } }
|
|
145
72
|
]
|
|
146
73
|
},
|
|
147
74
|
{
|
|
148
|
-
id: 'data
|
|
75
|
+
id: 'fetch-data',
|
|
149
76
|
concurrentExecution: true,
|
|
150
77
|
requests: [
|
|
151
78
|
{ id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
|
|
152
|
-
{ id: '
|
|
79
|
+
{ id: 'orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } }
|
|
153
80
|
]
|
|
154
81
|
}
|
|
155
|
-
]
|
|
156
|
-
|
|
157
|
-
const result = await stableWorkflow(phases, {
|
|
158
|
-
workflowId: 'data-pipeline',
|
|
82
|
+
], {
|
|
83
|
+
workflowId: 'user-data-sync',
|
|
159
84
|
commonRequestData: { hostname: 'api.example.com' },
|
|
160
|
-
stopOnFirstPhaseError: true
|
|
161
|
-
logPhaseResults: true
|
|
85
|
+
stopOnFirstPhaseError: true
|
|
162
86
|
});
|
|
163
|
-
|
|
164
|
-
console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
|
|
165
87
|
```
|
|
166
88
|
|
|
167
|
-
|
|
89
|
+
## Core Features
|
|
168
90
|
|
|
169
|
-
|
|
170
|
-
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
91
|
+
### Intelligent Retry Strategies
|
|
171
92
|
|
|
172
|
-
|
|
173
|
-
{
|
|
174
|
-
id: 'check-status',
|
|
175
|
-
requests: [
|
|
176
|
-
{ id: 'status', requestOptions: { reqData: { path: '/status' }, resReq: true } }
|
|
177
|
-
],
|
|
178
|
-
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
179
|
-
const status = phaseResult.responses[0]?.data?.status;
|
|
180
|
-
|
|
181
|
-
if (status === 'completed') {
|
|
182
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' };
|
|
183
|
-
} else if (status === 'processing') {
|
|
184
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
185
|
-
return { action: PHASE_DECISION_ACTIONS.REPLAY }; // Replay this phase
|
|
186
|
-
} else {
|
|
187
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-handler' };
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
allowReplay: true,
|
|
191
|
-
maxReplayCount: 10
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
id: 'process',
|
|
195
|
-
requests: [
|
|
196
|
-
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
197
|
-
]
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
id: 'error-handler',
|
|
201
|
-
requests: [
|
|
202
|
-
{ id: 'error', requestOptions: { reqData: { path: '/error' }, resReq: true } }
|
|
203
|
-
]
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
id: 'finalize',
|
|
207
|
-
requests: [
|
|
208
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
209
|
-
]
|
|
210
|
-
}
|
|
211
|
-
];
|
|
93
|
+
Automatically retry failed requests with configurable strategies:
|
|
212
94
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
maxWorkflowIterations: 50,
|
|
218
|
-
sharedBuffer: {}
|
|
219
|
-
});
|
|
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
|
|
220
99
|
|
|
221
|
-
|
|
222
|
-
console.log('Terminated early:', result.terminatedEarly);
|
|
223
|
-
```
|
|
100
|
+
Each request can have individual retry configurations, or inherit from workflow-level defaults.
|
|
224
101
|
|
|
225
|
-
|
|
102
|
+
### Circuit Breaker Pattern
|
|
226
103
|
|
|
227
|
-
|
|
104
|
+
Prevent cascade failures and system overload with built-in circuit breakers:
|
|
228
105
|
|
|
229
|
-
|
|
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
|
|
230
110
|
|
|
231
|
-
|
|
111
|
+
### Response Caching
|
|
232
112
|
|
|
233
|
-
|
|
113
|
+
Reduce redundant API calls with intelligent caching:
|
|
234
114
|
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
-
|
|
238
|
-
-
|
|
239
|
-
- **`terminate`**: Stop the workflow immediately
|
|
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
|
|
240
119
|
|
|
241
|
-
|
|
120
|
+
### Rate Limiting and Concurrency Control
|
|
242
121
|
|
|
243
|
-
|
|
244
|
-
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PhaseExecutionDecision, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
122
|
+
Respect API rate limits and control system load:
|
|
245
123
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
{ id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
251
|
-
],
|
|
252
|
-
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
253
|
-
const isValid = phaseResult.responses[0]?.data?.valid;
|
|
254
|
-
|
|
255
|
-
if (isValid) {
|
|
256
|
-
sharedBuffer.validationPassed = true;
|
|
257
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to next phase
|
|
258
|
-
} else {
|
|
259
|
-
return {
|
|
260
|
-
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
261
|
-
metadata: { reason: 'Validation failed' }
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
id: 'process-data',
|
|
268
|
-
requests: [
|
|
269
|
-
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
270
|
-
]
|
|
271
|
-
}
|
|
272
|
-
];
|
|
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
|
|
273
128
|
|
|
274
|
-
|
|
275
|
-
workflowId: 'validation-workflow',
|
|
276
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
277
|
-
enableNonLinearExecution: true,
|
|
278
|
-
sharedBuffer: {}
|
|
279
|
-
});
|
|
129
|
+
## Workflow Execution Patterns
|
|
280
130
|
|
|
281
|
-
|
|
282
|
-
console.log('Workflow terminated:', result.terminationReason);
|
|
283
|
-
}
|
|
284
|
-
```
|
|
131
|
+
### Sequential and Concurrent Phases
|
|
285
132
|
|
|
286
|
-
|
|
133
|
+
Control execution order at the phase level:
|
|
134
|
+
|
|
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
|
|
287
138
|
|
|
288
139
|
```typescript
|
|
289
|
-
const phases
|
|
290
|
-
{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
]
|
|
295
|
-
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
296
|
-
const userType = phaseResult.responses[0]?.data?.type;
|
|
297
|
-
sharedBuffer.userType = userType;
|
|
298
|
-
|
|
299
|
-
if (userType === 'premium') {
|
|
300
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'premium-flow' };
|
|
301
|
-
} else if (userType === 'trial') {
|
|
302
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'trial-flow' };
|
|
303
|
-
} else {
|
|
304
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'free-flow' };
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
id: 'premium-flow',
|
|
310
|
-
requests: [
|
|
311
|
-
{ id: 'premium', requestOptions: { reqData: { path: '/premium/data' }, resReq: true } }
|
|
312
|
-
],
|
|
313
|
-
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
|
|
314
|
-
},
|
|
315
|
-
{
|
|
316
|
-
id: 'trial-flow',
|
|
317
|
-
requests: [
|
|
318
|
-
{ id: 'trial', requestOptions: { reqData: { path: '/trial/data' }, resReq: true } }
|
|
319
|
-
],
|
|
320
|
-
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
id: 'free-flow',
|
|
324
|
-
requests: [
|
|
325
|
-
{ id: 'free', requestOptions: { reqData: { path: '/free/data' }, resReq: true } }
|
|
326
|
-
]
|
|
327
|
-
},
|
|
328
|
-
{
|
|
329
|
-
id: 'finalize',
|
|
330
|
-
requests: [
|
|
331
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
332
|
-
]
|
|
140
|
+
const phases = [
|
|
141
|
+
{ id: 'init', requests: [...] }, // Sequential phase
|
|
142
|
+
{
|
|
143
|
+
id: 'parallel-fetch',
|
|
144
|
+
concurrentExecution: true, // Concurrent requests within phase
|
|
145
|
+
requests: [...]
|
|
333
146
|
}
|
|
334
147
|
];
|
|
335
148
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
sharedBuffer: {},
|
|
339
|
-
handlePhaseDecision: (decision, phaseResult) => {
|
|
340
|
-
console.log(`Phase ${phaseResult.phaseId} decided: ${decision.action}`);
|
|
341
|
-
}
|
|
149
|
+
await stableWorkflow(phases, {
|
|
150
|
+
concurrentPhaseExecution: true // Run phases in parallel
|
|
342
151
|
});
|
|
343
152
|
```
|
|
344
153
|
|
|
345
|
-
|
|
154
|
+
### Mixed Execution Mode
|
|
346
155
|
|
|
347
|
-
|
|
348
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
349
|
-
{
|
|
350
|
-
id: 'poll-job-status',
|
|
351
|
-
allowReplay: true,
|
|
352
|
-
maxReplayCount: 20,
|
|
353
|
-
requests: [
|
|
354
|
-
{ id: 'check', requestOptions: { reqData: { path: '/job/status' }, resReq: true } }
|
|
355
|
-
],
|
|
356
|
-
phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
|
|
357
|
-
const status = phaseResult.responses[0]?.data?.status;
|
|
358
|
-
const attempts = executionHistory.filter(h => h.phaseId === 'poll-job-status').length;
|
|
359
|
-
|
|
360
|
-
if (status === 'completed') {
|
|
361
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
362
|
-
} else if (status === 'failed') {
|
|
363
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-recovery' };
|
|
364
|
-
} else if (attempts < 20) {
|
|
365
|
-
// Still processing, wait and replay
|
|
366
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
367
|
-
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
368
|
-
} else {
|
|
369
|
-
return {
|
|
370
|
-
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
371
|
-
metadata: { reason: 'Job timeout after 20 attempts' }
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
id: 'process-results',
|
|
378
|
-
requests: [
|
|
379
|
-
{ id: 'process', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
|
|
380
|
-
]
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
id: 'error-recovery',
|
|
384
|
-
requests: [
|
|
385
|
-
{ id: 'recover', requestOptions: { reqData: { path: '/job/retry' }, resReq: true } }
|
|
386
|
-
]
|
|
387
|
-
}
|
|
388
|
-
];
|
|
156
|
+
Combine sequential and concurrent phases in a single workflow:
|
|
389
157
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
enableNonLinearExecution: true,
|
|
394
|
-
maxWorkflowIterations: 100
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
console.log('Total iterations:', result.executionHistory.length);
|
|
398
|
-
console.log('Phases executed:', result.completedPhases);
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
#### Retry Logic with Replay
|
|
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"
|
|
402
161
|
|
|
403
162
|
```typescript
|
|
404
|
-
const phases
|
|
405
|
-
{
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
requests: [
|
|
410
|
-
{
|
|
411
|
-
id: 'operation',
|
|
412
|
-
requestOptions: {
|
|
413
|
-
reqData: { path: '/risky-operation', method: 'POST' },
|
|
414
|
-
resReq: true,
|
|
415
|
-
attempts: 1 // No retries at request level
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
],
|
|
419
|
-
phaseDecisionHook: async ({ phaseResult, executionHistory, sharedBuffer }) => {
|
|
420
|
-
const success = phaseResult.responses[0]?.success;
|
|
421
|
-
const attemptCount = executionHistory.filter(h => h.phaseId === 'attempt-operation').length;
|
|
422
|
-
|
|
423
|
-
if (success) {
|
|
424
|
-
sharedBuffer.operationResult = phaseResult.responses[0]?.data;
|
|
425
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
426
|
-
} else if (attemptCount < 3) {
|
|
427
|
-
// Exponential backoff
|
|
428
|
-
const delay = 1000 * Math.pow(2, attemptCount);
|
|
429
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
430
|
-
|
|
431
|
-
sharedBuffer.retryAttempts = attemptCount;
|
|
432
|
-
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
433
|
-
} else {
|
|
434
|
-
return {
|
|
435
|
-
action: PHASE_DECISION_ACTIONS.JUMP,
|
|
436
|
-
targetPhaseId: 'fallback-operation',
|
|
437
|
-
metadata: { reason: 'Max retries exceeded' }
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
}
|
|
163
|
+
const phases = [
|
|
164
|
+
{ id: 'auth', requests: [...] }, // Sequential
|
|
165
|
+
{
|
|
166
|
+
id: 'fetch',
|
|
167
|
+
markConcurrentPhase: true, // Runs concurrently with next phase
|
|
168
|
+
requests: [...]
|
|
441
169
|
},
|
|
442
|
-
{
|
|
443
|
-
id: '
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
]
|
|
170
|
+
{
|
|
171
|
+
id: 'more-fetch',
|
|
172
|
+
markConcurrentPhase: true, // Runs concurrently with previous
|
|
173
|
+
requests: [...]
|
|
447
174
|
},
|
|
448
|
-
{
|
|
449
|
-
id: 'fallback-operation',
|
|
450
|
-
requests: [
|
|
451
|
-
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
452
|
-
]
|
|
453
|
-
}
|
|
175
|
+
{ id: 'process', requests: [...] } // Sequential, waits for above
|
|
454
176
|
];
|
|
455
177
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
sharedBuffer: { retryAttempts: 0 },
|
|
459
|
-
logPhaseResults: true
|
|
178
|
+
await stableWorkflow(phases, {
|
|
179
|
+
enableMixedExecution: true
|
|
460
180
|
});
|
|
461
181
|
```
|
|
462
182
|
|
|
463
|
-
|
|
183
|
+
### Non-Linear Workflows
|
|
184
|
+
|
|
185
|
+
Build dynamic workflows with conditional branching and looping:
|
|
186
|
+
|
|
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)
|
|
464
192
|
|
|
465
193
|
```typescript
|
|
466
|
-
const phases
|
|
194
|
+
const phases = [
|
|
467
195
|
{
|
|
468
|
-
id: '
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
474
|
-
const cached = phaseResult.responses[0]?.data?.cached;
|
|
475
|
-
|
|
476
|
-
if (cached) {
|
|
477
|
-
sharedBuffer.cachedData = phaseResult.responses[0]?.data;
|
|
478
|
-
// Skip expensive-computation and go directly to finalize
|
|
479
|
-
return { action: PHASE_DECISION_ACTIONS.SKIP, targetPhaseId: 'finalize' };
|
|
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' };
|
|
480
201
|
}
|
|
481
202
|
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
482
203
|
}
|
|
483
204
|
},
|
|
484
|
-
{
|
|
485
|
-
|
|
486
|
-
requests: [
|
|
487
|
-
{ id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
|
|
488
|
-
]
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
id: 'save-to-cache',
|
|
492
|
-
requests: [
|
|
493
|
-
{ id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
|
|
494
|
-
]
|
|
495
|
-
},
|
|
496
|
-
{
|
|
497
|
-
id: 'finalize',
|
|
498
|
-
requests: [
|
|
499
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
500
|
-
]
|
|
501
|
-
}
|
|
205
|
+
{ id: 'retry-logic', requests: [...] },
|
|
206
|
+
{ id: 'success', requests: [...] }
|
|
502
207
|
];
|
|
503
208
|
|
|
504
|
-
|
|
505
|
-
enableNonLinearExecution: true
|
|
506
|
-
sharedBuffer: {}
|
|
507
|
-
});
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
#### Execution History and Tracking
|
|
511
|
-
|
|
512
|
-
```typescript
|
|
513
|
-
const result = await stableWorkflow(phases, {
|
|
514
|
-
workflowId: 'tracked-workflow',
|
|
515
|
-
enableNonLinearExecution: true,
|
|
516
|
-
handlePhaseCompletion: ({ phaseResult, workflowId }) => {
|
|
517
|
-
console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
|
|
518
|
-
executionNumber: phaseResult.executionNumber,
|
|
519
|
-
success: phaseResult.success,
|
|
520
|
-
decision: phaseResult.decision
|
|
521
|
-
});
|
|
522
|
-
},
|
|
523
|
-
handlePhaseDecision: (decision, phaseResult) => {
|
|
524
|
-
console.log(`Decision made:`, {
|
|
525
|
-
phase: phaseResult.phaseId,
|
|
526
|
-
action: decision.action,
|
|
527
|
-
target: decision.targetPhaseId,
|
|
528
|
-
metadata: decision.metadata
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
// Analyze execution history
|
|
534
|
-
console.log('Total phase executions:', result.executionHistory.length);
|
|
535
|
-
console.log('Unique phases executed:', new Set(result.executionHistory.map(h => h.phaseId)).size);
|
|
536
|
-
console.log('Replay count:', result.executionHistory.filter(h => h.decision?.action === 'replay').length);
|
|
537
|
-
|
|
538
|
-
result.executionHistory.forEach(record => {
|
|
539
|
-
console.log(`- ${record.phaseId} (attempt ${record.executionNumber}): ${record.success ? '✓' : '✗'}`);
|
|
209
|
+
await stableWorkflow(phases, {
|
|
210
|
+
enableNonLinearExecution: true
|
|
540
211
|
});
|
|
541
212
|
```
|
|
542
213
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
maxWorkflowIterations: 50, // Prevent infinite loops
|
|
549
|
-
handlePhaseCompletion: ({ phaseResult }) => {
|
|
550
|
-
if (phaseResult.executionNumber && phaseResult.executionNumber > 10) {
|
|
551
|
-
console.warn(`Phase ${phaseResult.phaseId} executed ${phaseResult.executionNumber} times`);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
if (result.terminatedEarly && result.terminationReason?.includes('iterations')) {
|
|
557
|
-
console.error('Workflow hit iteration limit - possible infinite loop');
|
|
558
|
-
}
|
|
559
|
-
```
|
|
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)
|
|
560
219
|
|
|
561
220
|
### Branched Workflows
|
|
562
221
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
#### Why Branched Workflows?
|
|
566
|
-
|
|
567
|
-
- **Organize complex logic**: Group related phases into logical branches
|
|
568
|
-
- **Parallel processing**: Execute independent branches concurrently for better performance
|
|
569
|
-
- **Conditional routing**: Branches can decide whether to continue, jump to other branches, or terminate
|
|
570
|
-
- **Clean architecture**: Separate concerns into distinct branches (validation, processing, error handling)
|
|
571
|
-
- **Shared state**: Branches share a common buffer for state management
|
|
222
|
+
Execute multiple independent workflow paths in parallel or sequentially:
|
|
572
223
|
|
|
573
|
-
|
|
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
|
|
574
228
|
|
|
575
229
|
```typescript
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
230
|
+
const branches = [
|
|
579
231
|
{
|
|
580
|
-
id: '
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
id: 'validate-input',
|
|
584
|
-
requests: [
|
|
585
|
-
{ id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
586
|
-
]
|
|
587
|
-
}
|
|
588
|
-
]
|
|
232
|
+
id: 'user-flow',
|
|
233
|
+
markConcurrentBranch: true, // Parallel
|
|
234
|
+
phases: [...]
|
|
589
235
|
},
|
|
590
236
|
{
|
|
591
|
-
id: '
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
id: 'process-data',
|
|
595
|
-
requests: [
|
|
596
|
-
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
597
|
-
]
|
|
598
|
-
}
|
|
599
|
-
]
|
|
237
|
+
id: 'analytics-flow',
|
|
238
|
+
markConcurrentBranch: true, // Parallel
|
|
239
|
+
phases: [...]
|
|
600
240
|
},
|
|
601
241
|
{
|
|
602
|
-
id: '
|
|
603
|
-
phases: [
|
|
604
|
-
{
|
|
605
|
-
id: 'finalize',
|
|
606
|
-
requests: [
|
|
607
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
608
|
-
]
|
|
609
|
-
}
|
|
610
|
-
]
|
|
242
|
+
id: 'cleanup-flow', // Sequential (default)
|
|
243
|
+
phases: [...]
|
|
611
244
|
}
|
|
612
245
|
];
|
|
613
246
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
branches,
|
|
618
|
-
executeBranchesConcurrently: false, // Execute branches serially
|
|
619
|
-
sharedBuffer: {}
|
|
247
|
+
await stableWorkflow([], {
|
|
248
|
+
enableBranchExecution: true,
|
|
249
|
+
branches
|
|
620
250
|
});
|
|
621
|
-
|
|
622
|
-
console.log('Branches executed:', result.branches?.length);
|
|
623
251
|
```
|
|
624
252
|
|
|
625
|
-
|
|
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
|
|
626
258
|
|
|
627
|
-
|
|
628
|
-
// Parallel execution - all branches run concurrently
|
|
629
|
-
const result = await stableWorkflow([], {
|
|
630
|
-
workflowId: 'parallel-branches',
|
|
631
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
632
|
-
branches: [
|
|
633
|
-
{ id: 'fetch-users', phases: [/* ... */] },
|
|
634
|
-
{ id: 'fetch-products', phases: [/* ... */] },
|
|
635
|
-
{ id: 'fetch-orders', phases: [/* ... */] }
|
|
636
|
-
],
|
|
637
|
-
executeBranchesConcurrently: true, // Parallel execution
|
|
638
|
-
sharedBuffer: {}
|
|
639
|
-
});
|
|
259
|
+
## Advanced Capabilities
|
|
640
260
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
261
|
+
### Config Cascading
|
|
262
|
+
|
|
263
|
+
Configuration inheritance across workflow → branch → phase → request levels:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
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
|
|
278
|
+
},
|
|
279
|
+
phases: [{
|
|
280
|
+
id: 'my-phase',
|
|
281
|
+
commonConfig: {
|
|
282
|
+
// Phase-level config (overrides branch and workflow)
|
|
283
|
+
commonAttempts: 1
|
|
284
|
+
},
|
|
285
|
+
requests: [{
|
|
286
|
+
requestOptions: {
|
|
287
|
+
// Request-level config (highest priority)
|
|
288
|
+
attempts: 10,
|
|
289
|
+
cache: { enabled: false }
|
|
290
|
+
}
|
|
291
|
+
}]
|
|
292
|
+
}]
|
|
293
|
+
}]
|
|
652
294
|
});
|
|
653
295
|
```
|
|
654
296
|
|
|
655
|
-
|
|
297
|
+
### Shared Buffer and Pre-Execution Hooks
|
|
656
298
|
|
|
657
|
-
|
|
299
|
+
Share state and transform requests dynamically:
|
|
658
300
|
|
|
301
|
+
**Shared Buffer**: Cross-phase/branch communication
|
|
659
302
|
```typescript
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
663
|
-
{
|
|
664
|
-
id: 'validation',
|
|
665
|
-
phases: [
|
|
666
|
-
{
|
|
667
|
-
id: 'validate',
|
|
668
|
-
requests: [
|
|
669
|
-
{ id: 'val', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
670
|
-
]
|
|
671
|
-
}
|
|
672
|
-
],
|
|
673
|
-
branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
|
|
674
|
-
const isValid = branchResult.phases[0]?.responses[0]?.data?.valid;
|
|
675
|
-
|
|
676
|
-
if (!isValid) {
|
|
677
|
-
return {
|
|
678
|
-
action: BRANCH_DECISION_ACTIONS.TERMINATE,
|
|
679
|
-
metadata: { reason: 'Validation failed' }
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
sharedBuffer!.validated = true;
|
|
684
|
-
return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
|
|
685
|
-
}
|
|
686
|
-
},
|
|
687
|
-
{
|
|
688
|
-
id: 'processing',
|
|
689
|
-
phases: [/* ... */]
|
|
690
|
-
}
|
|
691
|
-
];
|
|
303
|
+
const sharedBuffer = { authToken: null };
|
|
692
304
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
branches,
|
|
697
|
-
executeBranchesConcurrently: false,
|
|
698
|
-
sharedBuffer: {}
|
|
305
|
+
await stableWorkflow(phases, {
|
|
306
|
+
sharedBuffer,
|
|
307
|
+
// Phases can read/write to sharedBuffer via preExecution hooks
|
|
699
308
|
});
|
|
700
|
-
|
|
701
|
-
if (result.terminatedEarly) {
|
|
702
|
-
console.log('Workflow terminated:', result.terminationReason);
|
|
703
|
-
}
|
|
704
309
|
```
|
|
705
310
|
|
|
706
|
-
|
|
707
|
-
|
|
311
|
+
**Pre-Execution Hooks**: Modify requests before execution
|
|
708
312
|
```typescript
|
|
709
|
-
|
|
710
|
-
{
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
id: 'cache-check',
|
|
715
|
-
requests: [
|
|
716
|
-
{ id: 'check', requestOptions: { reqData: { path: '/cache/check' }, resReq: true } }
|
|
717
|
-
]
|
|
718
|
-
}
|
|
719
|
-
],
|
|
720
|
-
branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
|
|
721
|
-
const cached = branchResult.phases[0]?.responses[0]?.data?.cached;
|
|
722
|
-
|
|
723
|
-
if (cached) {
|
|
724
|
-
sharedBuffer!.cachedData = branchResult.phases[0]?.responses[0]?.data;
|
|
725
|
-
// Skip expensive computation, jump directly to finalize
|
|
313
|
+
{
|
|
314
|
+
requestOptions: {
|
|
315
|
+
preExecution: {
|
|
316
|
+
preExecutionHook: ({ commonBuffer, inputParams }) => {
|
|
317
|
+
// Access buffer, compute values, return config overrides
|
|
726
318
|
return {
|
|
727
|
-
|
|
728
|
-
|
|
319
|
+
reqData: {
|
|
320
|
+
headers: { 'Authorization': `Bearer ${commonBuffer.authToken}` }
|
|
321
|
+
}
|
|
729
322
|
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
|
|
323
|
+
},
|
|
324
|
+
applyPreExecutionConfigOverride: true
|
|
733
325
|
}
|
|
734
|
-
},
|
|
735
|
-
{
|
|
736
|
-
id: 'expensive-computation',
|
|
737
|
-
phases: [
|
|
738
|
-
{
|
|
739
|
-
id: 'compute',
|
|
740
|
-
requests: [
|
|
741
|
-
{ id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
|
|
742
|
-
]
|
|
743
|
-
}
|
|
744
|
-
]
|
|
745
|
-
},
|
|
746
|
-
{
|
|
747
|
-
id: 'save-cache',
|
|
748
|
-
phases: [
|
|
749
|
-
{
|
|
750
|
-
id: 'save',
|
|
751
|
-
requests: [
|
|
752
|
-
{ id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
|
|
753
|
-
]
|
|
754
|
-
}
|
|
755
|
-
]
|
|
756
|
-
},
|
|
757
|
-
{
|
|
758
|
-
id: 'finalize',
|
|
759
|
-
phases: [
|
|
760
|
-
{
|
|
761
|
-
id: 'final',
|
|
762
|
-
requests: [
|
|
763
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
764
|
-
]
|
|
765
|
-
}
|
|
766
|
-
]
|
|
767
326
|
}
|
|
768
|
-
|
|
327
|
+
}
|
|
328
|
+
```
|
|
769
329
|
|
|
770
|
-
|
|
771
|
-
workflowId: 'cache-optimization',
|
|
772
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
773
|
-
branches,
|
|
774
|
-
executeBranchesConcurrently: false,
|
|
775
|
-
sharedBuffer: {}
|
|
776
|
-
});
|
|
330
|
+
### Comprehensive Observability
|
|
777
331
|
|
|
778
|
-
|
|
779
|
-
// If cache miss: check-cache → expensive-computation → save-cache → finalize
|
|
780
|
-
```
|
|
332
|
+
Built-in hooks for monitoring, logging, and analysis:
|
|
781
333
|
|
|
782
|
-
|
|
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
|
|
783
339
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
id: 'user-info',
|
|
791
|
-
requests: [
|
|
792
|
-
{ id: 'user', requestOptions: { reqData: { path: '/user/info' }, resReq: true } }
|
|
793
|
-
]
|
|
794
|
-
}
|
|
795
|
-
],
|
|
796
|
-
branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
|
|
797
|
-
const userType = branchResult.phases[0]?.responses[0]?.data?.type;
|
|
798
|
-
sharedBuffer!.userType = userType;
|
|
799
|
-
|
|
800
|
-
if (userType === 'premium') {
|
|
801
|
-
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'premium-flow' };
|
|
802
|
-
} else if (userType === 'trial') {
|
|
803
|
-
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'trial-flow' };
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
return { action: BRANCH_DECISION_ACTIONS.CONTINUE }; // free-flow
|
|
807
|
-
}
|
|
808
|
-
},
|
|
809
|
-
{
|
|
810
|
-
id: 'free-flow',
|
|
811
|
-
phases: [
|
|
812
|
-
{
|
|
813
|
-
id: 'free-data',
|
|
814
|
-
requests: [
|
|
815
|
-
{ id: 'free', requestOptions: { reqData: { path: '/free/data' }, resReq: true } }
|
|
816
|
-
]
|
|
817
|
-
}
|
|
818
|
-
],
|
|
819
|
-
branchDecisionHook: async () => {
|
|
820
|
-
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'finalize' };
|
|
821
|
-
}
|
|
822
|
-
},
|
|
823
|
-
{
|
|
824
|
-
id: 'trial-flow',
|
|
825
|
-
phases: [
|
|
826
|
-
{
|
|
827
|
-
id: 'trial-data',
|
|
828
|
-
requests: [
|
|
829
|
-
{ id: 'trial', requestOptions: { reqData: { path: '/trial/data' }, resReq: true } }
|
|
830
|
-
]
|
|
831
|
-
}
|
|
832
|
-
],
|
|
833
|
-
branchDecisionHook: async () => {
|
|
834
|
-
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'finalize' };
|
|
835
|
-
}
|
|
836
|
-
},
|
|
837
|
-
{
|
|
838
|
-
id: 'premium-flow',
|
|
839
|
-
phases: [
|
|
840
|
-
{
|
|
841
|
-
id: 'premium-data',
|
|
842
|
-
requests: [
|
|
843
|
-
{ id: 'premium', requestOptions: { reqData: { path: '/premium/data' }, resReq: true } }
|
|
844
|
-
]
|
|
845
|
-
}
|
|
846
|
-
],
|
|
847
|
-
branchDecisionHook: async () => {
|
|
848
|
-
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'finalize' };
|
|
849
|
-
}
|
|
850
|
-
},
|
|
851
|
-
{
|
|
852
|
-
id: 'finalize',
|
|
853
|
-
phases: [
|
|
854
|
-
{
|
|
855
|
-
id: 'final',
|
|
856
|
-
requests: [
|
|
857
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
858
|
-
]
|
|
859
|
-
}
|
|
860
|
-
]
|
|
861
|
-
}
|
|
862
|
-
];
|
|
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
|
|
863
346
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
867
|
-
branches,
|
|
868
|
-
executeBranchesConcurrently: false,
|
|
869
|
-
sharedBuffer: {}
|
|
870
|
-
});
|
|
871
|
-
```
|
|
347
|
+
**Execution History**:
|
|
348
|
+
Every workflow result includes detailed execution history with timestamps, decisions, and metadata.
|
|
872
349
|
|
|
873
|
-
|
|
350
|
+
## API Surface
|
|
874
351
|
|
|
875
|
-
|
|
876
|
-
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
877
|
-
{
|
|
878
|
-
id: 'retry-branch',
|
|
879
|
-
phases: [
|
|
880
|
-
{
|
|
881
|
-
id: 'retry-phase',
|
|
882
|
-
commonConfig: {
|
|
883
|
-
commonAttempts: 5,
|
|
884
|
-
commonWait: 100,
|
|
885
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
886
|
-
},
|
|
887
|
-
requests: [
|
|
888
|
-
{
|
|
889
|
-
id: 'retry-req',
|
|
890
|
-
requestOptions: {
|
|
891
|
-
reqData: { path: '/unstable-endpoint' },
|
|
892
|
-
resReq: true
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
]
|
|
896
|
-
}
|
|
897
|
-
]
|
|
898
|
-
}
|
|
899
|
-
];
|
|
900
|
-
|
|
901
|
-
const result = await stableWorkflow([], {
|
|
902
|
-
workflowId: 'retry-workflow',
|
|
903
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
904
|
-
branches,
|
|
905
|
-
executeBranchesConcurrently: false
|
|
906
|
-
});
|
|
907
|
-
```
|
|
908
|
-
|
|
909
|
-
#### Error Handling in Branches
|
|
910
|
-
|
|
911
|
-
```typescript
|
|
912
|
-
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
913
|
-
{
|
|
914
|
-
id: 'risky-operation',
|
|
915
|
-
phases: [
|
|
916
|
-
{
|
|
917
|
-
id: 'operation',
|
|
918
|
-
requests: [
|
|
919
|
-
{
|
|
920
|
-
id: 'op',
|
|
921
|
-
requestOptions: {
|
|
922
|
-
reqData: { path: '/risky' },
|
|
923
|
-
resReq: true,
|
|
924
|
-
attempts: 3
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
]
|
|
928
|
-
}
|
|
929
|
-
],
|
|
930
|
-
branchDecisionHook: async ({ branchResult }) => {
|
|
931
|
-
if (!branchResult.success) {
|
|
932
|
-
return {
|
|
933
|
-
action: BRANCH_DECISION_ACTIONS.JUMP,
|
|
934
|
-
targetBranchId: 'error-handler'
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'success-handler' };
|
|
938
|
-
}
|
|
939
|
-
},
|
|
940
|
-
{
|
|
941
|
-
id: 'success-handler',
|
|
942
|
-
phases: [
|
|
943
|
-
{
|
|
944
|
-
id: 'success',
|
|
945
|
-
requests: [
|
|
946
|
-
{ id: 'success', requestOptions: { reqData: { path: '/success' }, resReq: true } }
|
|
947
|
-
]
|
|
948
|
-
}
|
|
949
|
-
],
|
|
950
|
-
branchDecisionHook: async () => {
|
|
951
|
-
return { action: BRANCH_DECISION_ACTIONS.TERMINATE };
|
|
952
|
-
}
|
|
953
|
-
},
|
|
954
|
-
{
|
|
955
|
-
id: 'error-handler',
|
|
956
|
-
phases: [
|
|
957
|
-
{
|
|
958
|
-
id: 'error',
|
|
959
|
-
requests: [
|
|
960
|
-
{ id: 'error', requestOptions: { reqData: { path: '/error' }, resReq: true } }
|
|
961
|
-
]
|
|
962
|
-
}
|
|
963
|
-
]
|
|
964
|
-
}
|
|
965
|
-
];
|
|
966
|
-
|
|
967
|
-
const result = await stableWorkflow([], {
|
|
968
|
-
workflowId: 'error-handling-workflow',
|
|
969
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
970
|
-
branches,
|
|
971
|
-
executeBranchesConcurrently: false,
|
|
972
|
-
stopOnFirstPhaseError: false // Continue to error handler branch
|
|
973
|
-
});
|
|
974
|
-
```
|
|
975
|
-
|
|
976
|
-
#### Branch Completion Hooks
|
|
977
|
-
|
|
978
|
-
```typescript
|
|
979
|
-
const result = await stableWorkflow([], {
|
|
980
|
-
workflowId: 'tracked-branches',
|
|
981
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
982
|
-
branches,
|
|
983
|
-
executeBranchesConcurrently: true,
|
|
984
|
-
handleBranchCompletion: ({ branchResult, workflowId }) => {
|
|
985
|
-
console.log(`[${workflowId}] Branch ${branchResult.branchId} completed:`, {
|
|
986
|
-
success: branchResult.success,
|
|
987
|
-
phases: branchResult.phases.length,
|
|
988
|
-
decision: branchResult.decision?.action
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
});
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
#### Mixed Parallel and Serial Branches
|
|
995
|
-
|
|
996
|
-
```typescript
|
|
997
|
-
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
998
|
-
{
|
|
999
|
-
id: 'init',
|
|
1000
|
-
phases: [/* initialization */]
|
|
1001
|
-
},
|
|
1002
|
-
{
|
|
1003
|
-
id: 'parallel-1',
|
|
1004
|
-
markConcurrentBranch: true,
|
|
1005
|
-
phases: [/* independent task 1 */]
|
|
1006
|
-
},
|
|
1007
|
-
{
|
|
1008
|
-
id: 'parallel-2',
|
|
1009
|
-
markConcurrentBranch: true,
|
|
1010
|
-
phases: [/* independent task 2 */]
|
|
1011
|
-
},
|
|
1012
|
-
{
|
|
1013
|
-
id: 'parallel-3',
|
|
1014
|
-
markConcurrentBranch: true,
|
|
1015
|
-
phases: [/* independent task 3 */],
|
|
1016
|
-
branchDecisionHook: async ({ concurrentBranchResults }) => {
|
|
1017
|
-
// All parallel branches completed, make decision
|
|
1018
|
-
const allSuccessful = concurrentBranchResults!.every(b => b.success);
|
|
1019
|
-
if (!allSuccessful) {
|
|
1020
|
-
return { action: BRANCH_DECISION_ACTIONS.TERMINATE };
|
|
1021
|
-
}
|
|
1022
|
-
return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
|
|
1023
|
-
}
|
|
1024
|
-
},
|
|
1025
|
-
{
|
|
1026
|
-
id: 'finalize',
|
|
1027
|
-
phases: [/* finalization */]
|
|
1028
|
-
}
|
|
1029
|
-
];
|
|
1030
|
-
|
|
1031
|
-
const result = await stableWorkflow([], {
|
|
1032
|
-
workflowId: 'mixed-execution',
|
|
1033
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1034
|
-
branches,
|
|
1035
|
-
executeBranchesConcurrently: false // Base mode is serial
|
|
1036
|
-
});
|
|
1037
|
-
|
|
1038
|
-
// Execution: init → [parallel-1, parallel-2, parallel-3] → finalize
|
|
1039
|
-
```
|
|
1040
|
-
|
|
1041
|
-
#### Configuration Options
|
|
1042
|
-
|
|
1043
|
-
**Workflow Options:**
|
|
1044
|
-
- `branches`: Array of branch definitions
|
|
1045
|
-
- `executeBranchesConcurrently`: Execute all branches in parallel (default: false)
|
|
1046
|
-
- `handleBranchCompletion`: Called when each branch completes
|
|
1047
|
-
|
|
1048
|
-
**Branch Options:**
|
|
1049
|
-
- `id`: Unique branch identifier
|
|
1050
|
-
- `phases`: Array of phases to execute in this branch
|
|
1051
|
-
- `branchDecisionHook`: Function returning `BranchExecutionDecision`
|
|
1052
|
-
- `markConcurrentBranch`: Mark as part of concurrent group (default: false)
|
|
1053
|
-
|
|
1054
|
-
**Branch Decision Actions:**
|
|
1055
|
-
- `CONTINUE`: Proceed to next branch
|
|
1056
|
-
- `JUMP`: Jump to specific branch by ID
|
|
1057
|
-
- `TERMINATE`: Stop workflow execution
|
|
1058
|
-
|
|
1059
|
-
**Decision Hook Parameters:**
|
|
1060
|
-
```typescript
|
|
1061
|
-
interface BranchDecisionHookOptions {
|
|
1062
|
-
workflowId: string;
|
|
1063
|
-
branchResult: STABLE_WORKFLOW_BRANCH_RESULT;
|
|
1064
|
-
branchId: string;
|
|
1065
|
-
branchIndex: number;
|
|
1066
|
-
sharedBuffer?: Record<string, any>;
|
|
1067
|
-
concurrentBranchResults?: STABLE_WORKFLOW_BRANCH_RESULT[]; // For concurrent groups
|
|
1068
|
-
}
|
|
1069
|
-
```
|
|
1070
|
-
|
|
1071
|
-
**Decision Object:**
|
|
1072
|
-
```typescript
|
|
1073
|
-
interface BranchExecutionDecision {
|
|
1074
|
-
action: BRANCH_DECISION_ACTIONS;
|
|
1075
|
-
targetBranchId?: string;
|
|
1076
|
-
metadata?: Record<string, any>;
|
|
1077
|
-
}
|
|
1078
|
-
```
|
|
352
|
+
### Core Functions
|
|
1079
353
|
|
|
1080
|
-
|
|
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
|
|
1081
357
|
|
|
1082
|
-
|
|
358
|
+
### Utility Exports
|
|
1083
359
|
|
|
1084
|
-
**
|
|
360
|
+
- **Circuit Breaker**: `CircuitBreaker`, `CircuitBreakerOpenError`
|
|
361
|
+
- **Rate Limiting**: `RateLimiter`
|
|
362
|
+
- **Concurrency**: `ConcurrencyLimiter`
|
|
363
|
+
- **Caching**: `CacheManager`, `getGlobalCacheManager`, `resetGlobalCacheManager`
|
|
364
|
+
- **Execution Utilities**: `executeNonLinearWorkflow`, `executeBranchWorkflow`, `executePhase`
|
|
1085
365
|
|
|
1086
|
-
|
|
1087
|
-
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
366
|
+
### Enums
|
|
1088
367
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
],
|
|
1095
|
-
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.CONTINUE })
|
|
1096
|
-
},
|
|
1097
|
-
// These two phases execute in parallel
|
|
1098
|
-
{
|
|
1099
|
-
id: 'check-inventory',
|
|
1100
|
-
markConcurrentPhase: true,
|
|
1101
|
-
requests: [
|
|
1102
|
-
{ id: 'inv', requestOptions: { reqData: { path: '/inventory' }, resReq: true } }
|
|
1103
|
-
]
|
|
1104
|
-
},
|
|
1105
|
-
{
|
|
1106
|
-
id: 'check-pricing',
|
|
1107
|
-
markConcurrentPhase: true,
|
|
1108
|
-
requests: [
|
|
1109
|
-
{ id: 'price', requestOptions: { reqData: { path: '/pricing' }, resReq: true } }
|
|
1110
|
-
],
|
|
1111
|
-
// Decision hook receives results from all concurrent phases
|
|
1112
|
-
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
1113
|
-
const inventory = concurrentPhaseResults![0].responses[0]?.data;
|
|
1114
|
-
const pricing = concurrentPhaseResults![1].responses[0]?.data;
|
|
1115
|
-
|
|
1116
|
-
if (inventory.available && pricing.inBudget) {
|
|
1117
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
1118
|
-
}
|
|
1119
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'out-of-stock' };
|
|
1120
|
-
}
|
|
1121
|
-
},
|
|
1122
|
-
// This phase executes serially after the parallel group
|
|
1123
|
-
{
|
|
1124
|
-
id: 'process-order',
|
|
1125
|
-
requests: [
|
|
1126
|
-
{ id: 'order', requestOptions: { reqData: { path: '/order' }, resReq: true } }
|
|
1127
|
-
]
|
|
1128
|
-
},
|
|
1129
|
-
{
|
|
1130
|
-
id: 'out-of-stock',
|
|
1131
|
-
requests: [
|
|
1132
|
-
{ id: 'notify', requestOptions: { reqData: { path: '/notify' }, resReq: true } }
|
|
1133
|
-
]
|
|
1134
|
-
}
|
|
1135
|
-
];
|
|
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
|
|
1136
373
|
|
|
1137
|
-
|
|
1138
|
-
workflowId: 'mixed-execution',
|
|
1139
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1140
|
-
enableNonLinearExecution: true
|
|
1141
|
-
});
|
|
1142
|
-
```
|
|
374
|
+
### TypeScript Types
|
|
1143
375
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
```typescript
|
|
1147
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1148
|
-
{
|
|
1149
|
-
id: 'authenticate',
|
|
1150
|
-
requests: [
|
|
1151
|
-
{ id: 'auth', requestOptions: { reqData: { path: '/auth' }, resReq: true } }
|
|
1152
|
-
]
|
|
1153
|
-
},
|
|
1154
|
-
// First parallel group: Data validation
|
|
1155
|
-
{
|
|
1156
|
-
id: 'validate-user',
|
|
1157
|
-
markConcurrentPhase: true,
|
|
1158
|
-
requests: [
|
|
1159
|
-
{ id: 'val-user', requestOptions: { reqData: { path: '/validate/user' }, resReq: true } }
|
|
1160
|
-
]
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
id: 'validate-payment',
|
|
1164
|
-
markConcurrentPhase: true,
|
|
1165
|
-
requests: [
|
|
1166
|
-
{ id: 'val-pay', requestOptions: { reqData: { path: '/validate/payment' }, resReq: true } }
|
|
1167
|
-
]
|
|
1168
|
-
},
|
|
1169
|
-
{
|
|
1170
|
-
id: 'validate-shipping',
|
|
1171
|
-
markConcurrentPhase: true,
|
|
1172
|
-
requests: [
|
|
1173
|
-
{ id: 'val-ship', requestOptions: { reqData: { path: '/validate/shipping' }, resReq: true } }
|
|
1174
|
-
],
|
|
1175
|
-
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
1176
|
-
const allValid = concurrentPhaseResults!.every(r => r.success && r.responses[0]?.data?.valid);
|
|
1177
|
-
if (!allValid) {
|
|
1178
|
-
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Validation failed' } };
|
|
1179
|
-
}
|
|
1180
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
1181
|
-
}
|
|
1182
|
-
},
|
|
1183
|
-
// Serial processing phase
|
|
1184
|
-
{
|
|
1185
|
-
id: 'calculate-total',
|
|
1186
|
-
requests: [
|
|
1187
|
-
{ id: 'calc', requestOptions: { reqData: { path: '/calculate' }, resReq: true } }
|
|
1188
|
-
]
|
|
1189
|
-
},
|
|
1190
|
-
// Second parallel group: External integrations
|
|
1191
|
-
{
|
|
1192
|
-
id: 'notify-warehouse',
|
|
1193
|
-
markConcurrentPhase: true,
|
|
1194
|
-
requests: [
|
|
1195
|
-
{ id: 'warehouse', requestOptions: { reqData: { path: '/notify/warehouse' }, resReq: true } }
|
|
1196
|
-
]
|
|
1197
|
-
},
|
|
1198
|
-
{
|
|
1199
|
-
id: 'notify-shipping',
|
|
1200
|
-
markConcurrentPhase: true,
|
|
1201
|
-
requests: [
|
|
1202
|
-
{ id: 'shipping', requestOptions: { reqData: { path: '/notify/shipping' }, resReq: true } }
|
|
1203
|
-
]
|
|
1204
|
-
},
|
|
1205
|
-
{
|
|
1206
|
-
id: 'update-inventory',
|
|
1207
|
-
markConcurrentPhase: true,
|
|
1208
|
-
requests: [
|
|
1209
|
-
{ id: 'inventory', requestOptions: { reqData: { path: '/update/inventory' }, resReq: true } }
|
|
1210
|
-
]
|
|
1211
|
-
},
|
|
1212
|
-
// Final serial phase
|
|
1213
|
-
{
|
|
1214
|
-
id: 'finalize',
|
|
1215
|
-
requests: [
|
|
1216
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
1217
|
-
]
|
|
1218
|
-
}
|
|
1219
|
-
];
|
|
1220
|
-
|
|
1221
|
-
const result = await stableWorkflow(phases, {
|
|
1222
|
-
workflowId: 'multi-parallel-workflow',
|
|
1223
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1224
|
-
enableNonLinearExecution: true
|
|
1225
|
-
});
|
|
1226
|
-
|
|
1227
|
-
console.log('Execution order demonstrates mixed serial/parallel execution');
|
|
1228
|
-
```
|
|
1229
|
-
|
|
1230
|
-
**Decision Making with Concurrent Results:**
|
|
1231
|
-
|
|
1232
|
-
```typescript
|
|
1233
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1234
|
-
{
|
|
1235
|
-
id: 'api-check-1',
|
|
1236
|
-
markConcurrentPhase: true,
|
|
1237
|
-
requests: [
|
|
1238
|
-
{ id: 'api1', requestOptions: { reqData: { path: '/health/api1' }, resReq: true } }
|
|
1239
|
-
]
|
|
1240
|
-
},
|
|
1241
|
-
{
|
|
1242
|
-
id: 'api-check-2',
|
|
1243
|
-
markConcurrentPhase: true,
|
|
1244
|
-
requests: [
|
|
1245
|
-
{ id: 'api2', requestOptions: { reqData: { path: '/health/api2' }, resReq: true } }
|
|
1246
|
-
]
|
|
1247
|
-
},
|
|
1248
|
-
{
|
|
1249
|
-
id: 'api-check-3',
|
|
1250
|
-
markConcurrentPhase: true,
|
|
1251
|
-
requests: [
|
|
1252
|
-
{ id: 'api3', requestOptions: { reqData: { path: '/health/api3' }, resReq: true } }
|
|
1253
|
-
],
|
|
1254
|
-
phaseDecisionHook: async ({ concurrentPhaseResults, sharedBuffer }) => {
|
|
1255
|
-
// Aggregate results from all parallel phases
|
|
1256
|
-
const healthScores = concurrentPhaseResults!.map(result =>
|
|
1257
|
-
result.responses[0]?.data?.score || 0
|
|
1258
|
-
);
|
|
1259
|
-
|
|
1260
|
-
const averageScore = healthScores.reduce((a, b) => a + b, 0) / healthScores.length;
|
|
1261
|
-
sharedBuffer!.healthScore = averageScore;
|
|
1262
|
-
|
|
1263
|
-
if (averageScore > 0.8) {
|
|
1264
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'optimal-path' };
|
|
1265
|
-
} else if (averageScore > 0.5) {
|
|
1266
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to degraded-path
|
|
1267
|
-
} else {
|
|
1268
|
-
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'fallback-path' };
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
},
|
|
1272
|
-
{
|
|
1273
|
-
id: 'degraded-path',
|
|
1274
|
-
requests: [
|
|
1275
|
-
{ id: 'degraded', requestOptions: { reqData: { path: '/degraded' }, resReq: true } }
|
|
1276
|
-
]
|
|
1277
|
-
},
|
|
1278
|
-
{
|
|
1279
|
-
id: 'optimal-path',
|
|
1280
|
-
requests: [
|
|
1281
|
-
{ id: 'optimal', requestOptions: { reqData: { path: '/optimal' }, resReq: true } }
|
|
1282
|
-
]
|
|
1283
|
-
},
|
|
1284
|
-
{
|
|
1285
|
-
id: 'fallback-path',
|
|
1286
|
-
requests: [
|
|
1287
|
-
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
1288
|
-
]
|
|
1289
|
-
}
|
|
1290
|
-
];
|
|
1291
|
-
|
|
1292
|
-
const sharedBuffer = {};
|
|
1293
|
-
const result = await stableWorkflow(phases, {
|
|
1294
|
-
workflowId: 'adaptive-routing',
|
|
1295
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1296
|
-
enableNonLinearExecution: true,
|
|
1297
|
-
sharedBuffer
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
console.log('Average health score:', sharedBuffer.healthScore);
|
|
1301
|
-
```
|
|
1302
|
-
|
|
1303
|
-
**Error Handling in Parallel Groups:**
|
|
1304
|
-
|
|
1305
|
-
```typescript
|
|
1306
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1307
|
-
{
|
|
1308
|
-
id: 'critical-check',
|
|
1309
|
-
markConcurrentPhase: true,
|
|
1310
|
-
requests: [
|
|
1311
|
-
{
|
|
1312
|
-
id: 'check1',
|
|
1313
|
-
requestOptions: {
|
|
1314
|
-
reqData: { path: '/critical/check1' },
|
|
1315
|
-
resReq: true,
|
|
1316
|
-
attempts: 3
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
]
|
|
1320
|
-
},
|
|
1321
|
-
{
|
|
1322
|
-
id: 'optional-check',
|
|
1323
|
-
markConcurrentPhase: true,
|
|
1324
|
-
requests: [
|
|
1325
|
-
{
|
|
1326
|
-
id: 'check2',
|
|
1327
|
-
requestOptions: {
|
|
1328
|
-
reqData: { path: '/optional/check2' },
|
|
1329
|
-
resReq: true,
|
|
1330
|
-
attempts: 1,
|
|
1331
|
-
finalErrorAnalyzer: async () => true // Suppress errors
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
],
|
|
1335
|
-
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
1336
|
-
// Check if critical phase succeeded
|
|
1337
|
-
const criticalSuccess = concurrentPhaseResults![0].success;
|
|
1338
|
-
|
|
1339
|
-
if (!criticalSuccess) {
|
|
1340
|
-
return {
|
|
1341
|
-
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
1342
|
-
metadata: { reason: 'Critical check failed' }
|
|
1343
|
-
};
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
// Continue even if optional check failed
|
|
1347
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
1348
|
-
}
|
|
1349
|
-
},
|
|
1350
|
-
{
|
|
1351
|
-
id: 'process',
|
|
1352
|
-
requests: [
|
|
1353
|
-
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
1354
|
-
]
|
|
1355
|
-
}
|
|
1356
|
-
];
|
|
1357
|
-
|
|
1358
|
-
const result = await stableWorkflow(phases, {
|
|
1359
|
-
workflowId: 'resilient-parallel',
|
|
1360
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1361
|
-
enableNonLinearExecution: true,
|
|
1362
|
-
stopOnFirstPhaseError: false // Continue even with phase errors
|
|
1363
|
-
});
|
|
1364
|
-
```
|
|
1365
|
-
|
|
1366
|
-
**Key Points:**
|
|
1367
|
-
- Only **consecutive phases** with `markConcurrentPhase: true` execute in parallel
|
|
1368
|
-
- The **last phase** in a concurrent group can have a `phaseDecisionHook` that receives `concurrentPhaseResults`
|
|
1369
|
-
- Parallel groups are separated by phases **without** `markConcurrentPhase` (or phases with it set to false)
|
|
1370
|
-
- All decision actions work with parallel groups except `REPLAY` (not supported for concurrent groups)
|
|
1371
|
-
- Error handling follows normal workflow rules - use `stopOnFirstPhaseError` to control behavior
|
|
1372
|
-
|
|
1373
|
-
#### Configuration Options
|
|
1374
|
-
|
|
1375
|
-
**Workflow Options:**
|
|
1376
|
-
- `enableNonLinearExecution`: Enable non-linear workflow (required)
|
|
1377
|
-
- `maxWorkflowIterations`: Maximum total iterations (default: 1000)
|
|
1378
|
-
- `handlePhaseDecision`: Called when phase makes a decision
|
|
1379
|
-
- `stopOnFirstPhaseError`: Stop on phase failure (default: false)
|
|
1380
|
-
|
|
1381
|
-
**Phase Options:**
|
|
1382
|
-
- `phaseDecisionHook`: Function returning `PhaseExecutionDecision`
|
|
1383
|
-
- `allowReplay`: Allow phase replay (default: false)
|
|
1384
|
-
- `allowSkip`: Allow phase skip (default: false)
|
|
1385
|
-
- `maxReplayCount`: Maximum replays (default: Infinity)
|
|
1386
|
-
|
|
1387
|
-
**Decision Hook Parameters:**
|
|
1388
|
-
```typescript
|
|
1389
|
-
interface PhaseDecisionHookOptions {
|
|
1390
|
-
workflowId: string;
|
|
1391
|
-
phaseResult: STABLE_WORKFLOW_PHASE_RESULT;
|
|
1392
|
-
phaseId: string;
|
|
1393
|
-
phaseIndex: number;
|
|
1394
|
-
executionHistory: PhaseExecutionRecord[];
|
|
1395
|
-
sharedBuffer?: Record<string, any>;
|
|
1396
|
-
params?: any;
|
|
1397
|
-
}
|
|
1398
|
-
```
|
|
1399
|
-
|
|
1400
|
-
**Decision Object:**
|
|
1401
|
-
```typescript
|
|
1402
|
-
interface PhaseExecutionDecision {
|
|
1403
|
-
action: PHASE_DECISION_ACTIONS;
|
|
1404
|
-
targetPhaseId?: string;
|
|
1405
|
-
replayCount?: number;
|
|
1406
|
-
metadata?: Record<string, any>;
|
|
1407
|
-
}
|
|
1408
|
-
```
|
|
1409
|
-
|
|
1410
|
-
### Retry Strategies
|
|
1411
|
-
|
|
1412
|
-
Control the delay between retry attempts:
|
|
1413
|
-
|
|
1414
|
-
```typescript
|
|
1415
|
-
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
1416
|
-
|
|
1417
|
-
// Fixed delay: 1000ms between each retry
|
|
1418
|
-
await stableRequest({
|
|
1419
|
-
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
1420
|
-
attempts: 3,
|
|
1421
|
-
wait: 1000,
|
|
1422
|
-
retryStrategy: RETRY_STRATEGIES.FIXED
|
|
1423
|
-
});
|
|
1424
|
-
|
|
1425
|
-
// Linear backoff: 1000ms, 2000ms, 3000ms
|
|
1426
|
-
await stableRequest({
|
|
1427
|
-
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
1428
|
-
attempts: 3,
|
|
1429
|
-
wait: 1000,
|
|
1430
|
-
retryStrategy: RETRY_STRATEGIES.LINEAR
|
|
1431
|
-
});
|
|
1432
|
-
|
|
1433
|
-
// Exponential backoff: 1000ms, 2000ms, 4000ms
|
|
1434
|
-
await stableRequest({
|
|
1435
|
-
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
1436
|
-
attempts: 3,
|
|
1437
|
-
wait: 1000,
|
|
1438
|
-
maxAllowedWait: 10000,
|
|
1439
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1440
|
-
});
|
|
1441
|
-
```
|
|
1442
|
-
|
|
1443
|
-
### Circuit Breaker
|
|
1444
|
-
|
|
1445
|
-
Prevent cascading failures by automatically blocking requests when error thresholds are exceeded:
|
|
1446
|
-
|
|
1447
|
-
```typescript
|
|
1448
|
-
import { stableApiGateway, CircuitBreakerState } from '@emmvish/stable-request';
|
|
1449
|
-
|
|
1450
|
-
const results = await stableApiGateway(requests, {
|
|
1451
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1452
|
-
circuitBreaker: {
|
|
1453
|
-
failureThresholdPercentage: 50, // Open circuit at 50% failure rate
|
|
1454
|
-
minimumRequests: 5, // Need at least 5 requests to calculate
|
|
1455
|
-
recoveryTimeoutMs: 30000, // Try recovery after 30 seconds
|
|
1456
|
-
trackIndividualAttempts: false // Track per-request success/failure
|
|
1457
|
-
}
|
|
1458
|
-
});
|
|
1459
|
-
|
|
1460
|
-
// Circuit breaker can be shared across workflows
|
|
1461
|
-
const breaker = new CircuitBreaker({
|
|
1462
|
-
failureThresholdPercentage: 50,
|
|
1463
|
-
minimumRequests: 10,
|
|
1464
|
-
recoveryTimeoutMs: 60000
|
|
1465
|
-
});
|
|
1466
|
-
|
|
1467
|
-
const result = await stableWorkflow(phases, {
|
|
1468
|
-
circuitBreaker: breaker,
|
|
1469
|
-
// ... other options
|
|
1470
|
-
});
|
|
1471
|
-
|
|
1472
|
-
// Check circuit breaker state
|
|
1473
|
-
const state = breaker.getState();
|
|
1474
|
-
console.log(`Circuit breaker state: ${state.state}`); // CLOSED, OPEN, or HALF_OPEN
|
|
1475
|
-
```
|
|
1476
|
-
|
|
1477
|
-
### Rate Limiting
|
|
1478
|
-
|
|
1479
|
-
Control request throughput to prevent overwhelming APIs:
|
|
1480
|
-
|
|
1481
|
-
```typescript
|
|
1482
|
-
import { stableApiGateway } from '@emmvish/stable-request';
|
|
1483
|
-
|
|
1484
|
-
const results = await stableApiGateway(requests, {
|
|
1485
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1486
|
-
concurrentExecution: true,
|
|
1487
|
-
rateLimit: {
|
|
1488
|
-
maxRequests: 10, // Maximum 10 requests
|
|
1489
|
-
windowMs: 1000 // Per 1 second window
|
|
1490
|
-
}
|
|
1491
|
-
});
|
|
1492
|
-
|
|
1493
|
-
// Rate limiting in workflows
|
|
1494
|
-
const result = await stableWorkflow(phases, {
|
|
1495
|
-
rateLimit: {
|
|
1496
|
-
maxRequests: 5,
|
|
1497
|
-
windowMs: 1000
|
|
1498
|
-
}
|
|
1499
|
-
});
|
|
1500
|
-
```
|
|
1501
|
-
|
|
1502
|
-
### Caching
|
|
1503
|
-
|
|
1504
|
-
Cache responses with TTL to reduce redundant API calls:
|
|
1505
|
-
|
|
1506
|
-
```typescript
|
|
1507
|
-
import { stableRequest, getGlobalCacheManager } from '@emmvish/stable-request';
|
|
1508
|
-
|
|
1509
|
-
// Enable caching for a request
|
|
1510
|
-
const data = await stableRequest({
|
|
1511
|
-
reqData: { hostname: 'api.example.com', path: '/users/123' },
|
|
1512
|
-
resReq: true,
|
|
1513
|
-
cache: {
|
|
1514
|
-
enabled: true,
|
|
1515
|
-
ttl: 60000 // Cache for 60 seconds
|
|
1516
|
-
}
|
|
1517
|
-
});
|
|
1518
|
-
|
|
1519
|
-
// Use global cache manager across requests
|
|
1520
|
-
const results = await stableApiGateway(requests, {
|
|
1521
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1522
|
-
commonCache: { enabled: true, ttl: 300000 } // 5 minutes
|
|
1523
|
-
});
|
|
1524
|
-
|
|
1525
|
-
// Manage cache manually
|
|
1526
|
-
const cacheManager = getGlobalCacheManager();
|
|
1527
|
-
const stats = cacheManager.getStats();
|
|
1528
|
-
console.log(`Cache size: ${stats.size}, Valid entries: ${stats.validEntries}`);
|
|
1529
|
-
cacheManager.clear(); // Clear all cache
|
|
1530
|
-
```
|
|
1531
|
-
|
|
1532
|
-
### Pre-Execution Hooks
|
|
1533
|
-
|
|
1534
|
-
Transform requests dynamically before execution:
|
|
1535
|
-
|
|
1536
|
-
```typescript
|
|
1537
|
-
import { stableRequest } from '@emmvish/stable-request';
|
|
1538
|
-
|
|
1539
|
-
const commonBuffer: Record<string, any> = {};
|
|
1540
|
-
|
|
1541
|
-
const data = await stableRequest({
|
|
1542
|
-
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
1543
|
-
resReq: true,
|
|
1544
|
-
preExecution: {
|
|
1545
|
-
preExecutionHook: async ({ inputParams, commonBuffer }) => {
|
|
1546
|
-
// Fetch authentication token
|
|
1547
|
-
const token = await getAuthToken();
|
|
1548
|
-
|
|
1549
|
-
// Store in shared buffer
|
|
1550
|
-
commonBuffer.token = token;
|
|
1551
|
-
commonBuffer.timestamp = Date.now();
|
|
1552
|
-
|
|
1553
|
-
// Override request configuration
|
|
1554
|
-
return {
|
|
1555
|
-
reqData: {
|
|
1556
|
-
hostname: 'api.example.com',
|
|
1557
|
-
path: '/authenticated-data',
|
|
1558
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
1559
|
-
}
|
|
1560
|
-
};
|
|
1561
|
-
},
|
|
1562
|
-
preExecutionHookParams: { userId: 'user123' },
|
|
1563
|
-
applyPreExecutionConfigOverride: true, // Apply returned config
|
|
1564
|
-
continueOnPreExecutionHookFailure: false
|
|
1565
|
-
},
|
|
1566
|
-
commonBuffer
|
|
1567
|
-
});
|
|
1568
|
-
|
|
1569
|
-
console.log('Token used:', commonBuffer.token);
|
|
1570
|
-
```
|
|
1571
|
-
|
|
1572
|
-
### Shared Buffer
|
|
1573
|
-
|
|
1574
|
-
Share state across requests in gateways and workflows:
|
|
1575
|
-
|
|
1576
|
-
```typescript
|
|
1577
|
-
import { stableWorkflow } from '@emmvish/stable-request';
|
|
1578
|
-
|
|
1579
|
-
const sharedBuffer: Record<string, any> = { requestCount: 0 };
|
|
1580
|
-
|
|
1581
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1582
|
-
{
|
|
1583
|
-
id: 'phase-1',
|
|
1584
|
-
requests: [
|
|
1585
|
-
{
|
|
1586
|
-
id: 'req-1',
|
|
1587
|
-
requestOptions: {
|
|
1588
|
-
reqData: { path: '/step1' },
|
|
1589
|
-
resReq: true,
|
|
1590
|
-
preExecution: {
|
|
1591
|
-
preExecutionHook: ({ commonBuffer }) => {
|
|
1592
|
-
commonBuffer.requestCount++;
|
|
1593
|
-
commonBuffer.phase1Data = 'initialized';
|
|
1594
|
-
return {};
|
|
1595
|
-
},
|
|
1596
|
-
preExecutionHookParams: {},
|
|
1597
|
-
applyPreExecutionConfigOverride: false,
|
|
1598
|
-
continueOnPreExecutionHookFailure: false
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
]
|
|
1603
|
-
},
|
|
1604
|
-
{
|
|
1605
|
-
id: 'phase-2',
|
|
1606
|
-
requests: [
|
|
1607
|
-
{
|
|
1608
|
-
id: 'req-2',
|
|
1609
|
-
requestOptions: {
|
|
1610
|
-
reqData: { path: '/step2' },
|
|
1611
|
-
resReq: true,
|
|
1612
|
-
preExecution: {
|
|
1613
|
-
preExecutionHook: ({ commonBuffer }) => {
|
|
1614
|
-
commonBuffer.requestCount++;
|
|
1615
|
-
// Access data from phase-1
|
|
1616
|
-
console.log('Phase 1 data:', commonBuffer.phase1Data);
|
|
1617
|
-
return {};
|
|
1618
|
-
},
|
|
1619
|
-
preExecutionHookParams: {},
|
|
1620
|
-
applyPreExecutionConfigOverride: false,
|
|
1621
|
-
continueOnPreExecutionHookFailure: false
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
]
|
|
1626
|
-
}
|
|
1627
|
-
];
|
|
1628
|
-
|
|
1629
|
-
const result = await stableWorkflow(phases, {
|
|
1630
|
-
workflowId: 'stateful-workflow',
|
|
1631
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1632
|
-
sharedBuffer
|
|
1633
|
-
});
|
|
1634
|
-
|
|
1635
|
-
console.log('Total requests processed:', sharedBuffer.requestCount);
|
|
1636
|
-
```
|
|
1637
|
-
|
|
1638
|
-
### Request Grouping
|
|
1639
|
-
|
|
1640
|
-
Apply different configurations to request groups:
|
|
1641
|
-
|
|
1642
|
-
```typescript
|
|
1643
|
-
import { stableApiGateway } from '@emmvish/stable-request';
|
|
1644
|
-
|
|
1645
|
-
const requests = [
|
|
1646
|
-
{
|
|
1647
|
-
id: 'critical-1',
|
|
1648
|
-
groupId: 'critical',
|
|
1649
|
-
requestOptions: { reqData: { path: '/critical/1' }, resReq: true }
|
|
1650
|
-
},
|
|
1651
|
-
{
|
|
1652
|
-
id: 'critical-2',
|
|
1653
|
-
groupId: 'critical',
|
|
1654
|
-
requestOptions: { reqData: { path: '/critical/2' }, resReq: true }
|
|
1655
|
-
},
|
|
1656
|
-
{
|
|
1657
|
-
id: 'optional-1',
|
|
1658
|
-
groupId: 'optional',
|
|
1659
|
-
requestOptions: { reqData: { path: '/optional/1' }, resReq: true }
|
|
1660
|
-
}
|
|
1661
|
-
];
|
|
1662
|
-
|
|
1663
|
-
const results = await stableApiGateway(requests, {
|
|
1664
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1665
|
-
commonAttempts: 1,
|
|
1666
|
-
commonWait: 100,
|
|
1667
|
-
requestGroups: [
|
|
1668
|
-
{
|
|
1669
|
-
id: 'critical',
|
|
1670
|
-
commonConfig: {
|
|
1671
|
-
commonAttempts: 5, // More retries for critical requests
|
|
1672
|
-
commonWait: 2000,
|
|
1673
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1674
|
-
}
|
|
1675
|
-
},
|
|
1676
|
-
{
|
|
1677
|
-
id: 'optional',
|
|
1678
|
-
commonConfig: {
|
|
1679
|
-
commonAttempts: 1, // No retries for optional requests
|
|
1680
|
-
commonFinalErrorAnalyzer: async () => true // Suppress errors
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
]
|
|
1684
|
-
});
|
|
1685
|
-
```
|
|
1686
|
-
|
|
1687
|
-
### Concurrency Control
|
|
1688
|
-
|
|
1689
|
-
Limit concurrent request execution:
|
|
1690
|
-
|
|
1691
|
-
```typescript
|
|
1692
|
-
import { stableApiGateway } from '@emmvish/stable-request';
|
|
1693
|
-
|
|
1694
|
-
// Limit to 5 concurrent requests
|
|
1695
|
-
const results = await stableApiGateway(requests, {
|
|
1696
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1697
|
-
concurrentExecution: true,
|
|
1698
|
-
maxConcurrentRequests: 5
|
|
1699
|
-
});
|
|
1700
|
-
|
|
1701
|
-
// Phase-level concurrency control
|
|
1702
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1703
|
-
{
|
|
1704
|
-
id: 'limited-phase',
|
|
1705
|
-
concurrentExecution: true,
|
|
1706
|
-
maxConcurrentRequests: 3,
|
|
1707
|
-
requests: [/* ... */]
|
|
1708
|
-
}
|
|
1709
|
-
];
|
|
1710
|
-
```
|
|
1711
|
-
|
|
1712
|
-
### Response Analysis
|
|
1713
|
-
|
|
1714
|
-
Validate response content and trigger retries:
|
|
1715
|
-
|
|
1716
|
-
```typescript
|
|
1717
|
-
import { stableRequest } from '@emmvish/stable-request';
|
|
1718
|
-
|
|
1719
|
-
const data = await stableRequest({
|
|
1720
|
-
reqData: { hostname: 'api.example.com', path: '/job/status' },
|
|
1721
|
-
resReq: true,
|
|
1722
|
-
attempts: 10,
|
|
1723
|
-
wait: 2000,
|
|
1724
|
-
responseAnalyzer: async ({ data, reqData, params }) => {
|
|
1725
|
-
// Retry until job is completed
|
|
1726
|
-
if (data.status === 'processing') {
|
|
1727
|
-
console.log('Job still processing, will retry...');
|
|
1728
|
-
return false; // Trigger retry
|
|
1729
|
-
}
|
|
1730
|
-
return data.status === 'completed';
|
|
1731
|
-
}
|
|
1732
|
-
});
|
|
1733
|
-
|
|
1734
|
-
console.log('Job completed:', data);
|
|
1735
|
-
```
|
|
1736
|
-
|
|
1737
|
-
### Error Handling
|
|
1738
|
-
|
|
1739
|
-
Comprehensive error handling with observability hooks:
|
|
1740
|
-
|
|
1741
|
-
```typescript
|
|
1742
|
-
import { stableRequest } from '@emmvish/stable-request';
|
|
1743
|
-
|
|
1744
|
-
const data = await stableRequest({
|
|
1745
|
-
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
1746
|
-
resReq: true,
|
|
1747
|
-
attempts: 3,
|
|
1748
|
-
wait: 1000,
|
|
1749
|
-
logAllErrors: true,
|
|
1750
|
-
handleErrors: ({ reqData, errorLog, params }) => {
|
|
1751
|
-
// Custom error logging
|
|
1752
|
-
console.error('Request failed:', {
|
|
1753
|
-
url: reqData.url,
|
|
1754
|
-
attempt: errorLog.attempt,
|
|
1755
|
-
statusCode: errorLog.statusCode,
|
|
1756
|
-
error: errorLog.error,
|
|
1757
|
-
isRetryable: errorLog.isRetryable
|
|
1758
|
-
});
|
|
1759
|
-
|
|
1760
|
-
// Send to monitoring service
|
|
1761
|
-
monitoringService.trackError(errorLog);
|
|
1762
|
-
},
|
|
1763
|
-
logAllSuccessfulAttempts: true,
|
|
1764
|
-
handleSuccessfulAttemptData: ({ successfulAttemptData }) => {
|
|
1765
|
-
console.log('Request succeeded on attempt:', successfulAttemptData.attempt);
|
|
1766
|
-
},
|
|
1767
|
-
finalErrorAnalyzer: async ({ error, reqData }) => {
|
|
1768
|
-
// Gracefully handle specific errors
|
|
1769
|
-
if (error.response?.status === 404) {
|
|
1770
|
-
console.warn('Resource not found, continuing...');
|
|
1771
|
-
return true; // Return false to suppress error
|
|
1772
|
-
}
|
|
1773
|
-
return false; // Throw error
|
|
1774
|
-
}
|
|
1775
|
-
});
|
|
1776
|
-
```
|
|
1777
|
-
|
|
1778
|
-
## Advanced Use Cases
|
|
1779
|
-
|
|
1780
|
-
### Use Case 1: Multi-Tenant API with Dynamic Authentication
|
|
1781
|
-
|
|
1782
|
-
```typescript
|
|
1783
|
-
import { stableWorkflow, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
1784
|
-
|
|
1785
|
-
interface TenantConfig {
|
|
1786
|
-
tenantId: string;
|
|
1787
|
-
apiKey: string;
|
|
1788
|
-
baseUrl: string;
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
async function executeTenantWorkflow(tenantConfig: TenantConfig) {
|
|
1792
|
-
const sharedBuffer: Record<string, any> = {
|
|
1793
|
-
tenantId: tenantConfig.tenantId,
|
|
1794
|
-
authToken: null,
|
|
1795
|
-
processedItems: []
|
|
1796
|
-
};
|
|
1797
|
-
|
|
1798
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1799
|
-
{
|
|
1800
|
-
id: 'authentication',
|
|
1801
|
-
requests: [
|
|
1802
|
-
{
|
|
1803
|
-
id: 'get-token',
|
|
1804
|
-
requestOptions: {
|
|
1805
|
-
reqData: {
|
|
1806
|
-
path: '/auth/token',
|
|
1807
|
-
method: 'POST',
|
|
1808
|
-
headers: { 'X-API-Key': tenantConfig.apiKey }
|
|
1809
|
-
},
|
|
1810
|
-
resReq: true,
|
|
1811
|
-
attempts: 3,
|
|
1812
|
-
wait: 2000,
|
|
1813
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
1814
|
-
responseAnalyzer: async ({ data, commonBuffer }) => {
|
|
1815
|
-
if (data?.token) {
|
|
1816
|
-
commonBuffer.authToken = data.token;
|
|
1817
|
-
commonBuffer.tokenExpiry = Date.now() + (data.expiresIn * 1000);
|
|
1818
|
-
return true;
|
|
1819
|
-
}
|
|
1820
|
-
return false;
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
]
|
|
1825
|
-
},
|
|
1826
|
-
{
|
|
1827
|
-
id: 'data-fetching',
|
|
1828
|
-
concurrentExecution: true,
|
|
1829
|
-
maxConcurrentRequests: 5,
|
|
1830
|
-
requests: [
|
|
1831
|
-
{
|
|
1832
|
-
id: 'fetch-users',
|
|
1833
|
-
requestOptions: {
|
|
1834
|
-
reqData: { path: '/users' },
|
|
1835
|
-
resReq: true,
|
|
1836
|
-
preExecution: {
|
|
1837
|
-
preExecutionHook: ({ commonBuffer }) => ({
|
|
1838
|
-
reqData: {
|
|
1839
|
-
path: '/users',
|
|
1840
|
-
headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
|
|
1841
|
-
}
|
|
1842
|
-
}),
|
|
1843
|
-
applyPreExecutionConfigOverride: true
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
},
|
|
1847
|
-
{
|
|
1848
|
-
id: 'fetch-settings',
|
|
1849
|
-
requestOptions: {
|
|
1850
|
-
reqData: { path: '/settings' },
|
|
1851
|
-
resReq: true,
|
|
1852
|
-
preExecution: {
|
|
1853
|
-
preExecutionHook: ({ commonBuffer }) => ({
|
|
1854
|
-
reqData: {
|
|
1855
|
-
path: '/settings',
|
|
1856
|
-
headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
|
|
1857
|
-
}
|
|
1858
|
-
}),
|
|
1859
|
-
applyPreExecutionConfigOverride: true
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
]
|
|
1864
|
-
},
|
|
1865
|
-
{
|
|
1866
|
-
id: 'data-processing',
|
|
1867
|
-
concurrentExecution: true,
|
|
1868
|
-
requests: [
|
|
1869
|
-
{
|
|
1870
|
-
id: 'process-users',
|
|
1871
|
-
requestOptions: {
|
|
1872
|
-
reqData: { path: '/process/users', method: 'POST' },
|
|
1873
|
-
resReq: true,
|
|
1874
|
-
preExecution: {
|
|
1875
|
-
preExecutionHook: ({ commonBuffer }) => {
|
|
1876
|
-
const usersPhase = commonBuffer.phases?.find(p => p.phaseId === 'data-fetching');
|
|
1877
|
-
const usersData = usersPhase?.responses?.find(r => r.requestId === 'fetch-users')?.data;
|
|
1878
|
-
|
|
1879
|
-
return {
|
|
1880
|
-
reqData: {
|
|
1881
|
-
path: '/process/users',
|
|
1882
|
-
method: 'POST',
|
|
1883
|
-
headers: { Authorization: `Bearer ${commonBuffer.authToken}` },
|
|
1884
|
-
body: { users: usersData }
|
|
1885
|
-
}
|
|
1886
|
-
};
|
|
1887
|
-
},
|
|
1888
|
-
applyPreExecutionConfigOverride: true
|
|
1889
|
-
},
|
|
1890
|
-
responseAnalyzer: async ({ data, commonBuffer }) => {
|
|
1891
|
-
if (data?.processed) {
|
|
1892
|
-
commonBuffer.processedItems.push(...data.processed);
|
|
1893
|
-
return true;
|
|
1894
|
-
}
|
|
1895
|
-
return false;
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
]
|
|
1900
|
-
}
|
|
1901
|
-
];
|
|
1902
|
-
|
|
1903
|
-
const result = await stableWorkflow(phases, {
|
|
1904
|
-
workflowId: `tenant-${tenantConfig.tenantId}-workflow`,
|
|
1905
|
-
commonRequestData: {
|
|
1906
|
-
hostname: tenantConfig.baseUrl,
|
|
1907
|
-
headers: { 'X-Tenant-ID': tenantConfig.tenantId }
|
|
1908
|
-
},
|
|
1909
|
-
stopOnFirstPhaseError: true,
|
|
1910
|
-
logPhaseResults: true,
|
|
1911
|
-
sharedBuffer,
|
|
1912
|
-
circuitBreaker: {
|
|
1913
|
-
failureThresholdPercentage: 40,
|
|
1914
|
-
minimumRequests: 5,
|
|
1915
|
-
recoveryTimeoutMs: 30000
|
|
1916
|
-
},
|
|
1917
|
-
rateLimit: {
|
|
1918
|
-
maxRequests: 20,
|
|
1919
|
-
windowMs: 1000
|
|
1920
|
-
},
|
|
1921
|
-
commonCache: {
|
|
1922
|
-
enabled: true,
|
|
1923
|
-
ttl: 300000 // Cache for 5 minutes
|
|
1924
|
-
},
|
|
1925
|
-
handlePhaseCompletion: ({ workflowId, phaseResult }) => {
|
|
1926
|
-
console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
|
|
1927
|
-
success: phaseResult.success,
|
|
1928
|
-
successfulRequests: phaseResult.successfulRequests,
|
|
1929
|
-
executionTime: `${phaseResult.executionTime}ms`
|
|
1930
|
-
});
|
|
1931
|
-
},
|
|
1932
|
-
handlePhaseError: ({ workflowId, error, phaseResult }) => {
|
|
1933
|
-
console.error(`[${workflowId}] Phase ${phaseResult.phaseId} failed:`, error);
|
|
1934
|
-
// Send to monitoring
|
|
1935
|
-
monitoringService.trackPhaseError(workflowId, phaseResult.phaseId, error);
|
|
1936
|
-
}
|
|
1937
|
-
});
|
|
1938
|
-
|
|
1939
|
-
return {
|
|
1940
|
-
success: result.success,
|
|
1941
|
-
tenantId: tenantConfig.tenantId,
|
|
1942
|
-
processedItems: sharedBuffer.processedItems,
|
|
1943
|
-
executionTime: result.executionTime,
|
|
1944
|
-
phases: result.phases.map(p => ({
|
|
1945
|
-
id: p.phaseId,
|
|
1946
|
-
success: p.success,
|
|
1947
|
-
requestCount: p.totalRequests
|
|
1948
|
-
}))
|
|
1949
|
-
};
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
// Execute workflows for multiple tenants
|
|
1953
|
-
const tenants: TenantConfig[] = [
|
|
1954
|
-
{ tenantId: 'tenant-1', apiKey: 'key1', baseUrl: 'api.tenant1.com' },
|
|
1955
|
-
{ tenantId: 'tenant-2', apiKey: 'key2', baseUrl: 'api.tenant2.com' }
|
|
1956
|
-
];
|
|
1957
|
-
|
|
1958
|
-
const results = await Promise.all(tenants.map(executeTenantWorkflow));
|
|
1959
|
-
results.forEach(result => {
|
|
1960
|
-
console.log(`Tenant ${result.tenantId}:`, result.success ? 'Success' : 'Failed');
|
|
1961
|
-
});
|
|
1962
|
-
```
|
|
1963
|
-
|
|
1964
|
-
### Use Case 2: Resilient Data Pipeline with Fallback Strategies
|
|
1965
|
-
|
|
1966
|
-
```typescript
|
|
1967
|
-
import { stableApiGateway, RETRY_STRATEGIES, CircuitBreaker } from '@emmvish/stable-request';
|
|
1968
|
-
|
|
1969
|
-
interface DataSource {
|
|
1970
|
-
id: string;
|
|
1971
|
-
priority: number;
|
|
1972
|
-
endpoint: string;
|
|
1973
|
-
hostname: string;
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
async function fetchDataWithFallback(dataSources: DataSource[]) {
|
|
1977
|
-
// Sort by priority
|
|
1978
|
-
const sortedSources = [...dataSources].sort((a, b) => a.priority - b.priority);
|
|
1979
|
-
|
|
1980
|
-
// Create circuit breakers for each source
|
|
1981
|
-
const circuitBreakers = new Map(
|
|
1982
|
-
sortedSources.map(source => [
|
|
1983
|
-
source.id,
|
|
1984
|
-
new CircuitBreaker({
|
|
1985
|
-
failureThresholdPercentage: 50,
|
|
1986
|
-
minimumRequests: 3,
|
|
1987
|
-
recoveryTimeoutMs: 60000
|
|
1988
|
-
})
|
|
1989
|
-
])
|
|
1990
|
-
);
|
|
1991
|
-
|
|
1992
|
-
// Try each data source in priority order
|
|
1993
|
-
for (const source of sortedSources) {
|
|
1994
|
-
const breaker = circuitBreakers.get(source.id)!;
|
|
1995
|
-
const breakerState = breaker.getState();
|
|
1996
|
-
|
|
1997
|
-
// Skip if circuit is open
|
|
1998
|
-
if (breakerState.state === 'OPEN') {
|
|
1999
|
-
console.warn(`Circuit breaker open for ${source.id}, skipping...`);
|
|
2000
|
-
continue;
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
console.log(`Attempting to fetch from ${source.id}...`);
|
|
2004
|
-
|
|
2005
|
-
try {
|
|
2006
|
-
const requests = [
|
|
2007
|
-
{
|
|
2008
|
-
id: 'users',
|
|
2009
|
-
requestOptions: {
|
|
2010
|
-
reqData: { path: `${source.endpoint}/users` },
|
|
2011
|
-
resReq: true,
|
|
2012
|
-
attempts: 3,
|
|
2013
|
-
wait: 1000,
|
|
2014
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
2015
|
-
}
|
|
2016
|
-
},
|
|
2017
|
-
{
|
|
2018
|
-
id: 'products',
|
|
2019
|
-
requestOptions: {
|
|
2020
|
-
reqData: { path: `${source.endpoint}/products` },
|
|
2021
|
-
resReq: true,
|
|
2022
|
-
attempts: 3,
|
|
2023
|
-
wait: 1000,
|
|
2024
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
2025
|
-
}
|
|
2026
|
-
},
|
|
2027
|
-
{
|
|
2028
|
-
id: 'orders',
|
|
2029
|
-
requestOptions: {
|
|
2030
|
-
reqData: { path: `${source.endpoint}/orders` },
|
|
2031
|
-
resReq: true,
|
|
2032
|
-
attempts: 3,
|
|
2033
|
-
wait: 1000,
|
|
2034
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
];
|
|
2038
|
-
|
|
2039
|
-
const results = await stableApiGateway(requests, {
|
|
2040
|
-
commonRequestData: {
|
|
2041
|
-
hostname: source.hostname,
|
|
2042
|
-
headers: { 'X-Source-ID': source.id }
|
|
2043
|
-
},
|
|
2044
|
-
concurrentExecution: true,
|
|
2045
|
-
maxConcurrentRequests: 10,
|
|
2046
|
-
circuitBreaker: breaker,
|
|
2047
|
-
rateLimit: {
|
|
2048
|
-
maxRequests: 50,
|
|
2049
|
-
windowMs: 1000
|
|
2050
|
-
},
|
|
2051
|
-
commonCache: {
|
|
2052
|
-
enabled: true,
|
|
2053
|
-
ttl: 60000
|
|
2054
|
-
},
|
|
2055
|
-
commonResponseAnalyzer: async ({ data }) => {
|
|
2056
|
-
// Validate data structure
|
|
2057
|
-
return data && typeof data === 'object' && !data.error;
|
|
2058
|
-
},
|
|
2059
|
-
commonHandleErrors: ({ errorLog }) => {
|
|
2060
|
-
console.error(`Error from ${source.id}:`, errorLog);
|
|
2061
|
-
}
|
|
2062
|
-
});
|
|
2063
|
-
|
|
2064
|
-
// Check if all requests succeeded
|
|
2065
|
-
const allSuccessful = results.every(r => r.success);
|
|
2066
|
-
|
|
2067
|
-
if (allSuccessful) {
|
|
2068
|
-
console.log(`Successfully fetched data from ${source.id}`);
|
|
2069
|
-
return {
|
|
2070
|
-
source: source.id,
|
|
2071
|
-
data: {
|
|
2072
|
-
users: results.find(r => r.requestId === 'users')?.data,
|
|
2073
|
-
products: results.find(r => r.requestId === 'products')?.data,
|
|
2074
|
-
orders: results.find(r => r.requestId === 'orders')?.data
|
|
2075
|
-
}
|
|
2076
|
-
};
|
|
2077
|
-
} else {
|
|
2078
|
-
console.warn(`Partial failure from ${source.id}, trying next source...`);
|
|
2079
|
-
}
|
|
2080
|
-
} catch (error) {
|
|
2081
|
-
console.error(`Failed to fetch from ${source.id}:`, error);
|
|
2082
|
-
// Continue to next source
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
throw new Error('All data sources failed');
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
// Usage
|
|
2090
|
-
const dataSources: DataSource[] = [
|
|
2091
|
-
{
|
|
2092
|
-
id: 'primary-db',
|
|
2093
|
-
priority: 1,
|
|
2094
|
-
endpoint: '/api/v1',
|
|
2095
|
-
hostname: 'primary.example.com'
|
|
2096
|
-
},
|
|
2097
|
-
{
|
|
2098
|
-
id: 'replica-db',
|
|
2099
|
-
priority: 2,
|
|
2100
|
-
endpoint: '/api/v1',
|
|
2101
|
-
hostname: 'replica.example.com'
|
|
2102
|
-
},
|
|
2103
|
-
{
|
|
2104
|
-
id: 'backup-cache',
|
|
2105
|
-
priority: 3,
|
|
2106
|
-
endpoint: '/cached',
|
|
2107
|
-
hostname: 'cache.example.com'
|
|
2108
|
-
}
|
|
2109
|
-
];
|
|
2110
|
-
|
|
2111
|
-
const result = await fetchDataWithFallback(dataSources);
|
|
2112
|
-
console.log('Data fetched from:', result.source);
|
|
2113
|
-
console.log('Users:', result.data.users?.length);
|
|
2114
|
-
console.log('Products:', result.data.products?.length);
|
|
2115
|
-
console.log('Orders:', result.data.orders?.length);
|
|
2116
|
-
```
|
|
376
|
+
Full TypeScript support with 40+ exported types for complete type safety across workflows, requests, configurations, and hooks.
|
|
2117
377
|
|
|
2118
378
|
## License
|
|
2119
379
|
|
|
2120
380
|
MIT © Manish Varma
|
|
2121
|
-
|
|
2122
|
-
[](https://opensource.org/licenses/MIT)
|
|
2123
|
-
|
|
2124
|
-
---
|
|
2125
|
-
|
|
2126
|
-
**Made with ❤️ for developers integrating with unreliable APIs**
|