@emmvish/stable-request 1.5.3 → 1.6.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 +1296 -204
- package/dist/core/stable-workflow.d.ts.map +1 -1
- package/dist/core/stable-workflow.js +95 -3
- package/dist/core/stable-workflow.js.map +1 -1
- package/dist/enums/index.d.ts +7 -0
- package/dist/enums/index.d.ts.map +1 -1
- package/dist/enums/index.js +8 -0
- package/dist/enums/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +126 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utilities/execute-branch-workflow.d.ts +3 -0
- package/dist/utilities/execute-branch-workflow.d.ts.map +1 -0
- package/dist/utilities/execute-branch-workflow.js +276 -0
- package/dist/utilities/execute-branch-workflow.js.map +1 -0
- package/dist/utilities/execute-non-linear-workflow.d.ts +3 -0
- package/dist/utilities/execute-non-linear-workflow.d.ts.map +1 -0
- package/dist/utilities/execute-non-linear-workflow.js +399 -0
- package/dist/utilities/execute-non-linear-workflow.js.map +1 -0
- package/dist/utilities/execute-phase.d.ts +2 -12
- package/dist/utilities/execute-phase.d.ts.map +1 -1
- package/dist/utilities/execute-phase.js.map +1 -1
- package/dist/utilities/index.d.ts +2 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +2 -0
- package/dist/utilities/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,8 @@ All in all, it provides you with the **entire ecosystem** to build **API-integra
|
|
|
32
32
|
| Batch or fan-out requests | `stableApiGateway` |
|
|
33
33
|
| Multi-step orchestration | `stableWorkflow` |
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
Start small and scale.
|
|
36
37
|
|
|
37
38
|
---
|
|
38
39
|
|
|
@@ -41,11 +42,9 @@ Start small and scale
|
|
|
41
42
|
- [Installation](#installation)
|
|
42
43
|
- [Core Features](#core-features)
|
|
43
44
|
- [Quick Start](#quick-start)
|
|
44
|
-
- [API Reference](#api-reference)
|
|
45
|
-
- [stableRequest](#stableRequest)
|
|
46
|
-
- [stableApiGateway](#stableApiGateway)
|
|
47
|
-
- [stableWorkflow](#stableWorkflow)
|
|
48
45
|
- [Advanced Features](#advanced-features)
|
|
46
|
+
- [Non-Linear Workflows](#non-linear-workflows)
|
|
47
|
+
- [Branched Workflows](#branched-workflows)
|
|
49
48
|
- [Retry Strategies](#retry-strategies)
|
|
50
49
|
- [Circuit Breaker](#circuit-breaker)
|
|
51
50
|
- [Rate Limiting](#rate-limiting)
|
|
@@ -57,192 +56,1356 @@ Start small and scale
|
|
|
57
56
|
- [Response Analysis](#response-analysis)
|
|
58
57
|
- [Error Handling](#error-handling)
|
|
59
58
|
- [Advanced Use Cases](#advanced-use-cases)
|
|
60
|
-
- [Configuration Options](#configuration-options)
|
|
61
59
|
- [License](#license)
|
|
62
60
|
<!-- TOC END -->
|
|
63
61
|
|
|
64
|
-
---
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install @emmvish/stable-request
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Core Features
|
|
71
|
+
|
|
72
|
+
- ✅ **Configurable Retry Strategies**: Fixed, Linear, and Exponential backoff
|
|
73
|
+
- ✅ **Circuit Breaker**: Prevent cascading failures with automatic circuit breaking
|
|
74
|
+
- ✅ **Rate Limiting**: Control request throughput across single or multiple requests
|
|
75
|
+
- ✅ **Response Caching**: Built-in TTL-based caching with global cache manager
|
|
76
|
+
- ✅ **Batch Processing**: Execute multiple requests concurrently or sequentially via API Gateway
|
|
77
|
+
- ✅ **Multi-Phase Non-Linear Workflows**: Orchestrate complex request workflows with phase dependencies
|
|
78
|
+
- ✅ **Branched Workflows**: Execute parallel or serial branches with conditional routing and decision hooks
|
|
79
|
+
- ✅ **Pre-Execution Hooks**: Transform requests before execution with dynamic configuration
|
|
80
|
+
- ✅ **Shared Buffer**: Share state across requests in workflows and gateways
|
|
81
|
+
- ✅ **Request Grouping**: Apply different configurations to request groups
|
|
82
|
+
- ✅ **Observability Hooks**: Track errors, successful attempts, and phase completions
|
|
83
|
+
- ✅ **Response Analysis**: Validate responses and trigger retries based on content
|
|
84
|
+
- ✅ **Trial Mode**: Test configurations without making real API calls
|
|
85
|
+
- ✅ **TypeScript Support**: Full type safety with generics for request/response data
|
|
86
|
+
|
|
87
|
+
## Quick Start
|
|
88
|
+
|
|
89
|
+
### Basic Request with Retry
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
93
|
+
|
|
94
|
+
const data = await stableRequest({
|
|
95
|
+
reqData: {
|
|
96
|
+
hostname: 'api.example.com',
|
|
97
|
+
path: '/users/123',
|
|
98
|
+
method: 'GET'
|
|
99
|
+
},
|
|
100
|
+
resReq: true,
|
|
101
|
+
attempts: 3,
|
|
102
|
+
wait: 1000,
|
|
103
|
+
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
console.log(data);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Batch Requests via API Gateway
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { stableApiGateway } from '@emmvish/stable-request';
|
|
113
|
+
|
|
114
|
+
const requests = [
|
|
115
|
+
{ id: 'user-1', requestOptions: { reqData: { path: '/users/1' }, resReq: true } },
|
|
116
|
+
{ id: 'user-2', requestOptions: { reqData: { path: '/users/2' }, resReq: true } },
|
|
117
|
+
{ id: 'user-3', requestOptions: { reqData: { path: '/users/3' }, resReq: true } }
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const results = await stableApiGateway(requests, {
|
|
121
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
122
|
+
concurrentExecution: true,
|
|
123
|
+
maxConcurrentRequests: 10
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
results.forEach(result => {
|
|
127
|
+
if (result.success) {
|
|
128
|
+
console.log(`Request ${result.requestId}:`, result.data);
|
|
129
|
+
} else {
|
|
130
|
+
console.error(`Request ${result.requestId} failed:`, result.error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Multi-Phase Workflow
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
|
|
139
|
+
|
|
140
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
141
|
+
{
|
|
142
|
+
id: 'authentication',
|
|
143
|
+
requests: [
|
|
144
|
+
{ id: 'login', requestOptions: { reqData: { path: '/auth/login' }, resReq: true } }
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'data-fetching',
|
|
149
|
+
concurrentExecution: true,
|
|
150
|
+
requests: [
|
|
151
|
+
{ id: 'users', requestOptions: { reqData: { path: '/users' }, resReq: true } },
|
|
152
|
+
{ id: 'posts', requestOptions: { reqData: { path: '/posts' }, resReq: true } }
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const result = await stableWorkflow(phases, {
|
|
158
|
+
workflowId: 'data-pipeline',
|
|
159
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
160
|
+
stopOnFirstPhaseError: true,
|
|
161
|
+
logPhaseResults: true
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Non-Linear Workflow with Dynamic Routing
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
171
|
+
|
|
172
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
173
|
+
{
|
|
174
|
+
id: 'check-status',
|
|
175
|
+
requests: [
|
|
176
|
+
{ id: 'status', requestOptions: { reqData: { path: '/status' }, resReq: true } }
|
|
177
|
+
],
|
|
178
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
179
|
+
const status = phaseResult.responses[0]?.data?.status;
|
|
180
|
+
|
|
181
|
+
if (status === 'completed') {
|
|
182
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' };
|
|
183
|
+
} else if (status === 'processing') {
|
|
184
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
185
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY }; // Replay this phase
|
|
186
|
+
} else {
|
|
187
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-handler' };
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
allowReplay: true,
|
|
191
|
+
maxReplayCount: 10
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'process',
|
|
195
|
+
requests: [
|
|
196
|
+
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'error-handler',
|
|
201
|
+
requests: [
|
|
202
|
+
{ id: 'error', requestOptions: { reqData: { path: '/error' }, resReq: true } }
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'finalize',
|
|
207
|
+
requests: [
|
|
208
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const result = await stableWorkflow(phases, {
|
|
214
|
+
workflowId: 'dynamic-workflow',
|
|
215
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
216
|
+
enableNonLinearExecution: true,
|
|
217
|
+
maxWorkflowIterations: 50,
|
|
218
|
+
sharedBuffer: {}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
console.log('Execution history:', result.executionHistory);
|
|
222
|
+
console.log('Terminated early:', result.terminatedEarly);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Advanced Features
|
|
226
|
+
|
|
227
|
+
### Non-Linear Workflows
|
|
228
|
+
|
|
229
|
+
Non-linear workflows enable dynamic phase execution based on runtime decisions, allowing you to build complex orchestrations with conditional branching, polling loops, error recovery, and adaptive routing.
|
|
230
|
+
|
|
231
|
+
#### Phase Decision Actions
|
|
232
|
+
|
|
233
|
+
Each phase can make decisions about workflow execution:
|
|
234
|
+
|
|
235
|
+
- **`continue`**: Proceed to the next sequential phase
|
|
236
|
+
- **`jump`**: Jump to a specific phase by ID
|
|
237
|
+
- **`replay`**: Re-execute the current phase
|
|
238
|
+
- **`skip`**: Skip to a target phase or skip the next phase
|
|
239
|
+
- **`terminate`**: Stop the workflow immediately
|
|
240
|
+
|
|
241
|
+
#### Basic Non-Linear Workflow
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PhaseExecutionDecision, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
245
|
+
|
|
246
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
247
|
+
{
|
|
248
|
+
id: 'validate-input',
|
|
249
|
+
requests: [
|
|
250
|
+
{ id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
251
|
+
],
|
|
252
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
253
|
+
const isValid = phaseResult.responses[0]?.data?.valid;
|
|
254
|
+
|
|
255
|
+
if (isValid) {
|
|
256
|
+
sharedBuffer.validationPassed = true;
|
|
257
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to next phase
|
|
258
|
+
} else {
|
|
259
|
+
return {
|
|
260
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
261
|
+
metadata: { reason: 'Validation failed' }
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: 'process-data',
|
|
268
|
+
requests: [
|
|
269
|
+
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
const result = await stableWorkflow(phases, {
|
|
275
|
+
workflowId: 'validation-workflow',
|
|
276
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
277
|
+
enableNonLinearExecution: true,
|
|
278
|
+
sharedBuffer: {}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (result.terminatedEarly) {
|
|
282
|
+
console.log('Workflow terminated:', result.terminationReason);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### Conditional Branching
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
290
|
+
{
|
|
291
|
+
id: 'check-user-type',
|
|
292
|
+
requests: [
|
|
293
|
+
{ id: 'user', requestOptions: { reqData: { path: '/user/info' }, resReq: true } }
|
|
294
|
+
],
|
|
295
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
296
|
+
const userType = phaseResult.responses[0]?.data?.type;
|
|
297
|
+
sharedBuffer.userType = userType;
|
|
298
|
+
|
|
299
|
+
if (userType === 'premium') {
|
|
300
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'premium-flow' };
|
|
301
|
+
} else if (userType === 'trial') {
|
|
302
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'trial-flow' };
|
|
303
|
+
} else {
|
|
304
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'free-flow' };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: 'premium-flow',
|
|
310
|
+
requests: [
|
|
311
|
+
{ id: 'premium', requestOptions: { reqData: { path: '/premium/data' }, resReq: true } }
|
|
312
|
+
],
|
|
313
|
+
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 'trial-flow',
|
|
317
|
+
requests: [
|
|
318
|
+
{ id: 'trial', requestOptions: { reqData: { path: '/trial/data' }, resReq: true } }
|
|
319
|
+
],
|
|
320
|
+
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: 'free-flow',
|
|
324
|
+
requests: [
|
|
325
|
+
{ id: 'free', requestOptions: { reqData: { path: '/free/data' }, resReq: true } }
|
|
326
|
+
]
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: 'finalize',
|
|
330
|
+
requests: [
|
|
331
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
const result = await stableWorkflow(phases, {
|
|
337
|
+
enableNonLinearExecution: true,
|
|
338
|
+
sharedBuffer: {},
|
|
339
|
+
handlePhaseDecision: (decision, phaseResult) => {
|
|
340
|
+
console.log(`Phase ${phaseResult.phaseId} decided: ${decision.action}`);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
#### Polling with Replay
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
349
|
+
{
|
|
350
|
+
id: 'poll-job-status',
|
|
351
|
+
allowReplay: true,
|
|
352
|
+
maxReplayCount: 20,
|
|
353
|
+
requests: [
|
|
354
|
+
{ id: 'check', requestOptions: { reqData: { path: '/job/status' }, resReq: true } }
|
|
355
|
+
],
|
|
356
|
+
phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
|
|
357
|
+
const status = phaseResult.responses[0]?.data?.status;
|
|
358
|
+
const attempts = executionHistory.filter(h => h.phaseId === 'poll-job-status').length;
|
|
359
|
+
|
|
360
|
+
if (status === 'completed') {
|
|
361
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
362
|
+
} else if (status === 'failed') {
|
|
363
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-recovery' };
|
|
364
|
+
} else if (attempts < 20) {
|
|
365
|
+
// Still processing, wait and replay
|
|
366
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
367
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
368
|
+
} else {
|
|
369
|
+
return {
|
|
370
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
371
|
+
metadata: { reason: 'Job timeout after 20 attempts' }
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
id: 'process-results',
|
|
378
|
+
requests: [
|
|
379
|
+
{ id: 'process', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
|
|
380
|
+
]
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: 'error-recovery',
|
|
384
|
+
requests: [
|
|
385
|
+
{ id: 'recover', requestOptions: { reqData: { path: '/job/retry' }, resReq: true } }
|
|
386
|
+
]
|
|
387
|
+
}
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
const result = await stableWorkflow(phases, {
|
|
391
|
+
workflowId: 'polling-workflow',
|
|
392
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
393
|
+
enableNonLinearExecution: true,
|
|
394
|
+
maxWorkflowIterations: 100
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
console.log('Total iterations:', result.executionHistory.length);
|
|
398
|
+
console.log('Phases executed:', result.completedPhases);
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
#### Retry Logic with Replay
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
405
|
+
{
|
|
406
|
+
id: 'attempt-operation',
|
|
407
|
+
allowReplay: true,
|
|
408
|
+
maxReplayCount: 3,
|
|
409
|
+
requests: [
|
|
410
|
+
{
|
|
411
|
+
id: 'operation',
|
|
412
|
+
requestOptions: {
|
|
413
|
+
reqData: { path: '/risky-operation', method: 'POST' },
|
|
414
|
+
resReq: true,
|
|
415
|
+
attempts: 1 // No retries at request level
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
],
|
|
419
|
+
phaseDecisionHook: async ({ phaseResult, executionHistory, sharedBuffer }) => {
|
|
420
|
+
const success = phaseResult.responses[0]?.success;
|
|
421
|
+
const attemptCount = executionHistory.filter(h => h.phaseId === 'attempt-operation').length;
|
|
422
|
+
|
|
423
|
+
if (success) {
|
|
424
|
+
sharedBuffer.operationResult = phaseResult.responses[0]?.data;
|
|
425
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
426
|
+
} else if (attemptCount < 3) {
|
|
427
|
+
// Exponential backoff
|
|
428
|
+
const delay = 1000 * Math.pow(2, attemptCount);
|
|
429
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
430
|
+
|
|
431
|
+
sharedBuffer.retryAttempts = attemptCount;
|
|
432
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
433
|
+
} else {
|
|
434
|
+
return {
|
|
435
|
+
action: PHASE_DECISION_ACTIONS.JUMP,
|
|
436
|
+
targetPhaseId: 'fallback-operation',
|
|
437
|
+
metadata: { reason: 'Max retries exceeded' }
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
id: 'primary-flow',
|
|
444
|
+
requests: [
|
|
445
|
+
{ id: 'primary', requestOptions: { reqData: { path: '/primary' }, resReq: true } }
|
|
446
|
+
]
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: 'fallback-operation',
|
|
450
|
+
requests: [
|
|
451
|
+
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
452
|
+
]
|
|
453
|
+
}
|
|
454
|
+
];
|
|
455
|
+
|
|
456
|
+
const result = await stableWorkflow(phases, {
|
|
457
|
+
enableNonLinearExecution: true,
|
|
458
|
+
sharedBuffer: { retryAttempts: 0 },
|
|
459
|
+
logPhaseResults: true
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
#### Skip Phases
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
467
|
+
{
|
|
468
|
+
id: 'check-cache',
|
|
469
|
+
allowSkip: true,
|
|
470
|
+
requests: [
|
|
471
|
+
{ id: 'cache', requestOptions: { reqData: { path: '/cache/check' }, resReq: true } }
|
|
472
|
+
],
|
|
473
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
474
|
+
const cached = phaseResult.responses[0]?.data?.cached;
|
|
475
|
+
|
|
476
|
+
if (cached) {
|
|
477
|
+
sharedBuffer.cachedData = phaseResult.responses[0]?.data;
|
|
478
|
+
// Skip expensive-computation and go directly to finalize
|
|
479
|
+
return { action: PHASE_DECISION_ACTIONS.SKIP, targetPhaseId: 'finalize' };
|
|
480
|
+
}
|
|
481
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: 'expensive-computation',
|
|
486
|
+
requests: [
|
|
487
|
+
{ id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
|
|
488
|
+
]
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
id: 'save-to-cache',
|
|
492
|
+
requests: [
|
|
493
|
+
{ id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
|
|
494
|
+
]
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
id: 'finalize',
|
|
498
|
+
requests: [
|
|
499
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
];
|
|
503
|
+
|
|
504
|
+
const result = await stableWorkflow(phases, {
|
|
505
|
+
enableNonLinearExecution: true,
|
|
506
|
+
sharedBuffer: {}
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### Execution History and Tracking
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
const result = await stableWorkflow(phases, {
|
|
514
|
+
workflowId: 'tracked-workflow',
|
|
515
|
+
enableNonLinearExecution: true,
|
|
516
|
+
handlePhaseCompletion: ({ phaseResult, workflowId }) => {
|
|
517
|
+
console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
|
|
518
|
+
executionNumber: phaseResult.executionNumber,
|
|
519
|
+
success: phaseResult.success,
|
|
520
|
+
decision: phaseResult.decision
|
|
521
|
+
});
|
|
522
|
+
},
|
|
523
|
+
handlePhaseDecision: (decision, phaseResult) => {
|
|
524
|
+
console.log(`Decision made:`, {
|
|
525
|
+
phase: phaseResult.phaseId,
|
|
526
|
+
action: decision.action,
|
|
527
|
+
target: decision.targetPhaseId,
|
|
528
|
+
metadata: decision.metadata
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// Analyze execution history
|
|
534
|
+
console.log('Total phase executions:', result.executionHistory.length);
|
|
535
|
+
console.log('Unique phases executed:', new Set(result.executionHistory.map(h => h.phaseId)).size);
|
|
536
|
+
console.log('Replay count:', result.executionHistory.filter(h => h.decision?.action === 'replay').length);
|
|
537
|
+
|
|
538
|
+
result.executionHistory.forEach(record => {
|
|
539
|
+
console.log(`- ${record.phaseId} (attempt ${record.executionNumber}): ${record.success ? '✓' : '✗'}`);
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
#### Loop Protection
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
const result = await stableWorkflow(phases, {
|
|
547
|
+
enableNonLinearExecution: true,
|
|
548
|
+
maxWorkflowIterations: 50, // Prevent infinite loops
|
|
549
|
+
handlePhaseCompletion: ({ phaseResult }) => {
|
|
550
|
+
if (phaseResult.executionNumber && phaseResult.executionNumber > 10) {
|
|
551
|
+
console.warn(`Phase ${phaseResult.phaseId} executed ${phaseResult.executionNumber} times`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
if (result.terminatedEarly && result.terminationReason?.includes('iterations')) {
|
|
557
|
+
console.error('Workflow hit iteration limit - possible infinite loop');
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Branched Workflows
|
|
562
|
+
|
|
563
|
+
Branched workflows enable orchestration of complex business logic by organizing phases into branches that can execute in parallel or serial order. Each branch is a self-contained workflow with its own phases, and branches can make decisions to control execution flow using JUMP, TERMINATE, or CONTINUE actions.
|
|
564
|
+
|
|
565
|
+
#### Why Branched Workflows?
|
|
566
|
+
|
|
567
|
+
- **Organize complex logic**: Group related phases into logical branches
|
|
568
|
+
- **Parallel processing**: Execute independent branches concurrently for better performance
|
|
569
|
+
- **Conditional routing**: Branches can decide whether to continue, jump to other branches, or terminate
|
|
570
|
+
- **Clean architecture**: Separate concerns into distinct branches (validation, processing, error handling)
|
|
571
|
+
- **Shared state**: Branches share a common buffer for state management
|
|
572
|
+
|
|
573
|
+
#### Basic Branched Workflow
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
import { stableWorkflow, STABLE_WORKFLOW_BRANCH } from '@emmvish/stable-request';
|
|
577
|
+
|
|
578
|
+
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
579
|
+
{
|
|
580
|
+
id: 'validation',
|
|
581
|
+
phases: [
|
|
582
|
+
{
|
|
583
|
+
id: 'validate-input',
|
|
584
|
+
requests: [
|
|
585
|
+
{ id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
586
|
+
]
|
|
587
|
+
}
|
|
588
|
+
]
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
id: 'processing',
|
|
592
|
+
phases: [
|
|
593
|
+
{
|
|
594
|
+
id: 'process-data',
|
|
595
|
+
requests: [
|
|
596
|
+
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
597
|
+
]
|
|
598
|
+
}
|
|
599
|
+
]
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
id: 'finalization',
|
|
603
|
+
phases: [
|
|
604
|
+
{
|
|
605
|
+
id: 'finalize',
|
|
606
|
+
requests: [
|
|
607
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
608
|
+
]
|
|
609
|
+
}
|
|
610
|
+
]
|
|
611
|
+
}
|
|
612
|
+
];
|
|
613
|
+
|
|
614
|
+
const result = await stableWorkflow([], {
|
|
615
|
+
workflowId: 'branched-workflow',
|
|
616
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
617
|
+
branches,
|
|
618
|
+
executeBranchesConcurrently: false, // Execute branches serially
|
|
619
|
+
sharedBuffer: {}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
console.log('Branches executed:', result.branches?.length);
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
#### Parallel vs Serial Branch Execution
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
// Parallel execution - all branches run concurrently
|
|
629
|
+
const result = await stableWorkflow([], {
|
|
630
|
+
workflowId: 'parallel-branches',
|
|
631
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
632
|
+
branches: [
|
|
633
|
+
{ id: 'fetch-users', phases: [/* ... */] },
|
|
634
|
+
{ id: 'fetch-products', phases: [/* ... */] },
|
|
635
|
+
{ id: 'fetch-orders', phases: [/* ... */] }
|
|
636
|
+
],
|
|
637
|
+
executeBranchesConcurrently: true, // Parallel execution
|
|
638
|
+
sharedBuffer: {}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Serial execution - branches run one after another
|
|
642
|
+
const result = await stableWorkflow([], {
|
|
643
|
+
workflowId: 'serial-branches',
|
|
644
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
645
|
+
branches: [
|
|
646
|
+
{ id: 'authenticate', phases: [/* ... */] },
|
|
647
|
+
{ id: 'fetch-data', phases: [/* ... */] },
|
|
648
|
+
{ id: 'process', phases: [/* ... */] }
|
|
649
|
+
],
|
|
650
|
+
executeBranchesConcurrently: false, // Serial execution
|
|
651
|
+
sharedBuffer: {}
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
#### Branch Decision Hooks
|
|
656
|
+
|
|
657
|
+
Each branch can have a decision hook to control workflow execution:
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
import { BRANCH_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
661
|
+
|
|
662
|
+
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
663
|
+
{
|
|
664
|
+
id: 'validation',
|
|
665
|
+
phases: [
|
|
666
|
+
{
|
|
667
|
+
id: 'validate',
|
|
668
|
+
requests: [
|
|
669
|
+
{ id: 'val', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
670
|
+
]
|
|
671
|
+
}
|
|
672
|
+
],
|
|
673
|
+
branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
|
|
674
|
+
const isValid = branchResult.phases[0]?.responses[0]?.data?.valid;
|
|
675
|
+
|
|
676
|
+
if (!isValid) {
|
|
677
|
+
return {
|
|
678
|
+
action: BRANCH_DECISION_ACTIONS.TERMINATE,
|
|
679
|
+
metadata: { reason: 'Validation failed' }
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
sharedBuffer!.validated = true;
|
|
684
|
+
return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
id: 'processing',
|
|
689
|
+
phases: [/* ... */]
|
|
690
|
+
}
|
|
691
|
+
];
|
|
692
|
+
|
|
693
|
+
const result = await stableWorkflow([], {
|
|
694
|
+
workflowId: 'validation-workflow',
|
|
695
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
696
|
+
branches,
|
|
697
|
+
executeBranchesConcurrently: false,
|
|
698
|
+
sharedBuffer: {}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
if (result.terminatedEarly) {
|
|
702
|
+
console.log('Workflow terminated:', result.terminationReason);
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
#### JUMP Action - Skip Branches
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
710
|
+
{
|
|
711
|
+
id: 'check-cache',
|
|
712
|
+
phases: [
|
|
713
|
+
{
|
|
714
|
+
id: 'cache-check',
|
|
715
|
+
requests: [
|
|
716
|
+
{ id: 'check', requestOptions: { reqData: { path: '/cache/check' }, resReq: true } }
|
|
717
|
+
]
|
|
718
|
+
}
|
|
719
|
+
],
|
|
720
|
+
branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
|
|
721
|
+
const cached = branchResult.phases[0]?.responses[0]?.data?.cached;
|
|
722
|
+
|
|
723
|
+
if (cached) {
|
|
724
|
+
sharedBuffer!.cachedData = branchResult.phases[0]?.responses[0]?.data;
|
|
725
|
+
// Skip expensive computation, jump directly to finalize
|
|
726
|
+
return {
|
|
727
|
+
action: BRANCH_DECISION_ACTIONS.JUMP,
|
|
728
|
+
targetBranchId: 'finalize'
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
id: 'expensive-computation',
|
|
737
|
+
phases: [
|
|
738
|
+
{
|
|
739
|
+
id: 'compute',
|
|
740
|
+
requests: [
|
|
741
|
+
{ id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
|
|
742
|
+
]
|
|
743
|
+
}
|
|
744
|
+
]
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
id: 'save-cache',
|
|
748
|
+
phases: [
|
|
749
|
+
{
|
|
750
|
+
id: 'save',
|
|
751
|
+
requests: [
|
|
752
|
+
{ id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
|
|
753
|
+
]
|
|
754
|
+
}
|
|
755
|
+
]
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
id: 'finalize',
|
|
759
|
+
phases: [
|
|
760
|
+
{
|
|
761
|
+
id: 'final',
|
|
762
|
+
requests: [
|
|
763
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
764
|
+
]
|
|
765
|
+
}
|
|
766
|
+
]
|
|
767
|
+
}
|
|
768
|
+
];
|
|
769
|
+
|
|
770
|
+
const result = await stableWorkflow([], {
|
|
771
|
+
workflowId: 'cache-optimization',
|
|
772
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
773
|
+
branches,
|
|
774
|
+
executeBranchesConcurrently: false,
|
|
775
|
+
sharedBuffer: {}
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// If cache hit: check-cache → finalize (skips expensive-computation and save-cache)
|
|
779
|
+
// If cache miss: check-cache → expensive-computation → save-cache → finalize
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
#### Conditional Branching
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
786
|
+
{
|
|
787
|
+
id: 'check-user-type',
|
|
788
|
+
phases: [
|
|
789
|
+
{
|
|
790
|
+
id: 'user-info',
|
|
791
|
+
requests: [
|
|
792
|
+
{ id: 'user', requestOptions: { reqData: { path: '/user/info' }, resReq: true } }
|
|
793
|
+
]
|
|
794
|
+
}
|
|
795
|
+
],
|
|
796
|
+
branchDecisionHook: async ({ branchResult, sharedBuffer }) => {
|
|
797
|
+
const userType = branchResult.phases[0]?.responses[0]?.data?.type;
|
|
798
|
+
sharedBuffer!.userType = userType;
|
|
799
|
+
|
|
800
|
+
if (userType === 'premium') {
|
|
801
|
+
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'premium-flow' };
|
|
802
|
+
} else if (userType === 'trial') {
|
|
803
|
+
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'trial-flow' };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return { action: BRANCH_DECISION_ACTIONS.CONTINUE }; // free-flow
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
id: 'free-flow',
|
|
811
|
+
phases: [
|
|
812
|
+
{
|
|
813
|
+
id: 'free-data',
|
|
814
|
+
requests: [
|
|
815
|
+
{ id: 'free', requestOptions: { reqData: { path: '/free/data' }, resReq: true } }
|
|
816
|
+
]
|
|
817
|
+
}
|
|
818
|
+
],
|
|
819
|
+
branchDecisionHook: async () => {
|
|
820
|
+
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'finalize' };
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
id: 'trial-flow',
|
|
825
|
+
phases: [
|
|
826
|
+
{
|
|
827
|
+
id: 'trial-data',
|
|
828
|
+
requests: [
|
|
829
|
+
{ id: 'trial', requestOptions: { reqData: { path: '/trial/data' }, resReq: true } }
|
|
830
|
+
]
|
|
831
|
+
}
|
|
832
|
+
],
|
|
833
|
+
branchDecisionHook: async () => {
|
|
834
|
+
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'finalize' };
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
id: 'premium-flow',
|
|
839
|
+
phases: [
|
|
840
|
+
{
|
|
841
|
+
id: 'premium-data',
|
|
842
|
+
requests: [
|
|
843
|
+
{ id: 'premium', requestOptions: { reqData: { path: '/premium/data' }, resReq: true } }
|
|
844
|
+
]
|
|
845
|
+
}
|
|
846
|
+
],
|
|
847
|
+
branchDecisionHook: async () => {
|
|
848
|
+
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'finalize' };
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
id: 'finalize',
|
|
853
|
+
phases: [
|
|
854
|
+
{
|
|
855
|
+
id: 'final',
|
|
856
|
+
requests: [
|
|
857
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
858
|
+
]
|
|
859
|
+
}
|
|
860
|
+
]
|
|
861
|
+
}
|
|
862
|
+
];
|
|
863
|
+
|
|
864
|
+
const result = await stableWorkflow([], {
|
|
865
|
+
workflowId: 'user-type-routing',
|
|
866
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
867
|
+
branches,
|
|
868
|
+
executeBranchesConcurrently: false,
|
|
869
|
+
sharedBuffer: {}
|
|
870
|
+
});
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
#### Retry Logic Within Branches
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
877
|
+
{
|
|
878
|
+
id: 'retry-branch',
|
|
879
|
+
phases: [
|
|
880
|
+
{
|
|
881
|
+
id: 'retry-phase',
|
|
882
|
+
commonConfig: {
|
|
883
|
+
commonAttempts: 5,
|
|
884
|
+
commonWait: 100,
|
|
885
|
+
commonRetryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
886
|
+
},
|
|
887
|
+
requests: [
|
|
888
|
+
{
|
|
889
|
+
id: 'retry-req',
|
|
890
|
+
requestOptions: {
|
|
891
|
+
reqData: { path: '/unstable-endpoint' },
|
|
892
|
+
resReq: true
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
]
|
|
896
|
+
}
|
|
897
|
+
]
|
|
898
|
+
}
|
|
899
|
+
];
|
|
900
|
+
|
|
901
|
+
const result = await stableWorkflow([], {
|
|
902
|
+
workflowId: 'retry-workflow',
|
|
903
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
904
|
+
branches,
|
|
905
|
+
executeBranchesConcurrently: false
|
|
906
|
+
});
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
#### Error Handling in Branches
|
|
910
|
+
|
|
911
|
+
```typescript
|
|
912
|
+
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
913
|
+
{
|
|
914
|
+
id: 'risky-operation',
|
|
915
|
+
phases: [
|
|
916
|
+
{
|
|
917
|
+
id: 'operation',
|
|
918
|
+
requests: [
|
|
919
|
+
{
|
|
920
|
+
id: 'op',
|
|
921
|
+
requestOptions: {
|
|
922
|
+
reqData: { path: '/risky' },
|
|
923
|
+
resReq: true,
|
|
924
|
+
attempts: 3
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
]
|
|
928
|
+
}
|
|
929
|
+
],
|
|
930
|
+
branchDecisionHook: async ({ branchResult }) => {
|
|
931
|
+
if (!branchResult.success) {
|
|
932
|
+
return {
|
|
933
|
+
action: BRANCH_DECISION_ACTIONS.JUMP,
|
|
934
|
+
targetBranchId: 'error-handler'
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
return { action: BRANCH_DECISION_ACTIONS.JUMP, targetBranchId: 'success-handler' };
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
id: 'success-handler',
|
|
942
|
+
phases: [
|
|
943
|
+
{
|
|
944
|
+
id: 'success',
|
|
945
|
+
requests: [
|
|
946
|
+
{ id: 'success', requestOptions: { reqData: { path: '/success' }, resReq: true } }
|
|
947
|
+
]
|
|
948
|
+
}
|
|
949
|
+
],
|
|
950
|
+
branchDecisionHook: async () => {
|
|
951
|
+
return { action: BRANCH_DECISION_ACTIONS.TERMINATE };
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
id: 'error-handler',
|
|
956
|
+
phases: [
|
|
957
|
+
{
|
|
958
|
+
id: 'error',
|
|
959
|
+
requests: [
|
|
960
|
+
{ id: 'error', requestOptions: { reqData: { path: '/error' }, resReq: true } }
|
|
961
|
+
]
|
|
962
|
+
}
|
|
963
|
+
]
|
|
964
|
+
}
|
|
965
|
+
];
|
|
966
|
+
|
|
967
|
+
const result = await stableWorkflow([], {
|
|
968
|
+
workflowId: 'error-handling-workflow',
|
|
969
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
970
|
+
branches,
|
|
971
|
+
executeBranchesConcurrently: false,
|
|
972
|
+
stopOnFirstPhaseError: false // Continue to error handler branch
|
|
973
|
+
});
|
|
974
|
+
```
|
|
65
975
|
|
|
66
|
-
|
|
976
|
+
#### Branch Completion Hooks
|
|
67
977
|
|
|
68
|
-
```
|
|
69
|
-
|
|
978
|
+
```typescript
|
|
979
|
+
const result = await stableWorkflow([], {
|
|
980
|
+
workflowId: 'tracked-branches',
|
|
981
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
982
|
+
branches,
|
|
983
|
+
executeBranchesConcurrently: true,
|
|
984
|
+
handleBranchCompletion: ({ branchResult, workflowId }) => {
|
|
985
|
+
console.log(`[${workflowId}] Branch ${branchResult.branchId} completed:`, {
|
|
986
|
+
success: branchResult.success,
|
|
987
|
+
phases: branchResult.phases.length,
|
|
988
|
+
decision: branchResult.decision?.action
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
});
|
|
70
992
|
```
|
|
71
993
|
|
|
72
|
-
|
|
994
|
+
#### Mixed Parallel and Serial Branches
|
|
73
995
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
-
|
|
996
|
+
```typescript
|
|
997
|
+
const branches: STABLE_WORKFLOW_BRANCH[] = [
|
|
998
|
+
{
|
|
999
|
+
id: 'init',
|
|
1000
|
+
phases: [/* initialization */]
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
id: 'parallel-1',
|
|
1004
|
+
markConcurrentBranch: true,
|
|
1005
|
+
phases: [/* independent task 1 */]
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
id: 'parallel-2',
|
|
1009
|
+
markConcurrentBranch: true,
|
|
1010
|
+
phases: [/* independent task 2 */]
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
id: 'parallel-3',
|
|
1014
|
+
markConcurrentBranch: true,
|
|
1015
|
+
phases: [/* independent task 3 */],
|
|
1016
|
+
branchDecisionHook: async ({ concurrentBranchResults }) => {
|
|
1017
|
+
// All parallel branches completed, make decision
|
|
1018
|
+
const allSuccessful = concurrentBranchResults!.every(b => b.success);
|
|
1019
|
+
if (!allSuccessful) {
|
|
1020
|
+
return { action: BRANCH_DECISION_ACTIONS.TERMINATE };
|
|
1021
|
+
}
|
|
1022
|
+
return { action: BRANCH_DECISION_ACTIONS.CONTINUE };
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
id: 'finalize',
|
|
1027
|
+
phases: [/* finalization */]
|
|
1028
|
+
}
|
|
1029
|
+
];
|
|
87
1030
|
|
|
88
|
-
|
|
1031
|
+
const result = await stableWorkflow([], {
|
|
1032
|
+
workflowId: 'mixed-execution',
|
|
1033
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
1034
|
+
branches,
|
|
1035
|
+
executeBranchesConcurrently: false // Base mode is serial
|
|
1036
|
+
});
|
|
89
1037
|
|
|
90
|
-
|
|
1038
|
+
// Execution: init → [parallel-1, parallel-2, parallel-3] → finalize
|
|
1039
|
+
```
|
|
91
1040
|
|
|
92
|
-
|
|
93
|
-
import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
|
|
1041
|
+
#### Configuration Options
|
|
94
1042
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
method: 'GET'
|
|
100
|
-
},
|
|
101
|
-
resReq: true,
|
|
102
|
-
attempts: 3,
|
|
103
|
-
wait: 1000,
|
|
104
|
-
retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
|
|
105
|
-
});
|
|
1043
|
+
**Workflow Options:**
|
|
1044
|
+
- `branches`: Array of branch definitions
|
|
1045
|
+
- `executeBranchesConcurrently`: Execute all branches in parallel (default: false)
|
|
1046
|
+
- `handleBranchCompletion`: Called when each branch completes
|
|
106
1047
|
|
|
107
|
-
|
|
1048
|
+
**Branch Options:**
|
|
1049
|
+
- `id`: Unique branch identifier
|
|
1050
|
+
- `phases`: Array of phases to execute in this branch
|
|
1051
|
+
- `branchDecisionHook`: Function returning `BranchExecutionDecision`
|
|
1052
|
+
- `markConcurrentBranch`: Mark as part of concurrent group (default: false)
|
|
1053
|
+
|
|
1054
|
+
**Branch Decision Actions:**
|
|
1055
|
+
- `CONTINUE`: Proceed to next branch
|
|
1056
|
+
- `JUMP`: Jump to specific branch by ID
|
|
1057
|
+
- `TERMINATE`: Stop workflow execution
|
|
1058
|
+
|
|
1059
|
+
**Decision Hook Parameters:**
|
|
1060
|
+
```typescript
|
|
1061
|
+
interface BranchDecisionHookOptions {
|
|
1062
|
+
workflowId: string;
|
|
1063
|
+
branchResult: STABLE_WORKFLOW_BRANCH_RESULT;
|
|
1064
|
+
branchId: string;
|
|
1065
|
+
branchIndex: number;
|
|
1066
|
+
sharedBuffer?: Record<string, any>;
|
|
1067
|
+
concurrentBranchResults?: STABLE_WORKFLOW_BRANCH_RESULT[]; // For concurrent groups
|
|
1068
|
+
}
|
|
108
1069
|
```
|
|
109
1070
|
|
|
110
|
-
|
|
1071
|
+
**Decision Object:**
|
|
1072
|
+
```typescript
|
|
1073
|
+
interface BranchExecutionDecision {
|
|
1074
|
+
action: BRANCH_DECISION_ACTIONS;
|
|
1075
|
+
targetBranchId?: string;
|
|
1076
|
+
metadata?: Record<string, any>;
|
|
1077
|
+
}
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
#### Mixed Serial and Parallel Execution
|
|
1081
|
+
|
|
1082
|
+
Non-linear workflows support mixing serial and parallel phase execution. Mark consecutive phases with `markConcurrentPhase: true` to execute them in parallel, while other phases execute serially.
|
|
1083
|
+
|
|
1084
|
+
**Basic Mixed Execution:**
|
|
111
1085
|
|
|
112
1086
|
```typescript
|
|
113
|
-
import {
|
|
1087
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
114
1088
|
|
|
115
|
-
const
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
1089
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1090
|
+
{
|
|
1091
|
+
id: 'init',
|
|
1092
|
+
requests: [
|
|
1093
|
+
{ id: 'init', requestOptions: { reqData: { path: '/init' }, resReq: true } }
|
|
1094
|
+
],
|
|
1095
|
+
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.CONTINUE })
|
|
1096
|
+
},
|
|
1097
|
+
// These two phases execute in parallel
|
|
1098
|
+
{
|
|
1099
|
+
id: 'check-inventory',
|
|
1100
|
+
markConcurrentPhase: true,
|
|
1101
|
+
requests: [
|
|
1102
|
+
{ id: 'inv', requestOptions: { reqData: { path: '/inventory' }, resReq: true } }
|
|
1103
|
+
]
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
id: 'check-pricing',
|
|
1107
|
+
markConcurrentPhase: true,
|
|
1108
|
+
requests: [
|
|
1109
|
+
{ id: 'price', requestOptions: { reqData: { path: '/pricing' }, resReq: true } }
|
|
1110
|
+
],
|
|
1111
|
+
// Decision hook receives results from all concurrent phases
|
|
1112
|
+
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
1113
|
+
const inventory = concurrentPhaseResults![0].responses[0]?.data;
|
|
1114
|
+
const pricing = concurrentPhaseResults![1].responses[0]?.data;
|
|
1115
|
+
|
|
1116
|
+
if (inventory.available && pricing.inBudget) {
|
|
1117
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
1118
|
+
}
|
|
1119
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'out-of-stock' };
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
// This phase executes serially after the parallel group
|
|
1123
|
+
{
|
|
1124
|
+
id: 'process-order',
|
|
1125
|
+
requests: [
|
|
1126
|
+
{ id: 'order', requestOptions: { reqData: { path: '/order' }, resReq: true } }
|
|
1127
|
+
]
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
id: 'out-of-stock',
|
|
1131
|
+
requests: [
|
|
1132
|
+
{ id: 'notify', requestOptions: { reqData: { path: '/notify' }, resReq: true } }
|
|
1133
|
+
]
|
|
1134
|
+
}
|
|
119
1135
|
];
|
|
120
1136
|
|
|
121
|
-
const
|
|
1137
|
+
const result = await stableWorkflow(phases, {
|
|
1138
|
+
workflowId: 'mixed-execution',
|
|
122
1139
|
commonRequestData: { hostname: 'api.example.com' },
|
|
123
|
-
|
|
124
|
-
maxConcurrentRequests: 10
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
results.forEach(result => {
|
|
128
|
-
if (result.success) {
|
|
129
|
-
console.log(`Request ${result.requestId}:`, result.data);
|
|
130
|
-
} else {
|
|
131
|
-
console.error(`Request ${result.requestId} failed:`, result.error);
|
|
132
|
-
}
|
|
1140
|
+
enableNonLinearExecution: true
|
|
133
1141
|
});
|
|
134
1142
|
```
|
|
135
1143
|
|
|
136
|
-
|
|
1144
|
+
**Multiple Parallel Groups:**
|
|
137
1145
|
|
|
138
1146
|
```typescript
|
|
139
|
-
import { stableWorkflow, STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
|
|
140
|
-
|
|
141
1147
|
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
142
1148
|
{
|
|
143
|
-
id: '
|
|
1149
|
+
id: 'authenticate',
|
|
144
1150
|
requests: [
|
|
145
|
-
{ id: '
|
|
1151
|
+
{ id: 'auth', requestOptions: { reqData: { path: '/auth' }, resReq: true } }
|
|
146
1152
|
]
|
|
147
1153
|
},
|
|
1154
|
+
// First parallel group: Data validation
|
|
148
1155
|
{
|
|
149
|
-
id: '
|
|
150
|
-
|
|
1156
|
+
id: 'validate-user',
|
|
1157
|
+
markConcurrentPhase: true,
|
|
151
1158
|
requests: [
|
|
152
|
-
{ id: '
|
|
153
|
-
|
|
1159
|
+
{ id: 'val-user', requestOptions: { reqData: { path: '/validate/user' }, resReq: true } }
|
|
1160
|
+
]
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
id: 'validate-payment',
|
|
1164
|
+
markConcurrentPhase: true,
|
|
1165
|
+
requests: [
|
|
1166
|
+
{ id: 'val-pay', requestOptions: { reqData: { path: '/validate/payment' }, resReq: true } }
|
|
1167
|
+
]
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
id: 'validate-shipping',
|
|
1171
|
+
markConcurrentPhase: true,
|
|
1172
|
+
requests: [
|
|
1173
|
+
{ id: 'val-ship', requestOptions: { reqData: { path: '/validate/shipping' }, resReq: true } }
|
|
1174
|
+
],
|
|
1175
|
+
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
1176
|
+
const allValid = concurrentPhaseResults!.every(r => r.success && r.responses[0]?.data?.valid);
|
|
1177
|
+
if (!allValid) {
|
|
1178
|
+
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Validation failed' } };
|
|
1179
|
+
}
|
|
1180
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
1181
|
+
}
|
|
1182
|
+
},
|
|
1183
|
+
// Serial processing phase
|
|
1184
|
+
{
|
|
1185
|
+
id: 'calculate-total',
|
|
1186
|
+
requests: [
|
|
1187
|
+
{ id: 'calc', requestOptions: { reqData: { path: '/calculate' }, resReq: true } }
|
|
1188
|
+
]
|
|
1189
|
+
},
|
|
1190
|
+
// Second parallel group: External integrations
|
|
1191
|
+
{
|
|
1192
|
+
id: 'notify-warehouse',
|
|
1193
|
+
markConcurrentPhase: true,
|
|
1194
|
+
requests: [
|
|
1195
|
+
{ id: 'warehouse', requestOptions: { reqData: { path: '/notify/warehouse' }, resReq: true } }
|
|
1196
|
+
]
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
id: 'notify-shipping',
|
|
1200
|
+
markConcurrentPhase: true,
|
|
1201
|
+
requests: [
|
|
1202
|
+
{ id: 'shipping', requestOptions: { reqData: { path: '/notify/shipping' }, resReq: true } }
|
|
1203
|
+
]
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
id: 'update-inventory',
|
|
1207
|
+
markConcurrentPhase: true,
|
|
1208
|
+
requests: [
|
|
1209
|
+
{ id: 'inventory', requestOptions: { reqData: { path: '/update/inventory' }, resReq: true } }
|
|
1210
|
+
]
|
|
1211
|
+
},
|
|
1212
|
+
// Final serial phase
|
|
1213
|
+
{
|
|
1214
|
+
id: 'finalize',
|
|
1215
|
+
requests: [
|
|
1216
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
154
1217
|
]
|
|
155
1218
|
}
|
|
156
1219
|
];
|
|
157
1220
|
|
|
158
1221
|
const result = await stableWorkflow(phases, {
|
|
159
|
-
workflowId: '
|
|
1222
|
+
workflowId: 'multi-parallel-workflow',
|
|
160
1223
|
commonRequestData: { hostname: 'api.example.com' },
|
|
161
|
-
|
|
162
|
-
logPhaseResults: true
|
|
1224
|
+
enableNonLinearExecution: true
|
|
163
1225
|
});
|
|
164
1226
|
|
|
165
|
-
console.log(
|
|
1227
|
+
console.log('Execution order demonstrates mixed serial/parallel execution');
|
|
166
1228
|
```
|
|
167
1229
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
### stableRequest
|
|
1230
|
+
**Decision Making with Concurrent Results:**
|
|
171
1231
|
|
|
172
|
-
Execute a single HTTP request with retry logic and observability.
|
|
173
|
-
|
|
174
|
-
**Signature:**
|
|
175
1232
|
```typescript
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
1233
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1234
|
+
{
|
|
1235
|
+
id: 'api-check-1',
|
|
1236
|
+
markConcurrentPhase: true,
|
|
1237
|
+
requests: [
|
|
1238
|
+
{ id: 'api1', requestOptions: { reqData: { path: '/health/api1' }, resReq: true } }
|
|
1239
|
+
]
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
id: 'api-check-2',
|
|
1243
|
+
markConcurrentPhase: true,
|
|
1244
|
+
requests: [
|
|
1245
|
+
{ id: 'api2', requestOptions: { reqData: { path: '/health/api2' }, resReq: true } }
|
|
1246
|
+
]
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
id: 'api-check-3',
|
|
1250
|
+
markConcurrentPhase: true,
|
|
1251
|
+
requests: [
|
|
1252
|
+
{ id: 'api3', requestOptions: { reqData: { path: '/health/api3' }, resReq: true } }
|
|
1253
|
+
],
|
|
1254
|
+
phaseDecisionHook: async ({ concurrentPhaseResults, sharedBuffer }) => {
|
|
1255
|
+
// Aggregate results from all parallel phases
|
|
1256
|
+
const healthScores = concurrentPhaseResults!.map(result =>
|
|
1257
|
+
result.responses[0]?.data?.score || 0
|
|
1258
|
+
);
|
|
1259
|
+
|
|
1260
|
+
const averageScore = healthScores.reduce((a, b) => a + b, 0) / healthScores.length;
|
|
1261
|
+
sharedBuffer!.healthScore = averageScore;
|
|
1262
|
+
|
|
1263
|
+
if (averageScore > 0.8) {
|
|
1264
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'optimal-path' };
|
|
1265
|
+
} else if (averageScore > 0.5) {
|
|
1266
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to degraded-path
|
|
1267
|
+
} else {
|
|
1268
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'fallback-path' };
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
id: 'degraded-path',
|
|
1274
|
+
requests: [
|
|
1275
|
+
{ id: 'degraded', requestOptions: { reqData: { path: '/degraded' }, resReq: true } }
|
|
1276
|
+
]
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
id: 'optimal-path',
|
|
1280
|
+
requests: [
|
|
1281
|
+
{ id: 'optimal', requestOptions: { reqData: { path: '/optimal' }, resReq: true } }
|
|
1282
|
+
]
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
id: 'fallback-path',
|
|
1286
|
+
requests: [
|
|
1287
|
+
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
1288
|
+
]
|
|
1289
|
+
}
|
|
1290
|
+
];
|
|
180
1291
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
- `finalErrorAnalyzer`: Handle final errors gracefully (return `true` to suppress error)
|
|
189
|
-
- `cache`: Enable response caching with TTL
|
|
190
|
-
- `circuitBreaker`: Circuit breaker configuration
|
|
191
|
-
- `preExecution`: Pre-execution hooks for dynamic request transformation
|
|
192
|
-
- `commonBuffer`: Shared state object across hooks
|
|
1292
|
+
const sharedBuffer = {};
|
|
1293
|
+
const result = await stableWorkflow(phases, {
|
|
1294
|
+
workflowId: 'adaptive-routing',
|
|
1295
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
1296
|
+
enableNonLinearExecution: true,
|
|
1297
|
+
sharedBuffer
|
|
1298
|
+
});
|
|
193
1299
|
|
|
194
|
-
|
|
1300
|
+
console.log('Average health score:', sharedBuffer.healthScore);
|
|
1301
|
+
```
|
|
195
1302
|
|
|
196
|
-
|
|
1303
|
+
**Error Handling in Parallel Groups:**
|
|
197
1304
|
|
|
198
|
-
**Signature:**
|
|
199
1305
|
```typescript
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
1306
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
1307
|
+
{
|
|
1308
|
+
id: 'critical-check',
|
|
1309
|
+
markConcurrentPhase: true,
|
|
1310
|
+
requests: [
|
|
1311
|
+
{
|
|
1312
|
+
id: 'check1',
|
|
1313
|
+
requestOptions: {
|
|
1314
|
+
reqData: { path: '/critical/check1' },
|
|
1315
|
+
resReq: true,
|
|
1316
|
+
attempts: 3
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
]
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
id: 'optional-check',
|
|
1323
|
+
markConcurrentPhase: true,
|
|
1324
|
+
requests: [
|
|
1325
|
+
{
|
|
1326
|
+
id: 'check2',
|
|
1327
|
+
requestOptions: {
|
|
1328
|
+
reqData: { path: '/optional/check2' },
|
|
1329
|
+
resReq: true,
|
|
1330
|
+
attempts: 1,
|
|
1331
|
+
finalErrorAnalyzer: async () => true // Suppress errors
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
],
|
|
1335
|
+
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
1336
|
+
// Check if critical phase succeeded
|
|
1337
|
+
const criticalSuccess = concurrentPhaseResults![0].success;
|
|
1338
|
+
|
|
1339
|
+
if (!criticalSuccess) {
|
|
1340
|
+
return {
|
|
1341
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
1342
|
+
metadata: { reason: 'Critical check failed' }
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Continue even if optional check failed
|
|
1347
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
1348
|
+
}
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
id: 'process',
|
|
1352
|
+
requests: [
|
|
1353
|
+
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
1354
|
+
]
|
|
1355
|
+
}
|
|
1356
|
+
];
|
|
1357
|
+
|
|
1358
|
+
const result = await stableWorkflow(phases, {
|
|
1359
|
+
workflowId: 'resilient-parallel',
|
|
1360
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
1361
|
+
enableNonLinearExecution: true,
|
|
1362
|
+
stopOnFirstPhaseError: false // Continue even with phase errors
|
|
1363
|
+
});
|
|
204
1364
|
```
|
|
205
1365
|
|
|
206
|
-
**Key
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
- `
|
|
210
|
-
- `
|
|
211
|
-
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
- `common*`: Common configuration applied to all requests (e.g., `commonAttempts`, `commonCache`)
|
|
1366
|
+
**Key Points:**
|
|
1367
|
+
- Only **consecutive phases** with `markConcurrentPhase: true` execute in parallel
|
|
1368
|
+
- The **last phase** in a concurrent group can have a `phaseDecisionHook` that receives `concurrentPhaseResults`
|
|
1369
|
+
- Parallel groups are separated by phases **without** `markConcurrentPhase` (or phases with it set to false)
|
|
1370
|
+
- All decision actions work with parallel groups except `REPLAY` (not supported for concurrent groups)
|
|
1371
|
+
- Error handling follows normal workflow rules - use `stopOnFirstPhaseError` to control behavior
|
|
1372
|
+
|
|
1373
|
+
#### Configuration Options
|
|
215
1374
|
|
|
216
|
-
|
|
1375
|
+
**Workflow Options:**
|
|
1376
|
+
- `enableNonLinearExecution`: Enable non-linear workflow (required)
|
|
1377
|
+
- `maxWorkflowIterations`: Maximum total iterations (default: 1000)
|
|
1378
|
+
- `handlePhaseDecision`: Called when phase makes a decision
|
|
1379
|
+
- `stopOnFirstPhaseError`: Stop on phase failure (default: false)
|
|
217
1380
|
|
|
218
|
-
|
|
1381
|
+
**Phase Options:**
|
|
1382
|
+
- `phaseDecisionHook`: Function returning `PhaseExecutionDecision`
|
|
1383
|
+
- `allowReplay`: Allow phase replay (default: false)
|
|
1384
|
+
- `allowSkip`: Allow phase skip (default: false)
|
|
1385
|
+
- `maxReplayCount`: Maximum replays (default: Infinity)
|
|
219
1386
|
|
|
220
|
-
**
|
|
1387
|
+
**Decision Hook Parameters:**
|
|
221
1388
|
```typescript
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
1389
|
+
interface PhaseDecisionHookOptions {
|
|
1390
|
+
workflowId: string;
|
|
1391
|
+
phaseResult: STABLE_WORKFLOW_PHASE_RESULT;
|
|
1392
|
+
phaseId: string;
|
|
1393
|
+
phaseIndex: number;
|
|
1394
|
+
executionHistory: PhaseExecutionRecord[];
|
|
1395
|
+
sharedBuffer?: Record<string, any>;
|
|
1396
|
+
params?: any;
|
|
1397
|
+
}
|
|
226
1398
|
```
|
|
227
1399
|
|
|
228
|
-
**
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
**Phase Configuration:**
|
|
238
|
-
- `id`: Phase identifier
|
|
239
|
-
- `requests`: Array of requests in this phase
|
|
240
|
-
- `concurrentExecution`: Execute phase requests concurrently
|
|
241
|
-
- `stopOnFirstError`: Stop phase on first request error
|
|
242
|
-
- `markConcurrentPhase`: Mark phase for concurrent execution in mixed mode
|
|
243
|
-
- `commonConfig`: Phase-level configuration overrides
|
|
244
|
-
|
|
245
|
-
## Advanced Features
|
|
1400
|
+
**Decision Object:**
|
|
1401
|
+
```typescript
|
|
1402
|
+
interface PhaseExecutionDecision {
|
|
1403
|
+
action: PHASE_DECISION_ACTIONS;
|
|
1404
|
+
targetPhaseId?: string;
|
|
1405
|
+
replayCount?: number;
|
|
1406
|
+
metadata?: Record<string, any>;
|
|
1407
|
+
}
|
|
1408
|
+
```
|
|
246
1409
|
|
|
247
1410
|
### Retry Strategies
|
|
248
1411
|
|
|
@@ -952,77 +2115,6 @@ console.log('Products:', result.data.products?.length);
|
|
|
952
2115
|
console.log('Orders:', result.data.orders?.length);
|
|
953
2116
|
```
|
|
954
2117
|
|
|
955
|
-
## Configuration Options
|
|
956
|
-
|
|
957
|
-
### Request Data Configuration
|
|
958
|
-
|
|
959
|
-
```typescript
|
|
960
|
-
interface REQUEST_DATA<RequestDataType> {
|
|
961
|
-
hostname: string;
|
|
962
|
-
protocol?: 'http' | 'https'; // default: 'https'
|
|
963
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; // default: 'GET'
|
|
964
|
-
path?: `/${string}`;
|
|
965
|
-
port?: number; // default: 443
|
|
966
|
-
headers?: Record<string, any>;
|
|
967
|
-
body?: RequestDataType;
|
|
968
|
-
query?: Record<string, any>;
|
|
969
|
-
timeout?: number; // default: 15000ms
|
|
970
|
-
signal?: AbortSignal;
|
|
971
|
-
}
|
|
972
|
-
```
|
|
973
|
-
|
|
974
|
-
### Retry Configuration
|
|
975
|
-
|
|
976
|
-
```typescript
|
|
977
|
-
interface RetryConfig {
|
|
978
|
-
attempts?: number; // default: 1
|
|
979
|
-
wait?: number; // default: 1000ms
|
|
980
|
-
maxAllowedWait?: number; // default: 60000ms
|
|
981
|
-
retryStrategy?: 'fixed' | 'linear' | 'exponential'; // default: 'fixed'
|
|
982
|
-
performAllAttempts?: boolean; // default: false
|
|
983
|
-
}
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
### Circuit Breaker Configuration
|
|
987
|
-
|
|
988
|
-
```typescript
|
|
989
|
-
interface CircuitBreakerConfig {
|
|
990
|
-
failureThresholdPercentage: number; // 0-100
|
|
991
|
-
minimumRequests: number;
|
|
992
|
-
recoveryTimeoutMs: number;
|
|
993
|
-
trackIndividualAttempts?: boolean; // default: false
|
|
994
|
-
}
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
### Rate Limit Configuration
|
|
998
|
-
|
|
999
|
-
```typescript
|
|
1000
|
-
interface RateLimitConfig {
|
|
1001
|
-
maxRequests: number;
|
|
1002
|
-
windowMs: number;
|
|
1003
|
-
}
|
|
1004
|
-
```
|
|
1005
|
-
|
|
1006
|
-
### Cache Configuration
|
|
1007
|
-
|
|
1008
|
-
```typescript
|
|
1009
|
-
interface CacheConfig {
|
|
1010
|
-
enabled: boolean;
|
|
1011
|
-
ttl?: number; // milliseconds, default: 300000 (5 minutes)
|
|
1012
|
-
}
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
### Pre-Execution Configuration
|
|
1016
|
-
|
|
1017
|
-
```typescript
|
|
1018
|
-
interface RequestPreExecutionOptions {
|
|
1019
|
-
preExecutionHook: (options: PreExecutionHookOptions) => any | Promise<any>;
|
|
1020
|
-
preExecutionHookParams?: any;
|
|
1021
|
-
applyPreExecutionConfigOverride?: boolean; // default: false
|
|
1022
|
-
continueOnPreExecutionHookFailure?: boolean; // default: false
|
|
1023
|
-
}
|
|
1024
|
-
```
|
|
1025
|
-
|
|
1026
2118
|
## License
|
|
1027
2119
|
|
|
1028
2120
|
MIT © Manish Varma
|