@emmvish/stable-request 1.5.2 → 1.6.0
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 +1223 -1680
- package/dist/core/stable-workflow.d.ts.map +1 -1
- package/dist/core/stable-workflow.js +53 -3
- package/dist/core/stable-workflow.js.map +1 -1
- package/dist/enums/index.d.ts +7 -0
- package/dist/enums/index.d.ts.map +1 -1
- package/dist/enums/index.js +8 -0
- package/dist/enums/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +55 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utilities/execute-non-linear-workflow.d.ts +11 -0
- package/dist/utilities/execute-non-linear-workflow.d.ts.map +1 -0
- package/dist/utilities/execute-non-linear-workflow.js +399 -0
- package/dist/utilities/execute-non-linear-workflow.js.map +1 -0
- package/dist/utilities/execute-phase.d.ts +2 -12
- package/dist/utilities/execute-phase.d.ts.map +1 -1
- package/dist/utilities/execute-phase.js.map +1 -1
- package/dist/utilities/index.d.ts +1 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +1 -0
- package/dist/utilities/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
It ensures that **every request attempt**, whether it succeeds or fails, can be:
|
|
6
6
|
|
|
7
|
+
- Sent reliably
|
|
7
8
|
- Observed
|
|
8
9
|
- Analyzed
|
|
9
10
|
- Retried intelligently
|
|
@@ -17,178 +18,12 @@ All without crashing your application or hiding context behind opaque errors.
|
|
|
17
18
|
> If you’ve ever logged `error.message` and thought
|
|
18
19
|
> **“This tells me absolutely nothing”** — this library is for you.
|
|
19
20
|
|
|
20
|
-
In addition, it enables **content-aware retries**, **hierarchical configuration**, **batch orchestration**, and **multi-phase workflows** with deep observability — all built on top of standard HTTP calls.
|
|
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.
|
|
21
22
|
|
|
22
23
|
All in all, it provides you with the **entire ecosystem** to build **API-integrations based workflows** with **complete flexibility**.
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
26
|
-
## 📚 Table of Contents
|
|
27
|
-
<!-- TOC START -->
|
|
28
|
-
- [Why stable-request exists](#why-stable-request-exists)
|
|
29
|
-
- [What stable-request gives you](#what-stable-request-gives-you)
|
|
30
|
-
- [Core capabilities](#core-capabilities)
|
|
31
|
-
- [Scaling beyond single requests](#scaling-beyond-single-requests)
|
|
32
|
-
- [Full workflow orchestration](#full-workflow-orchestration)
|
|
33
|
-
- [How stable-request is different](#how-stable-request-is-different)
|
|
34
|
-
- [Choose your entry point](#choose-your-entry-point)
|
|
35
|
-
- [Installation](#installation)
|
|
36
|
-
- [Quick Start](#quick-start)
|
|
37
|
-
- [Basic Request (No Retries)](#1-basic-request-no-retries)
|
|
38
|
-
- [Add Simple Retries](#2-add-simple-retries)
|
|
39
|
-
- [Validate Response Content](#3-validate-response-content-content-aware-retries)
|
|
40
|
-
- [Monitor Errors](#4-monitor-errors-observability)
|
|
41
|
-
- [Monitor Successful Attempts](#5-monitor-successful-attempts)
|
|
42
|
-
- [Handle Final Errors Gracefully](#6-handle-final-errors-gracefully)
|
|
43
|
-
- [Pass Custom Parameters to Hooks](#7-pass-custom-parameters-to-hooks)
|
|
44
|
-
- [Pre-Execution Hook](#8-pre-execution-hook-dynamic-configuration)
|
|
45
|
-
- [Intermediate Concepts](#intermediate-concepts)
|
|
46
|
-
- [Making POST/PUT/PATCH/DELETE Requests](#making-postputpatchdelete-requests)
|
|
47
|
-
- [Query Parameters](#query-parameters)
|
|
48
|
-
- [Custom Timeout and Port](#custom-timeout-and-port)
|
|
49
|
-
- [Request Cancellation](#request-cancellation)
|
|
50
|
-
- [Trial Mode](#trial-mode-testing-your-retry-logic)
|
|
51
|
-
- [Batch Processing - Multiple Requests](#batch-processing---multiple-requests)
|
|
52
|
-
- [Basic Batch Request](#basic-batch-request)
|
|
53
|
-
- [Sequential Execution (With Dependencies)](#sequential-execution-with-dependencies)
|
|
54
|
-
- [Shared Configuration (Common Options)](#shared-configuration-common-options)
|
|
55
|
-
- [Advanced: Request Grouping](#advanced-request-grouping)
|
|
56
|
-
- [Service Tiers](#example-service-tiers)
|
|
57
|
-
- [Multi-Region Configuration](#example-multi-region-configuration)
|
|
58
|
-
- [Shared Buffer Across Requests](#example-shared-buffer-is-common-across-the-entire-batch-of-requests)
|
|
59
|
-
- [Multi-Phase Workflows](#multi-phase-workflows)
|
|
60
|
-
- [Basic Workflow](#basic-workflow)
|
|
61
|
-
- [Phase Configuration](#phase-configuration)
|
|
62
|
-
- [Workflow with Request Groups](#workflow-with-request-groups)
|
|
63
|
-
- [Phase Observability Hooks](#phase-observability-hooks)
|
|
64
|
-
- [Workflow Buffer](#workflow-buffer)
|
|
65
|
-
- [Concurrent Execution of Phases](#concurrent-execution-of-phases)
|
|
66
|
-
- [Mixed Execution of Phases](#mixed-execution-of-phases)
|
|
67
|
-
- [Real-World Examples](#real-world-examples)
|
|
68
|
-
- [Polling for Job Completion](#1-polling-for-job-completion)
|
|
69
|
-
- [Database Replication Lag](#2-database-replication-lag)
|
|
70
|
-
- [Idempotent Payment Processing](#3-idempotent-payment-processing)
|
|
71
|
-
- [Batch User Creation](#4-batch-user-creation-with-error-handling)
|
|
72
|
-
- [Health Check Monitoring System](#5-health-check-monitoring-system)
|
|
73
|
-
- [Data Pipeline (ETL Workflow)](#6-data-pipeline-etl-workflow)
|
|
74
|
-
- [Complete API Reference](#complete-api-reference)
|
|
75
|
-
- [`stableRequest`](#stablerequestoptions)
|
|
76
|
-
- [`stableApiGateway`](#stableapigatewayrequests-options)
|
|
77
|
-
- [`stableWorkflow`](#stableworkflowphases-options)
|
|
78
|
-
- [Hooks Reference](#hooks-reference)
|
|
79
|
-
- [`preExecutionHook`](#preexecutionhook)
|
|
80
|
-
- [`responseAnalyzer`](#responseanalyzer)
|
|
81
|
-
- [`handleErrors`](#handleerrors)
|
|
82
|
-
- [`handleSuccessfulAttemptData`](#handlesuccessfulattemptdata)
|
|
83
|
-
- [`finalErrorAnalyzer`](#finalerroranalyzer)
|
|
84
|
-
- [`handlePhaseCompletion`](#handlephasecompletion)
|
|
85
|
-
- [`handlePhaseError`](#handlephaseerror)
|
|
86
|
-
- [Configuration Hierarchy](#configuration-hierarchy)
|
|
87
|
-
- [TypeScript Support](#typescript-support)
|
|
88
|
-
- [License](#license)
|
|
89
|
-
<!-- TOC END -->
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
## Why stable-request exists
|
|
94
|
-
|
|
95
|
-
Modern systems fail in subtle and dangerous ways:
|
|
96
|
-
|
|
97
|
-
- APIs return `200` but the resource isn’t ready
|
|
98
|
-
- Databases are eventually consistent
|
|
99
|
-
- Downstream services partially fail
|
|
100
|
-
- Some requests are critical, others are optional
|
|
101
|
-
- Blind retries amplify failures
|
|
102
|
-
- Workflows fail midway and leave systems inconsistent
|
|
103
|
-
|
|
104
|
-
Most HTTP clients answer only one question:
|
|
105
|
-
|
|
106
|
-
> “Did the request fail at the network or HTTP layer?”
|
|
107
|
-
|
|
108
|
-
**stable-request answers a different one:**
|
|
109
|
-
|
|
110
|
-
> “Is the system state actually correct yet?”
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## What stable-request gives you
|
|
115
|
-
|
|
116
|
-
### Core capabilities
|
|
117
|
-
|
|
118
|
-
✅ **Content-aware retries**
|
|
119
|
-
|
|
120
|
-
Retry based on response validation, not just status codes
|
|
121
|
-
|
|
122
|
-
🔄 **Deterministic execution semantics**
|
|
123
|
-
|
|
124
|
-
Fixed, linear, or exponential retry strategies with hard limits
|
|
125
|
-
|
|
126
|
-
🧠 **Graceful failure handling**
|
|
127
|
-
|
|
128
|
-
Suppress non-critical failures without crashing workflows
|
|
129
|
-
|
|
130
|
-
🧪 **Trial mode / chaos testing**
|
|
131
|
-
|
|
132
|
-
Simulate failures without depending on real outages
|
|
133
|
-
|
|
134
|
-
📊 **First-class observability hooks**
|
|
135
|
-
|
|
136
|
-
Inspect every failed and successful attempt
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
|
|
140
|
-
### Scaling beyond single requests
|
|
141
|
-
|
|
142
|
-
🚀 **Batch execution with shared state (`stableApiGateway`)**
|
|
143
|
-
|
|
144
|
-
Run many requests concurrently or sequentially with shared configuration and shared state
|
|
145
|
-
|
|
146
|
-
🎯 **Request groups**
|
|
147
|
-
|
|
148
|
-
Apply different reliability rules to critical, standard, and optional services
|
|
149
|
-
|
|
150
|
-
🧱 **Hierarchical configuration**
|
|
151
|
-
|
|
152
|
-
Workflow → Phase → Group → Request (predictable overrides)
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
### Full workflow orchestration
|
|
157
|
-
|
|
158
|
-
🧩 **Multi-phase workflows with shared state (`stableWorkflow`)**
|
|
159
|
-
|
|
160
|
-
Model real-world business flows as deterministic, observable execution graphs.
|
|
161
|
-
|
|
162
|
-
🔀 **Mix concurrent and sequential execution**
|
|
163
|
-
|
|
164
|
-
Parallelize where safe, serialize where correctness matters.
|
|
165
|
-
|
|
166
|
-
🛑 **Stop early or degrade gracefully**
|
|
167
|
-
|
|
168
|
-
Stop execution early or continue based on business criticality.
|
|
169
|
-
|
|
170
|
-
📈 **Phase-level metrics and hooks**
|
|
171
|
-
|
|
172
|
-
Track execution time, success rates, and failure boundaries per phase.
|
|
173
|
-
|
|
174
|
-
🧭 **Deterministic, observable execution paths**
|
|
175
|
-
|
|
176
|
-
Every decision is explicit, traceable, and reproducible.
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## How stable-request is different
|
|
181
|
-
|
|
182
|
-
| Traditional HTTP Clients | stable-request |
|
|
183
|
-
|--------------------------|---------------|
|
|
184
|
-
| Status-code based retries | Content-aware retries |
|
|
185
|
-
| Per-request thinking | System-level thinking |
|
|
186
|
-
| Fire-and-forget | Deterministic workflows |
|
|
187
|
-
| Best-effort retries | Business-aware execution |
|
|
188
|
-
| Little observability | Deep, structured hooks |
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
27
|
## Choose your entry point
|
|
193
28
|
|
|
194
29
|
| Need | Use |
|
|
@@ -197,7 +32,31 @@ Most HTTP clients answer only one question:
|
|
|
197
32
|
| Batch or fan-out requests | `stableApiGateway` |
|
|
198
33
|
| Multi-step orchestration | `stableWorkflow` |
|
|
199
34
|
|
|
200
|
-
|
|
35
|
+
|
|
36
|
+
Start small and scale.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 📚 Table of Contents
|
|
41
|
+
<!-- TOC START -->
|
|
42
|
+
- [Installation](#installation)
|
|
43
|
+
- [Core Features](#core-features)
|
|
44
|
+
- [Quick Start](#quick-start)
|
|
45
|
+
- [Advanced Features](#advanced-features)
|
|
46
|
+
- [Non-Linear Workflows](#non-linear-workflows)
|
|
47
|
+
- [Retry Strategies](#retry-strategies)
|
|
48
|
+
- [Circuit Breaker](#circuit-breaker)
|
|
49
|
+
- [Rate Limiting](#rate-limiting)
|
|
50
|
+
- [Caching](#caching)
|
|
51
|
+
- [Pre-Execution Hooks](#pre-execution-hooks)
|
|
52
|
+
- [Shared Buffer](#shared-buffer)
|
|
53
|
+
- [Request Grouping](#request-grouping)
|
|
54
|
+
- [Concurrency Control](#concurrency-control)
|
|
55
|
+
- [Response Analysis](#response-analysis)
|
|
56
|
+
- [Error Handling](#error-handling)
|
|
57
|
+
- [Advanced Use Cases](#advanced-use-cases)
|
|
58
|
+
- [License](#license)
|
|
59
|
+
<!-- TOC END -->
|
|
201
60
|
|
|
202
61
|
---
|
|
203
62
|
|
|
@@ -207,1114 +66,1010 @@ Start small and scale **without changing mental models**.
|
|
|
207
66
|
npm install @emmvish/stable-request
|
|
208
67
|
```
|
|
209
68
|
|
|
210
|
-
##
|
|
211
|
-
|
|
212
|
-
|
|
69
|
+
## Core Features
|
|
70
|
+
|
|
71
|
+
- ✅ **Configurable Retry Strategies**: Fixed, Linear, and Exponential backoff
|
|
72
|
+
- ✅ **Circuit Breaker**: Prevent cascading failures with automatic circuit breaking
|
|
73
|
+
- ✅ **Rate Limiting**: Control request throughput across single or multiple requests
|
|
74
|
+
- ✅ **Response Caching**: Built-in TTL-based caching with global cache manager
|
|
75
|
+
- ✅ **Batch Processing**: Execute multiple requests concurrently or sequentially via API Gateway
|
|
76
|
+
- ✅ **Multi-Phase Non-Linear Workflows**: Orchestrate complex request workflows with phase dependencies
|
|
77
|
+
- ✅ **Pre-Execution Hooks**: Transform requests before execution with dynamic configuration
|
|
78
|
+
- ✅ **Shared Buffer**: Share state across requests in workflows and gateways
|
|
79
|
+
- ✅ **Request Grouping**: Apply different configurations to request groups
|
|
80
|
+
- ✅ **Observability Hooks**: Track errors, successful attempts, and phase completions
|
|
81
|
+
- ✅ **Response Analysis**: Validate responses and trigger retries based on content
|
|
82
|
+
- ✅ **Trial Mode**: Test configurations without making real API calls
|
|
83
|
+
- ✅ **TypeScript Support**: Full type safety with generics for request/response data
|
|
213
84
|
|
|
214
|
-
|
|
215
|
-
import { stableRequest, REQUEST_METHODS } from '@emmvish/stable-request';
|
|
216
|
-
|
|
217
|
-
interface PatchRequestBodyParams {
|
|
218
|
-
id: number;
|
|
219
|
-
updates: {
|
|
220
|
-
name?: string;
|
|
221
|
-
age?: number;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
interface ResponseParams {
|
|
226
|
-
id: number;
|
|
227
|
-
name: string;
|
|
228
|
-
age: number;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const getStableResponse = async () => {
|
|
232
|
-
|
|
233
|
-
const token = 'my-auth-token';
|
|
234
|
-
|
|
235
|
-
const data = await stableRequest<PatchRequestBodyParams, ResponseParams>({
|
|
236
|
-
reqData: {
|
|
237
|
-
method: REQUEST_METHODS.PATCH,
|
|
238
|
-
hostname: 'api.example.com',
|
|
239
|
-
path: '/users',
|
|
240
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
241
|
-
body: { id: 123, updates: { age: 27 } }
|
|
242
|
-
},
|
|
243
|
-
resReq: true // Return the response data
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
console.log(data); // { id: 123, name: 'MV', age: 27 }
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
getStableResponse();
|
|
250
|
-
```
|
|
85
|
+
## Quick Start
|
|
251
86
|
|
|
252
|
-
###
|
|
87
|
+
### Basic Request with Retry
|
|
253
88
|
|
|
254
89
|
```typescript
|
|
255
90
|
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
256
91
|
|
|
257
|
-
const getStableResponse = async () => {
|
|
258
|
-
const data = await stableRequest({
|
|
259
|
-
reqData: {
|
|
260
|
-
hostname: 'api.example.com',
|
|
261
|
-
path: '/users/123'
|
|
262
|
-
},
|
|
263
|
-
resReq: true,
|
|
264
|
-
attempts: 3, // Retry up to 3 times
|
|
265
|
-
wait: 1000, // Wait 1 second between retries
|
|
266
|
-
maxAllowedWait: 8000, // Maximum permissible wait time between retries
|
|
267
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL // 1s, 2s, 4s, 8s...
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
console.log(data);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
getStableResponse();
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
**Retry Strategies:**
|
|
277
|
-
- `RETRY_STRATEGIES.FIXED` - Same delay every time (1s, 1s, 1s...)
|
|
278
|
-
- `RETRY_STRATEGIES.LINEAR` - Increasing delay (1s, 2s, 3s...)
|
|
279
|
-
- `RETRY_STRATEGIES.EXPONENTIAL` - Exponential backoff (1s, 2s, 4s, 8s...)
|
|
280
|
-
|
|
281
|
-
### 3. Validate Response Content (Content-Aware Retries)
|
|
282
|
-
|
|
283
|
-
Sometimes an API returns HTTP 200 but the data isn't ready yet. Use `responseAnalyzer`:
|
|
284
|
-
|
|
285
|
-
```typescript
|
|
286
92
|
const data = await stableRequest({
|
|
287
93
|
reqData: {
|
|
288
94
|
hostname: 'api.example.com',
|
|
289
|
-
path: '/
|
|
95
|
+
path: '/users/123',
|
|
96
|
+
method: 'GET'
|
|
290
97
|
},
|
|
291
98
|
resReq: true,
|
|
292
|
-
attempts:
|
|
293
|
-
wait:
|
|
294
|
-
|
|
295
|
-
// This hook validates the response content
|
|
296
|
-
responseAnalyzer: async ({ reqData, data, trialMode, params, commonBuffer }) => {
|
|
297
|
-
// Return true if response is valid, false to retry
|
|
298
|
-
if (data.status === 'completed') {
|
|
299
|
-
return true; // Success! Don't retry
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
console.log(`Job still processing... (${data.percentComplete}%)`);
|
|
303
|
-
return false; // Retry this request
|
|
304
|
-
}
|
|
99
|
+
attempts: 3,
|
|
100
|
+
wait: 1000,
|
|
101
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
305
102
|
});
|
|
306
103
|
|
|
307
|
-
console.log(
|
|
104
|
+
console.log(data);
|
|
308
105
|
```
|
|
309
106
|
|
|
310
|
-
|
|
107
|
+
### Batch Requests via API Gateway
|
|
108
|
+
|
|
311
109
|
```typescript
|
|
312
|
-
|
|
313
|
-
reqData: AxiosRequestConfig; // Request configuration
|
|
314
|
-
data: ResponseDataType; // Response data from API
|
|
315
|
-
trialMode?: TRIAL_MODE_OPTIONS; // Trial mode settings (if enabled)
|
|
316
|
-
params?: any; // Custom parameters (via hookParams)
|
|
317
|
-
commonBuffer: Record<string, any> // For communication between request hooks
|
|
318
|
-
}) => boolean | Promise<boolean>;
|
|
319
|
-
```
|
|
110
|
+
import { stableApiGateway } from '@emmvish/stable-request';
|
|
320
111
|
|
|
321
|
-
|
|
112
|
+
const requests = [
|
|
113
|
+
{ id: 'user-1', requestOptions: { reqData: { path: '/users/1' }, resReq: true } },
|
|
114
|
+
{ id: 'user-2', requestOptions: { reqData: { path: '/users/2' }, resReq: true } },
|
|
115
|
+
{ id: 'user-3', requestOptions: { reqData: { path: '/users/3' }, resReq: true } }
|
|
116
|
+
];
|
|
322
117
|
|
|
323
|
-
|
|
118
|
+
const results = await stableApiGateway(requests, {
|
|
119
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
120
|
+
concurrentExecution: true,
|
|
121
|
+
maxConcurrentRequests: 10
|
|
122
|
+
});
|
|
324
123
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
},
|
|
331
|
-
resReq: true,
|
|
332
|
-
attempts: 5,
|
|
333
|
-
logAllErrors: true, // Enable error logging
|
|
334
|
-
|
|
335
|
-
// This hook is called on every failed attempt
|
|
336
|
-
handleErrors: async ({ reqData, errorLog, maxSerializableChars, commonBuffer }) => {
|
|
337
|
-
// Log to your monitoring service
|
|
338
|
-
await monitoring.logError({
|
|
339
|
-
url: reqData.url,
|
|
340
|
-
attempt: errorLog.attempt, // e.g., "3/5"
|
|
341
|
-
error: errorLog.error, // Error message
|
|
342
|
-
isRetryable: errorLog.isRetryable, // Can we retry?
|
|
343
|
-
type: errorLog.type, // 'HTTP_ERROR' or 'INVALID_CONTENT'
|
|
344
|
-
statusCode: errorLog.statusCode, // HTTP status code
|
|
345
|
-
timestamp: errorLog.timestamp, // ISO timestamp
|
|
346
|
-
executionTime: errorLog.executionTime // ms
|
|
347
|
-
});
|
|
124
|
+
results.forEach(result => {
|
|
125
|
+
if (result.success) {
|
|
126
|
+
console.log(`Request ${result.requestId}:`, result.data);
|
|
127
|
+
} else {
|
|
128
|
+
console.error(`Request ${result.requestId} failed:`, result.error);
|
|
348
129
|
}
|
|
349
130
|
});
|
|
350
131
|
```
|
|
351
132
|
|
|
352
|
-
|
|
353
|
-
```typescript
|
|
354
|
-
handleErrors?: (options: {
|
|
355
|
-
reqData: AxiosRequestConfig; // Request configuration
|
|
356
|
-
errorLog: ERROR_LOG; // Detailed error information
|
|
357
|
-
maxSerializableChars?: number; // Max chars for stringification
|
|
358
|
-
commonBuffer: Record<string, any> // For communication between request hooks
|
|
359
|
-
}) => any | Promise<any>;
|
|
360
|
-
```
|
|
133
|
+
### Multi-Phase Workflow
|
|
361
134
|
|
|
362
|
-
**ERROR_LOG Structure:**
|
|
363
135
|
```typescript
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
136
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
|
|
137
|
+
|
|
138
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
139
|
+
{
|
|
140
|
+
id: 'authentication',
|
|
141
|
+
requests: [
|
|
142
|
+
{ id: 'login', requestOptions: { reqData: { path: '/auth/login' }, resReq: true } }
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'data-fetching',
|
|
147
|
+
concurrentExecution: true,
|
|
148
|
+
requests: [
|
|
149
|
+
{ id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
|
|
150
|
+
{ id: 'posts', requestOptions: { reqData: { path: '/posts' }, resReq: true } }
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const result = await stableWorkflow(phases, {
|
|
156
|
+
workflowId: 'data-pipeline',
|
|
157
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
158
|
+
stopOnFirstPhaseError: true,
|
|
159
|
+
logPhaseResults: true
|
|
160
|
+
});
|
|
374
161
|
|
|
375
|
-
|
|
162
|
+
console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
|
|
163
|
+
```
|
|
376
164
|
|
|
377
|
-
|
|
165
|
+
### Non-Linear Workflow with Dynamic Routing
|
|
378
166
|
|
|
379
167
|
```typescript
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
168
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
169
|
+
|
|
170
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
171
|
+
{
|
|
172
|
+
id: 'check-status',
|
|
173
|
+
requests: [
|
|
174
|
+
{ id: 'status', requestOptions: { reqData: { path: '/status' }, resReq: true } }
|
|
175
|
+
],
|
|
176
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
177
|
+
const status = phaseResult.responses[0]?.data?.status;
|
|
178
|
+
|
|
179
|
+
if (status === 'completed') {
|
|
180
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' };
|
|
181
|
+
} else if (status === 'processing') {
|
|
182
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
183
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY }; // Replay this phase
|
|
184
|
+
} else {
|
|
185
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-handler' };
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
allowReplay: true,
|
|
189
|
+
maxReplayCount: 10
|
|
384
190
|
},
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
191
|
+
{
|
|
192
|
+
id: 'process',
|
|
193
|
+
requests: [
|
|
194
|
+
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'error-handler',
|
|
199
|
+
requests: [
|
|
200
|
+
{ id: 'error', requestOptions: { reqData: { path: '/error' }, resReq: true } }
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'finalize',
|
|
205
|
+
requests: [
|
|
206
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
207
|
+
]
|
|
399
208
|
}
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const result = await stableWorkflow(phases, {
|
|
212
|
+
workflowId: 'dynamic-workflow',
|
|
213
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
214
|
+
enableNonLinearExecution: true,
|
|
215
|
+
maxWorkflowIterations: 50,
|
|
216
|
+
sharedBuffer: {}
|
|
400
217
|
});
|
|
401
|
-
```
|
|
402
218
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
handleSuccessfulAttemptData?: (options: {
|
|
406
|
-
reqData: AxiosRequestConfig; // Request configuration
|
|
407
|
-
successfulAttemptData: SUCCESSFUL_ATTEMPT_DATA; // Success details
|
|
408
|
-
maxSerializableChars?: number; // Max chars for stringification
|
|
409
|
-
commonBuffer: Record<string, any> // For communication between request hooks
|
|
410
|
-
}) => any | Promise<any>;
|
|
219
|
+
console.log('Execution history:', result.executionHistory);
|
|
220
|
+
console.log('Terminated early:', result.terminatedEarly);
|
|
411
221
|
```
|
|
412
222
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
executionTime: number; // Request duration in ms
|
|
419
|
-
data: ResponseDataType; // Response data
|
|
420
|
-
statusCode: number; // HTTP status code
|
|
421
|
-
}
|
|
422
|
-
```
|
|
223
|
+
## Advanced Features
|
|
224
|
+
|
|
225
|
+
### Non-Linear Workflows
|
|
226
|
+
|
|
227
|
+
Non-linear workflows enable dynamic phase execution based on runtime decisions, allowing you to build complex orchestrations with conditional branching, polling loops, error recovery, and adaptive routing.
|
|
423
228
|
|
|
424
|
-
|
|
229
|
+
#### Phase Decision Actions
|
|
425
230
|
|
|
426
|
-
|
|
231
|
+
Each phase can make decisions about workflow execution:
|
|
232
|
+
|
|
233
|
+
- **`continue`**: Proceed to the next sequential phase
|
|
234
|
+
- **`jump`**: Jump to a specific phase by ID
|
|
235
|
+
- **`replay`**: Re-execute the current phase
|
|
236
|
+
- **`skip`**: Skip to a target phase or skip the next phase
|
|
237
|
+
- **`terminate`**: Stop the workflow immediately
|
|
238
|
+
|
|
239
|
+
#### Basic Non-Linear Workflow
|
|
427
240
|
|
|
428
241
|
```typescript
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
242
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PhaseExecutionDecision, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
243
|
+
|
|
244
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
245
|
+
{
|
|
246
|
+
id: 'validate-input',
|
|
247
|
+
requests: [
|
|
248
|
+
{ id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
249
|
+
],
|
|
250
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
251
|
+
const isValid = phaseResult.responses[0]?.data?.valid;
|
|
252
|
+
|
|
253
|
+
if (isValid) {
|
|
254
|
+
sharedBuffer.validationPassed = true;
|
|
255
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to next phase
|
|
256
|
+
} else {
|
|
257
|
+
return {
|
|
258
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
259
|
+
metadata: { reason: 'Validation failed' }
|
|
260
|
+
};
|
|
261
|
+
}
|
|
443
262
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: 'process-data',
|
|
266
|
+
requests: [
|
|
267
|
+
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
268
|
+
]
|
|
448
269
|
}
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
const result = await stableWorkflow(phases, {
|
|
273
|
+
workflowId: 'validation-workflow',
|
|
274
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
275
|
+
enableNonLinearExecution: true,
|
|
276
|
+
sharedBuffer: {}
|
|
449
277
|
});
|
|
450
278
|
|
|
451
|
-
if (
|
|
452
|
-
console.log('
|
|
279
|
+
if (result.terminatedEarly) {
|
|
280
|
+
console.log('Workflow terminated:', result.terminationReason);
|
|
453
281
|
}
|
|
454
282
|
```
|
|
455
283
|
|
|
456
|
-
|
|
457
|
-
```typescript
|
|
458
|
-
finalErrorAnalyzer?: (options: {
|
|
459
|
-
reqData: AxiosRequestConfig; // Request configuration
|
|
460
|
-
error: any; // The final error object
|
|
461
|
-
trialMode?: TRIAL_MODE_OPTIONS; // Trial mode settings (if enabled)
|
|
462
|
-
params?: any; // Custom parameters (via hookParams)
|
|
463
|
-
commonBuffer: Record<string, any> // For communication between request hooks
|
|
464
|
-
}) => boolean | Promise<boolean>;
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
**Return value:**
|
|
468
|
-
- `true` - Suppress the error, function returns `false` instead of throwing
|
|
469
|
-
- `false` - Throw the error
|
|
470
|
-
|
|
471
|
-
### 7. Pass Custom Parameters to Hooks
|
|
472
|
-
|
|
473
|
-
You can pass custom data to `responseAnalyzer` and `finalErrorAnalyzer`:
|
|
284
|
+
#### Conditional Branching
|
|
474
285
|
|
|
475
286
|
```typescript
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
287
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
288
|
+
{
|
|
289
|
+
id: 'check-user-type',
|
|
290
|
+
requests: [
|
|
291
|
+
{ id: 'user', requestOptions: { reqData: { path: '/user/info' }, resReq: true } }
|
|
292
|
+
],
|
|
293
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
294
|
+
const userType = phaseResult.responses[0]?.data?.type;
|
|
295
|
+
sharedBuffer.userType = userType;
|
|
296
|
+
|
|
297
|
+
if (userType === 'premium') {
|
|
298
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'premium-flow' };
|
|
299
|
+
} else if (userType === 'trial') {
|
|
300
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'trial-flow' };
|
|
301
|
+
} else {
|
|
302
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'free-flow' };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
482
305
|
},
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
finalErrorAnalyzerParams: { alertTeam: true }
|
|
306
|
+
{
|
|
307
|
+
id: 'premium-flow',
|
|
308
|
+
requests: [
|
|
309
|
+
{ id: 'premium', requestOptions: { reqData: { path: '/premium/data' }, resReq: true } }
|
|
310
|
+
],
|
|
311
|
+
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
|
|
490
312
|
},
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
313
|
+
{
|
|
314
|
+
id: 'trial-flow',
|
|
315
|
+
requests: [
|
|
316
|
+
{ id: 'trial', requestOptions: { reqData: { path: '/trial/data' }, resReq: true } }
|
|
317
|
+
],
|
|
318
|
+
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
|
|
496
319
|
},
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
320
|
+
{
|
|
321
|
+
id: 'free-flow',
|
|
322
|
+
requests: [
|
|
323
|
+
{ id: 'free', requestOptions: { reqData: { path: '/free/data' }, resReq: true } }
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: 'finalize',
|
|
328
|
+
requests: [
|
|
329
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
const result = await stableWorkflow(phases, {
|
|
335
|
+
enableNonLinearExecution: true,
|
|
336
|
+
sharedBuffer: {},
|
|
337
|
+
handlePhaseDecision: (decision, phaseResult) => {
|
|
338
|
+
console.log(`Phase ${phaseResult.phaseId} decided: ${decision.action}`);
|
|
503
339
|
}
|
|
504
340
|
});
|
|
505
341
|
```
|
|
506
342
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
Use `preExecution` to modify request configuration dynamically before execution:
|
|
343
|
+
#### Polling with Replay
|
|
510
344
|
|
|
511
345
|
```typescript
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
346
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
347
|
+
{
|
|
348
|
+
id: 'poll-job-status',
|
|
349
|
+
allowReplay: true,
|
|
350
|
+
maxReplayCount: 20,
|
|
351
|
+
requests: [
|
|
352
|
+
{ id: 'check', requestOptions: { reqData: { path: '/job/status' }, resReq: true } }
|
|
353
|
+
],
|
|
354
|
+
phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
|
|
355
|
+
const status = phaseResult.responses[0]?.data?.status;
|
|
356
|
+
const attempts = executionHistory.filter(h => h.phaseId === 'poll-job-status').length;
|
|
357
|
+
|
|
358
|
+
if (status === 'completed') {
|
|
359
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
360
|
+
} else if (status === 'failed') {
|
|
361
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-recovery' };
|
|
362
|
+
} else if (attempts < 20) {
|
|
363
|
+
// Still processing, wait and replay
|
|
364
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
365
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
366
|
+
} else {
|
|
367
|
+
return {
|
|
368
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
369
|
+
metadata: { reason: 'Job timeout after 20 attempts' }
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
518
373
|
},
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
preExecutionHook: async ({ inputParams, commonBuffer }) => {
|
|
525
|
-
const token = await authService.getToken(inputParams.userId);
|
|
526
|
-
commonBuffer.token = token;
|
|
527
|
-
commonBuffer.fetchedAt = new Date().toISOString();
|
|
528
|
-
// Return configuration overrides
|
|
529
|
-
return {
|
|
530
|
-
reqData: {
|
|
531
|
-
hostname: 'api.example.com',
|
|
532
|
-
path: '/protected-resource',
|
|
533
|
-
headers: {
|
|
534
|
-
'Authorization': `Bearer ${token}`
|
|
535
|
-
}
|
|
536
|
-
},
|
|
537
|
-
attempts: 5
|
|
538
|
-
};
|
|
539
|
-
},
|
|
540
|
-
preExecutionHookParams: {
|
|
541
|
-
userId: 'user-123',
|
|
542
|
-
environment: 'production'
|
|
543
|
-
},
|
|
544
|
-
applyPreExecutionConfigOverride: true,
|
|
545
|
-
continueOnPreExecutionHookFailure: false
|
|
374
|
+
{
|
|
375
|
+
id: 'process-results',
|
|
376
|
+
requests: [
|
|
377
|
+
{ id: 'process', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
|
|
378
|
+
]
|
|
546
379
|
},
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
380
|
+
{
|
|
381
|
+
id: 'error-recovery',
|
|
382
|
+
requests: [
|
|
383
|
+
{ id: 'recover', requestOptions: { reqData: { path: '/job/retry' }, resReq: true } }
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
];
|
|
553
387
|
|
|
554
|
-
|
|
388
|
+
const result = await stableWorkflow(phases, {
|
|
389
|
+
workflowId: 'polling-workflow',
|
|
390
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
391
|
+
enableNonLinearExecution: true,
|
|
392
|
+
maxWorkflowIterations: 100
|
|
393
|
+
});
|
|
555
394
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
preExecutionHook: (options: {
|
|
559
|
-
inputParams: any; // Custom parameters you provide
|
|
560
|
-
}) => any | Promise<any>; // Returns config overrides
|
|
561
|
-
preExecutionHookParams?: any; // Custom input parameters
|
|
562
|
-
applyPreExecutionConfigOverride?: boolean; // Apply returned overrides (default: false)
|
|
563
|
-
continueOnPreExecutionHookFailure?: boolean; // Continue if hook fails (default: false)
|
|
564
|
-
}
|
|
395
|
+
console.log('Total iterations:', result.executionHistory.length);
|
|
396
|
+
console.log('Phases executed:', result.completedPhases);
|
|
565
397
|
```
|
|
566
398
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
### Making POST/PUT/PATCH/DELETE Requests
|
|
399
|
+
#### Retry Logic with Replay
|
|
570
400
|
|
|
571
401
|
```typescript
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
402
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
403
|
+
{
|
|
404
|
+
id: 'attempt-operation',
|
|
405
|
+
allowReplay: true,
|
|
406
|
+
maxReplayCount: 3,
|
|
407
|
+
requests: [
|
|
408
|
+
{
|
|
409
|
+
id: 'operation',
|
|
410
|
+
requestOptions: {
|
|
411
|
+
reqData: { path: '/risky-operation', method: 'POST' },
|
|
412
|
+
resReq: true,
|
|
413
|
+
attempts: 1 // No retries at request level
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
],
|
|
417
|
+
phaseDecisionHook: async ({ phaseResult, executionHistory, sharedBuffer }) => {
|
|
418
|
+
const success = phaseResult.responses[0]?.success;
|
|
419
|
+
const attemptCount = executionHistory.filter(h => h.phaseId === 'attempt-operation').length;
|
|
420
|
+
|
|
421
|
+
if (success) {
|
|
422
|
+
sharedBuffer.operationResult = phaseResult.responses[0]?.data;
|
|
423
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
424
|
+
} else if (attemptCount < 3) {
|
|
425
|
+
// Exponential backoff
|
|
426
|
+
const delay = 1000 * Math.pow(2, attemptCount);
|
|
427
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
428
|
+
|
|
429
|
+
sharedBuffer.retryAttempts = attemptCount;
|
|
430
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
431
|
+
} else {
|
|
432
|
+
return {
|
|
433
|
+
action: PHASE_DECISION_ACTIONS.JUMP,
|
|
434
|
+
targetPhaseId: 'fallback-operation',
|
|
435
|
+
metadata: { reason: 'Max retries exceeded' }
|
|
436
|
+
};
|
|
437
|
+
}
|
|
586
438
|
}
|
|
587
439
|
},
|
|
588
|
-
|
|
589
|
-
|
|
440
|
+
{
|
|
441
|
+
id: 'primary-flow',
|
|
442
|
+
requests: [
|
|
443
|
+
{ id: 'primary', requestOptions: { reqData: { path: '/primary' }, resReq: true } }
|
|
444
|
+
]
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
id: 'fallback-operation',
|
|
448
|
+
requests: [
|
|
449
|
+
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
450
|
+
]
|
|
451
|
+
}
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
const result = await stableWorkflow(phases, {
|
|
455
|
+
enableNonLinearExecution: true,
|
|
456
|
+
sharedBuffer: { retryAttempts: 0 },
|
|
457
|
+
logPhaseResults: true
|
|
590
458
|
});
|
|
591
459
|
```
|
|
592
460
|
|
|
593
|
-
|
|
461
|
+
#### Skip Phases
|
|
594
462
|
|
|
595
463
|
```typescript
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
464
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
465
|
+
{
|
|
466
|
+
id: 'check-cache',
|
|
467
|
+
allowSkip: true,
|
|
468
|
+
requests: [
|
|
469
|
+
{ id: 'cache', requestOptions: { reqData: { path: '/cache/check' }, resReq: true } }
|
|
470
|
+
],
|
|
471
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
472
|
+
const cached = phaseResult.responses[0]?.data?.cached;
|
|
473
|
+
|
|
474
|
+
if (cached) {
|
|
475
|
+
sharedBuffer.cachedData = phaseResult.responses[0]?.data;
|
|
476
|
+
// Skip expensive-computation and go directly to finalize
|
|
477
|
+
return { action: PHASE_DECISION_ACTIONS.SKIP, targetPhaseId: 'finalize' };
|
|
478
|
+
}
|
|
479
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
604
480
|
}
|
|
605
481
|
},
|
|
606
|
-
|
|
482
|
+
{
|
|
483
|
+
id: 'expensive-computation',
|
|
484
|
+
requests: [
|
|
485
|
+
{ id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
|
|
486
|
+
]
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
id: 'save-to-cache',
|
|
490
|
+
requests: [
|
|
491
|
+
{ id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
|
|
492
|
+
]
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
id: 'finalize',
|
|
496
|
+
requests: [
|
|
497
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
498
|
+
]
|
|
499
|
+
}
|
|
500
|
+
];
|
|
501
|
+
|
|
502
|
+
const result = await stableWorkflow(phases, {
|
|
503
|
+
enableNonLinearExecution: true,
|
|
504
|
+
sharedBuffer: {}
|
|
607
505
|
});
|
|
608
|
-
// Requests: https://api.example.com:443/users?page=1&limit=10&sort=createdAt
|
|
609
506
|
```
|
|
610
507
|
|
|
611
|
-
|
|
508
|
+
#### Execution History and Tracking
|
|
612
509
|
|
|
613
510
|
```typescript
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
511
|
+
const result = await stableWorkflow(phases, {
|
|
512
|
+
workflowId: 'tracked-workflow',
|
|
513
|
+
enableNonLinearExecution: true,
|
|
514
|
+
handlePhaseCompletion: ({ phaseResult, workflowId }) => {
|
|
515
|
+
console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
|
|
516
|
+
executionNumber: phaseResult.executionNumber,
|
|
517
|
+
success: phaseResult.success,
|
|
518
|
+
decision: phaseResult.decision
|
|
519
|
+
});
|
|
621
520
|
},
|
|
622
|
-
|
|
623
|
-
|
|
521
|
+
handlePhaseDecision: (decision, phaseResult) => {
|
|
522
|
+
console.log(`Decision made:`, {
|
|
523
|
+
phase: phaseResult.phaseId,
|
|
524
|
+
action: decision.action,
|
|
525
|
+
target: decision.targetPhaseId,
|
|
526
|
+
metadata: decision.metadata
|
|
527
|
+
});
|
|
528
|
+
}
|
|
624
529
|
});
|
|
625
|
-
```
|
|
626
530
|
|
|
627
|
-
|
|
531
|
+
// Analyze execution history
|
|
532
|
+
console.log('Total phase executions:', result.executionHistory.length);
|
|
533
|
+
console.log('Unique phases executed:', new Set(result.executionHistory.map(h => h.phaseId)).size);
|
|
534
|
+
console.log('Replay count:', result.executionHistory.filter(h => h.decision?.action === 'replay').length);
|
|
628
535
|
|
|
629
|
-
|
|
630
|
-
|
|
536
|
+
result.executionHistory.forEach(record => {
|
|
537
|
+
console.log(`- ${record.phaseId} (attempt ${record.executionNumber}): ${record.success ? '✓' : '✗'}`);
|
|
538
|
+
});
|
|
539
|
+
```
|
|
631
540
|
|
|
632
|
-
|
|
633
|
-
setTimeout(() => controller.abort(), 5000);
|
|
541
|
+
#### Loop Protection
|
|
634
542
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
});
|
|
644
|
-
} catch (error) {
|
|
645
|
-
if (error.message.includes('cancelled')) {
|
|
646
|
-
console.log('Request was cancelled');
|
|
543
|
+
```typescript
|
|
544
|
+
const result = await stableWorkflow(phases, {
|
|
545
|
+
enableNonLinearExecution: true,
|
|
546
|
+
maxWorkflowIterations: 50, // Prevent infinite loops
|
|
547
|
+
handlePhaseCompletion: ({ phaseResult }) => {
|
|
548
|
+
if (phaseResult.executionNumber && phaseResult.executionNumber > 10) {
|
|
549
|
+
console.warn(`Phase ${phaseResult.phaseId} executed ${phaseResult.executionNumber} times`);
|
|
550
|
+
}
|
|
647
551
|
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
if (result.terminatedEarly && result.terminationReason?.includes('iterations')) {
|
|
555
|
+
console.error('Workflow hit iteration limit - possible infinite loop');
|
|
648
556
|
}
|
|
649
557
|
```
|
|
650
558
|
|
|
651
|
-
|
|
559
|
+
#### Mixed Serial and Parallel Execution
|
|
560
|
+
|
|
561
|
+
Non-linear workflows support mixing serial and parallel phase execution. Mark consecutive phases with `markConcurrentPhase: true` to execute them in parallel, while other phases execute serially.
|
|
652
562
|
|
|
653
|
-
|
|
563
|
+
**Basic Mixed Execution:**
|
|
654
564
|
|
|
655
565
|
```typescript
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
566
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
567
|
+
|
|
568
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
569
|
+
{
|
|
570
|
+
id: 'init',
|
|
571
|
+
requests: [
|
|
572
|
+
{ id: 'init', requestOptions: { reqData: { path: '/init' }, resReq: true } }
|
|
573
|
+
],
|
|
574
|
+
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.CONTINUE })
|
|
660
575
|
},
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
576
|
+
// These two phases execute in parallel
|
|
577
|
+
{
|
|
578
|
+
id: 'check-inventory',
|
|
579
|
+
markConcurrentPhase: true,
|
|
580
|
+
requests: [
|
|
581
|
+
{ id: 'inv', requestOptions: { reqData: { path: '/inventory' }, resReq: true } }
|
|
582
|
+
]
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
id: 'check-pricing',
|
|
586
|
+
markConcurrentPhase: true,
|
|
587
|
+
requests: [
|
|
588
|
+
{ id: 'price', requestOptions: { reqData: { path: '/pricing' }, resReq: true } }
|
|
589
|
+
],
|
|
590
|
+
// Decision hook receives results from all concurrent phases
|
|
591
|
+
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
592
|
+
const inventory = concurrentPhaseResults![0].responses[0]?.data;
|
|
593
|
+
const pricing = concurrentPhaseResults![1].responses[0]?.data;
|
|
594
|
+
|
|
595
|
+
if (inventory.available && pricing.inBudget) {
|
|
596
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
597
|
+
}
|
|
598
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'out-of-stock' };
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
// This phase executes serially after the parallel group
|
|
602
|
+
{
|
|
603
|
+
id: 'process-order',
|
|
604
|
+
requests: [
|
|
605
|
+
{ id: 'order', requestOptions: { reqData: { path: '/order' }, resReq: true } }
|
|
606
|
+
]
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
id: 'out-of-stock',
|
|
610
|
+
requests: [
|
|
611
|
+
{ id: 'notify', requestOptions: { reqData: { path: '/notify' }, resReq: true } }
|
|
612
|
+
]
|
|
669
613
|
}
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
const result = await stableWorkflow(phases, {
|
|
617
|
+
workflowId: 'mixed-execution',
|
|
618
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
619
|
+
enableNonLinearExecution: true
|
|
670
620
|
});
|
|
671
621
|
```
|
|
672
622
|
|
|
673
|
-
**
|
|
674
|
-
- Test your error handling logic
|
|
675
|
-
- Verify monitoring alerts work
|
|
676
|
-
- Chaos engineering experiments
|
|
677
|
-
- Integration testing
|
|
678
|
-
|
|
679
|
-
## Batch Processing - Multiple Requests
|
|
680
|
-
|
|
681
|
-
### Basic Batch Request
|
|
623
|
+
**Multiple Parallel Groups:**
|
|
682
624
|
|
|
683
625
|
```typescript
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const requests = [
|
|
626
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
687
627
|
{
|
|
688
|
-
id: '
|
|
689
|
-
|
|
690
|
-
reqData: { path: '/
|
|
691
|
-
|
|
692
|
-
}
|
|
628
|
+
id: 'authenticate',
|
|
629
|
+
requests: [
|
|
630
|
+
{ id: 'auth', requestOptions: { reqData: { path: '/auth' }, resReq: true } }
|
|
631
|
+
]
|
|
693
632
|
},
|
|
633
|
+
// First parallel group: Data validation
|
|
694
634
|
{
|
|
695
|
-
id: 'user
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
resReq: true
|
|
699
|
-
|
|
635
|
+
id: 'validate-user',
|
|
636
|
+
markConcurrentPhase: true,
|
|
637
|
+
requests: [
|
|
638
|
+
{ id: 'val-user', requestOptions: { reqData: { path: '/validate/user' }, resReq: true } }
|
|
639
|
+
]
|
|
700
640
|
},
|
|
701
641
|
{
|
|
702
|
-
id: '
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
resReq: true
|
|
642
|
+
id: 'validate-payment',
|
|
643
|
+
markConcurrentPhase: true,
|
|
644
|
+
requests: [
|
|
645
|
+
{ id: 'val-pay', requestOptions: { reqData: { path: '/validate/payment' }, resReq: true } }
|
|
646
|
+
]
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
id: 'validate-shipping',
|
|
650
|
+
markConcurrentPhase: true,
|
|
651
|
+
requests: [
|
|
652
|
+
{ id: 'val-ship', requestOptions: { reqData: { path: '/validate/shipping' }, resReq: true } }
|
|
653
|
+
],
|
|
654
|
+
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
655
|
+
const allValid = concurrentPhaseResults!.every(r => r.success && r.responses[0]?.data?.valid);
|
|
656
|
+
if (!allValid) {
|
|
657
|
+
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Validation failed' } };
|
|
658
|
+
}
|
|
659
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
706
660
|
}
|
|
661
|
+
},
|
|
662
|
+
// Serial processing phase
|
|
663
|
+
{
|
|
664
|
+
id: 'calculate-total',
|
|
665
|
+
requests: [
|
|
666
|
+
{ id: 'calc', requestOptions: { reqData: { path: '/calculate' }, resReq: true } }
|
|
667
|
+
]
|
|
668
|
+
},
|
|
669
|
+
// Second parallel group: External integrations
|
|
670
|
+
{
|
|
671
|
+
id: 'notify-warehouse',
|
|
672
|
+
markConcurrentPhase: true,
|
|
673
|
+
requests: [
|
|
674
|
+
{ id: 'warehouse', requestOptions: { reqData: { path: '/notify/warehouse' }, resReq: true } }
|
|
675
|
+
]
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
id: 'notify-shipping',
|
|
679
|
+
markConcurrentPhase: true,
|
|
680
|
+
requests: [
|
|
681
|
+
{ id: 'shipping', requestOptions: { reqData: { path: '/notify/shipping' }, resReq: true } }
|
|
682
|
+
]
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
id: 'update-inventory',
|
|
686
|
+
markConcurrentPhase: true,
|
|
687
|
+
requests: [
|
|
688
|
+
{ id: 'inventory', requestOptions: { reqData: { path: '/update/inventory' }, resReq: true } }
|
|
689
|
+
]
|
|
690
|
+
},
|
|
691
|
+
// Final serial phase
|
|
692
|
+
{
|
|
693
|
+
id: 'finalize',
|
|
694
|
+
requests: [
|
|
695
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
696
|
+
]
|
|
707
697
|
}
|
|
708
698
|
];
|
|
709
699
|
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
commonRequestData: {
|
|
713
|
-
|
|
714
|
-
},
|
|
715
|
-
commonAttempts: 3,
|
|
716
|
-
commonWait: 1000,
|
|
717
|
-
concurrentExecution: true // Run all requests in parallel
|
|
700
|
+
const result = await stableWorkflow(phases, {
|
|
701
|
+
workflowId: 'multi-parallel-workflow',
|
|
702
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
703
|
+
enableNonLinearExecution: true
|
|
718
704
|
});
|
|
719
705
|
|
|
720
|
-
|
|
721
|
-
results.forEach(result => {
|
|
722
|
-
if (result.success) {
|
|
723
|
-
console.log(`${result.requestId} succeeded:`, result.data);
|
|
724
|
-
} else {
|
|
725
|
-
console.error(`${result.requestId} failed:`, result.error);
|
|
726
|
-
}
|
|
727
|
-
});
|
|
706
|
+
console.log('Execution order demonstrates mixed serial/parallel execution');
|
|
728
707
|
```
|
|
729
708
|
|
|
730
|
-
**
|
|
709
|
+
**Decision Making with Concurrent Results:**
|
|
710
|
+
|
|
731
711
|
```typescript
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
712
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
713
|
+
{
|
|
714
|
+
id: 'api-check-1',
|
|
715
|
+
markConcurrentPhase: true,
|
|
716
|
+
requests: [
|
|
717
|
+
{ id: 'api1', requestOptions: { reqData: { path: '/health/api1' }, resReq: true } }
|
|
718
|
+
]
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: 'api-check-2',
|
|
722
|
+
markConcurrentPhase: true,
|
|
723
|
+
requests: [
|
|
724
|
+
{ id: 'api2', requestOptions: { reqData: { path: '/health/api2' }, resReq: true } }
|
|
725
|
+
]
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
id: 'api-check-3',
|
|
729
|
+
markConcurrentPhase: true,
|
|
730
|
+
requests: [
|
|
731
|
+
{ id: 'api3', requestOptions: { reqData: { path: '/health/api3' }, resReq: true } }
|
|
732
|
+
],
|
|
733
|
+
phaseDecisionHook: async ({ concurrentPhaseResults, sharedBuffer }) => {
|
|
734
|
+
// Aggregate results from all parallel phases
|
|
735
|
+
const healthScores = concurrentPhaseResults!.map(result =>
|
|
736
|
+
result.responses[0]?.data?.score || 0
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
const averageScore = healthScores.reduce((a, b) => a + b, 0) / healthScores.length;
|
|
740
|
+
sharedBuffer!.healthScore = averageScore;
|
|
741
|
+
|
|
742
|
+
if (averageScore > 0.8) {
|
|
743
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'optimal-path' };
|
|
744
|
+
} else if (averageScore > 0.5) {
|
|
745
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to degraded-path
|
|
746
|
+
} else {
|
|
747
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'fallback-path' };
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
id: 'degraded-path',
|
|
753
|
+
requests: [
|
|
754
|
+
{ id: 'degraded', requestOptions: { reqData: { path: '/degraded' }, resReq: true } }
|
|
755
|
+
]
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
id: 'optimal-path',
|
|
759
|
+
requests: [
|
|
760
|
+
{ id: 'optimal', requestOptions: { reqData: { path: '/optimal' }, resReq: true } }
|
|
761
|
+
]
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
id: 'fallback-path',
|
|
765
|
+
requests: [
|
|
766
|
+
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
767
|
+
]
|
|
768
|
+
}
|
|
769
|
+
];
|
|
770
|
+
|
|
771
|
+
const sharedBuffer = {};
|
|
772
|
+
const result = await stableWorkflow(phases, {
|
|
773
|
+
workflowId: 'adaptive-routing',
|
|
774
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
775
|
+
enableNonLinearExecution: true,
|
|
776
|
+
sharedBuffer
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
console.log('Average health score:', sharedBuffer.healthScore);
|
|
739
780
|
```
|
|
740
781
|
|
|
741
|
-
|
|
782
|
+
**Error Handling in Parallel Groups:**
|
|
742
783
|
|
|
743
784
|
```typescript
|
|
744
|
-
const
|
|
785
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
745
786
|
{
|
|
746
|
-
id: '
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
787
|
+
id: 'critical-check',
|
|
788
|
+
markConcurrentPhase: true,
|
|
789
|
+
requests: [
|
|
790
|
+
{
|
|
791
|
+
id: 'check1',
|
|
792
|
+
requestOptions: {
|
|
793
|
+
reqData: { path: '/critical/check1' },
|
|
794
|
+
resReq: true,
|
|
795
|
+
attempts: 3
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
]
|
|
755
799
|
},
|
|
756
800
|
{
|
|
757
|
-
id: '
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
801
|
+
id: 'optional-check',
|
|
802
|
+
markConcurrentPhase: true,
|
|
803
|
+
requests: [
|
|
804
|
+
{
|
|
805
|
+
id: 'check2',
|
|
806
|
+
requestOptions: {
|
|
807
|
+
reqData: { path: '/optional/check2' },
|
|
808
|
+
resReq: true,
|
|
809
|
+
attempts: 1,
|
|
810
|
+
finalErrorAnalyzer: async () => true // Suppress errors
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
],
|
|
814
|
+
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
815
|
+
// Check if critical phase succeeded
|
|
816
|
+
const criticalSuccess = concurrentPhaseResults![0].success;
|
|
817
|
+
|
|
818
|
+
if (!criticalSuccess) {
|
|
819
|
+
return {
|
|
820
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
821
|
+
metadata: { reason: 'Critical check failed' }
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Continue even if optional check failed
|
|
826
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
764
827
|
}
|
|
765
828
|
},
|
|
766
829
|
{
|
|
767
|
-
id: '
|
|
768
|
-
|
|
769
|
-
reqData: { path: '/
|
|
770
|
-
|
|
771
|
-
}
|
|
830
|
+
id: 'process',
|
|
831
|
+
requests: [
|
|
832
|
+
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
833
|
+
]
|
|
772
834
|
}
|
|
773
835
|
];
|
|
774
836
|
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
},
|
|
781
|
-
commonAttempts: 3
|
|
837
|
+
const result = await stableWorkflow(phases, {
|
|
838
|
+
workflowId: 'resilient-parallel',
|
|
839
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
840
|
+
enableNonLinearExecution: true,
|
|
841
|
+
stopOnFirstPhaseError: false // Continue even with phase errors
|
|
782
842
|
});
|
|
783
|
-
|
|
784
|
-
if (results.every(r => r.success)) {
|
|
785
|
-
console.log('Workflow completed successfully');
|
|
786
|
-
} else {
|
|
787
|
-
const failedStep = results.findIndex(r => !r.success);
|
|
788
|
-
console.error(`Workflow failed at step ${failedStep + 1}`);
|
|
789
|
-
}
|
|
790
843
|
```
|
|
791
844
|
|
|
792
|
-
|
|
845
|
+
**Key Points:**
|
|
846
|
+
- Only **consecutive phases** with `markConcurrentPhase: true` execute in parallel
|
|
847
|
+
- The **last phase** in a concurrent group can have a `phaseDecisionHook` that receives `concurrentPhaseResults`
|
|
848
|
+
- Parallel groups are separated by phases **without** `markConcurrentPhase` (or phases with it set to false)
|
|
849
|
+
- All decision actions work with parallel groups except `REPLAY` (not supported for concurrent groups)
|
|
850
|
+
- Error handling follows normal workflow rules - use `stopOnFirstPhaseError` to control behavior
|
|
793
851
|
|
|
794
|
-
|
|
852
|
+
#### Configuration Options
|
|
795
853
|
|
|
854
|
+
**Workflow Options:**
|
|
855
|
+
- `enableNonLinearExecution`: Enable non-linear workflow (required)
|
|
856
|
+
- `maxWorkflowIterations`: Maximum total iterations (default: 1000)
|
|
857
|
+
- `handlePhaseDecision`: Called when phase makes a decision
|
|
858
|
+
- `stopOnFirstPhaseError`: Stop on phase failure (default: false)
|
|
859
|
+
|
|
860
|
+
**Phase Options:**
|
|
861
|
+
- `phaseDecisionHook`: Function returning `PhaseExecutionDecision`
|
|
862
|
+
- `allowReplay`: Allow phase replay (default: false)
|
|
863
|
+
- `allowSkip`: Allow phase skip (default: false)
|
|
864
|
+
- `maxReplayCount`: Maximum replays (default: Infinity)
|
|
865
|
+
|
|
866
|
+
**Decision Hook Parameters:**
|
|
796
867
|
```typescript
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
]
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
hostname: 'api.example.com',
|
|
807
|
-
headers: { 'Authorization': `Bearer ${token}` }
|
|
808
|
-
},
|
|
809
|
-
commonResReq: true,
|
|
810
|
-
commonAttempts: 5,
|
|
811
|
-
commonWait: 2000,
|
|
812
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
813
|
-
commonLogAllErrors: true,
|
|
814
|
-
|
|
815
|
-
// Shared hooks
|
|
816
|
-
commonHandleErrors: async ({ reqData, errorLog }) => {
|
|
817
|
-
console.log(`Request to ${reqData.url} failed (${errorLog.attempt})`);
|
|
818
|
-
},
|
|
819
|
-
|
|
820
|
-
commonResponseAnalyzer: async ({ data }) => {
|
|
821
|
-
return data?.success === true;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
);
|
|
868
|
+
interface PhaseDecisionHookOptions {
|
|
869
|
+
workflowId: string;
|
|
870
|
+
phaseResult: STABLE_WORKFLOW_PHASE_RESULT;
|
|
871
|
+
phaseId: string;
|
|
872
|
+
phaseIndex: number;
|
|
873
|
+
executionHistory: PhaseExecutionRecord[];
|
|
874
|
+
sharedBuffer?: Record<string, any>;
|
|
875
|
+
params?: any;
|
|
876
|
+
}
|
|
825
877
|
```
|
|
826
878
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
879
|
+
**Decision Object:**
|
|
880
|
+
```typescript
|
|
881
|
+
interface PhaseExecutionDecision {
|
|
882
|
+
action: PHASE_DECISION_ACTIONS;
|
|
883
|
+
targetPhaseId?: string;
|
|
884
|
+
replayCount?: number;
|
|
885
|
+
metadata?: Record<string, any>;
|
|
886
|
+
}
|
|
887
|
+
```
|
|
830
888
|
|
|
831
|
-
|
|
889
|
+
### Retry Strategies
|
|
832
890
|
|
|
833
|
-
|
|
891
|
+
Control the delay between retry attempts:
|
|
834
892
|
|
|
835
893
|
```typescript
|
|
836
|
-
|
|
837
|
-
[
|
|
838
|
-
// Critical services - need high reliability
|
|
839
|
-
{
|
|
840
|
-
id: 'auth-check',
|
|
841
|
-
groupId: 'critical',
|
|
842
|
-
requestOptions: {
|
|
843
|
-
reqData: { path: '/auth/verify' },
|
|
844
|
-
resReq: true
|
|
845
|
-
}
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
id: 'payment-process',
|
|
849
|
-
groupId: 'critical',
|
|
850
|
-
requestOptions: {
|
|
851
|
-
reqData: { path: '/payments/charge' },
|
|
852
|
-
resReq: true,
|
|
853
|
-
// Individual override: even MORE attempts for payments
|
|
854
|
-
attempts: 15
|
|
855
|
-
}
|
|
856
|
-
},
|
|
857
|
-
|
|
858
|
-
// Analytics - failures are acceptable
|
|
859
|
-
{
|
|
860
|
-
id: 'track-event',
|
|
861
|
-
groupId: 'analytics',
|
|
862
|
-
requestOptions: {
|
|
863
|
-
reqData: { path: '/analytics/track' },
|
|
864
|
-
resReq: true
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
],
|
|
868
|
-
{
|
|
869
|
-
// Global defaults (lowest priority)
|
|
870
|
-
commonRequestData: {
|
|
871
|
-
hostname: 'api.example.com'
|
|
872
|
-
},
|
|
873
|
-
commonAttempts: 2,
|
|
874
|
-
commonWait: 500,
|
|
875
|
-
|
|
876
|
-
// Define groups with their own configs
|
|
877
|
-
requestGroups: [
|
|
878
|
-
{
|
|
879
|
-
id: 'critical',
|
|
880
|
-
commonConfig: {
|
|
881
|
-
// Critical services: aggressive retries
|
|
882
|
-
commonAttempts: 10,
|
|
883
|
-
commonWait: 2000,
|
|
884
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
885
|
-
|
|
886
|
-
commonHandleErrors: async ({ errorLog }) => {
|
|
887
|
-
// Alert on critical failures
|
|
888
|
-
await pagerDuty.alert('Critical service failure', errorLog);
|
|
889
|
-
},
|
|
890
|
-
|
|
891
|
-
commonResponseAnalyzer: async ({ data }) => {
|
|
892
|
-
// Strict validation
|
|
893
|
-
return data?.status === 'success' && !data?.errors;
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
},
|
|
897
|
-
{
|
|
898
|
-
id: 'analytics',
|
|
899
|
-
commonConfig: {
|
|
900
|
-
// Analytics: minimal retries, don't throw on failure
|
|
901
|
-
commonAttempts: 1,
|
|
902
|
-
|
|
903
|
-
commonFinalErrorAnalyzer: async () => {
|
|
904
|
-
return true; // Suppress errors
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
]
|
|
909
|
-
}
|
|
910
|
-
);
|
|
894
|
+
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
911
895
|
|
|
912
|
-
//
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
896
|
+
// Fixed delay: 1000ms between each retry
|
|
897
|
+
await stableRequest({
|
|
898
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
899
|
+
attempts: 3,
|
|
900
|
+
wait: 1000,
|
|
901
|
+
retryStrategy: RETRY_STRATEGIES.FIXED
|
|
902
|
+
});
|
|
916
903
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
.
|
|
904
|
+
// Linear backoff: 1000ms, 2000ms, 3000ms
|
|
905
|
+
await stableRequest({
|
|
906
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
907
|
+
attempts: 3,
|
|
908
|
+
wait: 1000,
|
|
909
|
+
retryStrategy: RETRY_STRATEGIES.LINEAR
|
|
910
|
+
});
|
|
920
911
|
|
|
921
|
-
|
|
922
|
-
|
|
912
|
+
// Exponential backoff: 1000ms, 2000ms, 4000ms
|
|
913
|
+
await stableRequest({
|
|
914
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
915
|
+
attempts: 3,
|
|
916
|
+
wait: 1000,
|
|
917
|
+
maxAllowedWait: 10000,
|
|
918
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
919
|
+
});
|
|
923
920
|
```
|
|
924
921
|
|
|
925
|
-
###
|
|
922
|
+
### Circuit Breaker
|
|
926
923
|
|
|
927
|
-
|
|
928
|
-
const results = await stableApiGateway(
|
|
929
|
-
[
|
|
930
|
-
{ id: 'us-data', groupId: 'us-east', requestOptions: { reqData: { path: '/data' }, resReq: true } },
|
|
931
|
-
{ id: 'eu-data', groupId: 'eu-west', requestOptions: { reqData: { path: '/data' }, resReq: true } },
|
|
932
|
-
{ id: 'ap-data', groupId: 'ap-southeast', requestOptions: { reqData: { path: '/data' }, resReq: true } }
|
|
933
|
-
],
|
|
934
|
-
{
|
|
935
|
-
commonAttempts: 3,
|
|
936
|
-
|
|
937
|
-
requestGroups: [
|
|
938
|
-
{
|
|
939
|
-
id: 'us-east',
|
|
940
|
-
commonConfig: {
|
|
941
|
-
commonRequestData: {
|
|
942
|
-
hostname: 'api-us.example.com',
|
|
943
|
-
timeout: 5000, // Low latency expected
|
|
944
|
-
headers: { 'X-Region': 'us-east-1' }
|
|
945
|
-
},
|
|
946
|
-
commonAttempts: 3
|
|
947
|
-
}
|
|
948
|
-
},
|
|
949
|
-
{
|
|
950
|
-
id: 'eu-west',
|
|
951
|
-
commonConfig: {
|
|
952
|
-
commonRequestData: {
|
|
953
|
-
hostname: 'api-eu.example.com',
|
|
954
|
-
timeout: 8000, // Medium latency
|
|
955
|
-
headers: { 'X-Region': 'eu-west-1' }
|
|
956
|
-
},
|
|
957
|
-
commonAttempts: 5
|
|
958
|
-
}
|
|
959
|
-
},
|
|
960
|
-
{
|
|
961
|
-
id: 'ap-southeast',
|
|
962
|
-
commonConfig: {
|
|
963
|
-
commonRequestData: {
|
|
964
|
-
hostname: 'api-ap.example.com',
|
|
965
|
-
timeout: 12000, // Higher latency expected
|
|
966
|
-
headers: { 'X-Region': 'ap-southeast-1' }
|
|
967
|
-
},
|
|
968
|
-
commonAttempts: 7,
|
|
969
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
]
|
|
973
|
-
}
|
|
974
|
-
);
|
|
975
|
-
```
|
|
976
|
-
|
|
977
|
-
### Example: Shared Buffer is common across the entire batch of requests
|
|
924
|
+
Prevent cascading failures by automatically blocking requests when error thresholds are exceeded:
|
|
978
925
|
|
|
979
926
|
```typescript
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const requests = [
|
|
983
|
-
{
|
|
984
|
-
id: 'a',
|
|
985
|
-
requestOptions: {
|
|
986
|
-
reqData: { path: '/a' },
|
|
987
|
-
resReq: true,
|
|
988
|
-
preExecution: {
|
|
989
|
-
preExecutionHook: ({ commonBuffer }: any) => {
|
|
990
|
-
commonBuffer.fromA = true;
|
|
991
|
-
return {};
|
|
992
|
-
},
|
|
993
|
-
preExecutionHookParams: {},
|
|
994
|
-
applyPreExecutionConfigOverride: false,
|
|
995
|
-
continueOnPreExecutionHookFailure: false
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
},
|
|
999
|
-
{
|
|
1000
|
-
id: 'b',
|
|
1001
|
-
requestOptions: {
|
|
1002
|
-
reqData: { path: '/b' },
|
|
1003
|
-
resReq: true,
|
|
1004
|
-
preExecution: {
|
|
1005
|
-
preExecutionHook: ({ commonBuffer }: any) => {
|
|
1006
|
-
commonBuffer.fromB = true;
|
|
1007
|
-
return {};
|
|
1008
|
-
},
|
|
1009
|
-
preExecutionHookParams: {},
|
|
1010
|
-
applyPreExecutionConfigOverride: false,
|
|
1011
|
-
continueOnPreExecutionHookFailure: false
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
] satisfies API_GATEWAY_REQUEST[];
|
|
927
|
+
import { stableApiGateway, CircuitBreakerState } from '@emmvish/stable-request';
|
|
1016
928
|
|
|
1017
929
|
const results = await stableApiGateway(requests, {
|
|
1018
|
-
concurrentExecution: true,
|
|
1019
930
|
commonRequestData: { hostname: 'api.example.com' },
|
|
1020
|
-
|
|
931
|
+
circuitBreaker: {
|
|
932
|
+
failureThresholdPercentage: 50, // Open circuit at 50% failure rate
|
|
933
|
+
minimumRequests: 5, // Need at least 5 requests to calculate
|
|
934
|
+
recoveryTimeoutMs: 30000, // Try recovery after 30 seconds
|
|
935
|
+
trackIndividualAttempts: false // Track per-request success/failure
|
|
936
|
+
}
|
|
1021
937
|
});
|
|
1022
938
|
|
|
1023
|
-
|
|
1024
|
-
|
|
939
|
+
// Circuit breaker can be shared across workflows
|
|
940
|
+
const breaker = new CircuitBreaker({
|
|
941
|
+
failureThresholdPercentage: 50,
|
|
942
|
+
minimumRequests: 10,
|
|
943
|
+
recoveryTimeoutMs: 60000
|
|
944
|
+
});
|
|
1025
945
|
|
|
946
|
+
const result = await stableWorkflow(phases, {
|
|
947
|
+
circuitBreaker: breaker,
|
|
948
|
+
// ... other options
|
|
949
|
+
});
|
|
1026
950
|
|
|
1027
|
-
|
|
951
|
+
// Check circuit breaker state
|
|
952
|
+
const state = breaker.getState();
|
|
953
|
+
console.log(`Circuit breaker state: ${state.state}`); // CLOSED, OPEN, or HALF_OPEN
|
|
954
|
+
```
|
|
1028
955
|
|
|
1029
|
-
|
|
956
|
+
### Rate Limiting
|
|
1030
957
|
|
|
1031
|
-
|
|
958
|
+
Control request throughput to prevent overwhelming APIs:
|
|
1032
959
|
|
|
1033
960
|
```typescript
|
|
1034
|
-
import {
|
|
961
|
+
import { stableApiGateway } from '@emmvish/stable-request';
|
|
1035
962
|
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
{
|
|
1043
|
-
id: 'check-inventory',
|
|
1044
|
-
requestOptions: {
|
|
1045
|
-
reqData: { path: '/inventory/check' },
|
|
1046
|
-
resReq: true
|
|
1047
|
-
}
|
|
1048
|
-
},
|
|
1049
|
-
{
|
|
1050
|
-
id: 'validate-payment',
|
|
1051
|
-
requestOptions: {
|
|
1052
|
-
reqData: { path: '/payment/validate' },
|
|
1053
|
-
resReq: true
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
]
|
|
1057
|
-
},
|
|
1058
|
-
{
|
|
1059
|
-
id: 'processing',
|
|
1060
|
-
concurrentExecution: false,
|
|
1061
|
-
stopOnFirstError: true,
|
|
1062
|
-
requests: [
|
|
1063
|
-
{
|
|
1064
|
-
id: 'charge-payment',
|
|
1065
|
-
requestOptions: {
|
|
1066
|
-
reqData: { path: '/payment/charge', method: REQUEST_METHODS.POST },
|
|
1067
|
-
resReq: true
|
|
1068
|
-
}
|
|
1069
|
-
},
|
|
1070
|
-
{
|
|
1071
|
-
id: 'reserve-inventory',
|
|
1072
|
-
requestOptions: {
|
|
1073
|
-
reqData: { path: '/inventory/reserve', method: REQUEST_METHODS.POST },
|
|
1074
|
-
resReq: true
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
]
|
|
1078
|
-
}
|
|
1079
|
-
],
|
|
1080
|
-
{
|
|
1081
|
-
workflowId: 'order-processing-123',
|
|
1082
|
-
stopOnFirstPhaseError: true,
|
|
1083
|
-
logPhaseResults: true,
|
|
1084
|
-
commonRequestData: {
|
|
1085
|
-
hostname: 'api.example.com',
|
|
1086
|
-
headers: { 'X-Transaction-Id': 'txn-123' }
|
|
1087
|
-
},
|
|
1088
|
-
commonAttempts: 3,
|
|
1089
|
-
commonWait: 1000
|
|
963
|
+
const results = await stableApiGateway(requests, {
|
|
964
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
965
|
+
concurrentExecution: true,
|
|
966
|
+
rateLimit: {
|
|
967
|
+
maxRequests: 10, // Maximum 10 requests
|
|
968
|
+
windowMs: 1000 // Per 1 second window
|
|
1090
969
|
}
|
|
1091
|
-
);
|
|
1092
|
-
|
|
1093
|
-
console.log('Workflow completed:', workflow.success);
|
|
1094
|
-
console.log(`${workflow.successfulRequests}/${workflow.totalRequests} requests succeeded`);
|
|
1095
|
-
console.log(`Completed in ${workflow.executionTime}ms`);
|
|
1096
|
-
```
|
|
970
|
+
});
|
|
1097
971
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
totalPhases: number; // Number of phases defined
|
|
1106
|
-
completedPhases: number; // Number of phases executed
|
|
1107
|
-
totalRequests: number; // Total requests across all phases
|
|
1108
|
-
successfulRequests: number; // Successful requests
|
|
1109
|
-
failedRequests: number; // Failed requests
|
|
1110
|
-
phases: PHASE_RESULT[]; // Detailed results per phase
|
|
1111
|
-
error?: string; // Workflow-level error (if any)
|
|
1112
|
-
}
|
|
972
|
+
// Rate limiting in workflows
|
|
973
|
+
const result = await stableWorkflow(phases, {
|
|
974
|
+
rateLimit: {
|
|
975
|
+
maxRequests: 5,
|
|
976
|
+
windowMs: 1000
|
|
977
|
+
}
|
|
978
|
+
});
|
|
1113
979
|
```
|
|
1114
980
|
|
|
1115
|
-
###
|
|
981
|
+
### Caching
|
|
1116
982
|
|
|
1117
|
-
|
|
983
|
+
Cache responses with TTL to reduce redundant API calls:
|
|
1118
984
|
|
|
1119
985
|
```typescript
|
|
1120
|
-
{
|
|
1121
|
-
id: 'phase-name', // Optional: phase identifier
|
|
1122
|
-
concurrentExecution?: boolean, // true = parallel, false = sequential
|
|
1123
|
-
stopOnFirstError?: boolean, // Stop phase on first request failure
|
|
1124
|
-
commonConfig?: { /* phase-level common config */ },
|
|
1125
|
-
requests: [/* array of requests */],
|
|
1126
|
-
sharedBuffer?: Record<string, any> // State to be shared among all requests in the phase
|
|
1127
|
-
}
|
|
1128
|
-
```
|
|
1129
|
-
|
|
1130
|
-
**Configuration Priority:**
|
|
1131
|
-
Individual Request > Phase Common Config > Workflow Common Config
|
|
986
|
+
import { stableRequest, getGlobalCacheManager } from '@emmvish/stable-request';
|
|
1132
987
|
|
|
1133
|
-
|
|
988
|
+
// Enable caching for a request
|
|
989
|
+
const data = await stableRequest({
|
|
990
|
+
reqData: { hostname: 'api.example.com', path: '/users/123' },
|
|
991
|
+
resReq: true,
|
|
992
|
+
cache: {
|
|
993
|
+
enabled: true,
|
|
994
|
+
ttl: 60000 // Cache for 60 seconds
|
|
995
|
+
}
|
|
996
|
+
});
|
|
1134
997
|
|
|
1135
|
-
|
|
998
|
+
// Use global cache manager across requests
|
|
999
|
+
const results = await stableApiGateway(requests, {
|
|
1000
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
1001
|
+
commonCache: { enabled: true, ttl: 300000 } // 5 minutes
|
|
1002
|
+
});
|
|
1136
1003
|
|
|
1137
|
-
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
concurrentExecution: true,
|
|
1143
|
-
requests: [
|
|
1144
|
-
{
|
|
1145
|
-
id: 'auth-check',
|
|
1146
|
-
groupId: 'critical',
|
|
1147
|
-
requestOptions: {
|
|
1148
|
-
reqData: { path: '/auth/verify' },
|
|
1149
|
-
resReq: true
|
|
1150
|
-
}
|
|
1151
|
-
},
|
|
1152
|
-
{
|
|
1153
|
-
id: 'rate-limit-check',
|
|
1154
|
-
groupId: 'critical',
|
|
1155
|
-
requestOptions: {
|
|
1156
|
-
reqData: { path: '/ratelimit/check' },
|
|
1157
|
-
resReq: true
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
]
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
id: 'data-processing',
|
|
1164
|
-
concurrentExecution: false,
|
|
1165
|
-
commonConfig: {
|
|
1166
|
-
// Phase-specific overrides
|
|
1167
|
-
commonAttempts: 5,
|
|
1168
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1169
|
-
},
|
|
1170
|
-
requests: [
|
|
1171
|
-
{
|
|
1172
|
-
id: 'process-data',
|
|
1173
|
-
groupId: 'standard',
|
|
1174
|
-
requestOptions: {
|
|
1175
|
-
reqData: { path: '/data/process', method: REQUEST_METHODS.POST },
|
|
1176
|
-
resReq: true
|
|
1177
|
-
}
|
|
1178
|
-
},
|
|
1179
|
-
{
|
|
1180
|
-
id: 'store-result',
|
|
1181
|
-
groupId: 'standard',
|
|
1182
|
-
requestOptions: {
|
|
1183
|
-
reqData: { path: '/data/store', method: REQUEST_METHODS.POST },
|
|
1184
|
-
resReq: true
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
]
|
|
1188
|
-
},
|
|
1189
|
-
{
|
|
1190
|
-
id: 'notifications',
|
|
1191
|
-
concurrentExecution: true,
|
|
1192
|
-
requests: [
|
|
1193
|
-
{
|
|
1194
|
-
id: 'email-notification',
|
|
1195
|
-
groupId: 'optional',
|
|
1196
|
-
requestOptions: {
|
|
1197
|
-
reqData: { path: '/notify/email' },
|
|
1198
|
-
resReq: true
|
|
1199
|
-
}
|
|
1200
|
-
},
|
|
1201
|
-
{
|
|
1202
|
-
id: 'webhook-notification',
|
|
1203
|
-
groupId: 'optional',
|
|
1204
|
-
requestOptions: {
|
|
1205
|
-
reqData: { path: '/notify/webhook' },
|
|
1206
|
-
resReq: true
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
]
|
|
1210
|
-
}
|
|
1211
|
-
],
|
|
1212
|
-
{
|
|
1213
|
-
workflowId: 'data-pipeline-workflow',
|
|
1214
|
-
stopOnFirstPhaseError: true,
|
|
1215
|
-
logPhaseResults: true,
|
|
1216
|
-
|
|
1217
|
-
commonRequestData: {
|
|
1218
|
-
hostname: 'api.example.com'
|
|
1219
|
-
},
|
|
1220
|
-
commonAttempts: 3,
|
|
1221
|
-
commonWait: 1000,
|
|
1222
|
-
|
|
1223
|
-
// Request groups with different reliability requirements
|
|
1224
|
-
requestGroups: [
|
|
1225
|
-
{
|
|
1226
|
-
id: 'critical',
|
|
1227
|
-
commonConfig: {
|
|
1228
|
-
commonAttempts: 10,
|
|
1229
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
1230
|
-
commonWait: 2000,
|
|
1231
|
-
commonHandleErrors: async ({ errorLog }) => {
|
|
1232
|
-
await alerting.critical('Critical service failure', errorLog);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
},
|
|
1236
|
-
{
|
|
1237
|
-
id: 'standard',
|
|
1238
|
-
commonConfig: {
|
|
1239
|
-
commonAttempts: 5,
|
|
1240
|
-
commonRetryStrategy: RETRY_STRATEGIES.LINEAR,
|
|
1241
|
-
commonWait: 1000
|
|
1242
|
-
}
|
|
1243
|
-
},
|
|
1244
|
-
{
|
|
1245
|
-
id: 'optional',
|
|
1246
|
-
commonConfig: {
|
|
1247
|
-
commonAttempts: 2,
|
|
1248
|
-
commonFinalErrorAnalyzer: async () => true // Suppress errors
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
]
|
|
1252
|
-
}
|
|
1253
|
-
);
|
|
1004
|
+
// Manage cache manually
|
|
1005
|
+
const cacheManager = getGlobalCacheManager();
|
|
1006
|
+
const stats = cacheManager.getStats();
|
|
1007
|
+
console.log(`Cache size: ${stats.size}, Valid entries: ${stats.validEntries}`);
|
|
1008
|
+
cacheManager.clear(); // Clear all cache
|
|
1254
1009
|
```
|
|
1255
1010
|
|
|
1256
|
-
###
|
|
1011
|
+
### Pre-Execution Hooks
|
|
1257
1012
|
|
|
1258
|
-
|
|
1013
|
+
Transform requests dynamically before execution:
|
|
1259
1014
|
|
|
1260
1015
|
```typescript
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1016
|
+
import { stableRequest } from '@emmvish/stable-request';
|
|
1017
|
+
|
|
1018
|
+
const commonBuffer: Record<string, any> = {};
|
|
1019
|
+
|
|
1020
|
+
const data = await stableRequest({
|
|
1021
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
1022
|
+
resReq: true,
|
|
1023
|
+
preExecution: {
|
|
1024
|
+
preExecutionHook: async ({ inputParams, commonBuffer }) => {
|
|
1025
|
+
// Fetch authentication token
|
|
1026
|
+
const token = await getAuthToken();
|
|
1271
1027
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
duration: phaseResult.executionTime,
|
|
1276
|
-
successRate: phaseResult.successfulRequests / phaseResult.totalRequests
|
|
1277
|
-
});
|
|
1278
|
-
},
|
|
1279
|
-
|
|
1280
|
-
// Called when a phase fails
|
|
1281
|
-
handlePhaseError: async ({ workflowId, phaseResult, error, sharedBuffer }) => {
|
|
1282
|
-
console.error(`Phase ${phaseResult.phaseId} failed`);
|
|
1028
|
+
// Store in shared buffer
|
|
1029
|
+
commonBuffer.token = token;
|
|
1030
|
+
commonBuffer.timestamp = Date.now();
|
|
1283
1031
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1032
|
+
// Override request configuration
|
|
1033
|
+
return {
|
|
1034
|
+
reqData: {
|
|
1035
|
+
hostname: 'api.example.com',
|
|
1036
|
+
path: '/authenticated-data',
|
|
1037
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1290
1040
|
},
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1041
|
+
preExecutionHookParams: { userId: 'user123' },
|
|
1042
|
+
applyPreExecutionConfigOverride: true, // Apply returned config
|
|
1043
|
+
continueOnPreExecutionHookFailure: false
|
|
1044
|
+
},
|
|
1045
|
+
commonBuffer
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
console.log('Token used:', commonBuffer.token);
|
|
1295
1049
|
```
|
|
1296
1050
|
|
|
1297
|
-
###
|
|
1051
|
+
### Shared Buffer
|
|
1298
1052
|
|
|
1299
|
-
|
|
1053
|
+
Share state across requests in gateways and workflows:
|
|
1300
1054
|
|
|
1301
1055
|
```typescript
|
|
1302
|
-
|
|
1056
|
+
import { stableWorkflow } from '@emmvish/stable-request';
|
|
1303
1057
|
|
|
1304
|
-
const
|
|
1058
|
+
const sharedBuffer: Record<string, any> = { requestCount: 0 };
|
|
1059
|
+
|
|
1060
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1305
1061
|
{
|
|
1306
|
-
id: '
|
|
1307
|
-
concurrentExecution: false,
|
|
1062
|
+
id: 'phase-1',
|
|
1308
1063
|
requests: [
|
|
1309
1064
|
{
|
|
1310
|
-
id: '
|
|
1065
|
+
id: 'req-1',
|
|
1311
1066
|
requestOptions: {
|
|
1312
|
-
reqData: { path: '/
|
|
1067
|
+
reqData: { path: '/step1' },
|
|
1313
1068
|
resReq: true,
|
|
1314
1069
|
preExecution: {
|
|
1315
|
-
preExecutionHook: ({ commonBuffer }
|
|
1316
|
-
commonBuffer.
|
|
1317
|
-
commonBuffer.
|
|
1070
|
+
preExecutionHook: ({ commonBuffer }) => {
|
|
1071
|
+
commonBuffer.requestCount++;
|
|
1072
|
+
commonBuffer.phase1Data = 'initialized';
|
|
1318
1073
|
return {};
|
|
1319
1074
|
},
|
|
1320
1075
|
preExecutionHookParams: {},
|
|
@@ -1326,17 +1081,18 @@ const phases = [
|
|
|
1326
1081
|
]
|
|
1327
1082
|
},
|
|
1328
1083
|
{
|
|
1329
|
-
id: '
|
|
1330
|
-
concurrentExecution: false,
|
|
1084
|
+
id: 'phase-2',
|
|
1331
1085
|
requests: [
|
|
1332
1086
|
{
|
|
1333
|
-
id: '
|
|
1087
|
+
id: 'req-2',
|
|
1334
1088
|
requestOptions: {
|
|
1335
|
-
reqData: { path: '/
|
|
1089
|
+
reqData: { path: '/step2' },
|
|
1336
1090
|
resReq: true,
|
|
1337
1091
|
preExecution: {
|
|
1338
|
-
preExecutionHook: ({ commonBuffer }
|
|
1339
|
-
commonBuffer.
|
|
1092
|
+
preExecutionHook: ({ commonBuffer }) => {
|
|
1093
|
+
commonBuffer.requestCount++;
|
|
1094
|
+
// Access data from phase-1
|
|
1095
|
+
console.log('Phase 1 data:', commonBuffer.phase1Data);
|
|
1340
1096
|
return {};
|
|
1341
1097
|
},
|
|
1342
1098
|
preExecutionHookParams: {},
|
|
@@ -1347,715 +1103,502 @@ const phases = [
|
|
|
1347
1103
|
}
|
|
1348
1104
|
]
|
|
1349
1105
|
}
|
|
1350
|
-
]
|
|
1106
|
+
];
|
|
1351
1107
|
|
|
1352
1108
|
const result = await stableWorkflow(phases, {
|
|
1353
|
-
workflowId: '
|
|
1109
|
+
workflowId: 'stateful-workflow',
|
|
1354
1110
|
commonRequestData: { hostname: 'api.example.com' },
|
|
1355
|
-
sharedBuffer
|
|
1111
|
+
sharedBuffer
|
|
1356
1112
|
});
|
|
1357
1113
|
|
|
1358
|
-
console.log(
|
|
1114
|
+
console.log('Total requests processed:', sharedBuffer.requestCount);
|
|
1359
1115
|
```
|
|
1360
|
-
|
|
1116
|
+
|
|
1117
|
+
### Request Grouping
|
|
1118
|
+
|
|
1119
|
+
Apply different configurations to request groups:
|
|
1361
1120
|
|
|
1362
1121
|
```typescript
|
|
1363
|
-
|
|
1122
|
+
import { stableApiGateway } from '@emmvish/stable-request';
|
|
1123
|
+
|
|
1124
|
+
const requests = [
|
|
1364
1125
|
{
|
|
1365
|
-
id: '
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
]
|
|
1126
|
+
id: 'critical-1',
|
|
1127
|
+
groupId: 'critical',
|
|
1128
|
+
requestOptions: { reqData: { path: '/critical/1' }, resReq: true }
|
|
1369
1129
|
},
|
|
1370
1130
|
{
|
|
1371
|
-
id: '
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
]
|
|
1131
|
+
id: 'critical-2',
|
|
1132
|
+
groupId: 'critical',
|
|
1133
|
+
requestOptions: { reqData: { path: '/critical/2' }, resReq: true }
|
|
1375
1134
|
},
|
|
1376
1135
|
{
|
|
1377
|
-
id: '
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
]
|
|
1136
|
+
id: 'optional-1',
|
|
1137
|
+
groupId: 'optional',
|
|
1138
|
+
requestOptions: { reqData: { path: '/optional/1' }, resReq: true }
|
|
1381
1139
|
}
|
|
1382
|
-
]
|
|
1140
|
+
];
|
|
1383
1141
|
|
|
1384
|
-
const
|
|
1385
|
-
workflowId: 'wf-concurrent-phases',
|
|
1142
|
+
const results = await stableApiGateway(requests, {
|
|
1386
1143
|
commonRequestData: { hostname: 'api.example.com' },
|
|
1387
1144
|
commonAttempts: 1,
|
|
1388
|
-
commonWait:
|
|
1389
|
-
|
|
1390
|
-
});
|
|
1391
|
-
```
|
|
1392
|
-
### Mixed Execution of Phases
|
|
1393
|
-
|
|
1394
|
-
```typescript
|
|
1395
|
-
const workflow = await stableWorkflow(
|
|
1396
|
-
[
|
|
1397
|
-
{
|
|
1398
|
-
id: 'phase-1-sequential',
|
|
1399
|
-
requests: [/* ... */]
|
|
1400
|
-
},
|
|
1401
|
-
{
|
|
1402
|
-
id: 'phase-2-concurrent-start',
|
|
1403
|
-
markConcurrentPhase: true, // Will run concurrently with phase-3
|
|
1404
|
-
requests: [/* ... */]
|
|
1405
|
-
},
|
|
1145
|
+
commonWait: 100,
|
|
1146
|
+
requestGroups: [
|
|
1406
1147
|
{
|
|
1407
|
-
id: '
|
|
1408
|
-
|
|
1409
|
-
|
|
1148
|
+
id: 'critical',
|
|
1149
|
+
commonConfig: {
|
|
1150
|
+
commonAttempts: 5, // More retries for critical requests
|
|
1151
|
+
commonWait: 2000,
|
|
1152
|
+
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1153
|
+
}
|
|
1410
1154
|
},
|
|
1411
1155
|
{
|
|
1412
|
-
id: '
|
|
1413
|
-
|
|
1156
|
+
id: 'optional',
|
|
1157
|
+
commonConfig: {
|
|
1158
|
+
commonAttempts: 1, // No retries for optional requests
|
|
1159
|
+
commonFinalErrorAnalyzer: async () => true // Suppress errors
|
|
1160
|
+
}
|
|
1414
1161
|
}
|
|
1415
|
-
]
|
|
1416
|
-
|
|
1417
|
-
workflowId: 'mixed-execution-workflow',
|
|
1418
|
-
allowExecutionMixing: true // Enable mixed execution mode
|
|
1419
|
-
}
|
|
1420
|
-
);
|
|
1162
|
+
]
|
|
1163
|
+
});
|
|
1421
1164
|
```
|
|
1422
1165
|
|
|
1423
|
-
|
|
1166
|
+
### Concurrency Control
|
|
1424
1167
|
|
|
1425
|
-
|
|
1168
|
+
Limit concurrent request execution:
|
|
1426
1169
|
|
|
1427
1170
|
```typescript
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
},
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
wait: 3000, // Wait 3 seconds between polls
|
|
1436
|
-
retryStrategy: RETRY_STRATEGIES.FIXED,
|
|
1437
|
-
|
|
1438
|
-
responseAnalyzer: async ({ data }) => {
|
|
1439
|
-
if (data.status === 'completed') {
|
|
1440
|
-
console.log('Job completed!');
|
|
1441
|
-
return true; // Success
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
if (data.status === 'failed') {
|
|
1445
|
-
throw new Error(`Job failed: ${data.error}`);
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
console.log(`Job ${data.status}... ${data.progress}%`);
|
|
1449
|
-
return false; // Keep polling
|
|
1450
|
-
},
|
|
1451
|
-
|
|
1452
|
-
handleErrors: async ({ errorLog }) => {
|
|
1453
|
-
console.log(`Poll attempt ${errorLog.attempt}`);
|
|
1454
|
-
}
|
|
1171
|
+
import { stableApiGateway } from '@emmvish/stable-request';
|
|
1172
|
+
|
|
1173
|
+
// Limit to 5 concurrent requests
|
|
1174
|
+
const results = await stableApiGateway(requests, {
|
|
1175
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
1176
|
+
concurrentExecution: true,
|
|
1177
|
+
maxConcurrentRequests: 5
|
|
1455
1178
|
});
|
|
1456
1179
|
|
|
1457
|
-
|
|
1180
|
+
// Phase-level concurrency control
|
|
1181
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1182
|
+
{
|
|
1183
|
+
id: 'limited-phase',
|
|
1184
|
+
concurrentExecution: true,
|
|
1185
|
+
maxConcurrentRequests: 3,
|
|
1186
|
+
requests: [/* ... */]
|
|
1187
|
+
}
|
|
1188
|
+
];
|
|
1458
1189
|
```
|
|
1459
1190
|
|
|
1460
|
-
###
|
|
1191
|
+
### Response Analysis
|
|
1192
|
+
|
|
1193
|
+
Validate response content and trigger retries:
|
|
1461
1194
|
|
|
1462
1195
|
```typescript
|
|
1463
|
-
|
|
1196
|
+
import { stableRequest } from '@emmvish/stable-request';
|
|
1464
1197
|
|
|
1465
1198
|
const data = await stableRequest({
|
|
1466
|
-
reqData: {
|
|
1467
|
-
hostname: 'replica.db.example.com',
|
|
1468
|
-
path: '/records/123'
|
|
1469
|
-
},
|
|
1199
|
+
reqData: { hostname: 'api.example.com', path: '/job/status' },
|
|
1470
1200
|
resReq: true,
|
|
1471
1201
|
attempts: 10,
|
|
1472
|
-
wait:
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
responseAnalyzer: async ({ data, params }) => {
|
|
1480
|
-
// Wait until replica catches up
|
|
1481
|
-
if (data.version >= params.expectedVersion) {
|
|
1482
|
-
return true;
|
|
1202
|
+
wait: 2000,
|
|
1203
|
+
responseAnalyzer: async ({ data, reqData, params }) => {
|
|
1204
|
+
// Retry until job is completed
|
|
1205
|
+
if (data.status === 'processing') {
|
|
1206
|
+
console.log('Job still processing, will retry...');
|
|
1207
|
+
return false; // Trigger retry
|
|
1483
1208
|
}
|
|
1484
|
-
|
|
1485
|
-
console.log(`Replica at version ${data.version}, waiting for ${params.expectedVersion}`);
|
|
1486
|
-
return false;
|
|
1209
|
+
return data.status === 'completed';
|
|
1487
1210
|
}
|
|
1488
1211
|
});
|
|
1212
|
+
|
|
1213
|
+
console.log('Job completed:', data);
|
|
1489
1214
|
```
|
|
1490
1215
|
|
|
1491
|
-
###
|
|
1216
|
+
### Error Handling
|
|
1217
|
+
|
|
1218
|
+
Comprehensive error handling with observability hooks:
|
|
1492
1219
|
|
|
1493
1220
|
```typescript
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
method: REQUEST_METHODS.POST,
|
|
1499
|
-
headers: {
|
|
1500
|
-
'Authorization': 'Bearer sk_...',
|
|
1501
|
-
'Idempotency-Key': crypto.randomUUID() // Ensure idempotency
|
|
1502
|
-
},
|
|
1503
|
-
body: {
|
|
1504
|
-
amount: 1000,
|
|
1505
|
-
currency: 'usd',
|
|
1506
|
-
source: 'tok_visa'
|
|
1507
|
-
}
|
|
1508
|
-
},
|
|
1221
|
+
import { stableRequest } from '@emmvish/stable-request';
|
|
1222
|
+
|
|
1223
|
+
const data = await stableRequest({
|
|
1224
|
+
reqData: { hostname: 'api.example.com', path: '/data' },
|
|
1509
1225
|
resReq: true,
|
|
1510
|
-
attempts:
|
|
1511
|
-
wait:
|
|
1512
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
1513
|
-
|
|
1226
|
+
attempts: 3,
|
|
1227
|
+
wait: 1000,
|
|
1514
1228
|
logAllErrors: true,
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1229
|
+
handleErrors: ({ reqData, errorLog, params }) => {
|
|
1230
|
+
// Custom error logging
|
|
1231
|
+
console.error('Request failed:', {
|
|
1232
|
+
url: reqData.url,
|
|
1519
1233
|
attempt: errorLog.attempt,
|
|
1234
|
+
statusCode: errorLog.statusCode,
|
|
1520
1235
|
error: errorLog.error,
|
|
1521
1236
|
isRetryable: errorLog.isRetryable
|
|
1522
1237
|
});
|
|
1238
|
+
|
|
1239
|
+
// Send to monitoring service
|
|
1240
|
+
monitoringService.trackError(errorLog);
|
|
1523
1241
|
},
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
return data.status === 'succeeded' && data.paid === true;
|
|
1242
|
+
logAllSuccessfulAttempts: true,
|
|
1243
|
+
handleSuccessfulAttemptData: ({ successfulAttemptData }) => {
|
|
1244
|
+
console.log('Request succeeded on attempt:', successfulAttemptData.attempt);
|
|
1528
1245
|
},
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1246
|
+
finalErrorAnalyzer: async ({ error, reqData }) => {
|
|
1247
|
+
// Gracefully handle specific errors
|
|
1248
|
+
if (error.response?.status === 404) {
|
|
1249
|
+
console.warn('Resource not found, continuing...');
|
|
1250
|
+
return true; // Return false to suppress error
|
|
1251
|
+
}
|
|
1533
1252
|
return false; // Throw error
|
|
1534
1253
|
}
|
|
1535
1254
|
});
|
|
1536
1255
|
```
|
|
1537
1256
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
```typescript
|
|
1541
|
-
const users = [
|
|
1542
|
-
{ name: 'Alice', email: 'alice@example.com' },
|
|
1543
|
-
{ name: 'Bob', email: 'bob@example.com' },
|
|
1544
|
-
{ name: 'Charlie', email: 'charlie@example.com' }
|
|
1545
|
-
];
|
|
1546
|
-
|
|
1547
|
-
const requests = users.map((user, index) => ({
|
|
1548
|
-
id: `user-${index}`,
|
|
1549
|
-
requestOptions: {
|
|
1550
|
-
reqData: {
|
|
1551
|
-
body: user
|
|
1552
|
-
},
|
|
1553
|
-
resReq: true
|
|
1554
|
-
}
|
|
1555
|
-
}));
|
|
1556
|
-
|
|
1557
|
-
const results = await stableApiGateway(requests, {
|
|
1558
|
-
concurrentExecution: true,
|
|
1559
|
-
|
|
1560
|
-
commonRequestData: {
|
|
1561
|
-
hostname: 'api.example.com',
|
|
1562
|
-
path: '/users',
|
|
1563
|
-
method: REQUEST_METHODS.POST,
|
|
1564
|
-
headers: {
|
|
1565
|
-
'Content-Type': 'application/json'
|
|
1566
|
-
}
|
|
1567
|
-
},
|
|
1568
|
-
|
|
1569
|
-
commonAttempts: 3,
|
|
1570
|
-
commonWait: 1000,
|
|
1571
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
1572
|
-
commonResReq: true,
|
|
1573
|
-
commonLogAllErrors: true,
|
|
1574
|
-
|
|
1575
|
-
commonHandleErrors: async ({ reqData, errorLog }) => {
|
|
1576
|
-
const user = reqData.data;
|
|
1577
|
-
console.error(`Failed to create user ${user.name}: ${errorLog.error}`);
|
|
1578
|
-
},
|
|
1579
|
-
|
|
1580
|
-
commonResponseAnalyzer: async ({ data }) => {
|
|
1581
|
-
// Ensure user was created with an ID
|
|
1582
|
-
return data?.id && data?.email;
|
|
1583
|
-
}
|
|
1584
|
-
});
|
|
1585
|
-
|
|
1586
|
-
const successful = results.filter(r => r.success);
|
|
1587
|
-
const failed = results.filter(r => !r.success);
|
|
1588
|
-
|
|
1589
|
-
console.log(`✓ Created ${successful.length} users`);
|
|
1590
|
-
console.log(`✗ Failed to create ${failed.length} users`);
|
|
1591
|
-
|
|
1592
|
-
failed.forEach(r => {
|
|
1593
|
-
console.error(` - ${r.requestId}: ${r.error}`);
|
|
1594
|
-
});
|
|
1595
|
-
```
|
|
1257
|
+
## Advanced Use Cases
|
|
1596
1258
|
|
|
1597
|
-
###
|
|
1259
|
+
### Use Case 1: Multi-Tenant API with Dynamic Authentication
|
|
1598
1260
|
|
|
1599
1261
|
```typescript
|
|
1600
|
-
|
|
1601
|
-
[
|
|
1602
|
-
// Core services - must be healthy
|
|
1603
|
-
{ id: 'auth', groupId: 'core', requestOptions: { reqData: { hostname: 'auth.internal', path: '/health' } } },
|
|
1604
|
-
{ id: 'database', groupId: 'core', requestOptions: { reqData: { hostname: 'db.internal', path: '/health' } } },
|
|
1605
|
-
{ id: 'api', groupId: 'core', requestOptions: { reqData: { hostname: 'api.internal', path: '/health' } } },
|
|
1606
|
-
|
|
1607
|
-
// Optional services
|
|
1608
|
-
{ id: 'cache', groupId: 'optional', requestOptions: { reqData: { hostname: 'cache.internal', path: '/health' } } },
|
|
1609
|
-
{ id: 'search', groupId: 'optional', requestOptions: { reqData: { hostname: 'search.internal', path: '/health' } } }
|
|
1610
|
-
],
|
|
1611
|
-
{
|
|
1612
|
-
commonResReq: true,
|
|
1613
|
-
concurrentExecution: true,
|
|
1614
|
-
|
|
1615
|
-
requestGroups: [
|
|
1616
|
-
{
|
|
1617
|
-
id: 'core',
|
|
1618
|
-
commonConfig: {
|
|
1619
|
-
commonAttempts: 5,
|
|
1620
|
-
commonWait: 2000,
|
|
1621
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
1622
|
-
|
|
1623
|
-
commonResponseAnalyzer: async ({ data }) => {
|
|
1624
|
-
// Core services need strict validation
|
|
1625
|
-
return data?.status === 'healthy' &&
|
|
1626
|
-
data?.uptime > 0 &&
|
|
1627
|
-
data?.dependencies?.every(d => d.healthy);
|
|
1628
|
-
},
|
|
1629
|
-
|
|
1630
|
-
commonHandleErrors: async ({ reqData, errorLog }) => {
|
|
1631
|
-
// Alert on core service issues
|
|
1632
|
-
await pagerDuty.trigger({
|
|
1633
|
-
severity: 'critical',
|
|
1634
|
-
service: reqData.baseURL,
|
|
1635
|
-
message: errorLog.error
|
|
1636
|
-
});
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
},
|
|
1640
|
-
{
|
|
1641
|
-
id: 'optional',
|
|
1642
|
-
commonConfig: {
|
|
1643
|
-
commonAttempts: 2,
|
|
1644
|
-
|
|
1645
|
-
commonResponseAnalyzer: async ({ data }) => {
|
|
1646
|
-
// Optional services: basic check
|
|
1647
|
-
return data?.status === 'ok';
|
|
1648
|
-
},
|
|
1649
|
-
|
|
1650
|
-
commonFinalErrorAnalyzer: async ({ reqData, error }) => {
|
|
1651
|
-
// Log but don't alert
|
|
1652
|
-
console.warn(`Optional service ${reqData.baseURL} unhealthy`);
|
|
1653
|
-
return true; // Don't throw
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
]
|
|
1658
|
-
}
|
|
1659
|
-
);
|
|
1660
|
-
|
|
1661
|
-
const report = {
|
|
1662
|
-
timestamp: new Date().toISOString(),
|
|
1663
|
-
core: healthChecks.filter(r => r.groupId === 'core').every(r => r.success),
|
|
1664
|
-
optional: healthChecks.filter(r => r.groupId === 'optional').every(r => r.success),
|
|
1665
|
-
overall: healthChecks.every(r => r.success) ? 'HEALTHY' : 'DEGRADED'
|
|
1666
|
-
};
|
|
1262
|
+
import { stableWorkflow, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
1667
1263
|
|
|
1668
|
-
|
|
1669
|
-
|
|
1264
|
+
interface TenantConfig {
|
|
1265
|
+
tenantId: string;
|
|
1266
|
+
apiKey: string;
|
|
1267
|
+
baseUrl: string;
|
|
1268
|
+
}
|
|
1670
1269
|
|
|
1671
|
-
|
|
1270
|
+
async function executeTenantWorkflow(tenantConfig: TenantConfig) {
|
|
1271
|
+
const sharedBuffer: Record<string, any> = {
|
|
1272
|
+
tenantId: tenantConfig.tenantId,
|
|
1273
|
+
authToken: null,
|
|
1274
|
+
processedItems: []
|
|
1275
|
+
};
|
|
1672
1276
|
|
|
1673
|
-
|
|
1674
|
-
const etlWorkflow = await stableWorkflow(
|
|
1675
|
-
[
|
|
1277
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1676
1278
|
{
|
|
1677
|
-
id: '
|
|
1678
|
-
concurrentExecution: true,
|
|
1679
|
-
commonConfig: {
|
|
1680
|
-
commonAttempts: 5,
|
|
1681
|
-
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1682
|
-
},
|
|
1279
|
+
id: 'authentication',
|
|
1683
1280
|
requests: [
|
|
1684
|
-
{
|
|
1685
|
-
|
|
1686
|
-
|
|
1281
|
+
{
|
|
1282
|
+
id: 'get-token',
|
|
1283
|
+
requestOptions: {
|
|
1284
|
+
reqData: {
|
|
1285
|
+
path: '/auth/token',
|
|
1286
|
+
method: 'POST',
|
|
1287
|
+
headers: { 'X-API-Key': tenantConfig.apiKey }
|
|
1288
|
+
},
|
|
1289
|
+
resReq: true,
|
|
1290
|
+
attempts: 3,
|
|
1291
|
+
wait: 2000,
|
|
1292
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
|
|
1293
|
+
responseAnalyzer: async ({ data, commonBuffer }) => {
|
|
1294
|
+
if (data?.token) {
|
|
1295
|
+
commonBuffer.authToken = data.token;
|
|
1296
|
+
commonBuffer.tokenExpiry = Date.now() + (data.expiresIn * 1000);
|
|
1297
|
+
return true;
|
|
1298
|
+
}
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1687
1303
|
]
|
|
1688
1304
|
},
|
|
1689
1305
|
{
|
|
1690
|
-
id: '
|
|
1691
|
-
concurrentExecution:
|
|
1692
|
-
|
|
1306
|
+
id: 'data-fetching',
|
|
1307
|
+
concurrentExecution: true,
|
|
1308
|
+
maxConcurrentRequests: 5,
|
|
1693
1309
|
requests: [
|
|
1694
1310
|
{
|
|
1695
|
-
id: '
|
|
1696
|
-
requestOptions: {
|
|
1697
|
-
reqData: { path: '/transform/clean', method: REQUEST_METHODS.POST },
|
|
1698
|
-
resReq: true
|
|
1699
|
-
}
|
|
1700
|
-
},
|
|
1701
|
-
{
|
|
1702
|
-
id: 'enrich-data',
|
|
1311
|
+
id: 'fetch-users',
|
|
1703
1312
|
requestOptions: {
|
|
1704
|
-
reqData: { path: '/
|
|
1705
|
-
resReq: true
|
|
1313
|
+
reqData: { path: '/users' },
|
|
1314
|
+
resReq: true,
|
|
1315
|
+
preExecution: {
|
|
1316
|
+
preExecutionHook: ({ commonBuffer }) => ({
|
|
1317
|
+
reqData: {
|
|
1318
|
+
path: '/users',
|
|
1319
|
+
headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
|
|
1320
|
+
}
|
|
1321
|
+
}),
|
|
1322
|
+
applyPreExecutionConfigOverride: true
|
|
1323
|
+
}
|
|
1706
1324
|
}
|
|
1707
1325
|
},
|
|
1708
1326
|
{
|
|
1709
|
-
id: '
|
|
1327
|
+
id: 'fetch-settings',
|
|
1710
1328
|
requestOptions: {
|
|
1711
|
-
reqData: { path: '/
|
|
1329
|
+
reqData: { path: '/settings' },
|
|
1712
1330
|
resReq: true,
|
|
1713
|
-
|
|
1714
|
-
|
|
1331
|
+
preExecution: {
|
|
1332
|
+
preExecutionHook: ({ commonBuffer }) => ({
|
|
1333
|
+
reqData: {
|
|
1334
|
+
path: '/settings',
|
|
1335
|
+
headers: { Authorization: `Bearer ${commonBuffer.authToken}` }
|
|
1336
|
+
}
|
|
1337
|
+
}),
|
|
1338
|
+
applyPreExecutionConfigOverride: true
|
|
1715
1339
|
}
|
|
1716
1340
|
}
|
|
1717
1341
|
}
|
|
1718
1342
|
]
|
|
1719
1343
|
},
|
|
1720
1344
|
{
|
|
1721
|
-
id: '
|
|
1345
|
+
id: 'data-processing',
|
|
1722
1346
|
concurrentExecution: true,
|
|
1723
1347
|
requests: [
|
|
1724
1348
|
{
|
|
1725
|
-
id: '
|
|
1726
|
-
requestOptions: {
|
|
1727
|
-
reqData: { path: '/load/warehouse', method: REQUEST_METHODS.POST },
|
|
1728
|
-
resReq: true
|
|
1729
|
-
}
|
|
1730
|
-
},
|
|
1731
|
-
{
|
|
1732
|
-
id: 'update-indexes',
|
|
1733
|
-
requestOptions: {
|
|
1734
|
-
reqData: { path: '/load/indexes', method: REQUEST_METHODS.POST },
|
|
1735
|
-
resReq: true
|
|
1736
|
-
}
|
|
1737
|
-
},
|
|
1738
|
-
{
|
|
1739
|
-
id: 'refresh-cache',
|
|
1349
|
+
id: 'process-users',
|
|
1740
1350
|
requestOptions: {
|
|
1741
|
-
reqData: { path: '/
|
|
1742
|
-
resReq: true
|
|
1351
|
+
reqData: { path: '/process/users', method: 'POST' },
|
|
1352
|
+
resReq: true,
|
|
1353
|
+
preExecution: {
|
|
1354
|
+
preExecutionHook: ({ commonBuffer }) => {
|
|
1355
|
+
const usersPhase = commonBuffer.phases?.find(p => p.phaseId === 'data-fetching');
|
|
1356
|
+
const usersData = usersPhase?.responses?.find(r => r.requestId === 'fetch-users')?.data;
|
|
1357
|
+
|
|
1358
|
+
return {
|
|
1359
|
+
reqData: {
|
|
1360
|
+
path: '/process/users',
|
|
1361
|
+
method: 'POST',
|
|
1362
|
+
headers: { Authorization: `Bearer ${commonBuffer.authToken}` },
|
|
1363
|
+
body: { users: usersData }
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
},
|
|
1367
|
+
applyPreExecutionConfigOverride: true
|
|
1368
|
+
},
|
|
1369
|
+
responseAnalyzer: async ({ data, commonBuffer }) => {
|
|
1370
|
+
if (data?.processed) {
|
|
1371
|
+
commonBuffer.processedItems.push(...data.processed);
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1743
1376
|
}
|
|
1744
1377
|
}
|
|
1745
1378
|
]
|
|
1746
1379
|
}
|
|
1747
|
-
]
|
|
1748
|
-
|
|
1749
|
-
|
|
1380
|
+
];
|
|
1381
|
+
|
|
1382
|
+
const result = await stableWorkflow(phases, {
|
|
1383
|
+
workflowId: `tenant-${tenantConfig.tenantId}-workflow`,
|
|
1384
|
+
commonRequestData: {
|
|
1385
|
+
hostname: tenantConfig.baseUrl,
|
|
1386
|
+
headers: { 'X-Tenant-ID': tenantConfig.tenantId }
|
|
1387
|
+
},
|
|
1750
1388
|
stopOnFirstPhaseError: true,
|
|
1751
1389
|
logPhaseResults: true,
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1390
|
+
sharedBuffer,
|
|
1391
|
+
circuitBreaker: {
|
|
1392
|
+
failureThresholdPercentage: 40,
|
|
1393
|
+
minimumRequests: 5,
|
|
1394
|
+
recoveryTimeoutMs: 30000
|
|
1755
1395
|
},
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
handlePhaseCompletion: async ({ phaseResult }) => {
|
|
1760
|
-
const recordsProcessed = phaseResult.responses
|
|
1761
|
-
.filter(r => r.success)
|
|
1762
|
-
.reduce((sum, r) => sum + (r.data?.recordCount || 0), 0);
|
|
1763
|
-
|
|
1764
|
-
await metrics.gauge('etl.phase.records', recordsProcessed, {
|
|
1765
|
-
phase: phaseResult.phaseId
|
|
1766
|
-
});
|
|
1396
|
+
rateLimit: {
|
|
1397
|
+
maxRequests: 20,
|
|
1398
|
+
windowMs: 1000
|
|
1767
1399
|
},
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1400
|
+
commonCache: {
|
|
1401
|
+
enabled: true,
|
|
1402
|
+
ttl: 300000 // Cache for 5 minutes
|
|
1403
|
+
},
|
|
1404
|
+
handlePhaseCompletion: ({ workflowId, phaseResult }) => {
|
|
1405
|
+
console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
|
|
1406
|
+
success: phaseResult.success,
|
|
1407
|
+
successfulRequests: phaseResult.successfulRequests,
|
|
1408
|
+
executionTime: `${phaseResult.executionTime}ms`
|
|
1774
1409
|
});
|
|
1410
|
+
},
|
|
1411
|
+
handlePhaseError: ({ workflowId, error, phaseResult }) => {
|
|
1412
|
+
console.error(`[${workflowId}] Phase ${phaseResult.phaseId} failed:`, error);
|
|
1413
|
+
// Send to monitoring
|
|
1414
|
+
monitoringService.trackPhaseError(workflowId, phaseResult.phaseId, error);
|
|
1775
1415
|
}
|
|
1776
|
-
}
|
|
1777
|
-
);
|
|
1778
|
-
|
|
1779
|
-
console.log(`ETL Pipeline: ${etlWorkflow.success ? 'SUCCESS' : 'FAILED'}`);
|
|
1780
|
-
console.log(`Total time: ${etlWorkflow.executionTime}ms`);
|
|
1781
|
-
console.log(`Records processed: ${etlWorkflow.successfulRequests}/${etlWorkflow.totalRequests}`);
|
|
1782
|
-
```
|
|
1783
|
-
|
|
1784
|
-
## Complete API Reference
|
|
1785
|
-
|
|
1786
|
-
### `stableRequest(options)`
|
|
1787
|
-
|
|
1788
|
-
| Option | Type | Default | Description |
|
|
1789
|
-
|--------|------|---------|-------------|
|
|
1790
|
-
| `reqData` | `REQUEST_DATA` | **required** | Request configuration |
|
|
1791
|
-
| `resReq` | `boolean` | `false` | Return response data vs. just boolean |
|
|
1792
|
-
| `attempts` | `number` | `1` | Max retry attempts |
|
|
1793
|
-
| `wait` | `number` | `1000` | Base delay between retries (ms) |
|
|
1794
|
-
| `maxAllowedWait` | `number` | `60000` | Maximum permitted wait duration between retries (ms) |
|
|
1795
|
-
| `retryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Retry backoff strategy |
|
|
1796
|
-
| `performAllAttempts` | `boolean` | `false` | Execute all attempts regardless |
|
|
1797
|
-
| `logAllErrors` | `boolean` | `false` | Enable error logging |
|
|
1798
|
-
| `logAllSuccessfulAttempts` | `boolean` | `false` | Enable success logging |
|
|
1799
|
-
| `maxSerializableChars` | `number` | `1000` | Max chars for logs |
|
|
1800
|
-
| `trialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Failure simulation |
|
|
1801
|
-
| `hookParams` | `HookParams` | `{}` | Custom parameters for hooks |
|
|
1802
|
-
| `preExecution` | `RequestPreExecutionOptions` | `{}` | Executes before actually sending request, can modify request config |
|
|
1803
|
-
| `commonBuffer` | `Record<string, any>` | `{}` | For communication between various request hooks |
|
|
1804
|
-
| `responseAnalyzer` | `function` | `() => true` | Validate response content |
|
|
1805
|
-
| `handleErrors` | `function` | `console.log` | Error handler |
|
|
1806
|
-
| `handleSuccessfulAttemptData` | `function` | `console.log` | Success handler |
|
|
1807
|
-
| `finalErrorAnalyzer` | `function` | `() => false` | Final error handler |
|
|
1808
|
-
|
|
1809
|
-
### REQUEST_DATA
|
|
1810
|
-
|
|
1811
|
-
```typescript
|
|
1812
|
-
interface REQUEST_DATA<RequestDataType = any> {
|
|
1813
|
-
hostname: string; // Required
|
|
1814
|
-
protocol?: 'http' | 'https'; // Default: 'https'
|
|
1815
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; // Default: 'GET'
|
|
1816
|
-
path?: `/${string}`; // Default: ''
|
|
1817
|
-
port?: number; // Default: 443
|
|
1818
|
-
headers?: Record<string, any>; // Default: {}
|
|
1819
|
-
body?: RequestDataType; // Request body
|
|
1820
|
-
query?: Record<string, any>; // Query parameters
|
|
1821
|
-
timeout?: number; // Default: 15000ms
|
|
1822
|
-
signal?: AbortSignal; // For cancellation
|
|
1823
|
-
}
|
|
1824
|
-
```
|
|
1825
|
-
|
|
1826
|
-
### `stableApiGateway(requests, options)`
|
|
1827
|
-
|
|
1828
|
-
| Option | Type | Default | Description |
|
|
1829
|
-
|--------|------|---------|-------------|
|
|
1830
|
-
| `concurrentExecution` | `boolean` | `true` | Execute requests concurrently or sequentially |
|
|
1831
|
-
| `stopOnFirstError` | `boolean` | `false` | Stop execution on first error (sequential only) |
|
|
1832
|
-
| `requestGroups` | `RequestGroup[]` | `[]` | Define groups with their own common configurations |
|
|
1833
|
-
| `commonAttempts` | `number` | `1` | Default attempts for all requests |
|
|
1834
|
-
| `commonPerformAllAttempts` | `boolean` | `false` | Default performAllAttempts for all requests |
|
|
1835
|
-
| `commonWait` | `number` | `1000` | Default wait time for all requests |
|
|
1836
|
-
| `commonMaxAllowedWait` | `number` | `60000` | Default maximum permitted wait time for all requests |
|
|
1837
|
-
| `commonRetryStrategy` | `RETRY_STRATEGY_TYPES` | `'fixed'` | Default retry strategy for all requests |
|
|
1838
|
-
| `commonLogAllErrors` | `boolean` | `false` | Default error logging for all requests |
|
|
1839
|
-
| `commonLogAllSuccessfulAttempts` | `boolean` | `false` | Default success logging for all requests |
|
|
1840
|
-
| `commonMaxSerializableChars` | `number` | `1000` | Default max chars for serialization |
|
|
1841
|
-
| `commonTrialMode` | `TRIAL_MODE_OPTIONS` | `{ enabled: false }` | Default trial mode for all requests |
|
|
1842
|
-
| `commonResponseAnalyzer` | `function` | `() => true` | Default response analyzer for all requests |
|
|
1843
|
-
| `commonResReq` | `boolean` | `false` | Default resReq for all requests |
|
|
1844
|
-
| `commonFinalErrorAnalyzer` | `function` | `() => false` | Default final error analyzer for all requests |
|
|
1845
|
-
| `commonHandleErrors` | `function` | console.log | Default error handler for all requests |
|
|
1846
|
-
| `commonHandleSuccessfulAttemptData` | `function` | console.log | Default success handler for all requests |
|
|
1847
|
-
| `commonRequestData` | `Partial<REQUEST_DATA>` | `{ hostname: '' }` | Common set of request options for each request |
|
|
1848
|
-
| `commonHookParams` | `HookParams` | `{ }` | Common options for each request hook |
|
|
1849
|
-
| `sharedBuffer` | `Record<string, any>` | `undefined` | For communication between various requests |
|
|
1850
|
-
|
|
1851
|
-
### `stableWorkflow(phases, options)`
|
|
1852
|
-
|
|
1853
|
-
Execute a multi-phase workflow with full control over execution order and error handling.
|
|
1854
|
-
|
|
1855
|
-
**Phases Array:**
|
|
1856
|
-
```typescript
|
|
1857
|
-
interface STABLE_WORKFLOW_PHASE {
|
|
1858
|
-
id?: string; // Phase identifier (auto-generated if omitted)
|
|
1859
|
-
concurrentExecution?: boolean; // true = parallel, false = sequential (default: true)
|
|
1860
|
-
stopOnFirstError?: boolean; // Stop phase on first request failure (default: false)
|
|
1861
|
-
commonConfig?: Omit<API_GATEWAY_OPTIONS; 'concurrentExecution' | 'stopOnFirstError' | 'requestGroups'>;
|
|
1862
|
-
requests: API_GATEWAY_REQUEST[]; // Array of requests for this phase
|
|
1863
|
-
markConcurrentPhase?: boolean; // Allows this phase to be executed concurrently with immediately next phase marked as concurrent
|
|
1864
|
-
}
|
|
1865
|
-
```
|
|
1866
|
-
|
|
1867
|
-
**Workflow Options:**
|
|
1868
|
-
|
|
1869
|
-
| Option | Type | Default | Description |
|
|
1870
|
-
|--------|------|---------|-------------|
|
|
1871
|
-
| `workflowId` | `string` | `workflow-{timestamp}` | Workflow identifier |
|
|
1872
|
-
| `stopOnFirstPhaseError` | `boolean` | `false` | Stop workflow if any phase fails |
|
|
1873
|
-
| `logPhaseResults` | `boolean` | `false` | Log phase execution to console |
|
|
1874
|
-
| `concurrentPhaseExecution` | `boolean` | `false` | Execute all phases in parallel. Overrides `enableMixedExecution` |
|
|
1875
|
-
| `handlePhaseCompletion` | `function` | `undefined` | Hook called after each successful phase |
|
|
1876
|
-
| `handlePhaseError` | `function` | `undefined` | Hook called when a phase fails |
|
|
1877
|
-
| `maxSerializableChars` | `number` | `1000` | Max chars for serialization in hooks |
|
|
1878
|
-
| `workflowHookParams` | `WorkflowHookParams` | {} | Custom set of params passed to hooks |
|
|
1879
|
-
| `sharedBuffer` | `Record<string, any>` | `undefined` | Buffer shared by all phases and all requests within them |
|
|
1880
|
-
| `enableMixedExecution` | `boolean` | `false` | Enables mixing of sequential and parallel sub-workflows |
|
|
1881
|
-
| All `stableApiGateway` options | - | - | Applied as workflow-level defaults |
|
|
1882
|
-
|
|
1883
|
-
**STABLE_WORKFLOW_RESULT response:**
|
|
1884
|
-
```typescript
|
|
1885
|
-
interface STABLE_WORKFLOW_RESULT {
|
|
1886
|
-
workflowId: string;
|
|
1887
|
-
success: boolean; // All phases successful?
|
|
1888
|
-
executionTime: number; // Total workflow duration (ms)
|
|
1889
|
-
timestamp: string; // ISO timestamp
|
|
1890
|
-
totalPhases: number;
|
|
1891
|
-
completedPhases: number;
|
|
1892
|
-
totalRequests: number;
|
|
1893
|
-
successfulRequests: number;
|
|
1894
|
-
failedRequests: number;
|
|
1895
|
-
phases: PHASE_RESULT[]; // Detailed results per phase
|
|
1896
|
-
error?: string; // Workflow-level error
|
|
1897
|
-
}
|
|
1898
|
-
```
|
|
1899
|
-
|
|
1900
|
-
### Hooks Reference
|
|
1901
|
-
|
|
1902
|
-
#### preExecutionHook
|
|
1903
|
-
|
|
1904
|
-
**Purpose:** Dynamically configure request before execution
|
|
1905
|
-
|
|
1906
|
-
```typescript
|
|
1907
|
-
preExecution: {
|
|
1908
|
-
preExecutionHook: async ({ inputParams, commonBuffer }) => {
|
|
1909
|
-
// Fetch dynamic data
|
|
1910
|
-
const token = await getAuthToken();
|
|
1911
|
-
|
|
1912
|
-
// Store in common buffer
|
|
1913
|
-
commonBuffer.token = token;
|
|
1914
|
-
commonBuffer.timestamp = Date.now();
|
|
1915
|
-
|
|
1916
|
-
// Return config overrides
|
|
1917
|
-
return {
|
|
1918
|
-
reqData: {
|
|
1919
|
-
headers: { 'Authorization': `Bearer ${token}` }
|
|
1920
|
-
},
|
|
1921
|
-
attempts: 5
|
|
1922
|
-
};
|
|
1923
|
-
},
|
|
1924
|
-
preExecutionHookParams: { userId: 'user-123' },
|
|
1925
|
-
applyPreExecutionConfigOverride: true,
|
|
1926
|
-
continueOnPreExecutionHookFailure: false
|
|
1927
|
-
}
|
|
1928
|
-
```
|
|
1929
|
-
|
|
1930
|
-
#### responseAnalyzer
|
|
1931
|
-
|
|
1932
|
-
**Purpose:** Validate response content, retry even on HTTP 200
|
|
1933
|
-
|
|
1934
|
-
```typescript
|
|
1935
|
-
responseAnalyzer: async ({ reqData, data, trialMode, params, commonBuffer }) => {
|
|
1936
|
-
// Return true if valid, false to retry
|
|
1937
|
-
return data.status === 'ready';
|
|
1938
|
-
}
|
|
1939
|
-
```
|
|
1940
|
-
|
|
1941
|
-
#### handleErrors
|
|
1942
|
-
|
|
1943
|
-
**Purpose:** Monitor and log failed attempts
|
|
1944
|
-
|
|
1945
|
-
```typescript
|
|
1946
|
-
handleErrors: async ({ reqData, errorLog, maxSerializableChars, params, commonBuffer }) => {
|
|
1947
|
-
await logger.error({
|
|
1948
|
-
url: reqData.url,
|
|
1949
|
-
attempt: errorLog.attempt,
|
|
1950
|
-
error: errorLog.error
|
|
1951
1416
|
});
|
|
1952
|
-
}
|
|
1953
|
-
```
|
|
1954
|
-
|
|
1955
|
-
#### handleSuccessfulAttemptData
|
|
1956
1417
|
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1418
|
+
return {
|
|
1419
|
+
success: result.success,
|
|
1420
|
+
tenantId: tenantConfig.tenantId,
|
|
1421
|
+
processedItems: sharedBuffer.processedItems,
|
|
1422
|
+
executionTime: result.executionTime,
|
|
1423
|
+
phases: result.phases.map(p => ({
|
|
1424
|
+
id: p.phaseId,
|
|
1425
|
+
success: p.success,
|
|
1426
|
+
requestCount: p.totalRequests
|
|
1427
|
+
}))
|
|
1428
|
+
};
|
|
1965
1429
|
}
|
|
1966
|
-
```
|
|
1967
1430
|
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1431
|
+
// Execute workflows for multiple tenants
|
|
1432
|
+
const tenants: TenantConfig[] = [
|
|
1433
|
+
{ tenantId: 'tenant-1', apiKey: 'key1', baseUrl: 'api.tenant1.com' },
|
|
1434
|
+
{ tenantId: 'tenant-2', apiKey: 'key2', baseUrl: 'api.tenant2.com' }
|
|
1435
|
+
];
|
|
1971
1436
|
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
if (error.message.includes('404')) {
|
|
1977
|
-
return true; // Treat as non-critical
|
|
1978
|
-
}
|
|
1979
|
-
return false; // Throw
|
|
1980
|
-
}
|
|
1437
|
+
const results = await Promise.all(tenants.map(executeTenantWorkflow));
|
|
1438
|
+
results.forEach(result => {
|
|
1439
|
+
console.log(`Tenant ${result.tenantId}:`, result.success ? 'Success' : 'Failed');
|
|
1440
|
+
});
|
|
1981
1441
|
```
|
|
1982
1442
|
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
**Purpose:** Execute phase-bridging code upon successful completion of a phase
|
|
1443
|
+
### Use Case 2: Resilient Data Pipeline with Fallback Strategies
|
|
1986
1444
|
|
|
1987
1445
|
```typescript
|
|
1988
|
-
|
|
1989
|
-
await logger.log(phaseResult.phaseId, phaseResult.success);
|
|
1990
|
-
}
|
|
1991
|
-
```
|
|
1446
|
+
import { stableApiGateway, RETRY_STRATEGIES, CircuitBreaker } from '@emmvish/stable-request';
|
|
1992
1447
|
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
handlePhaseError: async ({ workflowId, phaseResult, error, maxSerializableChars, params, sharedBuffer }) => {
|
|
1999
|
-
await logger.error(error);
|
|
1448
|
+
interface DataSource {
|
|
1449
|
+
id: string;
|
|
1450
|
+
priority: number;
|
|
1451
|
+
endpoint: string;
|
|
1452
|
+
hostname: string;
|
|
2000
1453
|
}
|
|
2001
|
-
```
|
|
2002
|
-
|
|
2003
|
-
## Configuration Hierarchy
|
|
2004
|
-
|
|
2005
|
-
Configuration precedence across orchestration:
|
|
2006
|
-
|
|
2007
|
-
- Workflow-level (options.sharedBuffer)
|
|
2008
|
-
- Phase-level (commonConfig)
|
|
2009
|
-
- Request group (requestGroups[].commonConfig)
|
|
2010
|
-
- Individual request options (highest priority)
|
|
2011
1454
|
|
|
2012
|
-
|
|
1455
|
+
async function fetchDataWithFallback(dataSources: DataSource[]) {
|
|
1456
|
+
// Sort by priority
|
|
1457
|
+
const sortedSources = [...dataSources].sort((a, b) => a.priority - b.priority);
|
|
1458
|
+
|
|
1459
|
+
// Create circuit breakers for each source
|
|
1460
|
+
const circuitBreakers = new Map(
|
|
1461
|
+
sortedSources.map(source => [
|
|
1462
|
+
source.id,
|
|
1463
|
+
new CircuitBreaker({
|
|
1464
|
+
failureThresholdPercentage: 50,
|
|
1465
|
+
minimumRequests: 3,
|
|
1466
|
+
recoveryTimeoutMs: 60000
|
|
1467
|
+
})
|
|
1468
|
+
])
|
|
1469
|
+
);
|
|
1470
|
+
|
|
1471
|
+
// Try each data source in priority order
|
|
1472
|
+
for (const source of sortedSources) {
|
|
1473
|
+
const breaker = circuitBreakers.get(source.id)!;
|
|
1474
|
+
const breakerState = breaker.getState();
|
|
1475
|
+
|
|
1476
|
+
// Skip if circuit is open
|
|
1477
|
+
if (breakerState.state === 'OPEN') {
|
|
1478
|
+
console.warn(`Circuit breaker open for ${source.id}, skipping...`);
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
2013
1481
|
|
|
2014
|
-
|
|
2015
|
-
- Gateway scope: Gateway's / Phase's sharedBuffer
|
|
2016
|
-
- Workflow scope: Workflow's sharedBuffer
|
|
1482
|
+
console.log(`Attempting to fetch from ${source.id}...`);
|
|
2017
1483
|
|
|
2018
|
-
|
|
1484
|
+
try {
|
|
1485
|
+
const requests = [
|
|
1486
|
+
{
|
|
1487
|
+
id: 'users',
|
|
1488
|
+
requestOptions: {
|
|
1489
|
+
reqData: { path: `${source.endpoint}/users` },
|
|
1490
|
+
resReq: true,
|
|
1491
|
+
attempts: 3,
|
|
1492
|
+
wait: 1000,
|
|
1493
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1494
|
+
}
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
id: 'products',
|
|
1498
|
+
requestOptions: {
|
|
1499
|
+
reqData: { path: `${source.endpoint}/products` },
|
|
1500
|
+
resReq: true,
|
|
1501
|
+
attempts: 3,
|
|
1502
|
+
wait: 1000,
|
|
1503
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
id: 'orders',
|
|
1508
|
+
requestOptions: {
|
|
1509
|
+
reqData: { path: `${source.endpoint}/orders` },
|
|
1510
|
+
resReq: true,
|
|
1511
|
+
attempts: 3,
|
|
1512
|
+
wait: 1000,
|
|
1513
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
];
|
|
2019
1517
|
|
|
2020
|
-
|
|
1518
|
+
const results = await stableApiGateway(requests, {
|
|
1519
|
+
commonRequestData: {
|
|
1520
|
+
hostname: source.hostname,
|
|
1521
|
+
headers: { 'X-Source-ID': source.id }
|
|
1522
|
+
},
|
|
1523
|
+
concurrentExecution: true,
|
|
1524
|
+
maxConcurrentRequests: 10,
|
|
1525
|
+
circuitBreaker: breaker,
|
|
1526
|
+
rateLimit: {
|
|
1527
|
+
maxRequests: 50,
|
|
1528
|
+
windowMs: 1000
|
|
1529
|
+
},
|
|
1530
|
+
commonCache: {
|
|
1531
|
+
enabled: true,
|
|
1532
|
+
ttl: 60000
|
|
1533
|
+
},
|
|
1534
|
+
commonResponseAnalyzer: async ({ data }) => {
|
|
1535
|
+
// Validate data structure
|
|
1536
|
+
return data && typeof data === 'object' && !data.error;
|
|
1537
|
+
},
|
|
1538
|
+
commonHandleErrors: ({ errorLog }) => {
|
|
1539
|
+
console.error(`Error from ${source.id}:`, errorLog);
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
2021
1542
|
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
}
|
|
1543
|
+
// Check if all requests succeeded
|
|
1544
|
+
const allSuccessful = results.every(r => r.success);
|
|
1545
|
+
|
|
1546
|
+
if (allSuccessful) {
|
|
1547
|
+
console.log(`Successfully fetched data from ${source.id}`);
|
|
1548
|
+
return {
|
|
1549
|
+
source: source.id,
|
|
1550
|
+
data: {
|
|
1551
|
+
users: results.find(r => r.requestId === 'users')?.data,
|
|
1552
|
+
products: results.find(r => r.requestId === 'products')?.data,
|
|
1553
|
+
orders: results.find(r => r.requestId === 'orders')?.data
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
} else {
|
|
1557
|
+
console.warn(`Partial failure from ${source.id}, trying next source...`);
|
|
1558
|
+
}
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
console.error(`Failed to fetch from ${source.id}:`, error);
|
|
1561
|
+
// Continue to next source
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
2027
1564
|
|
|
2028
|
-
|
|
2029
|
-
id: string;
|
|
2030
|
-
name: string;
|
|
2031
|
-
email: string;
|
|
2032
|
-
createdAt: string;
|
|
1565
|
+
throw new Error('All data sources failed');
|
|
2033
1566
|
}
|
|
2034
1567
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
email: 'john@example.com'
|
|
2043
|
-
}
|
|
1568
|
+
// Usage
|
|
1569
|
+
const dataSources: DataSource[] = [
|
|
1570
|
+
{
|
|
1571
|
+
id: 'primary-db',
|
|
1572
|
+
priority: 1,
|
|
1573
|
+
endpoint: '/api/v1',
|
|
1574
|
+
hostname: 'primary.example.com'
|
|
2044
1575
|
},
|
|
2045
|
-
|
|
2046
|
-
|
|
1576
|
+
{
|
|
1577
|
+
id: 'replica-db',
|
|
1578
|
+
priority: 2,
|
|
1579
|
+
endpoint: '/api/v1',
|
|
1580
|
+
hostname: 'replica.example.com'
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
id: 'backup-cache',
|
|
1584
|
+
priority: 3,
|
|
1585
|
+
endpoint: '/cached',
|
|
1586
|
+
hostname: 'cache.example.com'
|
|
1587
|
+
}
|
|
1588
|
+
];
|
|
2047
1589
|
|
|
2048
|
-
|
|
2049
|
-
console.log(
|
|
1590
|
+
const result = await fetchDataWithFallback(dataSources);
|
|
1591
|
+
console.log('Data fetched from:', result.source);
|
|
1592
|
+
console.log('Users:', result.data.users?.length);
|
|
1593
|
+
console.log('Products:', result.data.products?.length);
|
|
1594
|
+
console.log('Orders:', result.data.orders?.length);
|
|
2050
1595
|
```
|
|
2051
1596
|
|
|
2052
1597
|
## License
|
|
2053
1598
|
|
|
2054
1599
|
MIT © Manish Varma
|
|
2055
1600
|
|
|
2056
|
-
|
|
2057
1601
|
[](https://opensource.org/licenses/MIT)
|
|
2058
|
-
|
|
2059
1602
|
---
|
|
2060
1603
|
|
|
2061
1604
|
**Made with ❤️ for developers integrating with unreliable APIs**
|