@emmvish/stable-request 1.6.1 → 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +935 -1891
- 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,57 @@
|
|
|
1
|
-
# stable-request
|
|
1
|
+
# @emmvish/stable-request
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A production-grade HTTP Workflow Execution Engine for Node.js that transforms unreliable API calls into resilient, observable, and sophisticated multi-phase workflows with intelligent retry strategies, circuit breakers, and advanced execution patterns.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Navigation
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
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)
|
|
8
|
+
- [Why stable-request?](#why-stable-request)
|
|
42
9
|
- [Installation](#installation)
|
|
43
|
-
- [Core Features](#core-features)
|
|
44
10
|
- [Quick Start](#quick-start)
|
|
45
|
-
- [
|
|
11
|
+
- [Single Request with Retry](#single-request-with-retry)
|
|
12
|
+
- [Batch Requests (API Gateway)](#batch-requests-api-gateway)
|
|
13
|
+
- [Multi-Phase Workflow](#multi-phase-workflow)
|
|
14
|
+
- [Core Features](#core-features)
|
|
15
|
+
- [Intelligent Retry Strategies](#intelligent-retry-strategies)
|
|
16
|
+
- [Circuit Breaker Pattern](#circuit-breaker-pattern)
|
|
17
|
+
- [Response Caching](#response-caching)
|
|
18
|
+
- [Rate Limiting and Concurrency Control](#rate-limiting-and-concurrency-control)
|
|
19
|
+
- [Workflow Execution Patterns](#workflow-execution-patterns)
|
|
20
|
+
- [Sequential and Concurrent Phases](#sequential-and-concurrent-phases)
|
|
21
|
+
- [Mixed Execution Mode](#mixed-execution-mode)
|
|
46
22
|
- [Non-Linear Workflows](#non-linear-workflows)
|
|
47
23
|
- [Branched Workflows](#branched-workflows)
|
|
48
|
-
|
|
49
|
-
- [
|
|
50
|
-
- [Rate Limiting](#rate-limiting)
|
|
51
|
-
- [Caching](#caching)
|
|
52
|
-
- [Pre-Execution Hooks](#pre-execution-hooks)
|
|
53
|
-
- [Shared Buffer](#shared-buffer)
|
|
24
|
+
- [Advanced Capabilities](#advanced-capabilities)
|
|
25
|
+
- [Config Cascading](#config-cascading)
|
|
54
26
|
- [Request Grouping](#request-grouping)
|
|
55
|
-
- [
|
|
56
|
-
- [
|
|
57
|
-
- [
|
|
58
|
-
- [
|
|
27
|
+
- [Shared Buffer and Pre-Execution Hooks](#shared-buffer-and-pre-execution-hooks)
|
|
28
|
+
- [Comprehensive Observability](#comprehensive-observability)
|
|
29
|
+
- [Trial Mode](#trial-mode)
|
|
30
|
+
- [Common Use Cases](#common-use-cases)
|
|
59
31
|
- [License](#license)
|
|
60
|
-
<!-- TOC END -->
|
|
61
32
|
|
|
62
|
-
|
|
33
|
+
## Overview
|
|
34
|
+
|
|
35
|
+
`@emmvish/stable-request` is engineered for applications requiring robust orchestration of complex, multi-step API interactions with enterprise-grade reliability, observability, and fault tolerance. It goes far beyond simple HTTP clients by providing:
|
|
36
|
+
|
|
37
|
+
- **Workflow-First Architecture**: Organize API calls into phases, branches, and decision trees with full control over execution order
|
|
38
|
+
- **Enterprise Resilience**: Built-in circuit breakers, configurable retry strategies, and sophisticated failure handling
|
|
39
|
+
- **Execution Flexibility**: Sequential, concurrent, mixed, and non-linear execution patterns to match your business logic
|
|
40
|
+
- **Production-Ready Observability**: Comprehensive hooks for monitoring, logging, error analysis, and execution history tracking
|
|
41
|
+
- **Performance Optimization**: Response caching, rate limiting, and concurrency control to maximize efficiency
|
|
42
|
+
- **Type Safety**: Full TypeScript support with 40+ exported types
|
|
43
|
+
|
|
44
|
+
## Why stable-request?
|
|
45
|
+
|
|
46
|
+
Modern applications often need to:
|
|
47
|
+
- **Orchestrate complex API workflows** with dependencies between steps
|
|
48
|
+
- **Handle unreliable APIs** with intelligent retry and fallback mechanisms
|
|
49
|
+
- **Prevent cascade failures** when downstream services fail
|
|
50
|
+
- **Optimize performance** by caching responses and controlling request rates
|
|
51
|
+
- **Monitor and debug** complex request flows in production
|
|
52
|
+
- **Implement conditional logic** based on API responses (branching, looping)
|
|
53
|
+
|
|
54
|
+
`@emmvish/stable-request` solves all these challenges with a unified, type-safe API that scales from simple requests to sophisticated multi-phase workflows.
|
|
63
55
|
|
|
64
56
|
## Installation
|
|
65
57
|
|
|
@@ -67,1581 +59,673 @@ Start small and scale.
|
|
|
67
59
|
npm install @emmvish/stable-request
|
|
68
60
|
```
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
**Requirements**: Node.js 14+ (ES Modules)
|
|
71
63
|
|
|
72
|
-
|
|
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
|
|
64
|
+
**Dependencies**: Built on [Axios](https://axios-http.com/) for HTTP requests
|
|
86
65
|
|
|
87
66
|
## Quick Start
|
|
88
67
|
|
|
89
|
-
###
|
|
68
|
+
### Single Request with Retry
|
|
69
|
+
|
|
70
|
+
Execute a single HTTP request with automatic retry on failure:
|
|
90
71
|
|
|
91
72
|
```typescript
|
|
92
|
-
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
73
|
+
import { stableRequest, RETRY_STRATEGIES, REQUEST_METHODS } from '@emmvish/stable-request';
|
|
93
74
|
|
|
94
|
-
const
|
|
75
|
+
const userData = await stableRequest({
|
|
95
76
|
reqData: {
|
|
96
77
|
hostname: 'api.example.com',
|
|
97
78
|
path: '/users/123',
|
|
98
|
-
method:
|
|
79
|
+
method: REQUEST_METHODS.GET,
|
|
80
|
+
headers: { 'Authorization': 'Bearer token' }
|
|
99
81
|
},
|
|
100
|
-
resReq: true,
|
|
101
|
-
attempts: 3,
|
|
102
|
-
wait: 1000,
|
|
103
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
82
|
+
resReq: true, // Return response data
|
|
83
|
+
attempts: 3, // Retry up to 3 times
|
|
84
|
+
wait: 1000, // 1 second between retries
|
|
85
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
86
|
+
logAllErrors: true // Log all failed attempts
|
|
104
87
|
});
|
|
105
88
|
|
|
106
|
-
console.log(
|
|
89
|
+
console.log(userData); // { id: 123, name: 'John' }
|
|
107
90
|
```
|
|
108
91
|
|
|
109
|
-
### Batch Requests
|
|
92
|
+
### Batch Requests (API Gateway)
|
|
93
|
+
|
|
94
|
+
Execute multiple requests concurrently or sequentially:
|
|
110
95
|
|
|
111
96
|
```typescript
|
|
112
97
|
import { stableApiGateway } from '@emmvish/stable-request';
|
|
113
98
|
|
|
114
99
|
const requests = [
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
100
|
+
{
|
|
101
|
+
id: 'users',
|
|
102
|
+
requestOptions: {
|
|
103
|
+
reqData: { path: '/users' },
|
|
104
|
+
resReq: true
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'orders',
|
|
109
|
+
requestOptions: {
|
|
110
|
+
reqData: { path: '/orders' },
|
|
111
|
+
resReq: true
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'products',
|
|
116
|
+
requestOptions: {
|
|
117
|
+
reqData: { path: '/products' },
|
|
118
|
+
resReq: true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
118
121
|
];
|
|
119
122
|
|
|
120
123
|
const results = await stableApiGateway(requests, {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
concurrentExecution: true, // Execute in parallel
|
|
125
|
+
commonRequestData: {
|
|
126
|
+
hostname: 'api.example.com',
|
|
127
|
+
headers: { 'X-API-Key': 'secret' }
|
|
128
|
+
},
|
|
129
|
+
commonAttempts: 2, // Retry each request twice
|
|
130
|
+
commonWait: 500
|
|
124
131
|
});
|
|
125
132
|
|
|
126
133
|
results.forEach(result => {
|
|
127
|
-
|
|
128
|
-
console.log(`Request ${result.requestId}:`, result.data);
|
|
129
|
-
} else {
|
|
130
|
-
console.error(`Request ${result.requestId} failed:`, result.error);
|
|
131
|
-
}
|
|
134
|
+
console.log(`${result.id}:`, result.data);
|
|
132
135
|
});
|
|
133
136
|
```
|
|
134
137
|
|
|
135
138
|
### Multi-Phase Workflow
|
|
136
139
|
|
|
140
|
+
Orchestrate complex workflows with multiple phases:
|
|
141
|
+
|
|
137
142
|
```typescript
|
|
138
|
-
import { stableWorkflow,
|
|
143
|
+
import { stableWorkflow, PHASE_DECISION_ACTIONS, REQUEST_METHODS } from '@emmvish/stable-request';
|
|
139
144
|
|
|
140
|
-
const phases
|
|
145
|
+
const phases = [
|
|
141
146
|
{
|
|
142
147
|
id: 'authentication',
|
|
143
148
|
requests: [
|
|
144
|
-
{
|
|
149
|
+
{
|
|
150
|
+
id: 'login',
|
|
151
|
+
requestOptions: {
|
|
152
|
+
reqData: {
|
|
153
|
+
path: '/auth/login',
|
|
154
|
+
method: REQUEST_METHODS.POST,
|
|
155
|
+
body: { username: 'user', password: 'pass' }
|
|
156
|
+
},
|
|
157
|
+
resReq: true
|
|
158
|
+
}
|
|
159
|
+
}
|
|
145
160
|
]
|
|
146
161
|
},
|
|
147
162
|
{
|
|
148
|
-
id: 'data
|
|
149
|
-
concurrentExecution: true,
|
|
163
|
+
id: 'fetch-data',
|
|
164
|
+
concurrentExecution: true, // Execute requests in parallel
|
|
150
165
|
requests: [
|
|
151
|
-
{ id: '
|
|
152
|
-
{ id: '
|
|
166
|
+
{ id: 'user-profile', requestOptions: { reqData: { path: '/profile' }, resReq: true } },
|
|
167
|
+
{ id: 'user-orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } },
|
|
168
|
+
{ id: 'user-settings', requestOptions: { reqData: { path: '/settings' }, resReq: true } }
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'process-data',
|
|
173
|
+
requests: [
|
|
174
|
+
{
|
|
175
|
+
id: 'update-analytics',
|
|
176
|
+
requestOptions: {
|
|
177
|
+
reqData: { path: '/analytics', method: REQUEST_METHODS.POST },
|
|
178
|
+
resReq: false
|
|
179
|
+
}
|
|
180
|
+
}
|
|
153
181
|
]
|
|
154
182
|
}
|
|
155
183
|
];
|
|
156
184
|
|
|
157
185
|
const result = await stableWorkflow(phases, {
|
|
158
|
-
workflowId: 'data-
|
|
186
|
+
workflowId: 'user-data-sync',
|
|
159
187
|
commonRequestData: { hostname: 'api.example.com' },
|
|
160
|
-
|
|
161
|
-
|
|
188
|
+
commonAttempts: 3,
|
|
189
|
+
stopOnFirstPhaseError: true, // Stop if any phase fails
|
|
190
|
+
logPhaseResults: true // Log each phase completion
|
|
162
191
|
});
|
|
163
192
|
|
|
164
|
-
console.log(`Workflow completed: ${result.
|
|
193
|
+
console.log(`Workflow completed: ${result.success}`);
|
|
194
|
+
console.log(`Total requests: ${result.totalRequests}`);
|
|
195
|
+
console.log(`Successful: ${result.successfulRequests}`);
|
|
196
|
+
console.log(`Failed: ${result.failedRequests}`);
|
|
197
|
+
console.log(`Execution time: ${result.executionTime}ms`);
|
|
165
198
|
```
|
|
166
199
|
|
|
167
|
-
|
|
200
|
+
## Core Features
|
|
201
|
+
|
|
202
|
+
### Intelligent Retry Strategies
|
|
203
|
+
|
|
204
|
+
Automatically retry failed requests with sophisticated backoff strategies:
|
|
168
205
|
|
|
169
206
|
```typescript
|
|
170
|
-
import {
|
|
207
|
+
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
171
208
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
209
|
+
// Fixed delay: constant wait time
|
|
210
|
+
await stableRequest({
|
|
211
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
212
|
+
attempts: 5,
|
|
213
|
+
wait: 1000, // 1 second between each retry
|
|
214
|
+
retryStrategy: RETRY_STRATEGIES.FIXED
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Linear backoff: incrementally increasing delays
|
|
218
|
+
await stableRequest({
|
|
219
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
220
|
+
attempts: 5,
|
|
221
|
+
wait: 1000, // 1s, 2s, 3s, 4s, 5s
|
|
222
|
+
retryStrategy: RETRY_STRATEGIES.LINEAR
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Exponential backoff: exponentially growing delays
|
|
226
|
+
await stableRequest({
|
|
227
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
228
|
+
attempts: 5,
|
|
229
|
+
wait: 1000, // 1s, 2s, 4s, 8s, 16s
|
|
230
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Features**:
|
|
235
|
+
- Automatic retry on 5xx errors and network failures
|
|
236
|
+
- No retry on 4xx client errors (configurable)
|
|
237
|
+
- Maximum allowed wait time to prevent excessive delays
|
|
238
|
+
- Per-request or workflow-level configuration
|
|
239
|
+
|
|
240
|
+
**Custom Response Validation**:
|
|
241
|
+
```typescript
|
|
242
|
+
await stableRequest({
|
|
243
|
+
reqData: { hostname: 'api.example.com', path: '/job/status' },
|
|
244
|
+
resReq: true,
|
|
245
|
+
attempts: 10,
|
|
246
|
+
wait: 2000,
|
|
247
|
+
responseAnalyzer: async ({ data }) => {
|
|
248
|
+
// Retry until job is complete
|
|
249
|
+
return data.status === 'completed';
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Circuit Breaker Pattern
|
|
255
|
+
|
|
256
|
+
Prevent cascade failures and system overload with built-in circuit breakers:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { stableRequest, CircuitBreakerState } from '@emmvish/stable-request';
|
|
260
|
+
|
|
261
|
+
await stableRequest({
|
|
262
|
+
reqData: { hostname: 'unreliable-api.example.com', path: '/data' },
|
|
263
|
+
attempts: 3,
|
|
264
|
+
circuitBreaker: {
|
|
265
|
+
failureThreshold: 5, // Open after 5 failures
|
|
266
|
+
successThreshold: 2, // Close after 2 successes in half-open
|
|
267
|
+
timeout: 60000, // Wait 60s before trying again (half-open)
|
|
268
|
+
trackIndividualAttempts: false // Track at request level (not attempt level)
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Circuit Breaker States**:
|
|
274
|
+
- **CLOSED**: Normal operation, requests flow through
|
|
275
|
+
- **OPEN**: Too many failures, requests blocked immediately
|
|
276
|
+
- **HALF_OPEN**: Testing if service recovered, limited requests allowed
|
|
277
|
+
|
|
278
|
+
**Workflow-Level Circuit Breakers**:
|
|
279
|
+
```typescript
|
|
280
|
+
import { CircuitBreaker } from '@emmvish/stable-request';
|
|
281
|
+
|
|
282
|
+
const sharedBreaker = new CircuitBreaker({
|
|
283
|
+
failureThreshold: 10,
|
|
284
|
+
successThreshold: 3,
|
|
285
|
+
timeout: 120000
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await stableWorkflow(phases, {
|
|
289
|
+
circuitBreaker: sharedBreaker, // Shared across all requests
|
|
290
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Check circuit breaker state
|
|
294
|
+
console.log(sharedBreaker.getState());
|
|
295
|
+
// { state: 'CLOSED', failures: 0, successes: 0, ... }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Response Caching
|
|
299
|
+
|
|
300
|
+
Reduce redundant API calls and improve performance with intelligent caching:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
await stableRequest({
|
|
304
|
+
reqData: { hostname: 'api.example.com', path: '/static-data' },
|
|
305
|
+
resReq: true,
|
|
306
|
+
cache: {
|
|
307
|
+
enabled: true,
|
|
308
|
+
ttl: 300000, // Cache for 5 minutes
|
|
309
|
+
key: 'custom-cache-key' // Optional: custom cache key
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Subsequent identical requests within 5 minutes will use cached response
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Global Cache Management**:
|
|
317
|
+
```typescript
|
|
318
|
+
import { getGlobalCacheManager, resetGlobalCacheManager } from '@emmvish/stable-request';
|
|
319
|
+
|
|
320
|
+
const cacheManager = getGlobalCacheManager();
|
|
321
|
+
|
|
322
|
+
// Inspect cache statistics
|
|
323
|
+
const stats = cacheManager.getStats();
|
|
324
|
+
console.log(stats);
|
|
325
|
+
// { size: 42, validEntries: 38, expiredEntries: 4 }
|
|
326
|
+
|
|
327
|
+
// Clear all cached responses
|
|
328
|
+
cacheManager.clearAll();
|
|
329
|
+
|
|
330
|
+
// Or reset the global cache instance
|
|
331
|
+
resetGlobalCacheManager();
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Cache Features**:
|
|
335
|
+
- Automatic request fingerprinting (method, URL, headers, body)
|
|
336
|
+
- TTL-based expiration
|
|
337
|
+
- Workflow-wide sharing across phases and branches
|
|
338
|
+
- Manual cache inspection and clearing
|
|
339
|
+
- Per-request cache configuration
|
|
340
|
+
|
|
341
|
+
### Rate Limiting and Concurrency Control
|
|
342
|
+
|
|
343
|
+
Respect API rate limits and control system load:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
await stableWorkflow(phases, {
|
|
347
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
348
|
+
|
|
349
|
+
// Rate limiting (token bucket algorithm)
|
|
350
|
+
rateLimit: {
|
|
351
|
+
maxRequests: 100, // 100 requests
|
|
352
|
+
timeWindow: 60000 // per 60 seconds
|
|
192
353
|
},
|
|
354
|
+
|
|
355
|
+
// Concurrency limiting
|
|
356
|
+
maxConcurrentRequests: 5 // Max 5 parallel requests
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Per-Phase Configuration**:
|
|
361
|
+
```typescript
|
|
362
|
+
const phases = [
|
|
193
363
|
{
|
|
194
|
-
id: '
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
364
|
+
id: 'bulk-import',
|
|
365
|
+
maxConcurrentRequests: 10, // Override workflow limit
|
|
366
|
+
rateLimit: {
|
|
367
|
+
maxRequests: 50,
|
|
368
|
+
timeWindow: 10000
|
|
369
|
+
},
|
|
370
|
+
requests: [...]
|
|
371
|
+
}
|
|
372
|
+
];
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Standalone Rate Limiter**:
|
|
376
|
+
```typescript
|
|
377
|
+
import { RateLimiter } from '@emmvish/stable-request';
|
|
378
|
+
|
|
379
|
+
const limiter = new RateLimiter({
|
|
380
|
+
maxRequests: 1000,
|
|
381
|
+
timeWindow: 3600000 // 1000 requests per hour
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await limiter.acquire(); // Waits if limit exceeded
|
|
385
|
+
// Make request
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Workflow Execution Patterns
|
|
389
|
+
|
|
390
|
+
### Sequential and Concurrent Phases
|
|
391
|
+
|
|
392
|
+
Control execution order at the phase and request level:
|
|
393
|
+
|
|
394
|
+
**Sequential Phases (Default)**:
|
|
395
|
+
```typescript
|
|
396
|
+
const phases = [
|
|
397
|
+
{ id: 'step-1', requests: [...] }, // Executes first
|
|
398
|
+
{ id: 'step-2', requests: [...] }, // Then this
|
|
399
|
+
{ id: 'step-3', requests: [...] } // Finally this
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
await stableWorkflow(phases, {
|
|
403
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Concurrent Phases**:
|
|
408
|
+
```typescript
|
|
409
|
+
const phases = [
|
|
410
|
+
{ id: 'init', requests: [...] },
|
|
411
|
+
{ id: 'parallel-1', requests: [...] },
|
|
412
|
+
{ id: 'parallel-2', requests: [...] }
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
await stableWorkflow(phases, {
|
|
416
|
+
concurrentPhaseExecution: true, // All phases run in parallel
|
|
417
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Concurrent Requests Within Phase**:
|
|
422
|
+
```typescript
|
|
423
|
+
const phases = [
|
|
199
424
|
{
|
|
200
|
-
id: '
|
|
425
|
+
id: 'data-fetch',
|
|
426
|
+
concurrentExecution: true, // Requests run in parallel
|
|
201
427
|
requests: [
|
|
202
|
-
{ id: '
|
|
428
|
+
{ id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
|
|
429
|
+
{ id: 'products', requestOptions: { reqData: { path: '/products' }, resReq: true } },
|
|
430
|
+
{ id: 'orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } }
|
|
203
431
|
]
|
|
204
|
-
}
|
|
432
|
+
}
|
|
433
|
+
];
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Stop on First Error**:
|
|
437
|
+
```typescript
|
|
438
|
+
const phases = [
|
|
205
439
|
{
|
|
206
|
-
id: '
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
]
|
|
440
|
+
id: 'critical-phase',
|
|
441
|
+
stopOnFirstError: true, // Stop phase if any request fails
|
|
442
|
+
requests: [...]
|
|
210
443
|
}
|
|
211
444
|
];
|
|
212
445
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
commonRequestData: { hostname: 'api.example.com' }
|
|
216
|
-
enableNonLinearExecution: true,
|
|
217
|
-
maxWorkflowIterations: 50,
|
|
218
|
-
sharedBuffer: {}
|
|
446
|
+
await stableWorkflow(phases, {
|
|
447
|
+
stopOnFirstPhaseError: true, // Stop workflow if any phase fails
|
|
448
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
219
449
|
});
|
|
220
|
-
|
|
221
|
-
console.log('Execution history:', result.executionHistory);
|
|
222
|
-
console.log('Terminated early:', result.terminatedEarly);
|
|
223
450
|
```
|
|
224
451
|
|
|
225
|
-
|
|
452
|
+
### Mixed Execution Mode
|
|
226
453
|
|
|
227
|
-
|
|
454
|
+
Combine sequential and concurrent phases for fine-grained control:
|
|
228
455
|
|
|
229
|
-
|
|
456
|
+
```typescript
|
|
457
|
+
const phases = [
|
|
458
|
+
{
|
|
459
|
+
id: 'authenticate',
|
|
460
|
+
requests: [{ id: 'login', requestOptions: {...} }]
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: 'fetch-user-data',
|
|
464
|
+
markConcurrentPhase: true, // This phase runs concurrently...
|
|
465
|
+
requests: [{ id: 'profile', requestOptions: {...} }]
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: 'fetch-orders',
|
|
469
|
+
markConcurrentPhase: true, // ...with this phase
|
|
470
|
+
requests: [{ id: 'orders', requestOptions: {...} }]
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
id: 'process-results', // This waits for above to complete
|
|
474
|
+
requests: [{ id: 'analytics', requestOptions: {...} }]
|
|
475
|
+
}
|
|
476
|
+
];
|
|
230
477
|
|
|
231
|
-
|
|
478
|
+
await stableWorkflow(phases, {
|
|
479
|
+
enableMixedExecution: true, // Enable mixed execution mode
|
|
480
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
481
|
+
});
|
|
482
|
+
```
|
|
232
483
|
|
|
233
|
-
|
|
484
|
+
**Use Case**: Authenticate first (sequential), then fetch multiple data sources in parallel (concurrent), then process results (sequential).
|
|
234
485
|
|
|
235
|
-
-
|
|
236
|
-
- **`jump`**: Jump to a specific phase by ID
|
|
237
|
-
- **`replay`**: Re-execute the current phase
|
|
238
|
-
- **`skip`**: Skip to a target phase or skip the next phase
|
|
239
|
-
- **`terminate`**: Stop the workflow immediately
|
|
486
|
+
### Non-Linear Workflows
|
|
240
487
|
|
|
241
|
-
|
|
488
|
+
Build dynamic workflows with conditional branching, looping, and early termination:
|
|
242
489
|
|
|
243
490
|
```typescript
|
|
244
|
-
import {
|
|
491
|
+
import { PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
245
492
|
|
|
246
|
-
const phases
|
|
493
|
+
const phases = [
|
|
247
494
|
{
|
|
248
|
-
id: 'validate-
|
|
495
|
+
id: 'validate-user',
|
|
249
496
|
requests: [
|
|
250
|
-
{ id: '
|
|
497
|
+
{ id: 'check', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
251
498
|
],
|
|
252
499
|
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
253
|
-
const isValid = phaseResult.responses[0]?.data?.
|
|
500
|
+
const isValid = phaseResult.responses[0]?.data?.isValid;
|
|
254
501
|
|
|
255
502
|
if (isValid) {
|
|
256
|
-
|
|
257
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to next phase
|
|
258
|
-
} else {
|
|
503
|
+
// Jump directly to success phase
|
|
259
504
|
return {
|
|
260
|
-
action: PHASE_DECISION_ACTIONS.
|
|
261
|
-
|
|
505
|
+
action: PHASE_DECISION_ACTIONS.JUMP,
|
|
506
|
+
targetPhaseId: 'success-flow'
|
|
262
507
|
};
|
|
508
|
+
} else {
|
|
509
|
+
// Continue to retry logic
|
|
510
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
263
511
|
}
|
|
264
512
|
}
|
|
265
513
|
},
|
|
266
514
|
{
|
|
267
|
-
id: '
|
|
515
|
+
id: 'retry-validation',
|
|
516
|
+
allowReplay: true,
|
|
517
|
+
maxReplayCount: 3,
|
|
518
|
+
requests: [
|
|
519
|
+
{ id: 'retry', requestOptions: { reqData: { path: '/retry-validate' }, resReq: true } }
|
|
520
|
+
],
|
|
521
|
+
phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
|
|
522
|
+
const replayCount = executionHistory.filter(
|
|
523
|
+
h => h.phaseId === 'retry-validation'
|
|
524
|
+
).length;
|
|
525
|
+
|
|
526
|
+
const success = phaseResult.responses[0]?.data?.success;
|
|
527
|
+
|
|
528
|
+
if (success) {
|
|
529
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'success-flow' };
|
|
530
|
+
} else if (replayCount < 3) {
|
|
531
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
532
|
+
} else {
|
|
533
|
+
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Max retries exceeded' } };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
id: 'success-flow',
|
|
268
539
|
requests: [
|
|
269
|
-
{ id: '
|
|
540
|
+
{ id: 'finalize', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
270
541
|
]
|
|
271
542
|
}
|
|
272
543
|
];
|
|
273
544
|
|
|
274
545
|
const result = await stableWorkflow(phases, {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
sharedBuffer: {}
|
|
546
|
+
enableNonLinearExecution: true, // Enable non-linear execution
|
|
547
|
+
workflowId: 'adaptive-validation',
|
|
548
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
279
549
|
});
|
|
280
550
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
551
|
+
console.log(result.executionHistory);
|
|
552
|
+
// Array of execution records showing which phases ran and why
|
|
284
553
|
```
|
|
285
554
|
|
|
286
|
-
|
|
555
|
+
**Phase Decision Actions**:
|
|
556
|
+
- **CONTINUE**: Proceed to next sequential phase (default)
|
|
557
|
+
- **JUMP**: Skip to a specific phase by ID
|
|
558
|
+
- **SKIP**: Skip upcoming phases until a target phase (or end)
|
|
559
|
+
- **REPLAY**: Re-execute the current phase (requires `allowReplay: true`)
|
|
560
|
+
- **TERMINATE**: Stop the entire workflow immediately
|
|
287
561
|
|
|
562
|
+
**Decision Hook Context**:
|
|
288
563
|
```typescript
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
564
|
+
phaseDecisionHook: async ({
|
|
565
|
+
phaseResult, // Current phase execution result
|
|
566
|
+
executionHistory, // Array of all executed phases
|
|
567
|
+
sharedBuffer, // Cross-phase shared state
|
|
568
|
+
concurrentResults // Results from concurrent phases (mixed execution)
|
|
569
|
+
}) => {
|
|
570
|
+
// Your decision logic
|
|
571
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Replay Limits**:
|
|
576
|
+
```typescript
|
|
577
|
+
{
|
|
578
|
+
id: 'polling-phase',
|
|
579
|
+
allowReplay: true,
|
|
580
|
+
maxReplayCount: 10, // Maximum 10 replays
|
|
581
|
+
requests: [...],
|
|
582
|
+
phaseDecisionHook: async ({ phaseResult }) => {
|
|
583
|
+
if (phaseResult.responses[0]?.data?.ready) {
|
|
584
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
306
585
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
586
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Branched Workflows
|
|
592
|
+
|
|
593
|
+
Execute multiple independent workflow paths in parallel or sequentially:
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
const branches = [
|
|
315
597
|
{
|
|
316
|
-
id: '
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
598
|
+
id: 'user-flow',
|
|
599
|
+
markConcurrentBranch: true, // Execute in parallel
|
|
600
|
+
phases: [
|
|
601
|
+
{ id: 'fetch-user', requests: [...] },
|
|
602
|
+
{ id: 'update-user', requests: [...] }
|
|
603
|
+
]
|
|
321
604
|
},
|
|
322
605
|
{
|
|
323
|
-
id: '
|
|
324
|
-
|
|
325
|
-
|
|
606
|
+
id: 'analytics-flow',
|
|
607
|
+
markConcurrentBranch: true, // Execute in parallel
|
|
608
|
+
phases: [
|
|
609
|
+
{ id: 'log-event', requests: [...] },
|
|
610
|
+
{ id: 'update-metrics', requests: [...] }
|
|
326
611
|
]
|
|
327
612
|
},
|
|
328
613
|
{
|
|
329
|
-
id: '
|
|
330
|
-
|
|
331
|
-
{ id: '
|
|
614
|
+
id: 'cleanup-flow', // Sequential (waits for above)
|
|
615
|
+
phases: [
|
|
616
|
+
{ id: 'clear-cache', requests: [...] },
|
|
617
|
+
{ id: 'notify', requests: [...] }
|
|
332
618
|
]
|
|
333
619
|
}
|
|
334
620
|
];
|
|
335
621
|
|
|
336
|
-
const result = await stableWorkflow(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
#### Polling with Replay
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
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
|
-
];
|
|
389
|
-
|
|
390
|
-
const result = await stableWorkflow(phases, {
|
|
391
|
-
workflowId: 'polling-workflow',
|
|
392
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
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
|
|
402
|
-
|
|
403
|
-
```typescript
|
|
404
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
405
|
-
{
|
|
406
|
-
id: 'attempt-operation',
|
|
407
|
-
allowReplay: true,
|
|
408
|
-
maxReplayCount: 3,
|
|
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
|
-
}
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
id: 'primary-flow',
|
|
444
|
-
requests: [
|
|
445
|
-
{ id: 'primary', requestOptions: { reqData: { path: '/primary' }, resReq: true } }
|
|
446
|
-
]
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
id: 'fallback-operation',
|
|
450
|
-
requests: [
|
|
451
|
-
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
452
|
-
]
|
|
453
|
-
}
|
|
454
|
-
];
|
|
455
|
-
|
|
456
|
-
const result = await stableWorkflow(phases, {
|
|
457
|
-
enableNonLinearExecution: true,
|
|
458
|
-
sharedBuffer: { retryAttempts: 0 },
|
|
459
|
-
logPhaseResults: true
|
|
460
|
-
});
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
#### Skip Phases
|
|
464
|
-
|
|
465
|
-
```typescript
|
|
466
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
467
|
-
{
|
|
468
|
-
id: 'check-cache',
|
|
469
|
-
allowSkip: true,
|
|
470
|
-
requests: [
|
|
471
|
-
{ id: 'cache', requestOptions: { reqData: { path: '/cache/check' }, resReq: true } }
|
|
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' };
|
|
480
|
-
}
|
|
481
|
-
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
482
|
-
}
|
|
483
|
-
},
|
|
484
|
-
{
|
|
485
|
-
id: 'expensive-computation',
|
|
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
|
-
}
|
|
502
|
-
];
|
|
503
|
-
|
|
504
|
-
const result = await stableWorkflow(phases, {
|
|
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 ? '✓' : '✗'}`);
|
|
540
|
-
});
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
#### Loop Protection
|
|
544
|
-
|
|
545
|
-
```typescript
|
|
546
|
-
const result = await stableWorkflow(phases, {
|
|
547
|
-
enableNonLinearExecution: true,
|
|
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
|
-
```
|
|
560
|
-
|
|
561
|
-
### Branched Workflows
|
|
562
|
-
|
|
563
|
-
Branched workflows enable orchestration of complex business logic by organizing phases into branches that can execute in parallel or serial order. Each branch is a self-contained workflow with its own phases, and branches can make decisions to control execution flow using JUMP, TERMINATE, or CONTINUE actions.
|
|
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
|
|
572
|
-
|
|
573
|
-
#### Basic Branched Workflow
|
|
574
|
-
|
|
575
|
-
```typescript
|
|
576
|
-
import { stableWorkflow, STABLE_WORKFLOW_BRANCH } from '@emmvish/stable-request';
|
|
577
|
-
|
|
578
|
-
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
579
|
-
{
|
|
580
|
-
id: 'validation',
|
|
581
|
-
phases: [
|
|
582
|
-
{
|
|
583
|
-
id: 'validate-input',
|
|
584
|
-
requests: [
|
|
585
|
-
{ id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
586
|
-
]
|
|
587
|
-
}
|
|
588
|
-
]
|
|
589
|
-
},
|
|
590
|
-
{
|
|
591
|
-
id: 'processing',
|
|
592
|
-
phases: [
|
|
593
|
-
{
|
|
594
|
-
id: 'process-data',
|
|
595
|
-
requests: [
|
|
596
|
-
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
597
|
-
]
|
|
598
|
-
}
|
|
599
|
-
]
|
|
600
|
-
},
|
|
601
|
-
{
|
|
602
|
-
id: 'finalization',
|
|
603
|
-
phases: [
|
|
604
|
-
{
|
|
605
|
-
id: 'finalize',
|
|
606
|
-
requests: [
|
|
607
|
-
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
608
|
-
]
|
|
609
|
-
}
|
|
610
|
-
]
|
|
611
|
-
}
|
|
612
|
-
];
|
|
613
|
-
|
|
614
|
-
const result = await stableWorkflow([], {
|
|
615
|
-
workflowId: 'branched-workflow',
|
|
616
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
617
|
-
branches,
|
|
618
|
-
executeBranchesConcurrently: false, // Execute branches serially
|
|
619
|
-
sharedBuffer: {}
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
console.log('Branches executed:', result.branches?.length);
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
#### Parallel vs Serial Branch Execution
|
|
626
|
-
|
|
627
|
-
```typescript
|
|
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
|
-
});
|
|
640
|
-
|
|
641
|
-
// Serial execution - branches run one after another
|
|
642
|
-
const result = await stableWorkflow([], {
|
|
643
|
-
workflowId: 'serial-branches',
|
|
644
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
645
|
-
branches: [
|
|
646
|
-
{ id: 'authenticate', phases: [/* ... */] },
|
|
647
|
-
{ id: 'fetch-data', phases: [/* ... */] },
|
|
648
|
-
{ id: 'process', phases: [/* ... */] }
|
|
649
|
-
],
|
|
650
|
-
executeBranchesConcurrently: false, // Serial execution
|
|
651
|
-
sharedBuffer: {}
|
|
652
|
-
});
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
#### Branch Decision Hooks
|
|
656
|
-
|
|
657
|
-
Each branch can have a decision hook to control workflow execution:
|
|
658
|
-
|
|
659
|
-
```typescript
|
|
660
|
-
import { BRANCH_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
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
|
-
];
|
|
692
|
-
|
|
693
|
-
const result = await stableWorkflow([], {
|
|
694
|
-
workflowId: 'validation-workflow',
|
|
695
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
696
|
-
branches,
|
|
697
|
-
executeBranchesConcurrently: false,
|
|
698
|
-
sharedBuffer: {}
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
if (result.terminatedEarly) {
|
|
702
|
-
console.log('Workflow terminated:', result.terminationReason);
|
|
703
|
-
}
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
#### JUMP Action - Skip Branches
|
|
707
|
-
|
|
708
|
-
```typescript
|
|
709
|
-
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
710
|
-
{
|
|
711
|
-
id: 'check-cache',
|
|
712
|
-
phases: [
|
|
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
|
|
726
|
-
return {
|
|
727
|
-
action: BRANCH_DECISION_ACTIONS.JUMP,
|
|
728
|
-
targetBranchId: 'finalize'
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
|
|
733
|
-
}
|
|
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
|
-
}
|
|
768
|
-
];
|
|
769
|
-
|
|
770
|
-
const result = await stableWorkflow([], {
|
|
771
|
-
workflowId: 'cache-optimization',
|
|
772
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
773
|
-
branches,
|
|
774
|
-
executeBranchesConcurrently: false,
|
|
775
|
-
sharedBuffer: {}
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
// If cache hit: check-cache → finalize (skips expensive-computation and save-cache)
|
|
779
|
-
// If cache miss: check-cache → expensive-computation → save-cache → finalize
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
#### Conditional Branching
|
|
783
|
-
|
|
784
|
-
```typescript
|
|
785
|
-
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
786
|
-
{
|
|
787
|
-
id: 'check-user-type',
|
|
788
|
-
phases: [
|
|
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
|
-
];
|
|
863
|
-
|
|
864
|
-
const result = await stableWorkflow([], {
|
|
865
|
-
workflowId: 'user-type-routing',
|
|
866
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
867
|
-
branches,
|
|
868
|
-
executeBranchesConcurrently: false,
|
|
869
|
-
sharedBuffer: {}
|
|
870
|
-
});
|
|
871
|
-
```
|
|
872
|
-
|
|
873
|
-
#### Retry Logic Within Branches
|
|
874
|
-
|
|
875
|
-
```typescript
|
|
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
|
-
```
|
|
1079
|
-
|
|
1080
|
-
#### Mixed Serial and Parallel Execution
|
|
1081
|
-
|
|
1082
|
-
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.
|
|
1083
|
-
|
|
1084
|
-
**Basic Mixed Execution:**
|
|
1085
|
-
|
|
1086
|
-
```typescript
|
|
1087
|
-
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
1088
|
-
|
|
1089
|
-
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1090
|
-
{
|
|
1091
|
-
id: 'init',
|
|
1092
|
-
requests: [
|
|
1093
|
-
{ id: 'init', requestOptions: { reqData: { path: '/init' }, resReq: true } }
|
|
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
|
-
];
|
|
1136
|
-
|
|
1137
|
-
const result = await stableWorkflow(phases, {
|
|
1138
|
-
workflowId: 'mixed-execution',
|
|
1139
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1140
|
-
enableNonLinearExecution: true
|
|
1141
|
-
});
|
|
1142
|
-
```
|
|
1143
|
-
|
|
1144
|
-
**Multiple Parallel Groups:**
|
|
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
|
|
622
|
+
const result = await stableWorkflow([], { // Empty phases array
|
|
623
|
+
enableBranchExecution: true,
|
|
624
|
+
branches,
|
|
625
|
+
workflowId: 'multi-branch-workflow',
|
|
626
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
1523
627
|
});
|
|
1524
628
|
|
|
1525
|
-
//
|
|
1526
|
-
|
|
1527
|
-
const stats = cacheManager.getStats();
|
|
1528
|
-
console.log(`Cache size: ${stats.size}, Valid entries: ${stats.validEntries}`);
|
|
1529
|
-
cacheManager.clear(); // Clear all cache
|
|
629
|
+
console.log(result.branches); // Branch execution results
|
|
630
|
+
console.log(result.branchExecutionHistory); // Branch-level execution history
|
|
1530
631
|
```
|
|
1531
632
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
Transform requests dynamically before execution:
|
|
1535
|
-
|
|
633
|
+
**Branch-Level Configuration**:
|
|
1536
634
|
```typescript
|
|
1537
|
-
|
|
635
|
+
const branches = [
|
|
636
|
+
{
|
|
637
|
+
id: 'high-priority-branch',
|
|
638
|
+
markConcurrentBranch: false,
|
|
639
|
+
commonConfig: { // Branch-level config overrides
|
|
640
|
+
commonAttempts: 5,
|
|
641
|
+
commonWait: 2000,
|
|
642
|
+
commonCache: { enabled: true, ttl: 120000 }
|
|
643
|
+
},
|
|
644
|
+
phases: [...]
|
|
645
|
+
}
|
|
646
|
+
];
|
|
647
|
+
```
|
|
1538
648
|
|
|
1539
|
-
|
|
649
|
+
**Branch Features**:
|
|
650
|
+
- Each branch has independent phase execution
|
|
651
|
+
- Branches share the workflow's `sharedBuffer`
|
|
652
|
+
- Branch decision hooks can terminate the entire workflow
|
|
653
|
+
- Supports all execution patterns (mixed, non-linear) within branches
|
|
1540
654
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
return {
|
|
1555
|
-
reqData: {
|
|
1556
|
-
hostname: 'api.example.com',
|
|
1557
|
-
path: '/authenticated-data',
|
|
1558
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
1559
|
-
}
|
|
1560
|
-
};
|
|
655
|
+
**Branch Decision Hooks**:
|
|
656
|
+
```typescript
|
|
657
|
+
const branches = [
|
|
658
|
+
{
|
|
659
|
+
id: 'conditional-branch',
|
|
660
|
+
branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
|
|
661
|
+
if (branchResult.failedRequests > 0) {
|
|
662
|
+
return {
|
|
663
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
664
|
+
terminateWorkflow: true, // Terminate entire workflow
|
|
665
|
+
metadata: { reason: 'Critical branch failed' }
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
1561
669
|
},
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
},
|
|
1566
|
-
commonBuffer
|
|
1567
|
-
});
|
|
1568
|
-
|
|
1569
|
-
console.log('Token used:', commonBuffer.token);
|
|
670
|
+
phases: [...]
|
|
671
|
+
}
|
|
672
|
+
];
|
|
1570
673
|
```
|
|
1571
674
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
Share state across requests in gateways and workflows:
|
|
675
|
+
## Advanced Capabilities
|
|
1575
676
|
|
|
1576
|
-
|
|
1577
|
-
import { stableWorkflow } from '@emmvish/stable-request';
|
|
677
|
+
### Config Cascading
|
|
1578
678
|
|
|
1579
|
-
|
|
679
|
+
Configuration inheritance across workflow → branch → phase → request levels:
|
|
1580
680
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
-
]
|
|
681
|
+
```typescript
|
|
682
|
+
await stableWorkflow(phases, {
|
|
683
|
+
// Workflow-level config (lowest priority)
|
|
684
|
+
commonAttempts: 3,
|
|
685
|
+
commonWait: 1000,
|
|
686
|
+
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
687
|
+
commonCache: { enabled: true, ttl: 60000 },
|
|
688
|
+
commonRequestData: {
|
|
689
|
+
hostname: 'api.example.com',
|
|
690
|
+
headers: { 'X-API-Version': 'v2' }
|
|
1603
691
|
},
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
692
|
+
|
|
693
|
+
branches: [{
|
|
694
|
+
id: 'my-branch',
|
|
695
|
+
commonConfig: {
|
|
696
|
+
// Branch-level config (overrides workflow)
|
|
697
|
+
commonAttempts: 5,
|
|
698
|
+
commonWait: 500
|
|
699
|
+
},
|
|
700
|
+
phases: [{
|
|
701
|
+
id: 'my-phase',
|
|
702
|
+
commonConfig: {
|
|
703
|
+
// Phase-level config (overrides branch and workflow)
|
|
704
|
+
commonAttempts: 1,
|
|
705
|
+
commonCache: { enabled: false }
|
|
706
|
+
},
|
|
707
|
+
requests: [{
|
|
708
|
+
id: 'my-request',
|
|
1609
709
|
requestOptions: {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
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
|
-
}
|
|
710
|
+
// Request-level config (highest priority)
|
|
711
|
+
reqData: { path: '/critical' },
|
|
712
|
+
attempts: 10,
|
|
713
|
+
wait: 100,
|
|
714
|
+
cache: { enabled: true, ttl: 300000 }
|
|
1623
715
|
}
|
|
1624
|
-
}
|
|
1625
|
-
]
|
|
1626
|
-
}
|
|
1627
|
-
];
|
|
1628
|
-
|
|
1629
|
-
const result = await stableWorkflow(phases, {
|
|
1630
|
-
workflowId: 'stateful-workflow',
|
|
1631
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1632
|
-
sharedBuffer
|
|
716
|
+
}]
|
|
717
|
+
}]
|
|
718
|
+
}]
|
|
1633
719
|
});
|
|
1634
|
-
|
|
1635
|
-
console.log('Total requests processed:', sharedBuffer.requestCount);
|
|
1636
720
|
```
|
|
1637
721
|
|
|
722
|
+
**Priority**: Request > Phase > Branch > Workflow
|
|
723
|
+
|
|
1638
724
|
### Request Grouping
|
|
1639
725
|
|
|
1640
|
-
|
|
726
|
+
Define reusable configurations for groups of related requests:
|
|
1641
727
|
|
|
1642
728
|
```typescript
|
|
1643
|
-
import { stableApiGateway } from '@emmvish/stable-request';
|
|
1644
|
-
|
|
1645
729
|
const requests = [
|
|
1646
730
|
{
|
|
1647
731
|
id: 'critical-1',
|
|
@@ -1656,471 +740,431 @@ const requests = [
|
|
|
1656
740
|
{
|
|
1657
741
|
id: 'optional-1',
|
|
1658
742
|
groupId: 'optional',
|
|
1659
|
-
requestOptions: { reqData: { path: '/optional/1' }, resReq:
|
|
743
|
+
requestOptions: { reqData: { path: '/optional/1' }, resReq: false }
|
|
1660
744
|
}
|
|
1661
745
|
];
|
|
1662
746
|
|
|
1663
|
-
|
|
747
|
+
await stableApiGateway(requests, {
|
|
1664
748
|
commonRequestData: { hostname: 'api.example.com' },
|
|
1665
|
-
commonAttempts: 1,
|
|
1666
|
-
|
|
749
|
+
commonAttempts: 1, // Default: 1 attempt
|
|
750
|
+
|
|
1667
751
|
requestGroups: [
|
|
1668
752
|
{
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
}
|
|
753
|
+
groupId: 'critical',
|
|
754
|
+
commonAttempts: 5, // Critical requests: 5 attempts
|
|
755
|
+
commonWait: 2000,
|
|
756
|
+
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
757
|
+
commonFinalErrorAnalyzer: async () => false // Never suppress errors
|
|
1675
758
|
},
|
|
1676
759
|
{
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
}
|
|
760
|
+
groupId: 'optional',
|
|
761
|
+
commonAttempts: 2, // Optional requests: 2 attempts
|
|
762
|
+
commonWait: 500,
|
|
763
|
+
commonFinalErrorAnalyzer: async () => true // Suppress errors (return false)
|
|
1682
764
|
}
|
|
1683
765
|
]
|
|
1684
766
|
});
|
|
1685
767
|
```
|
|
1686
768
|
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
769
|
+
**Use Cases**:
|
|
770
|
+
- Different retry strategies for critical vs. optional requests
|
|
771
|
+
- Separate error handling for different request types
|
|
772
|
+
- Grouped logging and monitoring
|
|
1690
773
|
|
|
1691
|
-
|
|
1692
|
-
import { stableApiGateway } from '@emmvish/stable-request';
|
|
774
|
+
### Shared Buffer and Pre-Execution Hooks
|
|
1693
775
|
|
|
1694
|
-
|
|
1695
|
-
const results = await stableApiGateway(requests, {
|
|
1696
|
-
commonRequestData: { hostname: 'api.example.com' },
|
|
1697
|
-
concurrentExecution: true,
|
|
1698
|
-
maxConcurrentRequests: 5
|
|
1699
|
-
});
|
|
776
|
+
Share state across phases/branches and dynamically transform requests:
|
|
1700
777
|
|
|
1701
|
-
|
|
1702
|
-
|
|
778
|
+
**Shared Buffer**:
|
|
779
|
+
```typescript
|
|
780
|
+
const sharedBuffer = {
|
|
781
|
+
authToken: null,
|
|
782
|
+
userId: null,
|
|
783
|
+
metrics: []
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const phases = [
|
|
787
|
+
{
|
|
788
|
+
id: 'auth',
|
|
789
|
+
requests: [{
|
|
790
|
+
id: 'login',
|
|
791
|
+
requestOptions: {
|
|
792
|
+
reqData: { path: '/login', method: REQUEST_METHODS.POST },
|
|
793
|
+
resReq: true,
|
|
794
|
+
preExecution: {
|
|
795
|
+
preExecutionHook: ({ commonBuffer }) => {
|
|
796
|
+
// Write to buffer after response
|
|
797
|
+
return {};
|
|
798
|
+
},
|
|
799
|
+
preExecutionHookParams: {},
|
|
800
|
+
applyPreExecutionConfigOverride: false,
|
|
801
|
+
continueOnPreExecutionHookFailure: false
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}]
|
|
805
|
+
},
|
|
1703
806
|
{
|
|
1704
|
-
id: '
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
807
|
+
id: 'fetch-data',
|
|
808
|
+
requests: [{
|
|
809
|
+
id: 'profile',
|
|
810
|
+
requestOptions: {
|
|
811
|
+
reqData: { path: '/profile' },
|
|
812
|
+
resReq: true,
|
|
813
|
+
preExecution: {
|
|
814
|
+
preExecutionHook: ({ commonBuffer }) => {
|
|
815
|
+
// Use token from buffer
|
|
816
|
+
return {
|
|
817
|
+
reqData: {
|
|
818
|
+
headers: {
|
|
819
|
+
'Authorization': `Bearer ${commonBuffer.authToken}`
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
},
|
|
824
|
+
applyPreExecutionConfigOverride: true // Apply returned config
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}]
|
|
1708
828
|
}
|
|
1709
829
|
];
|
|
1710
|
-
```
|
|
1711
830
|
|
|
1712
|
-
|
|
831
|
+
await stableWorkflow(phases, {
|
|
832
|
+
sharedBuffer,
|
|
833
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
834
|
+
});
|
|
1713
835
|
|
|
1714
|
-
|
|
836
|
+
console.log(sharedBuffer); // Updated with data from workflow
|
|
837
|
+
```
|
|
1715
838
|
|
|
1716
|
-
|
|
1717
|
-
|
|
839
|
+
**Pre-Execution Hook Use Cases**:
|
|
840
|
+
- Dynamic header injection (auth tokens, correlation IDs)
|
|
841
|
+
- Request payload transformation based on previous responses
|
|
842
|
+
- Conditional request configuration (skip, modify, enhance)
|
|
843
|
+
- Cross-phase state management
|
|
1718
844
|
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
}
|
|
1730
|
-
return data.status === 'completed';
|
|
845
|
+
**Hook Failure Handling**:
|
|
846
|
+
```typescript
|
|
847
|
+
{
|
|
848
|
+
preExecution: {
|
|
849
|
+
preExecutionHook: async ({ commonBuffer, inputParams }) => {
|
|
850
|
+
// May throw error
|
|
851
|
+
const token = await fetchTokenFromExternalSource();
|
|
852
|
+
return { reqData: { headers: { 'Authorization': token } } };
|
|
853
|
+
},
|
|
854
|
+
continueOnPreExecutionHookFailure: true // Continue even if hook fails
|
|
1731
855
|
}
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
console.log('Job completed:', data);
|
|
856
|
+
}
|
|
1735
857
|
```
|
|
1736
858
|
|
|
1737
|
-
###
|
|
859
|
+
### Comprehensive Observability
|
|
1738
860
|
|
|
1739
|
-
|
|
861
|
+
Built-in hooks for monitoring, logging, and analysis at every level:
|
|
1740
862
|
|
|
863
|
+
**Request-Level Hooks**:
|
|
1741
864
|
```typescript
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
const data = await stableRequest({
|
|
865
|
+
await stableRequest({
|
|
1745
866
|
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
1746
867
|
resReq: true,
|
|
1747
868
|
attempts: 3,
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
// Send to monitoring service
|
|
1761
|
-
monitoringService.trackError(errorLog);
|
|
869
|
+
|
|
870
|
+
// Validate response content
|
|
871
|
+
responseAnalyzer: async ({ data, reqData, params }) => {
|
|
872
|
+
console.log('Analyzing response:', data);
|
|
873
|
+
return data.status === 'success'; // false = retry
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
// Custom error handling
|
|
877
|
+
handleErrors: async ({ errorLog, reqData, commonBuffer }) => {
|
|
878
|
+
console.error('Request failed:', errorLog);
|
|
879
|
+
await sendToMonitoring(errorLog);
|
|
1762
880
|
},
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
881
|
+
|
|
882
|
+
// Log successful attempts
|
|
883
|
+
handleSuccessfulAttemptData: async ({ successfulAttemptData, reqData }) => {
|
|
884
|
+
console.log('Request succeeded:', successfulAttemptData);
|
|
1766
885
|
},
|
|
886
|
+
|
|
887
|
+
// Analyze final error after all retries
|
|
1767
888
|
finalErrorAnalyzer: async ({ error, reqData }) => {
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
889
|
+
console.error('All retries exhausted:', error);
|
|
890
|
+
return error.message.includes('404'); // true = return false instead of throw
|
|
891
|
+
},
|
|
892
|
+
|
|
893
|
+
// Pass custom parameters to hooks
|
|
894
|
+
hookParams: {
|
|
895
|
+
responseAnalyzerParams: { expectedFormat: 'json' },
|
|
896
|
+
handleErrorsParams: { alertChannel: 'slack' }
|
|
897
|
+
},
|
|
898
|
+
|
|
899
|
+
logAllErrors: true,
|
|
900
|
+
logAllSuccessfulAttempts: true
|
|
901
|
+
});
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
**Workflow-Level Hooks**:
|
|
905
|
+
```typescript
|
|
906
|
+
await stableWorkflow(phases, {
|
|
907
|
+
workflowId: 'monitored-workflow',
|
|
908
|
+
|
|
909
|
+
// Called after each phase completes
|
|
910
|
+
handlePhaseCompletion: async ({ workflowId, phaseResult, params }) => {
|
|
911
|
+
console.log(`Phase ${phaseResult.phaseId} completed`);
|
|
912
|
+
console.log(`Requests: ${phaseResult.totalRequests}`);
|
|
913
|
+
console.log(`Success: ${phaseResult.successfulRequests}`);
|
|
914
|
+
console.log(`Failed: ${phaseResult.failedRequests}`);
|
|
915
|
+
await sendMetrics(phaseResult);
|
|
916
|
+
},
|
|
917
|
+
|
|
918
|
+
// Called when a phase fails
|
|
919
|
+
handlePhaseError: async ({ workflowId, error, phaseResult }) => {
|
|
920
|
+
console.error(`Phase ${phaseResult.phaseId} failed:`, error);
|
|
921
|
+
await alertOnCall(error);
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
// Monitor non-linear execution decisions
|
|
925
|
+
handlePhaseDecision: async ({ workflowId, decision, phaseResult }) => {
|
|
926
|
+
console.log(`Phase decision: ${decision.action}`);
|
|
927
|
+
if (decision.targetPhaseId) {
|
|
928
|
+
console.log(`Target: ${decision.targetPhaseId}`);
|
|
1772
929
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
930
|
+
},
|
|
931
|
+
|
|
932
|
+
// Monitor branch completion
|
|
933
|
+
handleBranchCompletion: async ({ workflowId, branchResult }) => {
|
|
934
|
+
console.log(`Branch ${branchResult.branchId} completed`);
|
|
935
|
+
},
|
|
936
|
+
|
|
937
|
+
// Monitor branch decisions
|
|
938
|
+
handleBranchDecision: async ({ workflowId, decision, branchResult }) => {
|
|
939
|
+
console.log(`Branch decision: ${decision.action}`);
|
|
940
|
+
},
|
|
941
|
+
|
|
942
|
+
// Pass parameters to workflow hooks
|
|
943
|
+
workflowHookParams: {
|
|
944
|
+
handlePhaseCompletionParams: { environment: 'production' },
|
|
945
|
+
handlePhaseErrorParams: { severity: 'high' }
|
|
946
|
+
},
|
|
947
|
+
|
|
948
|
+
logPhaseResults: true,
|
|
949
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
1775
950
|
});
|
|
1776
951
|
```
|
|
1777
952
|
|
|
1778
|
-
|
|
953
|
+
**Execution History**:
|
|
954
|
+
```typescript
|
|
955
|
+
const result = await stableWorkflow(phases, {
|
|
956
|
+
enableNonLinearExecution: true,
|
|
957
|
+
workflowId: 'tracked-workflow',
|
|
958
|
+
commonRequestData: { hostname: 'api.example.com' }
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// Detailed execution history
|
|
962
|
+
result.executionHistory.forEach(record => {
|
|
963
|
+
console.log({
|
|
964
|
+
phaseId: record.phaseId,
|
|
965
|
+
executionNumber: record.executionNumber,
|
|
966
|
+
decision: record.decision,
|
|
967
|
+
timestamp: record.timestamp,
|
|
968
|
+
metadata: record.metadata
|
|
969
|
+
});
|
|
970
|
+
});
|
|
1779
971
|
|
|
1780
|
-
|
|
972
|
+
// Branch execution history
|
|
973
|
+
result.branchExecutionHistory?.forEach(record => {
|
|
974
|
+
console.log({
|
|
975
|
+
branchId: record.branchId,
|
|
976
|
+
action: record.action,
|
|
977
|
+
timestamp: record.timestamp
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
```
|
|
1781
981
|
|
|
1782
|
-
|
|
1783
|
-
import { stableWorkflow, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
982
|
+
### Trial Mode
|
|
1784
983
|
|
|
1785
|
-
|
|
1786
|
-
tenantId: string;
|
|
1787
|
-
apiKey: string;
|
|
1788
|
-
baseUrl: string;
|
|
1789
|
-
}
|
|
984
|
+
Test and debug workflows without making real API calls:
|
|
1790
985
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
986
|
+
```typescript
|
|
987
|
+
await stableRequest({
|
|
988
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
989
|
+
resReq: true,
|
|
990
|
+
attempts: 3,
|
|
991
|
+
trialMode: {
|
|
992
|
+
enabled: true,
|
|
993
|
+
successProbability: 0.5, // 50% chance of success
|
|
994
|
+
retryableProbability: 0.8, // 80% of failures are retryable
|
|
995
|
+
latencyRange: { min: 100, max: 500 } // Simulated latency: 100-500ms
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
```
|
|
1797
999
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
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
|
-
];
|
|
1000
|
+
**Use Cases**:
|
|
1001
|
+
- Test retry logic without hitting APIs
|
|
1002
|
+
- Simulate failure scenarios
|
|
1003
|
+
- Load testing with controlled failure rates
|
|
1004
|
+
- Development without backend dependencies
|
|
1902
1005
|
|
|
1903
|
-
|
|
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
|
-
});
|
|
1006
|
+
## Common Use Cases
|
|
1938
1007
|
|
|
1939
|
-
|
|
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
|
-
}
|
|
1008
|
+
### Multi-Step Data Synchronization
|
|
1951
1009
|
|
|
1952
|
-
|
|
1953
|
-
const
|
|
1954
|
-
{
|
|
1955
|
-
|
|
1010
|
+
```typescript
|
|
1011
|
+
const syncPhases = [
|
|
1012
|
+
{
|
|
1013
|
+
id: 'fetch-source-data',
|
|
1014
|
+
concurrentExecution: true,
|
|
1015
|
+
requests: [
|
|
1016
|
+
{ id: 'users', requestOptions: { reqData: { path: '/source/users' }, resReq: true } },
|
|
1017
|
+
{ id: 'orders', requestOptions: { reqData: { path: '/source/orders' }, resReq: true } }
|
|
1018
|
+
]
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
id: 'transform-data',
|
|
1022
|
+
requests: [
|
|
1023
|
+
{
|
|
1024
|
+
id: 'transform',
|
|
1025
|
+
requestOptions: {
|
|
1026
|
+
reqData: { path: '/transform', method: REQUEST_METHODS.POST },
|
|
1027
|
+
resReq: true
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
]
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
id: 'upload-to-destination',
|
|
1034
|
+
concurrentExecution: true,
|
|
1035
|
+
requests: [
|
|
1036
|
+
{ id: 'upload-users', requestOptions: { reqData: { path: '/dest/users', method: REQUEST_METHODS.POST }, resReq: false } },
|
|
1037
|
+
{ id: 'upload-orders', requestOptions: { reqData: { path: '/dest/orders', method: REQUEST_METHODS.POST }, resReq: false } }
|
|
1038
|
+
]
|
|
1039
|
+
}
|
|
1956
1040
|
];
|
|
1957
1041
|
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1042
|
+
await stableWorkflow(syncPhases, {
|
|
1043
|
+
workflowId: 'data-sync',
|
|
1044
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
1045
|
+
commonAttempts: 3,
|
|
1046
|
+
stopOnFirstPhaseError: true,
|
|
1047
|
+
logPhaseResults: true
|
|
1961
1048
|
});
|
|
1962
1049
|
```
|
|
1963
1050
|
|
|
1964
|
-
###
|
|
1051
|
+
### API Gateway with Fallbacks
|
|
1965
1052
|
|
|
1966
1053
|
```typescript
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
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;
|
|
1054
|
+
const requests = [
|
|
1055
|
+
{
|
|
1056
|
+
id: 'primary-service',
|
|
1057
|
+
groupId: 'critical',
|
|
1058
|
+
requestOptions: {
|
|
1059
|
+
reqData: { hostname: 'primary.api.com', path: '/data' },
|
|
1060
|
+
resReq: true,
|
|
1061
|
+
finalErrorAnalyzer: async ({ error }) => {
|
|
1062
|
+
// If primary fails, mark as handled (don't throw)
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
id: 'fallback-service',
|
|
1069
|
+
groupId: 'fallback',
|
|
1070
|
+
requestOptions: {
|
|
1071
|
+
reqData: { hostname: 'backup.api.com', path: '/data' },
|
|
1072
|
+
resReq: true
|
|
2001
1073
|
}
|
|
1074
|
+
}
|
|
1075
|
+
];
|
|
2002
1076
|
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
-
});
|
|
1077
|
+
const results = await stableApiGateway(requests, {
|
|
1078
|
+
concurrentExecution: false, // Sequential: try fallback only if primary fails
|
|
1079
|
+
requestGroups: [
|
|
1080
|
+
{ groupId: 'critical', commonAttempts: 3 },
|
|
1081
|
+
{ groupId: 'fallback', commonAttempts: 1 }
|
|
1082
|
+
]
|
|
1083
|
+
});
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
### Polling with Conditional Termination
|
|
2063
1087
|
|
|
2064
|
-
|
|
2065
|
-
|
|
1088
|
+
```typescript
|
|
1089
|
+
const pollingPhases = [
|
|
1090
|
+
{
|
|
1091
|
+
id: 'poll-job-status',
|
|
1092
|
+
allowReplay: true,
|
|
1093
|
+
maxReplayCount: 20,
|
|
1094
|
+
requests: [
|
|
1095
|
+
{
|
|
1096
|
+
id: 'status-check',
|
|
1097
|
+
requestOptions: {
|
|
1098
|
+
reqData: { path: '/job/status' },
|
|
1099
|
+
resReq: true
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
],
|
|
1103
|
+
phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
|
|
1104
|
+
const status = phaseResult.responses[0]?.data?.status;
|
|
1105
|
+
const attempts = executionHistory.filter(h => h.phaseId === 'poll-job-status').length;
|
|
2066
1106
|
|
|
2067
|
-
if (
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
products: results.find(r => r.requestId === 'products')?.data,
|
|
2074
|
-
orders: results.find(r => r.requestId === 'orders')?.data
|
|
2075
|
-
}
|
|
2076
|
-
};
|
|
1107
|
+
if (status === 'completed') {
|
|
1108
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
1109
|
+
} else if (status === 'failed') {
|
|
1110
|
+
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Job failed' } };
|
|
1111
|
+
} else if (attempts < 20) {
|
|
1112
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
2077
1113
|
} else {
|
|
2078
|
-
|
|
1114
|
+
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Timeout' } };
|
|
2079
1115
|
}
|
|
2080
|
-
} catch (error) {
|
|
2081
|
-
console.error(`Failed to fetch from ${source.id}:`, error);
|
|
2082
|
-
// Continue to next source
|
|
2083
1116
|
}
|
|
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
1117
|
},
|
|
2097
1118
|
{
|
|
2098
|
-
id: '
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
},
|
|
2103
|
-
{
|
|
2104
|
-
id: 'backup-cache',
|
|
2105
|
-
priority: 3,
|
|
2106
|
-
endpoint: '/cached',
|
|
2107
|
-
hostname: 'cache.example.com'
|
|
1119
|
+
id: 'process-results',
|
|
1120
|
+
requests: [
|
|
1121
|
+
{ id: 'fetch-results', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
|
|
1122
|
+
]
|
|
2108
1123
|
}
|
|
2109
1124
|
];
|
|
2110
1125
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
1126
|
+
await stableWorkflow(pollingPhases, {
|
|
1127
|
+
enableNonLinearExecution: true,
|
|
1128
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
1129
|
+
commonWait: 5000 // 5 second wait between polls
|
|
1130
|
+
});
|
|
2116
1131
|
```
|
|
2117
1132
|
|
|
2118
|
-
|
|
1133
|
+
### Webhook Retry with Circuit Breaker
|
|
2119
1134
|
|
|
2120
|
-
|
|
1135
|
+
```typescript
|
|
1136
|
+
import { CircuitBreaker, REQUEST_METHODS } from '@emmvish/stable-request';
|
|
1137
|
+
|
|
1138
|
+
const webhookBreaker = new CircuitBreaker({
|
|
1139
|
+
failureThreshold: 3,
|
|
1140
|
+
successThreshold: 2,
|
|
1141
|
+
timeout: 30000
|
|
1142
|
+
});
|
|
2121
1143
|
|
|
2122
|
-
|
|
1144
|
+
async function sendWebhook(eventData: any) {
|
|
1145
|
+
try {
|
|
1146
|
+
await stableRequest({
|
|
1147
|
+
reqData: {
|
|
1148
|
+
hostname: 'webhook.example.com',
|
|
1149
|
+
path: '/events',
|
|
1150
|
+
method: REQUEST_METHODS.POST,
|
|
1151
|
+
body: eventData
|
|
1152
|
+
},
|
|
1153
|
+
attempts: 5,
|
|
1154
|
+
wait: 1000,
|
|
1155
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
1156
|
+
circuitBreaker: webhookBreaker,
|
|
1157
|
+
handleErrors: async ({ errorLog }) => {
|
|
1158
|
+
console.error('Webhook delivery failed:', errorLog);
|
|
1159
|
+
await queueForRetry(eventData);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
console.error('Webhook permanently failed:', error);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
```
|
|
2123
1167
|
|
|
2124
|
-
|
|
1168
|
+
## License
|
|
2125
1169
|
|
|
2126
|
-
|
|
1170
|
+
MIT © Manish Varma
|