@emmvish/stable-request 2.0.0 → 2.1.1

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,2048 +1,2074 @@
1
1
  # @emmvish/stable-request
2
2
 
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.
3
+ A production-grade TypeScript library for resilient API integrations, batch processing, and orchestrating complex workflows with deterministic error handling, type safety, and comprehensive observability.
4
4
 
5
- ## Navigation
5
+ ## Table of Contents
6
6
 
7
7
  - [Overview](#overview)
8
- - [Why stable-request?](#why-stable-request)
9
- - [Installation](#installation)
10
- - [Quick Start](#quick-start)
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
- - [Metrics and Observability](#metrics-and-observability)
20
- - [Request-Level Metrics](#request-level-metrics)
21
- - [API Gateway Metrics](#api-gateway-metrics)
22
- - [Workflow Metrics](#workflow-metrics)
23
- - [MetricsAggregator Utility](#metricsaggregator-utility)
24
- - [Workflow Execution Patterns](#workflow-execution-patterns)
25
- - [Sequential and Concurrent Phases](#sequential-and-concurrent-phases)
26
- - [Mixed Execution Mode](#mixed-execution-mode)
8
+ - [Core Concepts](#core-concepts)
9
+ - [Core Modules](#core-modules)
10
+ - [stableRequest](#stablerequest)
11
+ - [stableFunction](#stablefunction)
12
+ - [stableApiGateway](#stableapigateway)
13
+ - [stableWorkflow](#stableworkflow)
14
+ - [stableWorkflowGraph](#stableworkflowgraph)
15
+ - [Resilience Mechanisms](#resilience-mechanisms)
16
+ - [Retry Strategies](#retry-strategies)
17
+ - [Circuit Breaker](#circuit-breaker)
18
+ - [Caching](#caching)
19
+ - [Rate Limiting](#rate-limiting)
20
+ - [Concurrency Limiting](#concurrency-limiting)
21
+ - [Workflow Patterns](#workflow-patterns)
22
+ - [Sequential & Concurrent Phases](#sequential--concurrent-phases)
27
23
  - [Non-Linear Workflows](#non-linear-workflows)
28
24
  - [Branched Workflows](#branched-workflows)
29
- - [Advanced Capabilities](#advanced-capabilities)
25
+ - [Graph-Based Workflows](#graph-based-workflows)
26
+ - [Configuration & State](#configuration--state)
30
27
  - [Config Cascading](#config-cascading)
31
- - [Request Grouping](#request-grouping)
32
- - [Shared Buffer and Pre-Execution Hooks](#shared-buffer-and-pre-execution-hooks)
33
- - [State Persistence and Recovery](#state-persistence-and-recovery)
34
- - [Comprehensive Observability](#comprehensive-observability)
28
+ - [Shared & State Buffers](#shared--state-buffers)
29
+ - [Hooks & Observability](#hooks--observability)
30
+ - [Pre-Execution Hooks](#pre-execution-hooks)
31
+ - [Analysis Hooks](#analysis-hooks)
32
+ - [Handler Hooks](#handler-hooks)
33
+ - [Decision Hooks](#decision-hooks)
34
+ - [Metrics & Logging](#metrics--logging)
35
+ - [Advanced Features](#advanced-features)
35
36
  - [Trial Mode](#trial-mode)
36
- - [Common Use Cases](#common-use-cases)
37
- - [License](#license)
37
+ - [State Persistence](#state-persistence)
38
+ - [Mixed Request & Function Phases](#mixed-request--function-phases)
39
+ - [Best Practices](#best-practices)
40
+
41
+ ---
38
42
 
39
43
  ## Overview
40
44
 
41
- `@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:
45
+ **@emmvish/stable-request** evolved from a focused library for resilient API calls to a comprehensive execution framework. Originally addressing API integration challenges, it expanded to include:
42
46
 
43
- - **Workflow-First Architecture**: Organize API calls into phases, branches, and decision trees with full control over execution order
44
- - **Enterprise Resilience**: Built-in circuit breakers, configurable retry strategies, and sophisticated failure handling
45
- - **Execution Flexibility**: Sequential, concurrent, mixed, and non-linear execution patterns to match your business logic
46
- - **Production-Ready Observability**: Comprehensive hooks for monitoring, logging, error analysis, and execution history tracking
47
- - **Performance Optimization**: Response caching, rate limiting, and concurrency control to maximize efficiency
48
- - **Type Safety**: Full TypeScript support with 40+ exported types
47
+ 1. **Batch orchestration** via `stableApiGateway` for processing groups of mixed requests/functions
48
+ 2. **Phased workflows** via `stableWorkflow` for array-based multi-phase execution with dynamic control flow
49
+ 3. **Graph-based workflows** via `stableWorkflowGraph` for DAG execution with higher parallelism
50
+ 4. **Generic function execution** via `stableFunction`, inheriting all resilience guards
49
51
 
50
- ## Why stable-request?
52
+ All four execution modes support the same resilience stack: retries, jitter, circuit breaking, caching, rate/concurrency limits, config cascading, shared buffers, trial mode, comprehensive hooks, and metrics. This uniformity makes it trivial to compose requests and functions in any topology.
51
53
 
52
- Modern applications often need to:
53
- - **Orchestrate complex API workflows** with dependencies between steps
54
- - **Handle unreliable APIs** with intelligent retry and fallback mechanisms
55
- - **Prevent cascade failures** when downstream services fail
56
- - **Optimize performance** by caching responses and controlling request rates
57
- - **Monitor and debug** complex request flows in production
58
- - **Implement conditional logic** based on API responses (branching, looping)
54
+ ---
59
55
 
60
- `@emmvish/stable-request` solves all these challenges with a unified, type-safe API that scales from simple requests to sophisticated multi-phase workflows.
56
+ ## Core Concepts
61
57
 
62
- ## Installation
58
+ ### Resilience as Default
63
59
 
64
- ```bash
65
- npm install @emmvish/stable-request
66
- ```
60
+ Every execution—whether a single request, a pure function, or an entire workflow—inherits built-in resilience:
61
+
62
+ - **Retries** with configurable backoff strategies (FIXED, LINEAR, EXPONENTIAL)
63
+ - **Jitter** to prevent thundering herd
64
+ - **Circuit breaker** to fail fast and protect downstream systems
65
+ - **Caching** for idempotent read operations
66
+ - **Rate & concurrency limits** to respect external constraints
67
+
68
+ ### Type Safety
69
+
70
+ All examples in this guide use TypeScript generics for type-safe request/response data and function arguments/returns. Analyzers validate shapes at runtime; TypeScript ensures compile-time safety.
71
+
72
+ ### Config Cascading
73
+
74
+ Global defaults → group overrides → phase overrides → branch overrides → item overrides. Lower levels always win, preventing repetition while maintaining expressiveness.
75
+
76
+ ### Shared State
77
+
78
+ Workflows and gateways support `sharedBuffer` for passing computed state across phases/branches/items without global state.
67
79
 
68
- **Requirements**: Node.js 14+ (ES Modules)
80
+ ---
69
81
 
70
- ## Quick Start
82
+ ## Core Modules
71
83
 
72
- ### Single Request with Retry
84
+ ### stableRequest
73
85
 
74
- Execute a single HTTP request with automatic retry on failure:
86
+ Single API call with resilience, type-safe request and response types.
75
87
 
76
88
  ```typescript
77
- import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
89
+ import { stableRequest, REQUEST_METHODS, VALID_REQUEST_PROTOCOLS } from '@emmvish/stable-request';
90
+
91
+ interface GetUserRequest {
92
+ // Empty for GET requests with no body
93
+ }
78
94
 
79
- const userData = await stableRequest({
95
+ interface User {
96
+ id: number;
97
+ name: string;
98
+ }
99
+
100
+ const result = await stableRequest<GetUserRequest, User>({
80
101
  reqData: {
102
+ method: REQUEST_METHODS.GET,
103
+ protocol: VALID_REQUEST_PROTOCOLS.HTTPS,
81
104
  hostname: 'api.example.com',
82
- path: '/users/123',
83
- headers: { 'Authorization': 'Bearer token' }
84
- }, // 'GET' is default HTTP method, if not specified
85
- resReq: true, // Return response data
86
- attempts: 3, // Retry up to 3 times
87
- wait: 1000, // 1 second between retries
105
+ path: '/users/1'
106
+ },
107
+ resReq: true,
108
+ attempts: 3,
109
+ wait: 500,
110
+ jitter: 100,
111
+ cache: { enabled: true, ttl: 5000 },
112
+ rateLimit: { maxRequests: 10, windowMs: 1000 },
113
+ maxConcurrentRequests: 5,
114
+ responseAnalyzer: ({ data }) => {
115
+ return typeof data === 'object' && data !== null && 'id' in data;
116
+ },
117
+ handleSuccessfulAttemptData: ({ successfulAttemptData }) => {
118
+ console.log(`User loaded: ${successfulAttemptData.data.name}`);
119
+ }
120
+ });
121
+
122
+ if (result.success) {
123
+ console.log(result.data.name, result.metrics.totalAttempts);
124
+ } else {
125
+ console.error(result.error);
126
+ }
127
+ ```
128
+
129
+ **Key responsibilities:**
130
+ - Execute a single HTTP request with automatic retry and backoff
131
+ - Validate response shape via analyzer; retry if invalid
132
+ - Cache successful responses with TTL
133
+ - Apply rate and concurrency limits
134
+ - Throw or gracefully suppress errors via finalErrorAnalyzer
135
+ - Collect attempt metrics and infra dashboards (circuit breaker, cache, rate limiter state)
136
+
137
+ ### stableFunction
138
+
139
+ Generic async/sync function execution with identical resilience.
140
+
141
+ ```typescript
142
+ import { stableFunction, RETRY_STRATEGIES } from '@emmvish/stable-request';
143
+
144
+ type ComputeArgs = [number, number];
145
+ type ComputeResult = number;
146
+
147
+ const multiply = (a: number, b: number) => a * b;
148
+
149
+ const result = await stableFunction<ComputeArgs, ComputeResult>({
150
+ fn: multiply,
151
+ args: [5, 3],
152
+ returnResult: true,
153
+ attempts: 2,
154
+ wait: 100,
88
155
  retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
89
- logAllErrors: true // Log all failed attempts
156
+ responseAnalyzer: ({ data }) => data > 0,
157
+ cache: { enabled: true, ttl: 10000 }
90
158
  });
91
159
 
92
- console.log(userData); // { id: 123, name: 'John' }
160
+ if (result.success) {
161
+ console.log('Result:', result.data); // 15
162
+ }
93
163
  ```
94
164
 
95
- ### Batch Requests (API Gateway)
165
+ **Key responsibilities:**
166
+ - Execute any async or sync function with typed arguments and return
167
+ - Support argument-based cache key generation
168
+ - Retry on error or analyzer rejection
169
+ - Enforce success criteria via analyzer
170
+ - Optionally suppress exceptions
96
171
 
97
- Execute multiple requests concurrently or sequentially:
172
+ ### stableApiGateway
173
+
174
+ Batch orchestration of mixed requests and functions.
98
175
 
99
176
  ```typescript
100
- import { stableApiGateway } from '@emmvish/stable-request';
177
+ import {
178
+ stableApiGateway,
179
+ REQUEST_METHODS,
180
+ VALID_REQUEST_PROTOCOLS,
181
+ RequestOrFunction
182
+ } from '@emmvish/stable-request';
183
+ import type { API_GATEWAY_ITEM } from '@emmvish/stable-request';
101
184
 
102
- const requests = [
103
- {
104
- id: 'users',
105
- requestOptions: {
106
- reqData: { path: '/users' },
107
- resReq: true
108
- }
185
+ // Define request types
186
+ interface ApiRequestData {
187
+ filters?: Record<string, any>;
188
+ }
189
+
190
+ interface ApiResponse {
191
+ id: number;
192
+ value: string;
193
+ }
194
+
195
+ // Define function types
196
+ type TransformArgs = [ApiResponse[], number];
197
+ type TransformResult = {
198
+ transformed: ApiResponse[];
199
+ count: number;
200
+ };
201
+
202
+ type ValidateArgs = [TransformResult];
203
+ type ValidateResult = boolean;
204
+
205
+ const items: API_GATEWAY_ITEM<ApiRequestData, ApiResponse, TransformArgs | ValidateArgs, TransformResult | ValidateResult>[] = [
206
+ {
207
+ type: RequestOrFunction.REQUEST,
208
+ request: {
209
+ id: 'fetch-data',
210
+ requestOptions: {
211
+ reqData: {
212
+ method: REQUEST_METHODS.GET,
213
+ protocol: VALID_REQUEST_PROTOCOLS.HTTPS,
214
+ hostname: 'api.example.com',
215
+ path: '/data'
216
+ },
217
+ resReq: true,
218
+ attempts: 3
219
+ }
220
+ }
109
221
  },
110
- {
111
- id: 'orders',
112
- requestOptions: {
113
- reqData: { path: '/orders' },
114
- resReq: true
115
- }
222
+ {
223
+ type: RequestOrFunction.FUNCTION,
224
+ function: {
225
+ id: 'transform-data',
226
+ functionOptions: {
227
+ fn: (data: ApiResponse[], threshold: number): TransformResult => ({
228
+ transformed: data.filter(item => item.id > threshold),
229
+ count: data.length
230
+ }),
231
+ args: [[], 10] as TransformArgs,
232
+ returnResult: true,
233
+ attempts: 2,
234
+ cache: { enabled: true, ttl: 5000 }
235
+ }
236
+ }
116
237
  },
117
- {
118
- id: 'products',
119
- requestOptions: {
120
- reqData: { path: '/products' },
121
- resReq: true
122
- }
238
+ {
239
+ type: RequestOrFunction.FUNCTION,
240
+ function: {
241
+ id: 'validate-result',
242
+ functionOptions: {
243
+ fn: (result: TransformResult): ValidateResult => result.count > 0,
244
+ args: [{ transformed: [], count: 0 }] as ValidateArgs,
245
+ returnResult: true
246
+ }
247
+ }
123
248
  }
124
249
  ];
125
250
 
126
- const results = await stableApiGateway(requests, {
127
- concurrentExecution: true, // Execute in parallel
128
- commonRequestData: {
129
- hostname: 'api.example.com',
130
- headers: { 'X-API-Key': 'secret' }
131
- },
132
- commonAttempts: 2, // Retry each request twice
133
- commonWait: 500
251
+ const responses = await stableApiGateway<ApiRequestData, ApiResponse>(items, {
252
+ concurrentExecution: true,
253
+ stopOnFirstError: false,
254
+ sharedBuffer: {},
255
+ commonAttempts: 2,
256
+ commonWait: 300,
257
+ maxConcurrentRequests: 3
134
258
  });
135
259
 
136
- results.forEach(result => {
137
- console.log(`${result.id}:`, result.data);
260
+ responses.forEach((resp, i) => {
261
+ console.log(`Item ${i}: success=${resp.success}`);
138
262
  });
139
263
  ```
140
264
 
141
- ### Multi-Phase Workflow
265
+ **Key responsibilities:**
266
+ - Execute a batch of requests and functions concurrently or sequentially
267
+ - Apply global, group-level, and item-level config overrides
268
+ - Maintain shared buffer across items for state passing
269
+ - Stop on first error or continue despite failures
270
+ - Collect per-item and aggregate metrics
271
+ - Support request grouping with group-specific config
272
+
273
+ ### stableWorkflow
142
274
 
143
- Orchestrate complex workflows with multiple phases:
275
+ Phased array-based workflows with sequential/concurrent phases, mixed items, and non-linear control flow.
144
276
 
145
277
  ```typescript
146
- import { stableWorkflow, PHASE_DECISION_ACTIONS, REQUEST_METHODS } from '@emmvish/stable-request';
278
+ import { stableWorkflow, PHASE_DECISION_ACTIONS, RequestOrFunction } from '@emmvish/stable-request';
279
+ import type { STABLE_WORKFLOW_PHASE, API_GATEWAY_ITEM } from '@emmvish/stable-request';
280
+
281
+ // Define types for requests
282
+ interface FetchRequestData {}
283
+ interface FetchResponse {
284
+ users: Array<{ id: number; name: string }>;
285
+ posts: Array<{ id: number; title: string }>;
286
+ }
147
287
 
148
- const phases = [
288
+ // Define types for functions
289
+ type ProcessArgs = [FetchResponse];
290
+ type ProcessResult = {
291
+ merged: Array<{ userId: number; userName: string; postTitle: string }>;
292
+ };
293
+
294
+ type AuditArgs = [ProcessResult, string];
295
+ type AuditResult = { logged: boolean; timestamp: string };
296
+
297
+ const phases: STABLE_WORKFLOW_PHASE<FetchRequestData, FetchResponse, ProcessArgs | AuditArgs, ProcessResult | AuditResult>[] = [
149
298
  {
150
- id: 'authentication',
299
+ id: 'fetch-data',
151
300
  requests: [
152
- {
153
- id: 'login',
154
- requestOptions: {
155
- reqData: {
156
- path: '/auth/login',
157
- method: REQUEST_METHODS.POST,
158
- body: { username: 'user', password: 'pass' }
159
- },
160
- resReq: true
161
- }
301
+ {
302
+ id: 'get-users-posts',
303
+ requestOptions: {
304
+ reqData: {
305
+ hostname: 'api.example.com',
306
+ path: '/users-and-posts'
307
+ },
308
+ resReq: true,
309
+ attempts: 3
310
+ }
162
311
  }
163
312
  ]
164
313
  },
165
314
  {
166
- id: 'fetch-data',
167
- concurrentExecution: true, // Execute requests in parallel
168
- requests: [
169
- { id: 'user-profile', requestOptions: { reqData: { path: '/profile' }, resReq: true } },
170
- { id: 'user-orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } },
171
- { id: 'user-settings', requestOptions: { reqData: { path: '/settings' }, resReq: true } }
172
- ]
315
+ id: 'process-and-audit',
316
+ markConcurrentPhase: true,
317
+ items: [
318
+ {
319
+ type: RequestOrFunction.FUNCTION,
320
+ function: {
321
+ id: 'process-data',
322
+ functionOptions: {
323
+ fn: (data: FetchResponse): ProcessResult => ({
324
+ merged: data.users.map((user, idx) => ({
325
+ userId: user.id,
326
+ userName: user.name,
327
+ postTitle: data.posts[idx]?.title || 'No post'
328
+ }))
329
+ }),
330
+ args: [{ users: [], posts: [] }] as ProcessArgs,
331
+ returnResult: true
332
+ }
333
+ }
334
+ },
335
+ {
336
+ type: RequestOrFunction.FUNCTION,
337
+ function: {
338
+ id: 'audit-processing',
339
+ functionOptions: {
340
+ fn: async (result: ProcessResult, auditId: string): Promise<AuditResult> => {
341
+ console.log(`Audit ${auditId}:`, result);
342
+ return { logged: true, timestamp: new Date().toISOString() };
343
+ },
344
+ args: [{ merged: [] }, 'audit-123'] as AuditArgs,
345
+ returnResult: true
346
+ }
347
+ }
348
+ }
349
+ ],
350
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
351
+ if (!phaseResult.success) {
352
+ return { action: PHASE_DECISION_ACTIONS.TERMINATE };
353
+ }
354
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
355
+ }
173
356
  },
174
357
  {
175
- id: 'process-data',
358
+ id: 'finalize',
176
359
  requests: [
177
- {
178
- id: 'update-analytics',
179
- requestOptions: {
180
- reqData: { path: '/analytics', method: REQUEST_METHODS.POST },
181
- resReq: false
182
- }
360
+ {
361
+ id: 'store-result',
362
+ requestOptions: {
363
+ reqData: {
364
+ hostname: 'api.example.com',
365
+ path: '/store',
366
+ method: 'POST'
367
+ },
368
+ resReq: false
369
+ }
183
370
  }
184
371
  ]
185
372
  }
186
373
  ];
187
374
 
188
375
  const result = await stableWorkflow(phases, {
189
- workflowId: 'user-data-sync',
190
- commonRequestData: { hostname: 'api.example.com' },
191
- commonAttempts: 3,
192
- stopOnFirstPhaseError: true, // Stop if any phase fails
193
- logPhaseResults: true // Log each phase completion
376
+ workflowId: 'data-pipeline',
377
+ concurrentPhaseExecution: false, // Phases sequential
378
+ enableNonLinearExecution: true,
379
+ sharedBuffer: { userId: '123' },
380
+ commonAttempts: 2,
381
+ commonWait: 200,
382
+ handlePhaseCompletion: ({ phaseResult, workflowId }) => {
383
+ console.log(`Phase ${phaseResult.phaseId} complete in workflow ${workflowId}`);
384
+ }
194
385
  });
195
386
 
196
- console.log(`Workflow completed: ${result.success}`);
197
- console.log(`Total requests: ${result.totalRequests}`);
198
- console.log(`Successful: ${result.successfulRequests}`);
199
- console.log(`Failed: ${result.failedRequests}`);
200
- console.log(`Execution time: ${result.executionTime}ms`);
387
+ console.log(`Workflow succeeded: ${result.success}, phases: ${result.totalPhases}`);
201
388
  ```
202
389
 
203
- ### Graph-Based Workflow
390
+ **Key responsibilities:**
391
+ - Execute phases sequentially or concurrently
392
+ - Support mixed requests and functions per phase
393
+ - Enable non-linear flow (CONTINUE, SKIP, REPLAY, JUMP, TERMINATE)
394
+ - Maintain shared buffer across all phases
395
+ - Apply phase-level and request-level config cascading
396
+ - Support branching with parallel/sequential branches
397
+ - Collect per-phase metrics and workflow aggregates
398
+
399
+ ### stableWorkflowGraph
204
400
 
205
- Build sophisticated workflows with explicit graph structures, conditional routing, and parallel execution:
401
+ DAG-based execution for higher parallelism and explicit phase dependencies.
206
402
 
207
403
  ```typescript
208
- import { stableWorkflowGraph, WorkflowGraphBuilder, WorkflowEdgeConditionTypes, REQUEST_METHODS } from '@emmvish/stable-request';
404
+ import { stableWorkflowGraph, WorkflowGraphBuilder } from '@emmvish/stable-request';
209
405
 
210
406
  const graph = new WorkflowGraphBuilder()
211
- // Add validation phase
212
- .addPhase('validate-order', {
407
+ .addPhase('fetch-posts', {
213
408
  requests: [{
214
- id: 'validate-req',
409
+ id: 'get-posts',
215
410
  requestOptions: {
216
- reqData: { hostname: 'api.example.com', path: '/validate', method: REQUEST_METHODS.POST },
217
- resReq: true,
218
- logAllSuccessfulAttempts: true
411
+ reqData: { hostname: 'api.example.com', path: '/posts' },
412
+ resReq: true
219
413
  }
220
414
  }]
221
415
  })
222
-
223
- // Add conditional routing
224
- .addConditional('check-validation', async ({ sharedBuffer }) => {
225
- return sharedBuffer.valid ? 'process-order' : 'reject-order';
226
- })
227
-
228
- // Add parallel processing group
229
- .addParallelGroup('process-order', ['check-inventory', 'process-payment'])
230
-
231
- .addPhase('check-inventory', {
232
- requests: [{ id: 'inventory', requestOptions: { reqData: { path: '/inventory' }, resReq: true } }]
233
- })
234
-
235
- .addPhase('process-payment', {
236
- requests: [{ id: 'payment', requestOptions: { reqData: { path: '/payment' }, resReq: true } }]
237
- })
238
-
239
- // Add merge point to synchronize parallel paths
240
- .addMergePoint('processing-complete', ['check-inventory', 'process-payment'])
241
-
242
- .addPhase('fulfill-order', {
243
- requests: [{ id: 'fulfill', requestOptions: { reqData: { path: '/fulfill' }, resReq: true } }]
416
+ .addPhase('fetch-users', {
417
+ requests: [{
418
+ id: 'get-users',
419
+ requestOptions: {
420
+ reqData: { hostname: 'api.example.com', path: '/users' },
421
+ resReq: true
422
+ }
423
+ }]
244
424
  })
245
-
246
- .addPhase('reject-order', {
247
- requests: [{ id: 'reject', requestOptions: { reqData: { path: '/reject' }, resReq: true } }]
425
+ .addParallelGroup('fetch-all', ['fetch-posts', 'fetch-users'])
426
+ .addPhase('aggregate', {
427
+ functions: [{
428
+ id: 'combine',
429
+ functionOptions: {
430
+ fn: () => ({ posts: [], users: [] }),
431
+ args: [],
432
+ returnResult: true
433
+ }
434
+ }]
248
435
  })
249
-
250
- // Connect nodes with edge conditions
251
- .connect('validate-order', 'check-validation', { condition: { type: WorkflowEdgeConditionTypes.ALWAYS } })
252
- .connect('check-validation', 'process-order', { condition: { type: WorkflowEdgeConditionTypes.SUCCESS } })
253
- .connect('check-validation', 'reject-order', { condition: { type: WorkflowEdgeConditionTypes.FAILURE } })
254
- .connect('process-order', 'check-inventory', { condition: { type: WorkflowEdgeConditionTypes.ALWAYS } })
255
- .connect('process-order', 'process-payment', { condition: { type: WorkflowEdgeConditionTypes.ALWAYS } })
256
- .connect('check-inventory', 'processing-complete', { condition: { type: WorkflowEdgeConditionTypes.ALWAYS } })
257
- .connect('process-payment', 'processing-complete', { condition: { type: WorkflowEdgeConditionTypes.ALWAYS } })
258
- .connect('processing-complete', 'fulfill-order', { condition: { type: WorkflowEdgeConditionTypes.ALWAYS } })
259
-
260
- .setEntryPoint('validate-order')
436
+ .addMergePoint('sync', ['fetch-all'])
437
+ .connectSequence('fetch-all', 'sync', 'aggregate')
438
+ .setEntryPoint('fetch-all')
261
439
  .build();
262
440
 
263
441
  const result = await stableWorkflowGraph(graph, {
264
- workflowId: 'order-processing',
265
- sharedBuffer: {},
266
- validateGraph: true, // Enforce DAG constraints
267
- logPhaseResults: true
442
+ workflowId: 'data-aggregation'
268
443
  });
269
444
 
270
- console.log(`Graph workflow completed: ${result.success}`);
271
- console.log(`Phases executed: ${result.completedPhases}/${result.totalPhases}`);
272
- console.log(`Execution path:`, result.executionHistory.map(h => h.phaseId));
445
+ console.log(`Graph workflow success: ${result.success}`);
273
446
  ```
274
447
 
275
- ## Core Features
448
+ **Key responsibilities:**
449
+ - Define phases as DAG nodes with explicit dependency edges
450
+ - Execute independent phases in parallel automatically
451
+ - Support parallel groups, merge points, and conditional routing
452
+ - Validate graph structure (cycle detection, reachability, orphan detection)
453
+ - Provide deterministic execution order
454
+ - Offer higher parallelism than phased workflows for complex topologies
455
+
456
+ ---
457
+
458
+ ## Resilience Mechanisms
459
+
460
+ ### Retry Strategies
276
461
 
277
- ### Intelligent Retry Strategies
462
+ When a request or function fails and is retryable, retry with configurable backoff.
278
463
 
279
- Automatically retry failed requests with sophisticated backoff strategies:
464
+ #### FIXED Strategy
465
+
466
+ Constant wait between retries.
280
467
 
281
468
  ```typescript
282
469
  import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
283
470
 
284
- // Fixed delay: constant wait time
285
- await stableRequest({
471
+ interface DataRequest {}
472
+ interface DataResponse { data: any; }
473
+
474
+ const result = await stableRequest<DataRequest, DataResponse>({
286
475
  reqData: { hostname: 'api.example.com', path: '/data' },
287
- attempts: 5,
288
- wait: 1000, // 1 second between each retry
476
+ resReq: true,
477
+ attempts: 4,
478
+ wait: 500,
289
479
  retryStrategy: RETRY_STRATEGIES.FIXED
480
+ // Retries at: 500ms, 1000ms, 1500ms
290
481
  });
482
+ ```
483
+
484
+ #### LINEAR Strategy
485
+
486
+ Wait increases linearly with attempt number.
291
487
 
292
- // Linear backoff: incrementally increasing delays
293
- await stableRequest({
488
+ ```typescript
489
+ const result = await stableRequest<DataRequest, DataResponse>({
294
490
  reqData: { hostname: 'api.example.com', path: '/data' },
295
- attempts: 5,
296
- wait: 1000, // 1s, 2s, 3s, 4s, 5s
491
+ resReq: true,
492
+ attempts: 4,
493
+ wait: 100,
297
494
  retryStrategy: RETRY_STRATEGIES.LINEAR
495
+ // Retries at: 100ms, 200ms, 300ms (wait * attempt)
298
496
  });
497
+ ```
498
+
499
+ #### EXPONENTIAL Strategy
299
500
 
300
- // Exponential backoff: exponentially growing delays
301
- await stableRequest({
501
+ Wait increases exponentially; useful for heavily loaded services.
502
+
503
+ ```typescript
504
+ const result = await stableRequest<DataRequest, DataResponse>({
302
505
  reqData: { hostname: 'api.example.com', path: '/data' },
303
- attempts: 5,
304
- wait: 1000, // 1s, 2s, 4s, 8s, 16s
506
+ resReq: true,
507
+ attempts: 4,
508
+ wait: 100,
509
+ maxAllowedWait: 10000,
305
510
  retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
511
+ // Retries at: 100ms, 200ms, 400ms (wait * 2^(attempt-1))
512
+ // Capped at maxAllowedWait
306
513
  });
307
514
  ```
308
515
 
309
- **Features**:
310
- - Automatic retry on 5xx errors and network failures
311
- - No retry on 4xx client errors (configurable)
312
- - Maximum allowed wait time to prevent excessive delays
313
- - Per-request or workflow-level configuration
516
+ #### Jitter
517
+
518
+ Add random milliseconds to prevent synchronization.
314
519
 
315
- **Custom Response Validation**:
316
520
  ```typescript
317
- await stableRequest({
318
- reqData: { hostname: 'api.example.com', path: '/job/status' },
521
+ const result = await stableRequest<DataRequest, DataResponse>({
522
+ reqData: { hostname: 'api.example.com', path: '/data' },
319
523
  resReq: true,
320
- attempts: 10,
321
- wait: 2000,
322
- responseAnalyzer: async ({ data }) => {
323
- // Retry until job is complete
324
- return data.status === 'completed';
325
- }
524
+ attempts: 3,
525
+ wait: 500,
526
+ jitter: 200, // Add 0-200ms randomness
527
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
326
528
  });
327
529
  ```
328
530
 
329
- ### Circuit Breaker Pattern
531
+ #### Perform All Attempts
330
532
 
331
- Prevent cascade failures and system overload with built-in circuit breakers:
533
+ Collect all outcomes instead of failing on first error.
332
534
 
333
535
  ```typescript
334
- import { stableRequest, CircuitBreakerState } from '@emmvish/stable-request';
335
-
336
- await stableRequest({
337
- reqData: { hostname: 'unreliable-api.example.com', path: '/data' },
536
+ const result = await stableRequest<DataRequest, DataResponse>({
537
+ reqData: { hostname: 'api.example.com', path: '/data' },
538
+ resReq: true,
338
539
  attempts: 3,
339
- circuitBreaker: {
340
- failureThresholdPercentage: 50, // Open after 50% failures
341
- minimumRequests: 10, // Minimum requests before evaluation
342
- recoveryTimeoutMs: 60000, // Wait 60s before trying again (half-open)
343
- successThresholdPercentage: 20, // Close after 20% successes in half-open
344
- trackIndividualAttempts: false // Track at request level (not attempt level)
345
- }
540
+ performAllAttempts: true
541
+ // All 3 attempts execute; check result.successfulAttempts
346
542
  });
347
543
  ```
348
544
 
349
- **Circuit Breaker States**:
350
- - **CLOSED**: Normal operation, requests flow through
351
- - **OPEN**: Too many failures, requests blocked immediately
352
- - **HALF_OPEN**: Testing if service recovered, limited requests allowed
545
+ ### Circuit Breaker
546
+
547
+ Prevent cascading failures by failing fast when a dependency becomes unhealthy.
353
548
 
354
- **Workflow-Level Circuit Breakers**:
355
549
  ```typescript
356
- import { CircuitBreaker } from '@emmvish/stable-request';
550
+ import { stableApiGateway, CircuitBreaker } from '@emmvish/stable-request';
357
551
 
358
- const sharedBreaker = new CircuitBreaker({
359
- failureThresholdPercentage: 50, // 50% failure rate triggers open
360
- minimumRequests: 10, // Minimum 10 requests before evaluation
361
- recoveryTimeoutMs: 120000, // 120s timeout in open state
362
- successThresholdPercentage: 50 // 50% success rate closes circuit
363
- });
552
+ interface FlakyRequest {}
553
+ interface FlakyResponse { status: string; }
364
554
 
365
- await stableWorkflow(phases, {
366
- circuitBreaker: sharedBreaker, // Shared across all requests
367
- commonRequestData: { hostname: 'api.example.com' }
555
+ const breaker = new CircuitBreaker({
556
+ failureThresholdPercentage: 50,
557
+ minimumRequests: 10,
558
+ recoveryTimeoutMs: 30000,
559
+ successThresholdPercentage: 80,
560
+ halfOpenMaxRequests: 5
368
561
  });
369
562
 
370
- // Check circuit breaker state
371
- console.log(sharedBreaker.getState());
372
- // { state: 'CLOSED', failures: 0, successes: 0, ... }
373
- ```
374
-
375
- ### Response Caching
376
-
377
- Reduce redundant API calls and improve performance with intelligent caching:
563
+ const requests = [
564
+ { id: 'req-1', requestOptions: { reqData: { path: '/flaky' }, resReq: true } },
565
+ { id: 'req-2', requestOptions: { reqData: { path: '/flaky' }, resReq: true } }
566
+ ];
378
567
 
379
- ```typescript
380
- await stableRequest({
381
- reqData: { hostname: 'api.example.com', path: '/static-data' },
382
- resReq: true,
383
- cache: {
384
- enabled: true,
385
- ttl: 300000, // Cache for 5 minutes
386
- key: 'custom-cache-key' // Optional: custom cache key
387
- }
568
+ const responses = await stableApiGateway<FlakyRequest, FlakyResponse>(requests, {
569
+ circuitBreaker: breaker
388
570
  });
389
571
 
390
- // Subsequent identical requests within 5 minutes will use cached response
572
+ // Circuit breaker states:
573
+ // CLOSED: Normal operation (accept all requests)
574
+ // OPEN: Too many failures; reject immediately
575
+ // HALF_OPEN: Testing recovery; allow limited requests
391
576
  ```
392
577
 
393
- **Global Cache Management**:
394
- ```typescript
395
- import { getGlobalCacheManager, resetGlobalCacheManager } from '@emmvish/stable-request';
578
+ **State Transitions:**
396
579
 
397
- const cacheManager = getGlobalCacheManager();
580
+ - **CLOSED OPEN:** Failure rate exceeds threshold after minimum requests
581
+ - **OPEN → HALF_OPEN:** Recovery timeout elapsed; attempt recovery
582
+ - **HALF_OPEN → CLOSED:** Success rate exceeds recovery threshold
583
+ - **HALF_OPEN → OPEN:** Success rate below recovery threshold; reopen
398
584
 
399
- // Inspect cache statistics
400
- const stats = cacheManager.getStats();
401
- console.log(stats);
402
- // { size: 42, validEntries: 38, expiredEntries: 4 }
585
+ ### Caching
403
586
 
404
- // Clear all cached responses
405
- cacheManager.clearAll();
587
+ Cache responses to avoid redundant calls.
406
588
 
407
- // Or reset the global cache instance
408
- resetGlobalCacheManager();
409
- ```
589
+ ```typescript
590
+ import { stableRequest, CacheManager } from '@emmvish/stable-request';
591
+
592
+ interface UserRequest {}
593
+ interface UserResponse {
594
+ id: number;
595
+ name: string;
596
+ email: string;
597
+ }
410
598
 
411
- **Cache Features**:
412
- - Automatic request fingerprinting (method, URL, headers, body)
413
- - TTL-based expiration
414
- - Workflow-wide sharing across phases and branches
415
- - Manual cache inspection and clearing
416
- - Per-request cache configuration
599
+ const cache = new CacheManager({
600
+ enabled: true,
601
+ ttl: 5000 // 5 seconds
602
+ });
417
603
 
418
- ### Rate Limiting and Concurrency Control
604
+ // First call: cache miss, hits API
605
+ const result1 = await stableRequest<UserRequest, UserResponse>({
606
+ reqData: { hostname: 'api.example.com', path: '/user/1' },
607
+ resReq: true,
608
+ cache
609
+ });
419
610
 
420
- Respect API rate limits and control system load:
611
+ // Second call within 5s: cache hit, returns cached response
612
+ const result2 = await stableRequest<UserRequest, UserResponse>({
613
+ reqData: { hostname: 'api.example.com', path: '/user/1' },
614
+ resReq: true,
615
+ cache
616
+ });
421
617
 
422
- ```typescript
423
- await stableWorkflow(phases, {
424
- commonRequestData: { hostname: 'api.example.com' },
425
-
426
- // Rate limiting (token bucket algorithm)
427
- rateLimit: {
428
- maxRequests: 100, // 100 requests
429
- windowMs: 60000 // per 60 seconds
430
- },
431
-
432
- // Concurrency limiting
433
- maxConcurrentRequests: 5 // Max 5 parallel requests
618
+ // Respects Cache-Control headers if enabled
619
+ const cache2 = new CacheManager({
620
+ enabled: true,
621
+ ttl: 60000,
622
+ respectCacheControl: true // Uses max-age, no-cache, no-store
434
623
  });
435
624
  ```
436
625
 
437
- **Per-Phase Configuration**:
438
- ```typescript
439
- const phases = [
440
- {
441
- id: 'bulk-import',
442
- maxConcurrentRequests: 10, // Override workflow limit
443
- rateLimit: {
444
- maxRequests: 50,
445
- windowMs: 10000
446
- },
447
- requests: [...]
448
- }
449
- ];
450
- ```
626
+ **Function Caching:**
451
627
 
452
- **Standalone Rate Limiter**:
453
- ```typescript
454
- import { RateLimiter } from '@emmvish/stable-request';
628
+ Arguments become cache key; identical args hit cache.
455
629
 
456
- const limiter = new RateLimiter(1000, 3600000); // 1000 requests per hour
630
+ ```typescript
631
+ import { stableFunction } from '@emmvish/stable-request';
457
632
 
458
- const state = await limiter.getState(); // Get current state
459
- console.log(state);
460
- // { availableTokens: 1000, queueLength: 0, maxRequests: 1000, windowMs: 3600000 }
461
- ```
633
+ const expensive = (x: number) => x * x * x; // Cubic calculation
462
634
 
463
- ## Metrics and Observability
635
+ const result1 = await stableFunction({
636
+ fn: expensive,
637
+ args: [5],
638
+ returnResult: true,
639
+ cache: { enabled: true, ttl: 10000 }
640
+ });
464
641
 
465
- `@emmvish/stable-request` provides comprehensive metrics at every level of execution, from individual requests to complete workflows. All metrics are automatically computed and included in results.
642
+ const result2 = await stableFunction({
643
+ fn: expensive,
644
+ args: [5], // Same args → cache hit
645
+ returnResult: true,
646
+ cache: { enabled: true, ttl: 10000 }
647
+ });
648
+ ```
466
649
 
467
- ### Request-Level Metrics
650
+ ### Rate Limiting
468
651
 
469
- Every `stableRequest` call returns detailed metrics about the request execution:
652
+ Enforce max requests per time window.
470
653
 
471
654
  ```typescript
472
- import { stableRequest } from '@emmvish/stable-request';
655
+ import { stableApiGateway } from '@emmvish/stable-request';
473
656
 
474
- const result = await stableRequest({
475
- reqData: {
476
- hostname: 'api.example.com',
477
- path: '/users/123'
478
- },
479
- resReq: true,
480
- attempts: 3,
481
- wait: 1000,
482
- logAllErrors: true
483
- });
657
+ interface ItemRequest {}
658
+ interface ItemResponse {
659
+ id: number;
660
+ data: any;
661
+ }
484
662
 
485
- // Access request metrics
486
- console.log('Request Result:', {
487
- success: result.success, // true/false
488
- data: result.data, // Response data
489
- error: result.error, // Error message (if failed)
490
- errorLogs: result.errorLogs, // All failed attempts
491
- successfulAttempts: result.successfulAttempts, // All successful attempts
492
- metrics: {
493
- totalAttempts: result.metrics.totalAttempts, // 3
494
- successfulAttempts: result.metrics.successfulAttempts, // 1
495
- failedAttempts: result.metrics.failedAttempts, // 2
496
- totalExecutionTime: result.metrics.totalExecutionTime, // ms
497
- averageAttemptTime: result.metrics.averageAttemptTime, // ms
498
- infrastructureMetrics: {
499
- circuitBreaker: result.metrics.infrastructureMetrics?.circuitBreaker,
500
- cache: result.metrics.infrastructureMetrics?.cache
501
- }
663
+ const requests = Array.from({ length: 20 }, (_, i) => ({
664
+ id: `req-${i}`,
665
+ requestOptions: {
666
+ reqData: { path: `/item/${i}` },
667
+ resReq: true
502
668
  }
503
- });
504
-
505
- // Error logs provide detailed attempt information
506
- result.errorLogs?.forEach(log => {
507
- console.log({
508
- attempt: log.attempt, // "1/3"
509
- timestamp: log.timestamp,
510
- error: log.error,
511
- statusCode: log.statusCode,
512
- type: log.type, // "HTTP_ERROR" | "INVALID_CONTENT"
513
- isRetryable: log.isRetryable,
514
- executionTime: log.executionTime
515
- });
516
- });
669
+ }));
517
670
 
518
- // Successful attempts show what worked
519
- result.successfulAttempts?.forEach(attempt => {
520
- console.log({
521
- attempt: attempt.attempt, // "3/3"
522
- timestamp: attempt.timestamp,
523
- executionTime: attempt.executionTime,
524
- data: attempt.data,
525
- statusCode: attempt.statusCode
526
- });
671
+ const responses = await stableApiGateway<ItemRequest, ItemResponse>(requests, {
672
+ concurrentExecution: true,
673
+ rateLimit: {
674
+ maxRequests: 5,
675
+ windowMs: 1000 // 5 requests per second
676
+ }
677
+ // Requests queued until window allows; prevents overwhelming API
527
678
  });
528
679
  ```
529
680
 
530
- **STABLE_REQUEST_RESULT Structure:**
531
- - `success`: Boolean indicating if request succeeded
532
- - `data`: Response data (if `resReq: true`)
533
- - `error`: Error message (if request failed)
534
- - `errorLogs`: Array of all failed attempt details
535
- - `successfulAttempts`: Array of all successful attempt details
536
- - `metrics`: Computed execution metrics and infrastructure statistics
681
+ ### Concurrency Limiting
537
682
 
538
- ### API Gateway Metrics
539
-
540
- `stableApiGateway` provides aggregated metrics for batch requests:
683
+ Limit concurrent in-flight requests.
541
684
 
542
685
  ```typescript
543
686
  import { stableApiGateway } from '@emmvish/stable-request';
544
687
 
545
- const requests = [
546
- { id: 'user-1', groupId: 'users', requestOptions: { reqData: { path: '/users/1' }, resReq: true } },
547
- { id: 'user-2', groupId: 'users', requestOptions: { reqData: { path: '/users/2' }, resReq: true } },
548
- { id: 'order-1', groupId: 'orders', requestOptions: { reqData: { path: '/orders/1' }, resReq: true } },
549
- { id: 'product-1', requestOptions: { reqData: { path: '/products/1' }, resReq: true } }
550
- ];
688
+ interface ItemRequest {}
689
+ interface ItemResponse {
690
+ id: number;
691
+ data: any;
692
+ }
693
+
694
+ const requests = Array.from({ length: 50 }, (_, i) => ({
695
+ id: `req-${i}`,
696
+ requestOptions: {
697
+ reqData: { path: `/item/${i}` },
698
+ resReq: true,
699
+ attempts: 1
700
+ }
701
+ }));
551
702
 
552
- const results = await stableApiGateway(requests, {
703
+ const responses = await stableApiGateway<ItemRequest, ItemResponse>(requests, {
553
704
  concurrentExecution: true,
554
- commonRequestData: { hostname: 'api.example.com' },
555
- commonAttempts: 3,
556
- circuitBreaker: { failureThresholdPercentage: 50, minimumRequests: 5 },
557
- rateLimit: { maxRequests: 100, windowMs: 60000 },
558
- maxConcurrentRequests: 5
705
+ maxConcurrentRequests: 5 // Only 5 requests in-flight at a time
706
+ // Others queued and executed as slots free
559
707
  });
708
+ ```
560
709
 
561
- // Gateway-level metrics
562
- console.log('Gateway Metrics:', {
563
- totalRequests: results.metrics.totalRequests, // 4
564
- successfulRequests: results.metrics.successfulRequests, // 3
565
- failedRequests: results.metrics.failedRequests, // 1
566
- successRate: results.metrics.successRate, // 75%
567
- failureRate: results.metrics.failureRate // 25%
568
- });
710
+ ---
569
711
 
570
- // Request group metrics
571
- results.metrics.requestGroups?.forEach(group => {
572
- console.log(`Group ${group.groupId}:`, {
573
- totalRequests: group.totalRequests,
574
- successfulRequests: group.successfulRequests,
575
- failedRequests: group.failedRequests,
576
- successRate: group.successRate, // %
577
- failureRate: group.failureRate, // %
578
- requestIds: group.requestIds // Array of request IDs
579
- });
580
- });
712
+ ## Workflow Patterns
581
713
 
582
- // Infrastructure metrics (when utilities are used)
583
- if (results.metrics.infrastructureMetrics) {
584
- const infra = results.metrics.infrastructureMetrics;
585
-
586
- // Circuit Breaker metrics
587
- if (infra.circuitBreaker) {
588
- console.log('Circuit Breaker:', {
589
- state: infra.circuitBreaker.state, // CLOSED | OPEN | HALF_OPEN
590
- isHealthy: infra.circuitBreaker.isHealthy,
591
- totalRequests: infra.circuitBreaker.totalRequests,
592
- failurePercentage: infra.circuitBreaker.failurePercentage,
593
- openCount: infra.circuitBreaker.openCount,
594
- recoveryAttempts: infra.circuitBreaker.recoveryAttempts
595
- });
596
- }
597
-
598
- // Cache metrics
599
- if (infra.cache) {
600
- console.log('Cache:', {
601
- hitRate: infra.cache.hitRate, // %
602
- currentSize: infra.cache.currentSize,
603
- networkRequestsSaved: infra.cache.networkRequestsSaved,
604
- cacheEfficiency: infra.cache.cacheEfficiency // %
605
- });
606
- }
607
-
608
- // Rate Limiter metrics
609
- if (infra.rateLimiter) {
610
- console.log('Rate Limiter:', {
611
- throttledRequests: infra.rateLimiter.throttledRequests,
612
- throttleRate: infra.rateLimiter.throttleRate, // %
613
- peakRequestRate: infra.rateLimiter.peakRequestRate
614
- });
615
- }
616
-
617
- // Concurrency Limiter metrics
618
- if (infra.concurrencyLimiter) {
619
- console.log('Concurrency:', {
620
- peakConcurrency: infra.concurrencyLimiter.peakConcurrency,
621
- utilizationPercentage: infra.concurrencyLimiter.utilizationPercentage,
622
- averageQueueWaitTime: infra.concurrencyLimiter.averageQueueWaitTime
623
- });
624
- }
625
- }
626
- ```
714
+ ### Sequential & Concurrent Phases
627
715
 
628
- ### Workflow Metrics
716
+ #### Sequential (Default)
629
717
 
630
- `stableWorkflow` provides end-to-end metrics for complex orchestrations:
718
+ Each phase waits for the previous to complete.
631
719
 
632
720
  ```typescript
633
- import { stableWorkflow, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
721
+ import { stableWorkflow } from '@emmvish/stable-request';
722
+ import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
634
723
 
635
- const phases = [
724
+ const phases: STABLE_WORKFLOW_PHASE[] = [
636
725
  {
637
- id: 'fetch-users',
638
- requests: [/* ... */]
726
+ id: 'phase-1',
727
+ requests: [{ id: 'r1', requestOptions: { reqData: { path: '/p1' }, resReq: true } }]
639
728
  },
640
729
  {
641
- id: 'process-data',
642
- concurrent: true,
643
- requests: [/* ... */]
730
+ id: 'phase-2',
731
+ requests: [{ id: 'r2', requestOptions: { reqData: { path: '/p2' }, resReq: true } }]
644
732
  },
645
733
  {
646
- id: 'store-results',
647
- requests: [/* ... */]
734
+ id: 'phase-3',
735
+ requests: [{ id: 'r3', requestOptions: { reqData: { path: '/p3' }, resReq: true } }]
648
736
  }
649
737
  ];
650
738
 
651
739
  const result = await stableWorkflow(phases, {
652
- workflowId: 'data-processing-pipeline',
653
- enableMixedExecution: true,
654
- commonRequestData: { hostname: 'api.example.com' },
655
- logPhaseResults: true
656
- });
657
-
658
- // Workflow-level metrics
659
- console.log('Workflow Metrics:', {
660
- workflowId: result.metrics.workflowId,
661
- success: result.metrics.success,
662
- executionTime: result.metrics.executionTime, // Total time in ms
663
-
664
- // Phase statistics
665
- totalPhases: result.metrics.totalPhases,
666
- completedPhases: result.metrics.completedPhases,
667
- skippedPhases: result.metrics.skippedPhases,
668
- failedPhases: result.metrics.failedPhases,
669
- phaseCompletionRate: result.metrics.phaseCompletionRate, // %
670
- averagePhaseExecutionTime: result.metrics.averagePhaseExecutionTime, // ms
671
-
672
- // Request statistics
673
- totalRequests: result.metrics.totalRequests,
674
- successfulRequests: result.metrics.successfulRequests,
675
- failedRequests: result.metrics.failedRequests,
676
- requestSuccessRate: result.metrics.requestSuccessRate, // %
677
- requestFailureRate: result.metrics.requestFailureRate, // %
678
-
679
- // Performance
680
- throughput: result.metrics.throughput, // requests/second
681
- totalPhaseReplays: result.metrics.totalPhaseReplays,
682
- totalPhaseSkips: result.metrics.totalPhaseSkips,
683
-
684
- // Branch metrics (if using branch execution)
685
- totalBranches: result.metrics.totalBranches,
686
- completedBranches: result.metrics.completedBranches,
687
- failedBranches: result.metrics.failedBranches,
688
- branchSuccessRate: result.metrics.branchSuccessRate // %
689
- });
690
-
691
- // Request group metrics aggregated across entire workflow
692
- result.requestGroupMetrics?.forEach(group => {
693
- console.log(`Request Group ${group.groupId}:`, {
694
- totalRequests: group.totalRequests,
695
- successRate: group.successRate, // %
696
- requestIds: group.requestIds
697
- });
698
- });
699
-
700
- // Per-phase metrics
701
- result.phases.forEach(phase => {
702
- console.log(`Phase ${phase.phaseId}:`, {
703
- executionTime: phase.metrics?.executionTime,
704
- totalRequests: phase.metrics?.totalRequests,
705
- successfulRequests: phase.metrics?.successfulRequests,
706
- requestSuccessRate: phase.metrics?.requestSuccessRate, // %
707
- hasDecision: phase.metrics?.hasDecision,
708
- decisionAction: phase.metrics?.decisionAction // CONTINUE | JUMP | REPLAY | etc.
709
- });
710
- });
711
-
712
- // Branch metrics (for branched workflows)
713
- result.branches?.forEach(branch => {
714
- console.log(`Branch ${branch.branchId}:`, {
715
- success: branch.metrics?.success,
716
- executionTime: branch.metrics?.executionTime,
717
- totalPhases: branch.metrics?.totalPhases,
718
- completedPhases: branch.metrics?.completedPhases,
719
- totalRequests: branch.metrics?.totalRequests,
720
- requestSuccessRate: branch.metrics?.requestSuccessRate // %
721
- });
740
+ workflowId: 'sequential-phases',
741
+ concurrentPhaseExecution: false // Phase-1 → Phase-2 → Phase-3
722
742
  });
723
743
  ```
724
744
 
725
- ### MetricsAggregator Utility
745
+ #### Concurrent Phases
726
746
 
727
- For custom metrics extraction and analysis:
747
+ Multiple phases run in parallel.
728
748
 
729
749
  ```typescript
730
- import { MetricsAggregator } from '@emmvish/stable-request';
731
-
732
- // Extract workflow metrics
733
- const workflowMetrics = MetricsAggregator.extractWorkflowMetrics(workflowResult);
734
-
735
- // Extract phase metrics
736
- const phaseMetrics = MetricsAggregator.extractPhaseMetrics(phaseResult);
737
-
738
- // Extract branch metrics
739
- const branchMetrics = MetricsAggregator.extractBranchMetrics(branchResult);
740
-
741
- // Extract request group metrics
742
- const requestGroups = MetricsAggregator.extractRequestGroupMetrics(responses);
743
-
744
- // Extract individual request metrics
745
- const requestMetrics = MetricsAggregator.extractRequestMetrics(responses);
746
-
747
- // Extract circuit breaker metrics
748
- const cbMetrics = MetricsAggregator.extractCircuitBreakerMetrics(circuitBreaker);
749
-
750
- // Extract cache metrics
751
- const cacheMetrics = MetricsAggregator.extractCacheMetrics(cacheManager);
752
-
753
- // Extract rate limiter metrics
754
- const rateLimiterMetrics = MetricsAggregator.extractRateLimiterMetrics(rateLimiter);
755
-
756
- // Extract concurrency limiter metrics
757
- const concurrencyMetrics = MetricsAggregator.extractConcurrencyLimiterMetrics(limiter);
758
-
759
- // Aggregate all system metrics
760
- const systemMetrics = MetricsAggregator.aggregateSystemMetrics(
761
- workflowResult,
762
- circuitBreaker,
763
- cacheManager,
764
- rateLimiter,
765
- concurrencyLimiter
766
- );
767
-
768
- console.log('Complete System View:', {
769
- workflow: systemMetrics.workflow,
770
- phases: systemMetrics.phases,
771
- branches: systemMetrics.branches,
772
- requestGroups: systemMetrics.requestGroups,
773
- requests: systemMetrics.requests,
774
- circuitBreaker: systemMetrics.circuitBreaker,
775
- cache: systemMetrics.cache,
776
- rateLimiter: systemMetrics.rateLimiter,
777
- concurrencyLimiter: systemMetrics.concurrencyLimiter
750
+ const phases: STABLE_WORKFLOW_PHASE[] = [
751
+ {
752
+ id: 'fetch-users',
753
+ requests: [{ id: 'get-users', requestOptions: { reqData: { path: '/users' }, resReq: true } }]
754
+ },
755
+ {
756
+ id: 'fetch-posts',
757
+ requests: [{ id: 'get-posts', requestOptions: { reqData: { path: '/posts' }, resReq: true } }]
758
+ },
759
+ {
760
+ id: 'fetch-comments',
761
+ requests: [{ id: 'get-comments', requestOptions: { reqData: { path: '/comments' }, resReq: true } }]
762
+ }
763
+ ];
764
+
765
+ const result = await stableWorkflow(phases, {
766
+ workflowId: 'parallel-phases',
767
+ concurrentPhaseExecution: true // All 3 phases in parallel
778
768
  });
779
769
  ```
780
770
 
781
- **Available Metrics Types:**
782
- - `WorkflowMetrics`: Complete workflow statistics
783
- - `BranchMetrics`: Branch execution metrics
784
- - `PhaseMetrics`: Individual phase metrics
785
- - `RequestGroupMetrics`: Grouped request statistics
786
- - `RequestMetrics`: Individual request metrics
787
- - `CircuitBreakerDashboardMetrics`: Circuit breaker state and performance
788
- - `CacheDashboardMetrics`: Cache hit rates and efficiency
789
- - `RateLimiterDashboardMetrics`: Throttling and rate limit statistics
790
- - `ConcurrencyLimiterDashboardMetrics`: Concurrency and queue metrics
791
- - `SystemMetrics`: Complete system-wide aggregation
792
-
793
- ## Workflow Execution Patterns
794
-
795
- ### Sequential and Concurrent Phases
771
+ #### Mixed Phases
796
772
 
797
- Control execution order at the phase and request level:
773
+ Combine sequential and concurrent phases in one workflow.
798
774
 
799
- **Sequential Phases (Default)**:
800
775
  ```typescript
801
- const phases = [
802
- { id: 'step-1', requests: [...] }, // Executes first
803
- { id: 'step-2', requests: [...] }, // Then this
804
- { id: 'step-3', requests: [...] } // Finally this
805
- ];
806
-
807
- await stableWorkflow(phases, {
808
- commonRequestData: { hostname: 'api.example.com' }
809
- });
810
- ```
811
-
812
- **Concurrent Phases**:
813
- ```typescript
814
- const phases = [
815
- { id: 'init', requests: [...] },
816
- { id: 'parallel-1', requests: [...] },
817
- { id: 'parallel-2', requests: [...] }
776
+ const phases: STABLE_WORKFLOW_PHASE[] = [
777
+ {
778
+ id: 'init', // Sequential
779
+ requests: [{ id: 'setup', requestOptions: { reqData: { path: '/init' }, resReq: true } }]
780
+ },
781
+ {
782
+ id: 'fetch-a',
783
+ markConcurrentPhase: true, // Concurrent with next
784
+ requests: [{ id: 'data-a', requestOptions: { reqData: { path: '/a' }, resReq: true } }]
785
+ },
786
+ {
787
+ id: 'fetch-b',
788
+ markConcurrentPhase: true, // Concurrent with fetch-a
789
+ requests: [{ id: 'data-b', requestOptions: { reqData: { path: '/b' }, resReq: true } }]
790
+ },
791
+ {
792
+ id: 'finalize', // Sequential after fetch-a/b complete
793
+ requests: [{ id: 'done', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }]
794
+ }
818
795
  ];
819
796
 
820
- await stableWorkflow(phases, {
821
- concurrentPhaseExecution: true, // All phases run in parallel
822
- commonRequestData: { hostname: 'api.example.com' }
797
+ const result = await stableWorkflow(phases, {
798
+ concurrentPhaseExecution: false // Respects markConcurrentPhase per phase
823
799
  });
824
800
  ```
825
801
 
826
- **Concurrent Requests Within Phase**:
827
- ```typescript
828
- const phases = [
829
- {
830
- id: 'data-fetch',
831
- concurrentExecution: true, // Requests run in parallel
832
- requests: [
833
- { id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
834
- { id: 'products', requestOptions: { reqData: { path: '/products' }, resReq: true } },
835
- { id: 'orders', requestOptions: { reqData: { path: '/orders' }, resReq: true } }
836
- ]
837
- }
838
- ];
839
- ```
802
+ ### Non-Linear Workflows
803
+
804
+ Use decision hooks to dynamically control phase flow.
805
+
806
+ #### CONTINUE
807
+
808
+ Standard flow to next sequential phase.
840
809
 
841
- **Stop on First Error**:
842
810
  ```typescript
843
- const phases = [
811
+ const phases: STABLE_WORKFLOW_PHASE[] = [
812
+ {
813
+ id: 'check-status',
814
+ requests: [{ id: 'api', requestOptions: { reqData: { path: '/status' }, resReq: true } }],
815
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
816
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
817
+ }
818
+ },
844
819
  {
845
- id: 'critical-phase',
846
- stopOnFirstError: true, // Stop phase if any request fails
847
- requests: [...]
820
+ id: 'process', // Executes after check-status
821
+ requests: [{ id: 'process-data', requestOptions: { reqData: { path: '/process' }, resReq: true } }]
848
822
  }
849
823
  ];
850
824
 
851
- await stableWorkflow(phases, {
852
- stopOnFirstPhaseError: true, // Stop workflow if any phase fails
853
- commonRequestData: { hostname: 'api.example.com' }
825
+ const result = await stableWorkflow(phases, {
826
+ enableNonLinearExecution: true
854
827
  });
855
828
  ```
856
829
 
857
- ### Mixed Execution Mode
830
+ #### SKIP
858
831
 
859
- Combine sequential and concurrent phases for fine-grained control:
832
+ Skip the next phase; execute the one after.
860
833
 
861
834
  ```typescript
862
- const phases = [
863
- {
864
- id: 'authenticate',
865
- requests: [{ id: 'login', requestOptions: {...} }]
866
- },
867
- {
868
- id: 'fetch-user-data',
869
- markConcurrentPhase: true, // This phase runs concurrently...
870
- requests: [{ id: 'profile', requestOptions: {...} }]
835
+ const phases: STABLE_WORKFLOW_PHASE[] = [
836
+ {
837
+ id: 'phase-1',
838
+ requests: [{ id: 'r1', requestOptions: { reqData: { path: '/p1' }, resReq: true } }],
839
+ phaseDecisionHook: async () => ({
840
+ action: PHASE_DECISION_ACTIONS.SKIP
841
+ })
871
842
  },
872
- {
873
- id: 'fetch-orders',
874
- markConcurrentPhase: true, // ...with this phase
875
- requests: [{ id: 'orders', requestOptions: {...} }]
843
+ {
844
+ id: 'phase-2', // Skipped
845
+ requests: [{ id: 'r2', requestOptions: { reqData: { path: '/p2' }, resReq: true } }]
876
846
  },
877
- {
878
- id: 'process-results', // This waits for above to complete
879
- requests: [{ id: 'analytics', requestOptions: {...} }]
847
+ {
848
+ id: 'phase-3', // Executes
849
+ requests: [{ id: 'r3', requestOptions: { reqData: { path: '/p3' }, resReq: true } }]
880
850
  }
881
851
  ];
882
852
 
883
- await stableWorkflow(phases, {
884
- enableMixedExecution: true, // Enable mixed execution mode
885
- commonRequestData: { hostname: 'api.example.com' }
853
+ const result = await stableWorkflow(phases, {
854
+ enableNonLinearExecution: true
886
855
  });
887
- ```
888
856
 
889
- **Use Case**: Authenticate first (sequential), then fetch multiple data sources in parallel (concurrent), then process results (sequential).
857
+ // Execution: phase-1 phase-3
858
+ ```
890
859
 
891
- ### Non-Linear Workflows
860
+ #### JUMP
892
861
 
893
- Build dynamic workflows with conditional branching, looping, and early termination:
862
+ Jump to a specific phase by ID.
894
863
 
895
864
  ```typescript
896
- import { PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
897
-
898
- const phases = [
865
+ const phases: STABLE_WORKFLOW_PHASE[] = [
899
866
  {
900
- id: 'validate-user',
901
- requests: [
902
- { id: 'check', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
903
- ],
904
- phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
905
- const isValid = phaseResult.responses[0]?.data?.isValid;
906
-
907
- if (isValid) {
908
- // Jump directly to success phase
909
- return {
910
- action: PHASE_DECISION_ACTIONS.JUMP,
911
- targetPhaseId: 'success-flow'
912
- };
913
- } else {
914
- // Continue to retry logic
915
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
916
- }
917
- }
867
+ id: 'phase-1',
868
+ requests: [{ id: 'r1', requestOptions: { reqData: { path: '/p1' }, resReq: true } }],
869
+ phaseDecisionHook: async () => ({
870
+ action: PHASE_DECISION_ACTIONS.JUMP,
871
+ targetPhaseId: 'recovery'
872
+ })
918
873
  },
919
874
  {
920
- id: 'retry-validation',
921
- allowReplay: true,
922
- maxReplayCount: 3,
923
- requests: [
924
- { id: 'retry', requestOptions: { reqData: { path: '/retry-validate' }, resReq: true } }
925
- ],
926
- phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
927
- const replayCount = executionHistory.filter(
928
- h => h.phaseId === 'retry-validation'
929
- ).length;
930
-
931
- const success = phaseResult.responses[0]?.data?.success;
932
-
933
- if (success) {
934
- return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'success-flow' };
935
- } else if (replayCount < 3) {
936
- return { action: PHASE_DECISION_ACTIONS.REPLAY };
937
- } else {
938
- return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Max retries exceeded' } };
939
- }
940
- }
875
+ id: 'phase-2', // Skipped
876
+ requests: [{ id: 'r2', requestOptions: { reqData: { path: '/p2' }, resReq: true } }]
941
877
  },
942
878
  {
943
- id: 'success-flow',
944
- requests: [
945
- { id: 'finalize', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
946
- ]
879
+ id: 'recovery',
880
+ requests: [{ id: 'recover', requestOptions: { reqData: { path: '/recovery' }, resReq: true } }]
947
881
  }
948
882
  ];
949
883
 
950
884
  const result = await stableWorkflow(phases, {
951
- enableNonLinearExecution: true, // Enable non-linear execution
952
- workflowId: 'adaptive-validation',
953
- commonRequestData: { hostname: 'api.example.com' }
885
+ enableNonLinearExecution: true
954
886
  });
955
887
 
956
- console.log(result.executionHistory);
957
- // Array of execution records showing which phases ran and why
958
- ```
959
-
960
- **Phase Decision Actions**:
961
- - **CONTINUE**: Proceed to next sequential phase (default)
962
- - **JUMP**: Skip to a specific phase by ID
963
- - **SKIP**: Skip upcoming phases until a target phase (or end)
964
- - **REPLAY**: Re-execute the current phase (requires `allowReplay: true`)
965
- - **TERMINATE**: Stop the entire workflow immediately
966
-
967
- **Decision Hook Context**:
968
- ```typescript
969
- phaseDecisionHook: async ({
970
- phaseResult, // Current phase execution result
971
- executionHistory, // Array of all executed phases
972
- sharedBuffer, // Cross-phase shared state
973
- concurrentResults // Results from concurrent phases (mixed execution)
974
- }) => {
975
- // Your decision logic
976
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
977
- }
978
- ```
979
-
980
- **Replay Limits**:
981
- ```typescript
982
- {
983
- id: 'polling-phase',
984
- allowReplay: true,
985
- maxReplayCount: 10, // Maximum 10 replays
986
- requests: [...],
987
- phaseDecisionHook: async ({ phaseResult }) => {
988
- if (phaseResult.responses[0]?.data?.ready) {
989
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
990
- }
991
- return { action: PHASE_DECISION_ACTIONS.REPLAY };
992
- }
993
- }
888
+ // Execution: phase-1 → recovery
994
889
  ```
995
890
 
996
- ### Dynamic Phase and Branch Addition
891
+ #### REPLAY
997
892
 
998
- Dynamically add phases or branches during workflow execution based on runtime conditions:
893
+ Re-execute current phase; useful for polling.
999
894
 
1000
- **Adding Phases Dynamically**:
1001
895
  ```typescript
1002
- const phases = [
896
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1003
897
  {
1004
- id: 'initial-phase',
1005
- requests: [...],
1006
- phaseDecisionHook: async ({ phaseResult }) => {
1007
- const needsExtraProcessing = phaseResult.responses[0]?.data?.requiresValidation;
1008
-
1009
- if (needsExtraProcessing) {
1010
- return {
1011
- action: PHASE_DECISION_ACTIONS.CONTINUE,
1012
- addPhases: [
1013
- {
1014
- id: 'validation-phase',
1015
- requests: [{ id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }]
1016
- }
1017
- ]
1018
- };
898
+ id: 'wait-for-job',
899
+ allowReplay: true,
900
+ maxReplayCount: 5,
901
+ requests: [
902
+ {
903
+ id: 'check-job',
904
+ requestOptions: { reqData: { path: '/job/status' }, resReq: true, attempts: 1 }
905
+ }
906
+ ],
907
+ phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
908
+ const lastResponse = phaseResult.responses?.[0];
909
+ if ((lastResponse as any)?.data?.status === 'pending' && executionHistory.length < 5) {
910
+ return { action: PHASE_DECISION_ACTIONS.REPLAY };
1019
911
  }
1020
912
  return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1021
913
  }
914
+ },
915
+ {
916
+ id: 'process-result',
917
+ requests: [{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }]
1022
918
  }
1023
919
  ];
1024
920
 
1025
- await stableWorkflow(phases, {
921
+ const result = await stableWorkflow(phases, {
1026
922
  enableNonLinearExecution: true,
1027
- commonRequestData: { hostname: 'api.example.com' }
923
+ maxWorkflowIterations: 100
1028
924
  });
925
+
926
+ // Polls up to 5 times before continuing
1029
927
  ```
1030
928
 
1031
- **Adding Branches Dynamically**:
929
+ #### TERMINATE
930
+
931
+ Stop workflow early.
932
+
1032
933
  ```typescript
1033
- const branches = [
934
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1034
935
  {
1035
- id: 'main-branch',
1036
- phases: [...],
1037
- branchDecisionHook: async ({ branchResults }) => {
1038
- const requiresAudit = branchResults.some(p => p.responses[0]?.data?.flagged);
1039
-
1040
- if (requiresAudit) {
1041
- return {
1042
- action: PHASE_DECISION_ACTIONS.CONTINUE,
1043
- addBranches: [
1044
- {
1045
- id: 'audit-branch',
1046
- phases: [{ id: 'audit', requests: [...] }]
1047
- }
1048
- ]
1049
- };
936
+ id: 'validate',
937
+ requests: [{ id: 'validate-input', requestOptions: { reqData: { path: '/validate' }, resReq: true } }],
938
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
939
+ if (!phaseResult.success) {
940
+ return { action: PHASE_DECISION_ACTIONS.TERMINATE };
1050
941
  }
1051
942
  return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1052
943
  }
944
+ },
945
+ {
946
+ id: 'phase-2', // Won't execute if validation fails
947
+ requests: [{ id: 'r2', requestOptions: { reqData: { path: '/p2' }, resReq: true } }]
1053
948
  }
1054
949
  ];
1055
950
 
1056
- await stableWorkflow([], {
1057
- enableBranchExecution: true,
1058
- branches,
1059
- commonRequestData: { hostname: 'api.example.com' }
951
+ const result = await stableWorkflow(phases, {
952
+ enableNonLinearExecution: true
1060
953
  });
1061
- ```
1062
954
 
1063
- **Extending Current Branch**:
1064
- ```typescript
1065
- branchDecisionHook: async ({ branchResults }) => {
1066
- return {
1067
- action: PHASE_DECISION_ACTIONS.CONTINUE,
1068
- addPhases: [
1069
- { id: 'extra-phase', requests: [...] } // Branch re-executes with new phases
1070
- ]
1071
- };
1072
- }
955
+ console.log(result.terminatedEarly); // true if TERMINATE triggered
1073
956
  ```
1074
957
 
1075
958
  ### Branched Workflows
1076
959
 
1077
- Execute multiple independent workflow paths in parallel or sequentially:
960
+ Execute multiple independent branches with shared state.
1078
961
 
1079
962
  ```typescript
1080
- const branches = [
1081
- {
1082
- id: 'user-flow',
1083
- markConcurrentBranch: true, // Execute in parallel
1084
- phases: [
1085
- { id: 'fetch-user', requests: [...] },
1086
- { id: 'update-user', requests: [...] }
1087
- ]
1088
- },
963
+ import { stableWorkflow } from '@emmvish/stable-request';
964
+ import type { STABLE_WORKFLOW_BRANCH } from '@emmvish/stable-request';
965
+
966
+ const branches: STABLE_WORKFLOW_BRANCH[] = [
1089
967
  {
1090
- id: 'analytics-flow',
1091
- markConcurrentBranch: true, // Execute in parallel
968
+ id: 'branch-payment',
1092
969
  phases: [
1093
- { id: 'log-event', requests: [...] },
1094
- { id: 'update-metrics', requests: [...] }
970
+ {
971
+ id: 'process-payment',
972
+ requests: [
973
+ {
974
+ id: 'charge-card',
975
+ requestOptions: {
976
+ reqData: { path: '/payment/charge' },
977
+ resReq: true
978
+ }
979
+ }
980
+ ]
981
+ }
1095
982
  ]
1096
983
  },
1097
984
  {
1098
- id: 'cleanup-flow', // Sequential (waits for above)
985
+ id: 'branch-notification',
1099
986
  phases: [
1100
- { id: 'clear-cache', requests: [...] },
1101
- { id: 'notify', requests: [...] }
987
+ {
988
+ id: 'send-email',
989
+ requests: [
990
+ {
991
+ id: 'send',
992
+ requestOptions: {
993
+ reqData: { path: '/notify/email' },
994
+ resReq: false
995
+ }
996
+ }
997
+ ]
998
+ }
1102
999
  ]
1103
1000
  }
1104
1001
  ];
1105
1002
 
1106
- const result = await stableWorkflow([], { // Empty phases array
1003
+ const result = await stableWorkflow([], {
1004
+ workflowId: 'checkout',
1107
1005
  enableBranchExecution: true,
1108
1006
  branches,
1109
- workflowId: 'multi-branch-workflow',
1110
- commonRequestData: { hostname: 'api.example.com' }
1007
+ sharedBuffer: { orderId: '12345' },
1008
+ markConcurrentBranch: true // Branches run in parallel
1111
1009
  });
1112
1010
 
1113
- console.log(result.branches); // Branch execution results
1114
- console.log(result.branchExecutionHistory); // Branch-level execution history
1011
+ // Both branches access/modify sharedBuffer
1115
1012
  ```
1116
1013
 
1117
- **Branch-Level Configuration**:
1118
- ```typescript
1119
- const branches = [
1120
- {
1121
- id: 'high-priority-branch',
1122
- markConcurrentBranch: false,
1123
- commonConfig: { // Branch-level config overrides
1124
- commonAttempts: 5,
1125
- commonWait: 2000,
1126
- commonCache: { enabled: true, ttl: 120000 }
1127
- },
1128
- phases: [...]
1129
- }
1130
- ];
1131
- ```
1014
+ ### Graph-Based Workflows with Mixed Items
1132
1015
 
1133
- **Branch Features**:
1134
- - Each branch has independent phase execution
1135
- - Branches share the workflow's `sharedBuffer`
1136
- - Branch decision hooks can terminate the entire workflow
1137
- - Supports all execution patterns (mixed, non-linear) within branches
1016
+ For complex topologies with explicit dependencies, use DAG execution mixing requests and functions.
1138
1017
 
1139
- **Branch Decision Hooks**:
1140
1018
  ```typescript
1141
- const branches = [
1142
- {
1143
- id: 'conditional-branch',
1144
- branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
1145
- if (branchResult.failedRequests > 0) {
1146
- return {
1147
- action: PHASE_DECISION_ACTIONS.TERMINATE,
1148
- metadata: { reason: 'Critical branch failed' }
1149
- };
1150
- }
1151
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1152
- },
1153
- phases: [...]
1154
- }
1155
- ];
1156
- ```
1157
-
1158
- ## Advanced Capabilities
1159
-
1160
- ### Config Cascading
1019
+ import { stableWorkflowGraph, WorkflowGraphBuilder, RequestOrFunction } from '@emmvish/stable-request';
1020
+ import type { API_GATEWAY_ITEM } from '@emmvish/stable-request';
1161
1021
 
1162
- Configuration inheritance across workflow → branch → phase → request levels:
1163
-
1164
- ```typescript
1165
- await stableWorkflow(phases, {
1166
- // Workflow-level config (lowest priority)
1167
- commonAttempts: 3,
1168
- commonWait: 1000,
1169
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1170
- commonCache: { enabled: true, ttl: 60000 },
1171
- commonRequestData: {
1172
- hostname: 'api.example.com',
1173
- headers: { 'X-API-Version': 'v2' }
1174
- },
1175
-
1176
- branches: [{
1177
- id: 'my-branch',
1178
- commonConfig: {
1179
- // Branch-level config (overrides workflow)
1180
- commonAttempts: 5,
1181
- commonWait: 500
1182
- },
1183
- phases: [{
1184
- id: 'my-phase',
1185
- commonConfig: {
1186
- // Phase-level config (overrides branch and workflow)
1187
- commonAttempts: 1,
1188
- commonCache: { enabled: false }
1189
- },
1190
- requests: [{
1191
- id: 'my-request',
1192
- requestOptions: {
1193
- // Request-level config (highest priority)
1194
- reqData: { path: '/critical' },
1195
- attempts: 10,
1196
- wait: 100,
1197
- cache: { enabled: true, ttl: 300000 }
1198
- }
1199
- }]
1200
- }]
1201
- }]
1202
- });
1203
- ```
1022
+ // Request types
1023
+ interface PostsRequest {}
1024
+ interface PostsResponse { posts: Array<{ id: number; title: string }> };
1204
1025
 
1205
- **Priority**: Request > Phase > Branch > Workflow
1026
+ interface UsersRequest {}
1027
+ interface UsersResponse { users: Array<{ id: number; name: string }> };
1206
1028
 
1207
- ### Request Grouping
1029
+ // Function types
1030
+ type AggregateArgs = [PostsResponse, UsersResponse];
1031
+ type AggregateResult = {
1032
+ combined: Array<{ userId: number; userName: string; postCount: number }>;
1033
+ };
1208
1034
 
1209
- Define reusable configurations for groups of related requests:
1035
+ type AnalyzeArgs = [AggregateResult];
1036
+ type AnalyzeResult = { totalPosts: number; activeUsers: number };
1210
1037
 
1211
- ```typescript
1212
- const requests = [
1213
- {
1214
- id: 'critical-1',
1215
- groupId: 'critical',
1216
- requestOptions: { reqData: { path: '/critical/1' }, resReq: true }
1217
- },
1218
- {
1219
- id: 'critical-2',
1220
- groupId: 'critical',
1221
- requestOptions: { reqData: { path: '/critical/2' }, resReq: true }
1222
- },
1223
- {
1224
- id: 'optional-1',
1225
- groupId: 'optional',
1226
- requestOptions: { reqData: { path: '/optional/1' }, resReq: false }
1227
- }
1228
- ];
1038
+ const graph = new WorkflowGraphBuilder<
1039
+ PostsRequest | UsersRequest,
1040
+ PostsResponse | UsersResponse,
1041
+ AggregateArgs | AnalyzeArgs,
1042
+ AggregateResult | AnalyzeResult
1043
+ >()
1044
+ .addPhase('fetch-posts', {
1045
+ requests: [{
1046
+ id: 'get-posts',
1047
+ requestOptions: {
1048
+ reqData: { path: '/posts' },
1049
+ resReq: true
1050
+ }
1051
+ }]
1052
+ })
1053
+ .addPhase('fetch-users', {
1054
+ requests: [{
1055
+ id: 'get-users',
1056
+ requestOptions: {
1057
+ reqData: { path: '/users' },
1058
+ resReq: true
1059
+ }
1060
+ }]
1061
+ })
1062
+ .addParallelGroup('fetch-all', ['fetch-posts', 'fetch-users'])
1063
+ .addPhase('aggregate', {
1064
+ functions: [{
1065
+ id: 'combine-data',
1066
+ functionOptions: {
1067
+ fn: (posts: PostsResponse, users: UsersResponse): AggregateResult => ({
1068
+ combined: users.users.map(user => ({
1069
+ userId: user.id,
1070
+ userName: user.name,
1071
+ postCount: posts.posts.filter(p => p.id === user.id).length
1072
+ }))
1073
+ }),
1074
+ args: [{ posts: [] }, { users: [] }] as AggregateArgs,
1075
+ returnResult: true
1076
+ }
1077
+ }]
1078
+ })
1079
+ .addPhase('analyze', {
1080
+ functions: [{
1081
+ id: 'analyze-data',
1082
+ functionOptions: {
1083
+ fn: (aggregated: AggregateResult): AnalyzeResult => ({
1084
+ totalPosts: aggregated.combined.reduce((sum, u) => sum + u.postCount, 0),
1085
+ activeUsers: aggregated.combined.filter(u => u.postCount > 0).length
1086
+ }),
1087
+ args: [{ combined: [] }] as AnalyzeArgs,
1088
+ returnResult: true
1089
+ }
1090
+ }]
1091
+ })
1092
+ .addMergePoint('sync', ['fetch-all'])
1093
+ .connectSequence('fetch-all', 'sync', 'aggregate', 'analyze')
1094
+ .setEntryPoint('fetch-all')
1095
+ .build();
1229
1096
 
1230
- await stableApiGateway(requests, {
1231
- commonRequestData: { hostname: 'api.example.com' },
1232
- commonAttempts: 1, // Default: 1 attempt
1233
-
1234
- requestGroups: [
1235
- {
1236
- groupId: 'critical',
1237
- commonAttempts: 5, // Critical requests: 5 attempts
1238
- commonWait: 2000,
1239
- commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1240
- commonFinalErrorAnalyzer: async () => false // Never suppress errors
1241
- },
1242
- {
1243
- groupId: 'optional',
1244
- commonAttempts: 2, // Optional requests: 2 attempts
1245
- commonWait: 500,
1246
- commonFinalErrorAnalyzer: async () => true // Suppress errors (return false)
1247
- }
1248
- ]
1097
+ const result = await stableWorkflowGraph(graph, {
1098
+ workflowId: 'data-aggregation'
1249
1099
  });
1100
+
1101
+ console.log(`Graph workflow success: ${result.success}`);
1250
1102
  ```
1251
1103
 
1252
- **Use Cases**:
1253
- - Different retry strategies for critical vs. optional requests
1254
- - Separate error handling for different request types
1255
- - Grouped logging and monitoring
1104
+ **Key responsibilities:**
1105
+ - Define phases as DAG nodes with explicit dependency edges
1106
+ - Execute independent phases in parallel automatically
1107
+ - Support parallel groups, merge points, and conditional routing
1108
+ - Validate graph structure (cycle detection, reachability, orphan detection)
1109
+ - Provide deterministic execution order
1110
+ - Offer higher parallelism than phased workflows for complex topologies
1111
+
1112
+ ---
1256
1113
 
1257
- ### Shared Buffer and Pre-Execution Hooks
1114
+ ## Workflow Patterns
1258
1115
 
1259
- Share state across phases/branches and dynamically transform requests:
1116
+ Execute multiple phases concurrently within a group.
1260
1117
 
1261
- **Shared Buffer**:
1262
1118
  ```typescript
1263
- const sharedBuffer = {
1264
- authToken: null,
1265
- userId: null,
1266
- metrics: []
1267
- };
1119
+ import { stableWorkflowGraph, WorkflowGraphBuilder } from '@emmvish/stable-request';
1268
1120
 
1269
- const phases = [
1270
- {
1271
- id: 'auth',
1121
+ const graph = new WorkflowGraphBuilder()
1122
+ .addPhase('fetch-users', {
1272
1123
  requests: [{
1273
- id: 'login',
1274
- requestOptions: {
1275
- reqData: { path: '/login', method: REQUEST_METHODS.POST },
1276
- resReq: true,
1277
- preExecution: {
1278
- preExecutionHook: ({ commonBuffer }) => {
1279
- // Write to buffer after response
1280
- return {};
1281
- },
1282
- preExecutionHookParams: {},
1283
- applyPreExecutionConfigOverride: false,
1284
- continueOnPreExecutionHookFailure: false
1285
- }
1286
- }
1124
+ id: 'users',
1125
+ requestOptions: { reqData: { path: '/users' }, resReq: true }
1287
1126
  }]
1288
- },
1289
- {
1290
- id: 'fetch-data',
1127
+ })
1128
+ .addPhase('fetch-posts', {
1291
1129
  requests: [{
1292
- id: 'profile',
1293
- requestOptions: {
1294
- reqData: { path: '/profile' },
1295
- resReq: true,
1296
- preExecution: {
1297
- preExecutionHook: ({ commonBuffer }) => {
1298
- // Use token from buffer
1299
- return {
1300
- reqData: {
1301
- headers: {
1302
- 'Authorization': `Bearer ${commonBuffer.authToken}`
1303
- }
1304
- }
1305
- };
1306
- },
1307
- applyPreExecutionConfigOverride: true // Apply returned config
1308
- }
1309
- }
1130
+ id: 'posts',
1131
+ requestOptions: { reqData: { path: '/posts' }, resReq: true }
1310
1132
  }]
1311
- }
1312
- ];
1133
+ })
1134
+ .addPhase('fetch-comments', {
1135
+ requests: [{
1136
+ id: 'comments',
1137
+ requestOptions: { reqData: { path: '/comments' }, resReq: true }
1138
+ }]
1139
+ })
1140
+ .addParallelGroup('data-fetch', ['fetch-users', 'fetch-posts', 'fetch-comments'])
1141
+ .setEntryPoint('data-fetch')
1142
+ .build();
1313
1143
 
1314
- await stableWorkflow(phases, {
1315
- sharedBuffer,
1316
- commonRequestData: { hostname: 'api.example.com' }
1144
+ const result = await stableWorkflowGraph(graph, {
1145
+ workflowId: 'data-aggregation'
1317
1146
  });
1318
1147
 
1319
- console.log(sharedBuffer); // Updated with data from workflow
1148
+ // All 3 phases run concurrently
1320
1149
  ```
1321
1150
 
1322
- **Pre-Execution Hook Use Cases**:
1323
- - Dynamic header injection (auth tokens, correlation IDs)
1324
- - Request payload transformation based on previous responses
1325
- - Conditional request configuration (skip, modify, enhance)
1326
- - Cross-phase state management
1151
+ #### Merge Points
1152
+
1153
+ Synchronize multiple predecessor phases.
1327
1154
 
1328
- **Hook Failure Handling**:
1329
1155
  ```typescript
1330
- {
1331
- preExecution: {
1332
- preExecutionHook: async ({ commonBuffer, inputParams }) => {
1333
- // May throw error
1334
- const token = await fetchTokenFromExternalSource();
1335
- return { reqData: { headers: { 'Authorization': token } } };
1336
- },
1337
- continueOnPreExecutionHookFailure: true // Continue even if hook fails
1338
- }
1339
- }
1156
+ const graph = new WorkflowGraphBuilder()
1157
+ .addPhase('fetch-a', {
1158
+ requests: [{ id: 'a', requestOptions: { reqData: { path: '/a' }, resReq: true } }]
1159
+ })
1160
+ .addPhase('fetch-b', {
1161
+ requests: [{ id: 'b', requestOptions: { reqData: { path: '/b' }, resReq: true } }]
1162
+ })
1163
+ .addMergePoint('sync', ['fetch-a', 'fetch-b'])
1164
+ .addPhase('aggregate', {
1165
+ functions: [{
1166
+ id: 'combine',
1167
+ functionOptions: {
1168
+ fn: () => 'combined',
1169
+ args: [],
1170
+ returnResult: true
1171
+ }
1172
+ }]
1173
+ })
1174
+ .connectSequence('fetch-a', 'sync')
1175
+ .connectSequence('fetch-b', 'sync')
1176
+ .connectSequence('sync', 'aggregate')
1177
+ .setEntryPoint('fetch-a')
1178
+ .build();
1179
+
1180
+ const result = await stableWorkflowGraph(graph, {
1181
+ workflowId: 'parallel-sync'
1182
+ });
1183
+
1184
+ // fetch-a and fetch-b run in parallel
1185
+ // aggregate waits for both to complete
1340
1186
  ```
1341
1187
 
1342
- **Pre-Phase Execution Hooks**:
1188
+ #### Linear Helper
1343
1189
 
1344
- Modify phase configuration before execution:
1190
+ Convenience function for sequential phase chains.
1345
1191
 
1346
1192
  ```typescript
1193
+ import { createLinearWorkflowGraph } from '@emmvish/stable-request';
1194
+
1347
1195
  const phases = [
1348
1196
  {
1349
- id: 'data-phase',
1350
- requests: [...],
1351
- prePhaseExecutionHook: async ({ phase, sharedBuffer, params }) => {
1352
- // Dynamically modify phase based on shared state
1353
- if (sharedBuffer.environment === 'production') {
1354
- phase.commonConfig = { commonAttempts: 5, commonWait: 2000 };
1355
- }
1356
- return phase;
1357
- }
1197
+ id: 'init',
1198
+ requests: [{ id: 'setup', requestOptions: { reqData: { path: '/init' }, resReq: true } }]
1199
+ },
1200
+ {
1201
+ id: 'process',
1202
+ requests: [{ id: 'do-work', requestOptions: { reqData: { path: '/work' }, resReq: true } }]
1203
+ },
1204
+ {
1205
+ id: 'finalize',
1206
+ requests: [{ id: 'cleanup', requestOptions: { reqData: { path: '/cleanup' }, resReq: true } }]
1358
1207
  }
1359
1208
  ];
1209
+
1210
+ const graph = createLinearWorkflowGraph(phases);
1211
+
1212
+ const result = await stableWorkflowGraph(graph, {
1213
+ workflowId: 'linear-workflow'
1214
+ });
1360
1215
  ```
1361
1216
 
1362
- **Pre-Branch Execution Hooks**:
1217
+ ---
1218
+
1219
+ ## Configuration & State
1220
+
1221
+ ### Config Cascading
1363
1222
 
1364
- Modify branch configuration before execution:
1223
+ Define defaults globally; override at group, phase, branch, or item level.
1365
1224
 
1366
1225
  ```typescript
1367
- const branches = [
1226
+ import { stableWorkflow } from '@emmvish/stable-request';
1227
+ import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1228
+
1229
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1368
1230
  {
1369
- id: 'api-branch',
1370
- phases: [...],
1371
- preBranchExecutionHook: async ({ branch, sharedBuffer }) => {
1372
- // Add authentication header dynamically
1373
- branch.commonConfig = {
1374
- ...branch.commonConfig,
1375
- commonRequestData: {
1376
- headers: { 'Authorization': `Bearer ${sharedBuffer.token}` }
1231
+ id: 'phase-1',
1232
+ attempts: 5, // Override global attempts for this phase
1233
+ wait: 1000,
1234
+ requests: [
1235
+ {
1236
+ id: 'req-1',
1237
+ requestOptions: {
1238
+ reqData: { path: '/data' },
1239
+ resReq: true,
1240
+ attempts: 2 // Override phase attempts for this item
1377
1241
  }
1378
- };
1379
- return branch;
1380
- }
1242
+ }
1243
+ ]
1381
1244
  }
1382
1245
  ];
1246
+
1247
+ const result = await stableWorkflow(phases, {
1248
+ workflowId: 'cascade-demo',
1249
+ commonAttempts: 1, // Global default
1250
+ commonWait: 500,
1251
+ retryStrategy: 'LINEAR' // Global default
1252
+ // Final config per item: merge common → phase → request
1253
+ });
1383
1254
  ```
1384
1255
 
1385
- ### State Persistence and Recovery
1256
+ Hierarchy: global group → phase → branch → item. Lower levels override.
1386
1257
 
1387
- Persist workflow state to external storage for recovery, distributed coordination, and long-running workflows.
1258
+ ### Shared & State Buffers
1388
1259
 
1389
- **How It Works**:
1390
- The persistence function operates in two modes:
1391
- - **LOAD Mode**: When `buffer` is empty/null, return the stored state
1392
- - **STORE Mode**: When `buffer` contains data, save it to your storage
1260
+ Pass mutable state across phases, branches, and items.
1393
1261
 
1394
- **Redis Persistence with Distributed Locking**:
1262
+ #### Shared Buffer (Workflow/Gateway)
1395
1263
 
1396
1264
  ```typescript
1397
- import Redis from 'ioredis';
1398
-
1399
- const redis = new Redis();
1265
+ import { stableWorkflow } from '@emmvish/stable-request';
1266
+ import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1400
1267
 
1401
- const persistToRedis = async ({ executionContext, params, buffer }) => {
1402
- const { workflowId, phaseId } = executionContext;
1403
- const { ttl = 86400, enableLocking = false } = params || {};
1404
-
1405
- const stateKey = `workflow:${workflowId}:${phaseId}`;
1406
- const lockKey = `lock:${stateKey}`;
1407
- const isStoring = buffer && Object.keys(buffer).length > 0;
1408
-
1409
- if (enableLocking) {
1410
- await redis.setex(lockKey, 5, Date.now().toString());
1411
- }
1412
-
1413
- try {
1414
- if (isStoring) {
1415
- // STORE MODE: Save with metadata
1416
- const stateWithMeta = {
1417
- ...buffer,
1418
- _meta: {
1419
- timestamp: new Date().toISOString(),
1420
- version: (buffer._meta?.version || 0) + 1
1268
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1269
+ {
1270
+ id: 'fetch',
1271
+ requests: [
1272
+ {
1273
+ id: 'user-data',
1274
+ requestOptions: {
1275
+ reqData: { path: '/users/1' },
1276
+ resReq: true,
1277
+ handleSuccessfulAttemptData: ({ successfulAttemptData, stableRequestOptions }) => {
1278
+ // Mutate shared buffer
1279
+ const sharedBuffer = (stableRequestOptions as any).sharedBuffer;
1280
+ sharedBuffer.userId = (successfulAttemptData.data as any).id;
1281
+ }
1421
1282
  }
1422
- };
1423
- await redis.setex(stateKey, ttl, JSON.stringify(stateWithMeta));
1424
- console.log(`💾 State saved (v${stateWithMeta._meta.version})`);
1425
- } else {
1426
- // LOAD MODE: Retrieve state
1427
- const data = await redis.get(stateKey);
1428
- return data ? JSON.parse(data) : {};
1429
- }
1430
- } finally {
1431
- if (enableLocking) {
1432
- await redis.del(lockKey); // Release lock
1433
- }
1283
+ }
1284
+ ]
1285
+ },
1286
+ {
1287
+ id: 'use-shared-data',
1288
+ requests: [
1289
+ {
1290
+ id: 'dependent-call',
1291
+ requestOptions: {
1292
+ reqData: { path: '/user-posts' },
1293
+ resReq: true,
1294
+ preExecution: {
1295
+ preExecutionHook: async ({ stableRequestOptions, commonBuffer }) => {
1296
+ const sharedBuffer = (stableRequestOptions as any).sharedBuffer;
1297
+ console.log(`Using userId: ${sharedBuffer.userId}`);
1298
+ }
1299
+ }
1300
+ }
1301
+ }
1302
+ ]
1434
1303
  }
1435
-
1436
- return {};
1437
- };
1304
+ ];
1438
1305
 
1439
- // Use with workflow-level persistence (applies to all phases)
1440
- await stableWorkflow(phases, {
1441
- workflowId: 'distributed-job-456',
1442
- commonStatePersistence: {
1443
- persistenceFunction: persistToRedis,
1444
- persistenceParams: {
1445
- ttl: 3600,
1446
- enableLocking: true // Enable distributed locking
1447
- },
1448
- loadBeforeHooks: true,
1449
- storeAfterHooks: true
1450
- },
1451
- commonRequestData: { hostname: 'api.example.com' }
1306
+ const result = await stableWorkflow(phases, {
1307
+ workflowId: 'shared-state-demo',
1308
+ sharedBuffer: {} // Mutable across phases
1452
1309
  });
1453
1310
  ```
1454
1311
 
1455
- **Checkpoint-Based Recovery Pattern**:
1312
+ #### Common Buffer (Request Level)
1456
1313
 
1457
1314
  ```typescript
1458
- const createCheckpoint = async ({ executionContext, params, buffer }) => {
1459
- const { workflowId } = executionContext;
1460
- const checkpointKey = `checkpoint:${workflowId}`;
1461
-
1462
- if (buffer && Object.keys(buffer).length > 0) {
1463
- // STORE: Save checkpoint with completed phases
1464
- const existing = JSON.parse(await redis.get(checkpointKey) || '{}');
1465
- const checkpoint = {
1466
- ...existing,
1467
- completedPhases: [...new Set([
1468
- ...(existing.completedPhases || []),
1469
- ...(buffer.completedPhases || [])
1470
- ])],
1471
- progress: buffer.progress || existing.progress || 0,
1472
- lastUpdated: new Date().toISOString()
1473
- };
1474
- await redis.setex(checkpointKey, 7200, JSON.stringify(checkpoint));
1475
- } else {
1476
- // LOAD: Return checkpoint data
1477
- const data = await redis.get(checkpointKey);
1478
- return data ? JSON.parse(data) : { completedPhases: [] };
1479
- }
1480
- return {};
1481
- };
1315
+ import { stableRequest } from '@emmvish/stable-request';
1482
1316
 
1483
- const phases = [
1484
- {
1485
- id: 'phase-1',
1486
- requests: [...],
1487
- phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
1488
- // Skip if already completed (recovery scenario)
1489
- if (sharedBuffer.completedPhases?.includes('phase-1')) {
1490
- console.log('✅ Phase-1 already completed, skipping...');
1491
- return {
1492
- action: PHASE_DECISION_ACTIONS.SKIP,
1493
- skipToPhaseId: 'phase-2'
1494
- };
1495
- }
1496
-
1497
- if (phaseResult.success) {
1498
- sharedBuffer.completedPhases = [
1499
- ...(sharedBuffer.completedPhases || []),
1500
- 'phase-1'
1501
- ];
1502
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1503
- }
1504
- return { action: PHASE_DECISION_ACTIONS.TERMINATE };
1317
+ const commonBuffer = { transactionId: null };
1318
+
1319
+ const result = await stableRequest({
1320
+ reqData: { path: '/transaction/start' },
1321
+ resReq: true,
1322
+ commonBuffer,
1323
+ preExecution: {
1324
+ preExecutionHook: async ({ commonBuffer, stableRequestOptions }) => {
1325
+ // commonBuffer writable here
1326
+ commonBuffer.userId = '123';
1505
1327
  }
1506
1328
  },
1507
- {
1508
- id: 'phase-2',
1509
- requests: [...],
1510
- phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
1511
- if (sharedBuffer.completedPhases?.includes('phase-2')) {
1512
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1513
- }
1514
- if (phaseResult.success) {
1515
- sharedBuffer.completedPhases = [
1516
- ...(sharedBuffer.completedPhases || []),
1517
- 'phase-2'
1518
- ];
1519
- }
1520
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1521
- }
1329
+ handleSuccessfulAttemptData: ({ successfulAttemptData }) => {
1330
+ // commonBuffer readable in handlers
1331
+ console.log(`Transaction for user ${commonBuffer.userId} done`);
1522
1332
  }
1523
- ];
1524
-
1525
- await stableWorkflow(phases, {
1526
- workflowId: 'resumable-workflow-789',
1527
- enableNonLinearExecution: true,
1528
- sharedBuffer: { completedPhases: [] },
1529
- commonStatePersistence: {
1530
- persistenceFunction: createCheckpoint,
1531
- persistenceParams: { ttl: 7200 },
1532
- loadBeforeHooks: true,
1533
- storeAfterHooks: true
1534
- },
1535
- commonRequestData: { hostname: 'api.example.com' }
1536
1333
  });
1537
1334
  ```
1538
1335
 
1539
- ### Comprehensive Observability
1336
+ ---
1540
1337
 
1541
- Built-in hooks for monitoring, logging, and analysis at every level:
1338
+ ## Hooks & Observability
1339
+
1340
+ ### Pre-Execution Hooks
1341
+
1342
+ Modify config or state before execution.
1542
1343
 
1543
- **Request-Level Hooks**:
1544
1344
  ```typescript
1545
- await stableRequest({
1546
- reqData: { hostname: 'api.example.com', path: '/data' },
1345
+ import { stableRequest } from '@emmvish/stable-request';
1346
+
1347
+ interface SecureRequest {}
1348
+ interface SecureResponse {
1349
+ data: any;
1350
+ token?: string;
1351
+ }
1352
+
1353
+ const result = await stableRequest<SecureRequest, SecureResponse>({
1354
+ reqData: { path: '/secure-data' },
1547
1355
  resReq: true,
1548
- attempts: 3,
1549
-
1550
- // Validate response content
1551
- responseAnalyzer: async ({ data, reqData, params }) => {
1552
- console.log('Analyzing response:', data);
1553
- return data.status === 'success'; // false = retry
1554
- },
1555
-
1556
- // Custom error handling
1557
- handleErrors: async ({ errorLog, reqData, commonBuffer }) => {
1558
- console.error('Request failed:', errorLog);
1559
- await sendToMonitoring(errorLog);
1560
- },
1561
-
1562
- // Log successful attempts
1563
- handleSuccessfulAttemptData: async ({ successfulAttemptData, reqData }) => {
1564
- console.log('Request succeeded:', successfulAttemptData);
1565
- },
1566
-
1567
- // Analyze final error after all retries
1568
- finalErrorAnalyzer: async ({ error, reqData }) => {
1569
- console.error('All retries exhausted:', error);
1570
- return error.message.includes('404'); // true = return false instead of throw
1571
- },
1572
-
1573
- // Pass custom parameters to hooks
1574
- hookParams: {
1575
- responseAnalyzerParams: { expectedFormat: 'json' },
1576
- handleErrorsParams: { alertChannel: 'slack' }
1577
- },
1578
-
1579
- logAllErrors: true,
1580
- logAllSuccessfulAttempts: true
1356
+ preExecution: {
1357
+ preExecutionHook: async ({ inputParams, commonBuffer, stableRequestOptions }) => {
1358
+ // Dynamically fetch auth token
1359
+ const token = await getAuthToken();
1360
+
1361
+ // Return partial config override
1362
+ return {
1363
+ reqData: {
1364
+ headers: { Authorization: `Bearer ${token}` }
1365
+ }
1366
+ };
1367
+ },
1368
+ preExecutionHookParams: { context: 'auth-fetch' },
1369
+ applyPreExecutionConfigOverride: true,
1370
+ continueOnPreExecutionHookFailure: false
1371
+ }
1581
1372
  });
1582
1373
  ```
1583
1374
 
1584
- **Workflow-Level Hooks**:
1375
+ ### Analysis Hooks
1376
+
1377
+ Validate responses and errors.
1378
+
1379
+ #### Response Analyzer
1380
+
1585
1381
  ```typescript
1586
- await stableWorkflow(phases, {
1587
- workflowId: 'monitored-workflow',
1588
-
1589
- // Called after each phase completes
1590
- handlePhaseCompletion: async ({ workflowId, phaseResult, params }) => {
1591
- console.log(`Phase ${phaseResult.phaseId} completed`);
1592
- console.log(`Requests: ${phaseResult.totalRequests}`);
1593
- console.log(`Success: ${phaseResult.successfulRequests}`);
1594
- console.log(`Failed: ${phaseResult.failedRequests}`);
1595
- await sendMetrics(phaseResult);
1596
- },
1597
-
1598
- // Called when a phase fails
1599
- handlePhaseError: async ({ workflowId, error, phaseResult }) => {
1600
- console.error(`Phase ${phaseResult.phaseId} failed:`, error);
1601
- await alertOnCall(error);
1602
- },
1603
-
1604
- // Monitor non-linear execution decisions
1605
- handlePhaseDecision: async ({ decision, phaseResult }) => {
1606
- console.log(`Phase decision: ${decision.action}`);
1607
- if (decision.targetPhaseId) {
1608
- console.log(`Target: ${decision.targetPhaseId}`);
1609
- }
1610
- },
1611
-
1612
- // Monitor branch completion
1613
- handleBranchCompletion: async ({ workflowId, branchResult }) => {
1614
- console.log(`Branch ${branchResult.branchId} completed`);
1615
- },
1616
-
1617
- // Monitor branch decisions
1618
- handleBranchDecision: async ({ workflowId, branchId, branchResults, success }) => {
1619
- console.log(`Branch ID: ${branchId}`);
1620
- },
1621
-
1622
- // Pass parameters to workflow hooks
1623
- workflowHookParams: {
1624
- handlePhaseCompletionParams: { environment: 'production' },
1625
- handlePhaseErrorParams: { severity: 'high' }
1626
- },
1627
-
1628
- logPhaseResults: true,
1629
- commonRequestData: { hostname: 'api.example.com' }
1382
+ import { stableRequest } from '@emmvish/stable-request';
1383
+
1384
+ interface ResourceRequest {}
1385
+ interface ApiResponse {
1386
+ id: number;
1387
+ status: 'active' | 'inactive';
1388
+ }
1389
+
1390
+ const result = await stableRequest<ResourceRequest, ApiResponse>({
1391
+ reqData: { path: '/resource' },
1392
+ resReq: true,
1393
+ responseAnalyzer: ({ data, reqData, trialMode }) => {
1394
+ // Return true to accept, false to retry
1395
+ if (!data || typeof data !== 'object') return false;
1396
+ if (!('id' in data)) return false;
1397
+ if ((data as any).status !== 'active') return false;
1398
+ return true;
1399
+ }
1630
1400
  });
1631
1401
  ```
1632
1402
 
1633
- **Execution History**:
1403
+ #### Error Analyzer
1404
+
1405
+ Decide whether to suppress error gracefully.
1406
+
1634
1407
  ```typescript
1635
- const result = await stableWorkflow(phases, {
1636
- enableNonLinearExecution: true,
1637
- workflowId: 'tracked-workflow',
1638
- commonRequestData: { hostname: 'api.example.com' }
1639
- });
1408
+ import { stableRequest } from '@emmvish/stable-request';
1640
1409
 
1641
- // Detailed execution history
1642
- result.executionHistory.forEach(record => {
1643
- console.log({
1644
- phaseId: record.phaseId,
1645
- executionNumber: record.executionNumber,
1646
- decision: record.decision,
1647
- timestamp: record.timestamp,
1648
- metadata: record.metadata
1649
- });
1650
- });
1410
+ interface FeatureRequest {}
1411
+ interface FeatureResponse {
1412
+ enabled: boolean;
1413
+ data?: any;
1414
+ }
1651
1415
 
1652
- // Branch execution history
1653
- result.branchExecutionHistory?.forEach(record => {
1654
- console.log({
1655
- branchId: record.branchId,
1656
- action: record.action,
1657
- timestamp: record.timestamp
1658
- });
1416
+ const result = await stableRequest<FeatureRequest, FeatureResponse>({
1417
+ reqData: { path: '/optional-feature' },
1418
+ resReq: true,
1419
+ finalErrorAnalyzer: ({ error, reqData, trialMode }) => {
1420
+ // Return true to suppress error and return failure result
1421
+ // Return false to throw error
1422
+ if (error.code === 'ECONNREFUSED') {
1423
+ console.warn('Service unavailable, continuing with fallback');
1424
+ return true; // Suppress, don't throw
1425
+ }
1426
+ return false; // Throw
1427
+ }
1659
1428
  });
1429
+
1430
+ if (result.success) {
1431
+ console.log('Got data:', result.data);
1432
+ } else {
1433
+ console.log('Service offline, but we continue');
1434
+ }
1660
1435
  ```
1661
1436
 
1662
- ### Trial Mode
1437
+ ### Handler Hooks
1663
1438
 
1664
- Test and debug workflows without making real API calls:
1439
+ Custom logging and processing.
1440
+
1441
+ #### Success Handler
1665
1442
 
1666
1443
  ```typescript
1667
- await stableRequest({
1668
- reqData: { hostname: 'api.example.com', path: '/data' },
1444
+ import { stableRequest } from '@emmvish/stable-request';
1445
+
1446
+ interface DataRequest {}
1447
+ interface DataResponse {
1448
+ id: number;
1449
+ value: string;
1450
+ }
1451
+
1452
+ const result = await stableRequest<DataRequest, DataResponse>({
1453
+ reqData: { path: '/data' },
1669
1454
  resReq: true,
1670
- attempts: 3,
1671
- trialMode: {
1672
- enabled: true,
1673
- successProbability: 0.5, // 50% chance of success
1674
- retryableProbability: 0.8, // 80% of failures are retryable
1675
- latencyRange: { min: 100, max: 500 } // Simulated latency: 100-500ms
1455
+ logAllSuccessfulAttempts: true,
1456
+ handleSuccessfulAttemptData: ({
1457
+ successfulAttemptData,
1458
+ reqData,
1459
+ maxSerializableChars,
1460
+ executionContext
1461
+ }) => {
1462
+ // Custom logging, metrics, state updates
1463
+ console.log(
1464
+ `Success in context ${executionContext.workflowId}`,
1465
+ `data:`,
1466
+ successfulAttemptData.data
1467
+ );
1676
1468
  }
1677
1469
  });
1678
1470
  ```
1679
1471
 
1680
- **Use Cases**:
1681
- - Test retry logic without hitting APIs
1682
- - Simulate failure scenarios
1683
- - Load testing with controlled failure rates
1684
- - Development without backend dependencies
1472
+ #### Error Handler
1685
1473
 
1686
- ## Common Use Cases
1474
+ ```typescript
1475
+ const result = await stableRequest<DataRequest, DataResponse>({
1476
+ reqData: { path: '/data' },
1477
+ resReq: true,
1478
+ logAllErrors: true,
1479
+ handleErrors: ({ errorLog, reqData, executionContext }) => {
1480
+ // Custom error logging, alerting, retry logic
1481
+ console.error(
1482
+ `Error in ${executionContext.workflowId}:`,
1483
+ errorLog.errorMessage,
1484
+ `Retryable: ${errorLog.isRetryable}`
1485
+ );
1486
+ }
1487
+ });
1488
+ ```
1687
1489
 
1688
- ### Multi-Step Data Synchronization
1490
+ #### Phase Handlers (Workflow)
1689
1491
 
1690
1492
  ```typescript
1691
- const syncPhases = [
1692
- {
1693
- id: 'fetch-source-data',
1694
- concurrentExecution: true,
1695
- requests: [
1696
- { id: 'users', requestOptions: { reqData: { path: '/source/users' }, resReq: true } },
1697
- { id: 'orders', requestOptions: { reqData: { path: '/source/orders' }, resReq: true } }
1698
- ]
1699
- },
1700
- {
1701
- id: 'transform-data',
1702
- requests: [
1703
- {
1704
- id: 'transform',
1705
- requestOptions: {
1706
- reqData: { path: '/transform', method: REQUEST_METHODS.POST },
1707
- resReq: true
1708
- }
1709
- }
1710
- ]
1711
- },
1493
+ import { stableWorkflow } from '@emmvish/stable-request';
1494
+ import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1495
+
1496
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1712
1497
  {
1713
- id: 'upload-to-destination',
1714
- concurrentExecution: true,
1715
- requests: [
1716
- { id: 'upload-users', requestOptions: { reqData: { path: '/dest/users', method: REQUEST_METHODS.POST }, resReq: false } },
1717
- { id: 'upload-orders', requestOptions: { reqData: { path: '/dest/orders', method: REQUEST_METHODS.POST }, resReq: false } }
1718
- ]
1498
+ id: 'phase-1',
1499
+ requests: [{ id: 'r1', requestOptions: { reqData: { path: '/data' }, resReq: true } }]
1719
1500
  }
1720
1501
  ];
1721
1502
 
1722
- await stableWorkflow(syncPhases, {
1723
- workflowId: 'data-sync',
1724
- commonRequestData: { hostname: 'api.example.com' },
1725
- commonAttempts: 3,
1726
- stopOnFirstPhaseError: true,
1727
- logPhaseResults: true
1503
+ const result = await stableWorkflow(phases, {
1504
+ workflowId: 'wf-handlers',
1505
+ handlePhaseCompletion: ({ phaseResult, workflowId }) => {
1506
+ console.log(`Phase ${phaseResult.phaseId} complete in ${workflowId}`);
1507
+ },
1508
+ handlePhaseError: ({ phaseResult, error, workflowId }) => {
1509
+ console.error(`Phase ${phaseResult.phaseId} failed:`, error);
1510
+ },
1511
+ handlePhaseDecision: ({ decision, phaseResult }) => {
1512
+ console.log(`Phase decision: ${decision.action}`);
1513
+ }
1728
1514
  });
1729
1515
  ```
1730
1516
 
1731
- ### API Gateway with Fallbacks
1517
+ ### Decision Hooks
1518
+
1519
+ Dynamically determine workflow flow.
1732
1520
 
1733
1521
  ```typescript
1734
- const requests = [
1522
+ import { stableWorkflow, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
1523
+ import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1524
+
1525
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1735
1526
  {
1736
- id: 'primary-service',
1737
- groupId: 'critical',
1738
- requestOptions: {
1739
- reqData: { hostname: 'primary.api.com', path: '/data' },
1740
- resReq: true,
1741
- finalErrorAnalyzer: async ({ error }) => {
1742
- // If primary fails, mark as handled (don't throw)
1743
- return true;
1527
+ id: 'fetch-data',
1528
+ requests: [{ id: 'api', requestOptions: { reqData: { path: '/data' }, resReq: true } }],
1529
+ phaseDecisionHook: async ({ phaseResult, sharedBuffer, executionHistory }) => {
1530
+ if (!phaseResult.success) {
1531
+ return { action: PHASE_DECISION_ACTIONS.TERMINATE };
1744
1532
  }
1745
- }
1746
- },
1747
- {
1748
- id: 'fallback-service',
1749
- groupId: 'fallback',
1750
- requestOptions: {
1751
- reqData: { hostname: 'backup.api.com', path: '/data' },
1752
- resReq: true
1533
+ if (phaseResult.responses[0].data?.needsRetry) {
1534
+ return { action: PHASE_DECISION_ACTIONS.REPLAY };
1535
+ }
1536
+ return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1753
1537
  }
1754
1538
  }
1755
1539
  ];
1756
1540
 
1757
- const results = await stableApiGateway(requests, {
1758
- concurrentExecution: false, // Sequential: try fallback only if primary fails
1759
- requestGroups: [
1760
- { groupId: 'critical', commonAttempts: 3 },
1761
- { groupId: 'fallback', commonAttempts: 1 }
1762
- ]
1541
+ const result = await stableWorkflow(phases, {
1542
+ enableNonLinearExecution: true
1543
+ });
1544
+ ```
1545
+
1546
+ ### Metrics & Logging
1547
+
1548
+ Automatic metrics collection across all execution modes.
1549
+
1550
+ #### Request Metrics
1551
+
1552
+ ```typescript
1553
+ import { stableRequest } from '@emmvish/stable-request';
1554
+
1555
+ interface DataRequest {}
1556
+ interface DataResponse { data: any; }
1557
+
1558
+ const result = await stableRequest<DataRequest, DataResponse>({
1559
+ reqData: { path: '/data' },
1560
+ resReq: true,
1561
+ attempts: 3
1763
1562
  });
1563
+
1564
+ console.log(result.metrics); // {
1565
+ // totalAttempts: 2,
1566
+ // successfulAttempts: 1,
1567
+ // failedAttempts: 1,
1568
+ // totalExecutionTime: 450,
1569
+ // averageAttemptTime: 225,
1570
+ // infrastructureMetrics: {
1571
+ // circuitBreaker: { /* state, stats, config */ },
1572
+ // cache: { /* hits, misses, size */ },
1573
+ // rateLimiter: { /* limit, current rate */ },
1574
+ // concurrencyLimiter: { /* limit, in-flight */ }
1575
+ // }
1576
+ // }
1764
1577
  ```
1765
1578
 
1766
- ### Polling with Conditional Termination
1579
+ #### Workflow Metrics
1767
1580
 
1768
1581
  ```typescript
1769
- const pollingPhases = [
1582
+ import { stableWorkflow } from '@emmvish/stable-request';
1583
+ import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1584
+
1585
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1586
+ { id: 'p1', requests: [{ id: 'r1', requestOptions: { reqData: { path: '/a' }, resReq: true } }] },
1587
+ { id: 'p2', requests: [{ id: 'r2', requestOptions: { reqData: { path: '/b' }, resReq: true } }] }
1588
+ ];
1589
+
1590
+ const result = await stableWorkflow(phases, {
1591
+ workflowId: 'wf-metrics'
1592
+ });
1593
+
1594
+ console.log(result); // {
1595
+ // workflowId: 'wf-metrics',
1596
+ // success: true,
1597
+ // totalPhases: 2,
1598
+ // completedPhases: 2,
1599
+ // totalRequests: 2,
1600
+ // successfulRequests: 2,
1601
+ // failedRequests: 0,
1602
+ // workflowExecutionTime: 1200,
1603
+ // phases: [
1604
+ // { phaseId: 'p1', success: true, responses: [...], ... },
1605
+ // { phaseId: 'p2', success: true, responses: [...], ... }
1606
+ // ]
1607
+ // }
1608
+ ```
1609
+
1610
+ #### Structured Error Logs
1611
+
1612
+ ```typescript
1613
+ const result = await stableRequest<DataRequest, DataResponse>({
1614
+ reqData: { path: '/flaky' },
1615
+ resReq: true,
1616
+ attempts: 3,
1617
+ logAllErrors: true,
1618
+ handleErrors: ({ errorLog }) => {
1619
+ console.log(errorLog); // {
1620
+ // attempt: '1/3',
1621
+ // type: 'NetworkError',
1622
+ // error: 'ECONNREFUSED',
1623
+ // isRetryable: true,
1624
+ // timestamp: 1234567890
1625
+ // }
1626
+ }
1627
+ });
1628
+
1629
+ if (result.errorLogs) {
1630
+ console.log(`${result.errorLogs.length} errors logged`);
1631
+ }
1632
+ ```
1633
+
1634
+ ---
1635
+
1636
+ ## Advanced Features
1637
+
1638
+ ### Trial Mode
1639
+
1640
+ Dry-run workflows without side effects; simulate failures.
1641
+
1642
+ ```typescript
1643
+ import { stableWorkflow } from '@emmvish/stable-request';
1644
+ import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1645
+
1646
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1770
1647
  {
1771
- id: 'poll-job-status',
1772
- allowReplay: true,
1773
- maxReplayCount: 20,
1648
+ id: 'process',
1774
1649
  requests: [
1775
- {
1776
- id: 'status-check',
1777
- requestOptions: {
1778
- reqData: { path: '/job/status' },
1779
- resReq: true
1780
- }
1781
- }
1782
- ],
1783
- phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
1784
- const status = phaseResult.responses[0]?.data?.status;
1785
- const attempts = executionHistory.filter(h => h.phaseId === 'poll-job-status').length;
1786
-
1787
- if (status === 'completed') {
1788
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1789
- } else if (status === 'failed') {
1790
- return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Job failed' } };
1791
- } else if (attempts < 20) {
1792
- return { action: PHASE_DECISION_ACTIONS.REPLAY };
1793
- } else {
1794
- return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Timeout' } };
1650
+ {
1651
+ id: 'api-call',
1652
+ requestOptions: {
1653
+ reqData: { path: '/payment/charge' },
1654
+ resReq: true,
1655
+ trialMode: {
1656
+ enabled: true,
1657
+ requestFailureProbability: 0.3 // 30% simulated failure rate
1658
+ }
1659
+ }
1795
1660
  }
1796
- }
1797
- },
1798
- {
1799
- id: 'process-results',
1800
- requests: [
1801
- { id: 'fetch-results', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
1802
1661
  ]
1803
1662
  }
1804
1663
  ];
1805
1664
 
1806
- await stableWorkflow(pollingPhases, {
1807
- enableNonLinearExecution: true,
1808
- commonRequestData: { hostname: 'api.example.com' },
1809
- commonWait: 5000 // 5 second wait between polls
1665
+ const result = await stableWorkflow(phases, {
1666
+ workflowId: 'payment-trial',
1667
+ trialMode: {
1668
+ enabled: true,
1669
+ functionFailureProbability: 0.2
1670
+ }
1810
1671
  });
1672
+
1673
+ // Requests/functions execute but failures are simulated
1674
+ // Real API calls happen; real side effects occur only if enabled
1675
+ // Useful for testing retry logic, decision hooks, workflow topology
1811
1676
  ```
1812
1677
 
1813
- ### Webhook Retry with Circuit Breaker
1678
+ ### State Persistence
1679
+
1680
+ Persist state across retry attempts for distributed tracing.
1814
1681
 
1815
1682
  ```typescript
1816
- import { CircuitBreaker, REQUEST_METHODS, RETRY_STRATEGIES } from '@emmvish/stable-request';
1683
+ import { stableRequest } from '@emmvish/stable-request';
1817
1684
 
1818
- const webhookBreaker = new CircuitBreaker({
1819
- failureThresholdPercentage: 60, // 60% failure rate triggers open
1820
- minimumRequests: 5, // Minimum 5 requests before evaluation
1821
- recoveryTimeoutMs: 30000, // 30s timeout in open state
1822
- successThresholdPercentage: 40 // 40% success rate closes circuit
1823
- });
1685
+ interface DataRequest {}
1686
+ interface DataResponse { data: any; }
1824
1687
 
1825
- async function sendWebhook(eventData: any) {
1826
- try {
1827
- await stableRequest({
1828
- reqData: {
1829
- hostname: 'webhook.example.com',
1830
- path: '/events',
1831
- method: REQUEST_METHODS.POST,
1832
- body: eventData
1833
- },
1834
- attempts: 5,
1835
- wait: 1000,
1836
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1837
- circuitBreaker: webhookBreaker,
1838
- handleErrors: async ({ errorLog }) => {
1839
- console.error('Webhook delivery failed:', errorLog);
1840
- await queueForRetry(eventData);
1841
- }
1842
- });
1843
- } catch (error) {
1844
- console.error('Webhook permanently failed:', error);
1688
+ const result = await stableRequest<DataRequest, DataResponse>({
1689
+ reqData: { path: '/data' },
1690
+ resReq: true,
1691
+ attempts: 3,
1692
+ statePersistence: {
1693
+ save: async (state, executionContext) => {
1694
+ // Save state to database or distributed cache
1695
+ await saveToDatabase({
1696
+ key: `${executionContext.workflowId}:${executionContext.requestId}`,
1697
+ state
1698
+ });
1699
+ },
1700
+ load: async (executionContext) => {
1701
+ // Load state for recovery
1702
+ return await loadFromDatabase(
1703
+ `${executionContext.workflowId}:${executionContext.requestId}`
1704
+ );
1705
+ }
1845
1706
  }
1846
- }
1707
+ });
1847
1708
  ```
1848
1709
 
1849
- ### Distributed Data Migration with State Persistence
1710
+ ### Mixed Request & Function Phases
1711
+
1712
+ Combine API calls and computations in single phases with full type safety.
1850
1713
 
1851
1714
  ```typescript
1852
- import Redis from 'ioredis';
1853
- import {
1854
- stableWorkflow,
1855
- PHASE_DECISION_ACTIONS,
1856
- REQUEST_METHODS,
1857
- VALID_REQUEST_PROTOCOLS
1858
- } from '@emmvish/stable-request';
1715
+ import { stableWorkflow, RequestOrFunction } from '@emmvish/stable-request';
1716
+ import type { STABLE_WORKFLOW_PHASE, API_GATEWAY_ITEM } from '@emmvish/stable-request';
1717
+
1718
+ // Request types
1719
+ interface ProductRequest {}\ninterface ProductResponse {
1720
+ id: number;
1721
+ name: string;
1722
+ price: number;
1723
+ }
1859
1724
 
1860
- const redis = new Redis();
1725
+ interface InventoryRequest {}
1726
+ interface InventoryResponse {
1727
+ productId: number;
1728
+ stock: number;
1729
+ }
1861
1730
 
1862
- // Checkpoint persistence for recovery
1863
- const createCheckpoint = async ({ executionContext, buffer }) => {
1864
- const { workflowId, phaseId } = executionContext;
1865
- const key = `checkpoint:${workflowId}`;
1866
-
1867
- if (buffer && Object.keys(buffer).length > 0) {
1868
- // Save checkpoint with progress
1869
- const existing = JSON.parse(await redis.get(key) || '{}');
1870
- const checkpoint = {
1871
- ...existing,
1872
- ...buffer,
1873
- completedPhases: [...new Set([
1874
- ...(existing.completedPhases || []),
1875
- ...(buffer.completedPhases || [])
1876
- ])],
1877
- lastPhase: phaseId,
1878
- updatedAt: new Date().toISOString()
1879
- };
1880
- await redis.setex(key, 86400, JSON.stringify(checkpoint));
1881
- console.log(`💾 Checkpoint: ${checkpoint.recordsProcessed}/${checkpoint.totalRecords} records`);
1882
- } else {
1883
- // Load checkpoint
1884
- const data = await redis.get(key);
1885
- return data ? JSON.parse(data) : {
1886
- completedPhases: [],
1887
- recordsProcessed: 0,
1888
- totalRecords: 0
1889
- };
1890
- }
1891
- return {};
1731
+ // Function types
1732
+ type EnrichArgs = [ProductResponse[], InventoryResponse[]];
1733
+ type EnrichResult = Array<{
1734
+ id: number;
1735
+ name: string;
1736
+ price: number;
1737
+ stock: number;
1738
+ inStock: boolean;
1739
+ }>;
1740
+
1741
+ type CalculateArgs = [EnrichResult];
1742
+ type CalculateResult = {
1743
+ totalValue: number;
1744
+ lowStockItems: number;
1892
1745
  };
1893
1746
 
1894
- const migrationPhases = [
1895
- {
1896
- id: 'extract',
1897
- requests: [{
1898
- id: 'fetch-data',
1899
- requestOptions: {
1900
- reqData: {
1901
- protocol: VALID_REQUEST_PROTOCOLS.HTTPS,
1902
- hostname: 'source-api.example.com',
1903
- path: '/data',
1904
- method: REQUEST_METHODS.GET
1905
- },
1906
- resReq: true
1907
- }
1908
- }],
1909
- phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
1910
- if (sharedBuffer.completedPhases?.includes('extract')) {
1911
- console.log('✅ Extract already completed, skipping...');
1912
- return {
1913
- action: PHASE_DECISION_ACTIONS.SKIP,
1914
- skipToPhaseId: 'transform'
1915
- };
1747
+ type NotifyArgs = [CalculateResult, string];
1748
+ type NotifyResult = { notified: boolean };
1749
+
1750
+ const phase: STABLE_WORKFLOW_PHASE<
1751
+ ProductRequest | InventoryRequest,
1752
+ ProductResponse | InventoryResponse,
1753
+ EnrichArgs | CalculateArgs | NotifyArgs,
1754
+ EnrichResult | CalculateResult | NotifyResult
1755
+ > = {
1756
+ id: 'mixed-phase',
1757
+ items: [
1758
+ {
1759
+ type: RequestOrFunction.REQUEST,
1760
+ request: {
1761
+ id: 'fetch-products',
1762
+ requestOptions: {
1763
+ reqData: { path: '/products' },
1764
+ resReq: true
1765
+ }
1916
1766
  }
1917
-
1918
- if (phaseResult.success) {
1919
- const records = phaseResult.responses[0]?.data?.records || [];
1920
- sharedBuffer.extractedData = records;
1921
- sharedBuffer.totalRecords = records.length;
1922
- sharedBuffer.completedPhases = ['extract'];
1923
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1767
+ },
1768
+ {
1769
+ type: RequestOrFunction.REQUEST,
1770
+ request: {
1771
+ id: 'fetch-inventory',
1772
+ requestOptions: {
1773
+ reqData: { path: '/inventory' },
1774
+ resReq: true
1775
+ }
1924
1776
  }
1925
- return { action: PHASE_DECISION_ACTIONS.TERMINATE };
1926
- }
1927
- },
1928
- {
1929
- id: 'transform',
1930
- allowReplay: true,
1931
- maxReplayCount: 3,
1932
- requests: [{
1933
- id: 'transform-batch',
1934
- requestOptions: {
1935
- reqData: {
1936
- protocol: VALID_REQUEST_PROTOCOLS.HTTPS,
1937
- hostname: 'transform-api.example.com',
1938
- path: '/transform',
1939
- method: REQUEST_METHODS.POST
1940
- },
1941
- resReq: true,
1942
- preExecution: {
1943
- preExecutionHook: ({ commonBuffer }) => {
1944
- // Process in batches
1945
- const batchSize = 100;
1946
- const processed = commonBuffer.recordsProcessed || 0;
1947
- const batch = commonBuffer.extractedData.slice(
1948
- processed,
1949
- processed + batchSize
1950
- );
1951
- return {
1952
- reqData: { body: { records: batch } }
1953
- };
1777
+ },
1778
+ {
1779
+ type: RequestOrFunction.FUNCTION,
1780
+ function: {
1781
+ id: 'enrich-products',
1782
+ functionOptions: {
1783
+ fn: (products: ProductResponse[], inventory: InventoryResponse[]): EnrichResult => {
1784
+ return products.map(product => {
1785
+ const inv = inventory.find(i => i.productId === product.id);
1786
+ return {
1787
+ ...product,
1788
+ stock: inv?.stock || 0,
1789
+ inStock: (inv?.stock || 0) > 0
1790
+ };
1791
+ });
1954
1792
  },
1955
- applyPreExecutionConfigOverride: true
1793
+ args: [[], []] as EnrichArgs,
1794
+ returnResult: true,
1795
+ cache: { enabled: true, ttl: 30000 }
1956
1796
  }
1957
1797
  }
1958
- }],
1959
- phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
1960
- if (sharedBuffer.completedPhases?.includes('transform')) {
1961
- return {
1962
- action: PHASE_DECISION_ACTIONS.SKIP,
1963
- skipToPhaseId: 'load'
1964
- };
1798
+ },
1799
+ {
1800
+ type: RequestOrFunction.FUNCTION,
1801
+ function: {
1802
+ id: 'calculate-metrics',
1803
+ functionOptions: {
1804
+ fn: (enriched: EnrichResult): CalculateResult => ({
1805
+ totalValue: enriched.reduce((sum, p) => sum + (p.price * p.stock), 0),
1806
+ lowStockItems: enriched.filter(p => p.stock < 10 && p.stock > 0).length
1807
+ }),
1808
+ args: [[]] as CalculateArgs,
1809
+ returnResult: true
1810
+ }
1965
1811
  }
1966
-
1967
- if (phaseResult.success) {
1968
- const transformed = phaseResult.responses[0]?.data?.transformed || [];
1969
- sharedBuffer.recordsProcessed =
1970
- (sharedBuffer.recordsProcessed || 0) + transformed.length;
1971
-
1972
- // Continue transforming if more records remain
1973
- if (sharedBuffer.recordsProcessed < sharedBuffer.totalRecords) {
1974
- console.log(
1975
- `🔄 Progress: ${sharedBuffer.recordsProcessed}/${sharedBuffer.totalRecords}`
1976
- );
1977
- return { action: PHASE_DECISION_ACTIONS.REPLAY };
1812
+ },
1813
+ {
1814
+ type: RequestOrFunction.FUNCTION,
1815
+ function: {
1816
+ id: 'notify-if-needed',
1817
+ functionOptions: {
1818
+ fn: async (metrics: CalculateResult, channel: string): Promise<NotifyResult> => {
1819
+ if (metrics.lowStockItems > 5) {
1820
+ console.log(`Sending alert to ${channel}: ${metrics.lowStockItems} items low`);
1821
+ return { notified: true };
1822
+ }
1823
+ return { notified: false };
1824
+ },
1825
+ args: [{ totalValue: 0, lowStockItems: 0 }, 'slack'] as NotifyArgs,
1826
+ returnResult: true,
1827
+ attempts: 3,
1828
+ wait: 1000
1978
1829
  }
1979
-
1980
- // All records transformed
1981
- sharedBuffer.completedPhases = [
1982
- ...(sharedBuffer.completedPhases || []),
1983
- 'transform'
1984
- ];
1985
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1986
1830
  }
1987
-
1988
- return { action: PHASE_DECISION_ACTIONS.TERMINATE };
1989
1831
  }
1990
- },
1832
+ ]
1833
+ };
1834
+
1835
+ const result = await stableWorkflow([phase], {
1836
+ workflowId: 'mixed-execution',
1837
+ sharedBuffer: {}
1838
+ });
1839
+ ```
1840
+
1841
+ ---
1842
+
1843
+ ## Best Practices
1844
+
1845
+ ### 1. Start Conservative, Override When Needed
1846
+
1847
+ Define global defaults; override only where necessary.
1848
+
1849
+ ```typescript
1850
+ await stableWorkflow(phases, {
1851
+ // Global defaults (conservative)
1852
+ commonAttempts: 3,
1853
+ commonWait: 500,
1854
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1855
+
1856
+ // Override for specific phase
1857
+ phases: [
1858
+ {
1859
+ id: 'fast-phase',
1860
+ attempts: 1, // Override: no retries
1861
+ requests: [...]
1862
+ }
1863
+ ]
1864
+ });
1865
+ ```
1866
+
1867
+ ### 2. Validate Responses
1868
+
1869
+ Use analyzers to ensure data shape and freshness.
1870
+
1871
+ ```typescript
1872
+ interface DataRequest {}
1873
+ interface ApiResponse {
1874
+ id: number;
1875
+ lastUpdated: string;
1876
+ }
1877
+
1878
+ const result = await stableRequest<DataRequest, ApiResponse>({
1879
+ reqData: { path: '/data' },
1880
+ resReq: true,
1881
+ responseAnalyzer: ({ data }) => {
1882
+ if (!data || typeof data !== 'object') return false;
1883
+ if (!('id' in data && 'lastUpdated' in data)) return false;
1884
+ const age = Date.now() - new Date((data as any).lastUpdated).getTime();
1885
+ if (age > 60000) return false; // Data older than 1 minute
1886
+ return true;
1887
+ }
1888
+ });
1889
+ ```
1890
+
1891
+ ### 3. Cache Idempotent Reads Aggressively
1892
+
1893
+ Reduce latency and load on dependencies.
1894
+
1895
+ ```typescript
1896
+ interface UserRequest {}
1897
+ interface UserResponse {
1898
+ id: number;
1899
+ name: string;
1900
+ }
1901
+
1902
+ const userCache = new CacheManager({
1903
+ enabled: true,
1904
+ ttl: 30000, // 30 seconds
1905
+ respectCacheControl: true
1906
+ });
1907
+
1908
+ await stableRequest<UserRequest, UserResponse>({
1909
+ reqData: { path: '/users/1' },
1910
+ resReq: true,
1911
+ cache: userCache
1912
+ });
1913
+
1914
+ await stableRequest<UserRequest, UserResponse>({
1915
+ reqData: { path: '/users/1' },
1916
+ resReq: true,
1917
+ cache: userCache // Cached within 30s
1918
+ });
1919
+ ```
1920
+
1921
+ ### 4. Use Circuit Breaker for Unstable Services
1922
+
1923
+ Protect against cascading failures.
1924
+
1925
+ ```typescript
1926
+ interface ServiceRequest {}
1927
+ interface ServiceResponse { status: string; data: any; }
1928
+
1929
+ const unstabledServiceBreaker = new CircuitBreaker({
1930
+ failureThresholdPercentage: 40,
1931
+ minimumRequests: 5,
1932
+ recoveryTimeoutMs: 30000,
1933
+ successThresholdPercentage: 80
1934
+ });
1935
+
1936
+ await stableApiGateway<ServiceRequest, ServiceResponse>(requests, {
1937
+ circuitBreaker: unstabledServiceBreaker
1938
+ });
1939
+ ```
1940
+
1941
+ ### 5. Apply Rate & Concurrency Limits
1942
+
1943
+ Respect external quotas and capacity.
1944
+
1945
+ ```typescript
1946
+ interface ApiRequest {}
1947
+ interface ApiResponse { result: any; }
1948
+
1949
+ // API allows 100 req/second, use 80% headroom
1950
+ const rateLimit = { maxRequests: 80, windowMs: 1000 };
1951
+
1952
+ // Database connection pool has 10 slots, use 5
1953
+ const maxConcurrent = 5;
1954
+
1955
+ await stableApiGateway<ApiRequest, ApiResponse>(requests, {
1956
+ rateLimit,
1957
+ maxConcurrentRequests: maxConcurrent
1958
+ });
1959
+ ```
1960
+
1961
+ ### 6. Use Shared Buffers for Cross-Phase Coordination
1962
+
1963
+ Avoid global state; pass computed data cleanly.
1964
+
1965
+ ```typescript
1966
+ const sharedBuffer = {};
1967
+
1968
+ await stableWorkflow(phases, {
1969
+ sharedBuffer,
1970
+ // Phase 1 writes userId to sharedBuffer
1971
+ // Phase 2 reads userId from sharedBuffer
1972
+ // Phase 3 uses both
1973
+ });
1974
+ ```
1975
+
1976
+ ### 7. Log Selectively with Max Serialization Cap
1977
+
1978
+ Prevent noisy logs from large payloads.
1979
+
1980
+ ```typescript
1981
+ interface DataRequest {}
1982
+ interface DataResponse { data: any; }
1983
+
1984
+ await stableRequest<DataRequest, DataResponse>({
1985
+ reqData: { path: '/data' },
1986
+ resReq: true,
1987
+ maxSerializableChars: 500, // Truncate logs to 500 chars
1988
+ handleSuccessfulAttemptData: ({ successfulAttemptData, maxSerializableChars }) => {
1989
+ console.log(safelyStringify(successfulAttemptData, maxSerializableChars));
1990
+ }
1991
+ });
1992
+ ```
1993
+
1994
+ ### 8. Use Non-Linear Workflows for Polling
1995
+
1996
+ REPLAY action simplifies polling logic.
1997
+
1998
+ ```typescript
1999
+ const phases: STABLE_WORKFLOW_PHASE[] = [
1991
2000
  {
1992
- id: 'load',
1993
- requests: [{
1994
- id: 'upload-data',
1995
- requestOptions: {
1996
- reqData: {
1997
- protocol: VALID_REQUEST_PROTOCOLS.HTTPS,
1998
- hostname: 'dest-api.example.com',
1999
- path: '/import',
2000
- method: REQUEST_METHODS.POST
2001
- },
2002
- resReq: false
2001
+ id: 'wait-for-job',
2002
+ allowReplay: true,
2003
+ maxReplayCount: 10,
2004
+ requests: [
2005
+ {
2006
+ id: 'check-status',
2007
+ requestOptions: {
2008
+ reqData: { path: '/jobs/123' },
2009
+ resReq: true,
2010
+ attempts: 1
2011
+ }
2003
2012
  }
2004
- }],
2005
- phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
2006
- if (phaseResult.success) {
2007
- sharedBuffer.completedPhases = [
2008
- ...(sharedBuffer.completedPhases || []),
2009
- 'load'
2010
- ];
2013
+ ],
2014
+ phaseDecisionHook: async ({ phaseResult }) => {
2015
+ const status = (phaseResult.responses[0].data as any)?.status;
2016
+ if (status === 'pending') {
2017
+ return { action: PHASE_DECISION_ACTIONS.REPLAY };
2011
2018
  }
2012
2019
  return { action: PHASE_DECISION_ACTIONS.CONTINUE };
2013
2020
  }
2014
2021
  }
2015
2022
  ];
2016
2023
 
2017
- // Execute with state persistence for recovery
2018
- const result = await stableWorkflow(migrationPhases, {
2019
- workflowId: 'data-migration-2024-01-08',
2020
- enableNonLinearExecution: true,
2021
- sharedBuffer: {
2022
- completedPhases: [],
2023
- recordsProcessed: 0,
2024
- totalRecords: 0
2025
- },
2026
- commonStatePersistence: {
2027
- persistenceFunction: createCheckpoint,
2028
- loadBeforeHooks: true,
2029
- storeAfterHooks: true
2030
- },
2031
- commonAttempts: 3,
2032
- commonWait: 2000,
2033
- stopOnFirstPhaseError: true,
2034
- logPhaseResults: true
2024
+ await stableWorkflow(phases, {
2025
+ enableNonLinearExecution: true
2035
2026
  });
2027
+ ```
2028
+
2029
+ ### 9. Use Graph Workflows for Complex Parallelism
2030
+
2031
+ DAGs make dependencies explicit and enable maximum parallelism.
2036
2032
 
2037
- console.log(`✅ Migration completed: ${result.successfulRequests}/${result.totalRequests}`);
2038
- console.log(`⏱️ Duration: ${result.executionTime}ms`);
2033
+ ```typescript
2034
+ // Clearer than 6 phases with conditional concurrency markers
2035
+ const graph = new WorkflowGraphBuilder()
2036
+ .addParallelGroup('fetch', ['fetch-users', 'fetch-posts', 'fetch-comments'])
2037
+ .addMergePoint('sync', ['fetch'])
2038
+ .addPhase('aggregate', {...})
2039
+ .connectSequence('fetch', 'sync', 'aggregate')
2040
+ .build();
2039
2041
 
2040
- // To resume a failed workflow, just re-run with the same workflowId
2041
- // It will load the checkpoint and skip completed phases
2042
+ await stableWorkflowGraph(graph);
2043
+ ```
2044
+
2045
+ ### 10. Prefer Dry-Run (Trial Mode) Before Production
2046
+
2047
+ Test workflows and retry logic safely.
2048
+
2049
+ ```typescript
2050
+ await stableWorkflow(phases, {
2051
+ workflowId: 'payment-pipeline',
2052
+ trialMode: { enabled: true }, // Dry-run before production
2053
+ handlePhaseCompletion: ({ phaseResult }) => {
2054
+ console.log(`Trial phase: ${phaseResult.phaseId}, success=${phaseResult.success}`);
2055
+ }
2056
+ });
2057
+
2058
+ // If satisfied, deploy with trialMode: { enabled: false }
2042
2059
  ```
2043
2060
 
2044
2061
  ---
2045
2062
 
2046
- ## License
2063
+ ## Summary
2064
+
2065
+ @emmvish/stable-request provides a unified, type-safe framework for resilient execution:
2066
+
2067
+ - **Single calls** via `stableRequest` (APIs) or `stableFunction` (pure functions)
2068
+ - **Batch orchestration** via `stableApiGateway` (concurrent/sequential mixed items)
2069
+ - **Phased workflows** via `stableWorkflow` (array-based, non-linear, branched)
2070
+ - **Graph workflows** via `stableWorkflowGraph` (DAG, explicit parallelism)
2071
+
2072
+ All modes inherit robust resilience (retries, jitter, circuit breaking, caching, rate/concurrency limits), config cascading, shared state, hooks, and metrics. Use together or independently; compose freely.
2047
2073
 
2048
- MIT © Manish Varma
2074
+ Build resilient, observable, type-safe systems with confidence.