@emmvish/stable-request 1.6.1 → 1.6.3

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