@emmvish/stable-request 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +708 -138
- package/dist/core/stable-workflow.d.ts.map +1 -1
- package/dist/core/stable-workflow.js +53 -3
- package/dist/core/stable-workflow.js.map +1 -1
- package/dist/enums/index.d.ts +7 -0
- package/dist/enums/index.d.ts.map +1 -1
- package/dist/enums/index.js +8 -0
- package/dist/enums/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +55 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utilities/execute-non-linear-workflow.d.ts +11 -0
- package/dist/utilities/execute-non-linear-workflow.d.ts.map +1 -0
- package/dist/utilities/execute-non-linear-workflow.js +399 -0
- package/dist/utilities/execute-non-linear-workflow.js.map +1 -0
- package/dist/utilities/execute-phase.d.ts +2 -12
- package/dist/utilities/execute-phase.d.ts.map +1 -1
- package/dist/utilities/execute-phase.js.map +1 -1
- package/dist/utilities/index.d.ts +1 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +1 -0
- package/dist/utilities/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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,8 @@ 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)
|
|
49
47
|
- [Retry Strategies](#retry-strategies)
|
|
50
48
|
- [Circuit Breaker](#circuit-breaker)
|
|
51
49
|
- [Rate Limiting](#rate-limiting)
|
|
@@ -57,7 +55,6 @@ Start small and scale
|
|
|
57
55
|
- [Response Analysis](#response-analysis)
|
|
58
56
|
- [Error Handling](#error-handling)
|
|
59
57
|
- [Advanced Use Cases](#advanced-use-cases)
|
|
60
|
-
- [Configuration Options](#configuration-options)
|
|
61
58
|
- [License](#license)
|
|
62
59
|
<!-- TOC END -->
|
|
63
60
|
|
|
@@ -76,7 +73,7 @@ npm install @emmvish/stable-request
|
|
|
76
73
|
- ✅ **Rate Limiting**: Control request throughput across single or multiple requests
|
|
77
74
|
- ✅ **Response Caching**: Built-in TTL-based caching with global cache manager
|
|
78
75
|
- ✅ **Batch Processing**: Execute multiple requests concurrently or sequentially via API Gateway
|
|
79
|
-
- ✅ **Multi-Phase Workflows**: Orchestrate complex request workflows with phase dependencies
|
|
76
|
+
- ✅ **Multi-Phase Non-Linear Workflows**: Orchestrate complex request workflows with phase dependencies
|
|
80
77
|
- ✅ **Pre-Execution Hooks**: Transform requests before execution with dynamic configuration
|
|
81
78
|
- ✅ **Shared Buffer**: Share state across requests in workflows and gateways
|
|
82
79
|
- ✅ **Request Grouping**: Apply different configurations to request groups
|
|
@@ -165,84 +162,729 @@ const result = await stableWorkflow(phases, {
|
|
|
165
162
|
console.log(`Workflow completed: ${result.successfulRequests}/${result.totalRequests} successful`);
|
|
166
163
|
```
|
|
167
164
|
|
|
168
|
-
|
|
165
|
+
### Non-Linear Workflow with Dynamic Routing
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
169
|
+
|
|
170
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
171
|
+
{
|
|
172
|
+
id: 'check-status',
|
|
173
|
+
requests: [
|
|
174
|
+
{ id: 'status', requestOptions: { reqData: { path: '/status' }, resReq: true } }
|
|
175
|
+
],
|
|
176
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
177
|
+
const status = phaseResult.responses[0]?.data?.status;
|
|
178
|
+
|
|
179
|
+
if (status === 'completed') {
|
|
180
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' };
|
|
181
|
+
} else if (status === 'processing') {
|
|
182
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
183
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY }; // Replay this phase
|
|
184
|
+
} else {
|
|
185
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-handler' };
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
allowReplay: true,
|
|
189
|
+
maxReplayCount: 10
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: 'process',
|
|
193
|
+
requests: [
|
|
194
|
+
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'error-handler',
|
|
199
|
+
requests: [
|
|
200
|
+
{ id: 'error', requestOptions: { reqData: { path: '/error' }, resReq: true } }
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'finalize',
|
|
205
|
+
requests: [
|
|
206
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const result = await stableWorkflow(phases, {
|
|
212
|
+
workflowId: 'dynamic-workflow',
|
|
213
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
214
|
+
enableNonLinearExecution: true,
|
|
215
|
+
maxWorkflowIterations: 50,
|
|
216
|
+
sharedBuffer: {}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
console.log('Execution history:', result.executionHistory);
|
|
220
|
+
console.log('Terminated early:', result.terminatedEarly);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Advanced Features
|
|
224
|
+
|
|
225
|
+
### Non-Linear Workflows
|
|
226
|
+
|
|
227
|
+
Non-linear workflows enable dynamic phase execution based on runtime decisions, allowing you to build complex orchestrations with conditional branching, polling loops, error recovery, and adaptive routing.
|
|
228
|
+
|
|
229
|
+
#### Phase Decision Actions
|
|
169
230
|
|
|
170
|
-
|
|
231
|
+
Each phase can make decisions about workflow execution:
|
|
171
232
|
|
|
172
|
-
|
|
233
|
+
- **`continue`**: Proceed to the next sequential phase
|
|
234
|
+
- **`jump`**: Jump to a specific phase by ID
|
|
235
|
+
- **`replay`**: Re-execute the current phase
|
|
236
|
+
- **`skip`**: Skip to a target phase or skip the next phase
|
|
237
|
+
- **`terminate`**: Stop the workflow immediately
|
|
238
|
+
|
|
239
|
+
#### Basic Non-Linear Workflow
|
|
173
240
|
|
|
174
|
-
**Signature:**
|
|
175
241
|
```typescript
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
242
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PhaseExecutionDecision, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
243
|
+
|
|
244
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
245
|
+
{
|
|
246
|
+
id: 'validate-input',
|
|
247
|
+
requests: [
|
|
248
|
+
{ id: 'validate', requestOptions: { reqData: { path: '/validate' }, resReq: true } }
|
|
249
|
+
],
|
|
250
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
251
|
+
const isValid = phaseResult.responses[0]?.data?.valid;
|
|
252
|
+
|
|
253
|
+
if (isValid) {
|
|
254
|
+
sharedBuffer.validationPassed = true;
|
|
255
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to next phase
|
|
256
|
+
} else {
|
|
257
|
+
return {
|
|
258
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
259
|
+
metadata: { reason: 'Validation failed' }
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: 'process-data',
|
|
266
|
+
requests: [
|
|
267
|
+
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
const result = await stableWorkflow(phases, {
|
|
273
|
+
workflowId: 'validation-workflow',
|
|
274
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
275
|
+
enableNonLinearExecution: true,
|
|
276
|
+
sharedBuffer: {}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (result.terminatedEarly) {
|
|
280
|
+
console.log('Workflow terminated:', result.terminationReason);
|
|
281
|
+
}
|
|
179
282
|
```
|
|
180
283
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
284
|
+
#### Conditional Branching
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
288
|
+
{
|
|
289
|
+
id: 'check-user-type',
|
|
290
|
+
requests: [
|
|
291
|
+
{ id: 'user', requestOptions: { reqData: { path: '/user/info' }, resReq: true } }
|
|
292
|
+
],
|
|
293
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
294
|
+
const userType = phaseResult.responses[0]?.data?.type;
|
|
295
|
+
sharedBuffer.userType = userType;
|
|
296
|
+
|
|
297
|
+
if (userType === 'premium') {
|
|
298
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'premium-flow' };
|
|
299
|
+
} else if (userType === 'trial') {
|
|
300
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'trial-flow' };
|
|
301
|
+
} else {
|
|
302
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'free-flow' };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: 'premium-flow',
|
|
308
|
+
requests: [
|
|
309
|
+
{ id: 'premium', requestOptions: { reqData: { path: '/premium/data' }, resReq: true } }
|
|
310
|
+
],
|
|
311
|
+
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: 'trial-flow',
|
|
315
|
+
requests: [
|
|
316
|
+
{ id: 'trial', requestOptions: { reqData: { path: '/trial/data' }, resReq: true } }
|
|
317
|
+
],
|
|
318
|
+
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'finalize' })
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'free-flow',
|
|
322
|
+
requests: [
|
|
323
|
+
{ id: 'free', requestOptions: { reqData: { path: '/free/data' }, resReq: true } }
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: 'finalize',
|
|
328
|
+
requests: [
|
|
329
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
];
|
|
193
333
|
|
|
194
|
-
|
|
334
|
+
const result = await stableWorkflow(phases, {
|
|
335
|
+
enableNonLinearExecution: true,
|
|
336
|
+
sharedBuffer: {},
|
|
337
|
+
handlePhaseDecision: (decision, phaseResult) => {
|
|
338
|
+
console.log(`Phase ${phaseResult.phaseId} decided: ${decision.action}`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
```
|
|
195
342
|
|
|
196
|
-
|
|
343
|
+
#### Polling with Replay
|
|
197
344
|
|
|
198
|
-
**Signature:**
|
|
199
345
|
```typescript
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
346
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
347
|
+
{
|
|
348
|
+
id: 'poll-job-status',
|
|
349
|
+
allowReplay: true,
|
|
350
|
+
maxReplayCount: 20,
|
|
351
|
+
requests: [
|
|
352
|
+
{ id: 'check', requestOptions: { reqData: { path: '/job/status' }, resReq: true } }
|
|
353
|
+
],
|
|
354
|
+
phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
|
|
355
|
+
const status = phaseResult.responses[0]?.data?.status;
|
|
356
|
+
const attempts = executionHistory.filter(h => h.phaseId === 'poll-job-status').length;
|
|
357
|
+
|
|
358
|
+
if (status === 'completed') {
|
|
359
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
360
|
+
} else if (status === 'failed') {
|
|
361
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'error-recovery' };
|
|
362
|
+
} else if (attempts < 20) {
|
|
363
|
+
// Still processing, wait and replay
|
|
364
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
365
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
366
|
+
} else {
|
|
367
|
+
return {
|
|
368
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
369
|
+
metadata: { reason: 'Job timeout after 20 attempts' }
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: 'process-results',
|
|
376
|
+
requests: [
|
|
377
|
+
{ id: 'process', requestOptions: { reqData: { path: '/job/results' }, resReq: true } }
|
|
378
|
+
]
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: 'error-recovery',
|
|
382
|
+
requests: [
|
|
383
|
+
{ id: 'recover', requestOptions: { reqData: { path: '/job/retry' }, resReq: true } }
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
const result = await stableWorkflow(phases, {
|
|
389
|
+
workflowId: 'polling-workflow',
|
|
390
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
391
|
+
enableNonLinearExecution: true,
|
|
392
|
+
maxWorkflowIterations: 100
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
console.log('Total iterations:', result.executionHistory.length);
|
|
396
|
+
console.log('Phases executed:', result.completedPhases);
|
|
204
397
|
```
|
|
205
398
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
399
|
+
#### Retry Logic with Replay
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
403
|
+
{
|
|
404
|
+
id: 'attempt-operation',
|
|
405
|
+
allowReplay: true,
|
|
406
|
+
maxReplayCount: 3,
|
|
407
|
+
requests: [
|
|
408
|
+
{
|
|
409
|
+
id: 'operation',
|
|
410
|
+
requestOptions: {
|
|
411
|
+
reqData: { path: '/risky-operation', method: 'POST' },
|
|
412
|
+
resReq: true,
|
|
413
|
+
attempts: 1 // No retries at request level
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
],
|
|
417
|
+
phaseDecisionHook: async ({ phaseResult, executionHistory, sharedBuffer }) => {
|
|
418
|
+
const success = phaseResult.responses[0]?.success;
|
|
419
|
+
const attemptCount = executionHistory.filter(h => h.phaseId === 'attempt-operation').length;
|
|
420
|
+
|
|
421
|
+
if (success) {
|
|
422
|
+
sharedBuffer.operationResult = phaseResult.responses[0]?.data;
|
|
423
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
424
|
+
} else if (attemptCount < 3) {
|
|
425
|
+
// Exponential backoff
|
|
426
|
+
const delay = 1000 * Math.pow(2, attemptCount);
|
|
427
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
428
|
+
|
|
429
|
+
sharedBuffer.retryAttempts = attemptCount;
|
|
430
|
+
return { action: PHASE_DECISION_ACTIONS.REPLAY };
|
|
431
|
+
} else {
|
|
432
|
+
return {
|
|
433
|
+
action: PHASE_DECISION_ACTIONS.JUMP,
|
|
434
|
+
targetPhaseId: 'fallback-operation',
|
|
435
|
+
metadata: { reason: 'Max retries exceeded' }
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: 'primary-flow',
|
|
442
|
+
requests: [
|
|
443
|
+
{ id: 'primary', requestOptions: { reqData: { path: '/primary' }, resReq: true } }
|
|
444
|
+
]
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
id: 'fallback-operation',
|
|
448
|
+
requests: [
|
|
449
|
+
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
450
|
+
]
|
|
451
|
+
}
|
|
452
|
+
];
|
|
215
453
|
|
|
216
|
-
|
|
454
|
+
const result = await stableWorkflow(phases, {
|
|
455
|
+
enableNonLinearExecution: true,
|
|
456
|
+
sharedBuffer: { retryAttempts: 0 },
|
|
457
|
+
logPhaseResults: true
|
|
458
|
+
});
|
|
459
|
+
```
|
|
217
460
|
|
|
218
|
-
|
|
461
|
+
#### Skip Phases
|
|
219
462
|
|
|
220
|
-
**Signature:**
|
|
221
463
|
```typescript
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
464
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
465
|
+
{
|
|
466
|
+
id: 'check-cache',
|
|
467
|
+
allowSkip: true,
|
|
468
|
+
requests: [
|
|
469
|
+
{ id: 'cache', requestOptions: { reqData: { path: '/cache/check' }, resReq: true } }
|
|
470
|
+
],
|
|
471
|
+
phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
|
|
472
|
+
const cached = phaseResult.responses[0]?.data?.cached;
|
|
473
|
+
|
|
474
|
+
if (cached) {
|
|
475
|
+
sharedBuffer.cachedData = phaseResult.responses[0]?.data;
|
|
476
|
+
// Skip expensive-computation and go directly to finalize
|
|
477
|
+
return { action: PHASE_DECISION_ACTIONS.SKIP, targetPhaseId: 'finalize' };
|
|
478
|
+
}
|
|
479
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: 'expensive-computation',
|
|
484
|
+
requests: [
|
|
485
|
+
{ id: 'compute', requestOptions: { reqData: { path: '/compute' }, resReq: true } }
|
|
486
|
+
]
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
id: 'save-to-cache',
|
|
490
|
+
requests: [
|
|
491
|
+
{ id: 'save', requestOptions: { reqData: { path: '/cache/save' }, resReq: true } }
|
|
492
|
+
]
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
id: 'finalize',
|
|
496
|
+
requests: [
|
|
497
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
498
|
+
]
|
|
499
|
+
}
|
|
500
|
+
];
|
|
501
|
+
|
|
502
|
+
const result = await stableWorkflow(phases, {
|
|
503
|
+
enableNonLinearExecution: true,
|
|
504
|
+
sharedBuffer: {}
|
|
505
|
+
});
|
|
226
506
|
```
|
|
227
507
|
|
|
228
|
-
|
|
229
|
-
- `workflowId`: Unique workflow identifier
|
|
230
|
-
- `concurrentPhaseExecution`: Execute phases concurrently (default: false)
|
|
231
|
-
- `stopOnFirstPhaseError`: Stop workflow on first phase failure
|
|
232
|
-
- `enableMixedExecution`: Allow mixed concurrent/sequential phase execution
|
|
233
|
-
- `handlePhaseCompletion`: Hook called after each phase completes
|
|
234
|
-
- `handlePhaseError`: Hook called when phase fails
|
|
235
|
-
- `sharedBuffer`: Shared state across all phases and requests
|
|
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
|
|
508
|
+
#### Execution History and Tracking
|
|
244
509
|
|
|
245
|
-
|
|
510
|
+
```typescript
|
|
511
|
+
const result = await stableWorkflow(phases, {
|
|
512
|
+
workflowId: 'tracked-workflow',
|
|
513
|
+
enableNonLinearExecution: true,
|
|
514
|
+
handlePhaseCompletion: ({ phaseResult, workflowId }) => {
|
|
515
|
+
console.log(`[${workflowId}] Phase ${phaseResult.phaseId} completed:`, {
|
|
516
|
+
executionNumber: phaseResult.executionNumber,
|
|
517
|
+
success: phaseResult.success,
|
|
518
|
+
decision: phaseResult.decision
|
|
519
|
+
});
|
|
520
|
+
},
|
|
521
|
+
handlePhaseDecision: (decision, phaseResult) => {
|
|
522
|
+
console.log(`Decision made:`, {
|
|
523
|
+
phase: phaseResult.phaseId,
|
|
524
|
+
action: decision.action,
|
|
525
|
+
target: decision.targetPhaseId,
|
|
526
|
+
metadata: decision.metadata
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Analyze execution history
|
|
532
|
+
console.log('Total phase executions:', result.executionHistory.length);
|
|
533
|
+
console.log('Unique phases executed:', new Set(result.executionHistory.map(h => h.phaseId)).size);
|
|
534
|
+
console.log('Replay count:', result.executionHistory.filter(h => h.decision?.action === 'replay').length);
|
|
535
|
+
|
|
536
|
+
result.executionHistory.forEach(record => {
|
|
537
|
+
console.log(`- ${record.phaseId} (attempt ${record.executionNumber}): ${record.success ? '✓' : '✗'}`);
|
|
538
|
+
});
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
#### Loop Protection
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
const result = await stableWorkflow(phases, {
|
|
545
|
+
enableNonLinearExecution: true,
|
|
546
|
+
maxWorkflowIterations: 50, // Prevent infinite loops
|
|
547
|
+
handlePhaseCompletion: ({ phaseResult }) => {
|
|
548
|
+
if (phaseResult.executionNumber && phaseResult.executionNumber > 10) {
|
|
549
|
+
console.warn(`Phase ${phaseResult.phaseId} executed ${phaseResult.executionNumber} times`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
if (result.terminatedEarly && result.terminationReason?.includes('iterations')) {
|
|
555
|
+
console.error('Workflow hit iteration limit - possible infinite loop');
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
#### Mixed Serial and Parallel Execution
|
|
560
|
+
|
|
561
|
+
Non-linear workflows support mixing serial and parallel phase execution. Mark consecutive phases with `markConcurrentPhase: true` to execute them in parallel, while other phases execute serially.
|
|
562
|
+
|
|
563
|
+
**Basic Mixed Execution:**
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
import { stableWorkflow, STABLE_WORKFLOW_PHASE, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
|
|
567
|
+
|
|
568
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
569
|
+
{
|
|
570
|
+
id: 'init',
|
|
571
|
+
requests: [
|
|
572
|
+
{ id: 'init', requestOptions: { reqData: { path: '/init' }, resReq: true } }
|
|
573
|
+
],
|
|
574
|
+
phaseDecisionHook: async () => ({ action: PHASE_DECISION_ACTIONS.CONTINUE })
|
|
575
|
+
},
|
|
576
|
+
// These two phases execute in parallel
|
|
577
|
+
{
|
|
578
|
+
id: 'check-inventory',
|
|
579
|
+
markConcurrentPhase: true,
|
|
580
|
+
requests: [
|
|
581
|
+
{ id: 'inv', requestOptions: { reqData: { path: '/inventory' }, resReq: true } }
|
|
582
|
+
]
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
id: 'check-pricing',
|
|
586
|
+
markConcurrentPhase: true,
|
|
587
|
+
requests: [
|
|
588
|
+
{ id: 'price', requestOptions: { reqData: { path: '/pricing' }, resReq: true } }
|
|
589
|
+
],
|
|
590
|
+
// Decision hook receives results from all concurrent phases
|
|
591
|
+
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
592
|
+
const inventory = concurrentPhaseResults![0].responses[0]?.data;
|
|
593
|
+
const pricing = concurrentPhaseResults![1].responses[0]?.data;
|
|
594
|
+
|
|
595
|
+
if (inventory.available && pricing.inBudget) {
|
|
596
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
597
|
+
}
|
|
598
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'out-of-stock' };
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
// This phase executes serially after the parallel group
|
|
602
|
+
{
|
|
603
|
+
id: 'process-order',
|
|
604
|
+
requests: [
|
|
605
|
+
{ id: 'order', requestOptions: { reqData: { path: '/order' }, resReq: true } }
|
|
606
|
+
]
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
id: 'out-of-stock',
|
|
610
|
+
requests: [
|
|
611
|
+
{ id: 'notify', requestOptions: { reqData: { path: '/notify' }, resReq: true } }
|
|
612
|
+
]
|
|
613
|
+
}
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
const result = await stableWorkflow(phases, {
|
|
617
|
+
workflowId: 'mixed-execution',
|
|
618
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
619
|
+
enableNonLinearExecution: true
|
|
620
|
+
});
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Multiple Parallel Groups:**
|
|
624
|
+
|
|
625
|
+
```typescript
|
|
626
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
627
|
+
{
|
|
628
|
+
id: 'authenticate',
|
|
629
|
+
requests: [
|
|
630
|
+
{ id: 'auth', requestOptions: { reqData: { path: '/auth' }, resReq: true } }
|
|
631
|
+
]
|
|
632
|
+
},
|
|
633
|
+
// First parallel group: Data validation
|
|
634
|
+
{
|
|
635
|
+
id: 'validate-user',
|
|
636
|
+
markConcurrentPhase: true,
|
|
637
|
+
requests: [
|
|
638
|
+
{ id: 'val-user', requestOptions: { reqData: { path: '/validate/user' }, resReq: true } }
|
|
639
|
+
]
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
id: 'validate-payment',
|
|
643
|
+
markConcurrentPhase: true,
|
|
644
|
+
requests: [
|
|
645
|
+
{ id: 'val-pay', requestOptions: { reqData: { path: '/validate/payment' }, resReq: true } }
|
|
646
|
+
]
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
id: 'validate-shipping',
|
|
650
|
+
markConcurrentPhase: true,
|
|
651
|
+
requests: [
|
|
652
|
+
{ id: 'val-ship', requestOptions: { reqData: { path: '/validate/shipping' }, resReq: true } }
|
|
653
|
+
],
|
|
654
|
+
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
655
|
+
const allValid = concurrentPhaseResults!.every(r => r.success && r.responses[0]?.data?.valid);
|
|
656
|
+
if (!allValid) {
|
|
657
|
+
return { action: PHASE_DECISION_ACTIONS.TERMINATE, metadata: { reason: 'Validation failed' } };
|
|
658
|
+
}
|
|
659
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
// Serial processing phase
|
|
663
|
+
{
|
|
664
|
+
id: 'calculate-total',
|
|
665
|
+
requests: [
|
|
666
|
+
{ id: 'calc', requestOptions: { reqData: { path: '/calculate' }, resReq: true } }
|
|
667
|
+
]
|
|
668
|
+
},
|
|
669
|
+
// Second parallel group: External integrations
|
|
670
|
+
{
|
|
671
|
+
id: 'notify-warehouse',
|
|
672
|
+
markConcurrentPhase: true,
|
|
673
|
+
requests: [
|
|
674
|
+
{ id: 'warehouse', requestOptions: { reqData: { path: '/notify/warehouse' }, resReq: true } }
|
|
675
|
+
]
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
id: 'notify-shipping',
|
|
679
|
+
markConcurrentPhase: true,
|
|
680
|
+
requests: [
|
|
681
|
+
{ id: 'shipping', requestOptions: { reqData: { path: '/notify/shipping' }, resReq: true } }
|
|
682
|
+
]
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
id: 'update-inventory',
|
|
686
|
+
markConcurrentPhase: true,
|
|
687
|
+
requests: [
|
|
688
|
+
{ id: 'inventory', requestOptions: { reqData: { path: '/update/inventory' }, resReq: true } }
|
|
689
|
+
]
|
|
690
|
+
},
|
|
691
|
+
// Final serial phase
|
|
692
|
+
{
|
|
693
|
+
id: 'finalize',
|
|
694
|
+
requests: [
|
|
695
|
+
{ id: 'final', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }
|
|
696
|
+
]
|
|
697
|
+
}
|
|
698
|
+
];
|
|
699
|
+
|
|
700
|
+
const result = await stableWorkflow(phases, {
|
|
701
|
+
workflowId: 'multi-parallel-workflow',
|
|
702
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
703
|
+
enableNonLinearExecution: true
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
console.log('Execution order demonstrates mixed serial/parallel execution');
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**Decision Making with Concurrent Results:**
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
713
|
+
{
|
|
714
|
+
id: 'api-check-1',
|
|
715
|
+
markConcurrentPhase: true,
|
|
716
|
+
requests: [
|
|
717
|
+
{ id: 'api1', requestOptions: { reqData: { path: '/health/api1' }, resReq: true } }
|
|
718
|
+
]
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: 'api-check-2',
|
|
722
|
+
markConcurrentPhase: true,
|
|
723
|
+
requests: [
|
|
724
|
+
{ id: 'api2', requestOptions: { reqData: { path: '/health/api2' }, resReq: true } }
|
|
725
|
+
]
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
id: 'api-check-3',
|
|
729
|
+
markConcurrentPhase: true,
|
|
730
|
+
requests: [
|
|
731
|
+
{ id: 'api3', requestOptions: { reqData: { path: '/health/api3' }, resReq: true } }
|
|
732
|
+
],
|
|
733
|
+
phaseDecisionHook: async ({ concurrentPhaseResults, sharedBuffer }) => {
|
|
734
|
+
// Aggregate results from all parallel phases
|
|
735
|
+
const healthScores = concurrentPhaseResults!.map(result =>
|
|
736
|
+
result.responses[0]?.data?.score || 0
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
const averageScore = healthScores.reduce((a, b) => a + b, 0) / healthScores.length;
|
|
740
|
+
sharedBuffer!.healthScore = averageScore;
|
|
741
|
+
|
|
742
|
+
if (averageScore > 0.8) {
|
|
743
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'optimal-path' };
|
|
744
|
+
} else if (averageScore > 0.5) {
|
|
745
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE }; // Go to degraded-path
|
|
746
|
+
} else {
|
|
747
|
+
return { action: PHASE_DECISION_ACTIONS.JUMP, targetPhaseId: 'fallback-path' };
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
id: 'degraded-path',
|
|
753
|
+
requests: [
|
|
754
|
+
{ id: 'degraded', requestOptions: { reqData: { path: '/degraded' }, resReq: true } }
|
|
755
|
+
]
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
id: 'optimal-path',
|
|
759
|
+
requests: [
|
|
760
|
+
{ id: 'optimal', requestOptions: { reqData: { path: '/optimal' }, resReq: true } }
|
|
761
|
+
]
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
id: 'fallback-path',
|
|
765
|
+
requests: [
|
|
766
|
+
{ id: 'fallback', requestOptions: { reqData: { path: '/fallback' }, resReq: true } }
|
|
767
|
+
]
|
|
768
|
+
}
|
|
769
|
+
];
|
|
770
|
+
|
|
771
|
+
const sharedBuffer = {};
|
|
772
|
+
const result = await stableWorkflow(phases, {
|
|
773
|
+
workflowId: 'adaptive-routing',
|
|
774
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
775
|
+
enableNonLinearExecution: true,
|
|
776
|
+
sharedBuffer
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
console.log('Average health score:', sharedBuffer.healthScore);
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
**Error Handling in Parallel Groups:**
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
const phases: STABLE_WORKFLOW_PHASE[] = [
|
|
786
|
+
{
|
|
787
|
+
id: 'critical-check',
|
|
788
|
+
markConcurrentPhase: true,
|
|
789
|
+
requests: [
|
|
790
|
+
{
|
|
791
|
+
id: 'check1',
|
|
792
|
+
requestOptions: {
|
|
793
|
+
reqData: { path: '/critical/check1' },
|
|
794
|
+
resReq: true,
|
|
795
|
+
attempts: 3
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
]
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
id: 'optional-check',
|
|
802
|
+
markConcurrentPhase: true,
|
|
803
|
+
requests: [
|
|
804
|
+
{
|
|
805
|
+
id: 'check2',
|
|
806
|
+
requestOptions: {
|
|
807
|
+
reqData: { path: '/optional/check2' },
|
|
808
|
+
resReq: true,
|
|
809
|
+
attempts: 1,
|
|
810
|
+
finalErrorAnalyzer: async () => true // Suppress errors
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
],
|
|
814
|
+
phaseDecisionHook: async ({ concurrentPhaseResults }) => {
|
|
815
|
+
// Check if critical phase succeeded
|
|
816
|
+
const criticalSuccess = concurrentPhaseResults![0].success;
|
|
817
|
+
|
|
818
|
+
if (!criticalSuccess) {
|
|
819
|
+
return {
|
|
820
|
+
action: PHASE_DECISION_ACTIONS.TERMINATE,
|
|
821
|
+
metadata: { reason: 'Critical check failed' }
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Continue even if optional check failed
|
|
826
|
+
return { action: PHASE_DECISION_ACTIONS.CONTINUE };
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
id: 'process',
|
|
831
|
+
requests: [
|
|
832
|
+
{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }
|
|
833
|
+
]
|
|
834
|
+
}
|
|
835
|
+
];
|
|
836
|
+
|
|
837
|
+
const result = await stableWorkflow(phases, {
|
|
838
|
+
workflowId: 'resilient-parallel',
|
|
839
|
+
commonRequestData: { hostname: 'api.example.com' },
|
|
840
|
+
enableNonLinearExecution: true,
|
|
841
|
+
stopOnFirstPhaseError: false // Continue even with phase errors
|
|
842
|
+
});
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**Key Points:**
|
|
846
|
+
- Only **consecutive phases** with `markConcurrentPhase: true` execute in parallel
|
|
847
|
+
- The **last phase** in a concurrent group can have a `phaseDecisionHook` that receives `concurrentPhaseResults`
|
|
848
|
+
- Parallel groups are separated by phases **without** `markConcurrentPhase` (or phases with it set to false)
|
|
849
|
+
- All decision actions work with parallel groups except `REPLAY` (not supported for concurrent groups)
|
|
850
|
+
- Error handling follows normal workflow rules - use `stopOnFirstPhaseError` to control behavior
|
|
851
|
+
|
|
852
|
+
#### Configuration Options
|
|
853
|
+
|
|
854
|
+
**Workflow Options:**
|
|
855
|
+
- `enableNonLinearExecution`: Enable non-linear workflow (required)
|
|
856
|
+
- `maxWorkflowIterations`: Maximum total iterations (default: 1000)
|
|
857
|
+
- `handlePhaseDecision`: Called when phase makes a decision
|
|
858
|
+
- `stopOnFirstPhaseError`: Stop on phase failure (default: false)
|
|
859
|
+
|
|
860
|
+
**Phase Options:**
|
|
861
|
+
- `phaseDecisionHook`: Function returning `PhaseExecutionDecision`
|
|
862
|
+
- `allowReplay`: Allow phase replay (default: false)
|
|
863
|
+
- `allowSkip`: Allow phase skip (default: false)
|
|
864
|
+
- `maxReplayCount`: Maximum replays (default: Infinity)
|
|
865
|
+
|
|
866
|
+
**Decision Hook Parameters:**
|
|
867
|
+
```typescript
|
|
868
|
+
interface PhaseDecisionHookOptions {
|
|
869
|
+
workflowId: string;
|
|
870
|
+
phaseResult: STABLE_WORKFLOW_PHASE_RESULT;
|
|
871
|
+
phaseId: string;
|
|
872
|
+
phaseIndex: number;
|
|
873
|
+
executionHistory: PhaseExecutionRecord[];
|
|
874
|
+
sharedBuffer?: Record<string, any>;
|
|
875
|
+
params?: any;
|
|
876
|
+
}
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
**Decision Object:**
|
|
880
|
+
```typescript
|
|
881
|
+
interface PhaseExecutionDecision {
|
|
882
|
+
action: PHASE_DECISION_ACTIONS;
|
|
883
|
+
targetPhaseId?: string;
|
|
884
|
+
replayCount?: number;
|
|
885
|
+
metadata?: Record<string, any>;
|
|
886
|
+
}
|
|
887
|
+
```
|
|
246
888
|
|
|
247
889
|
### Retry Strategies
|
|
248
890
|
|
|
@@ -952,83 +1594,11 @@ console.log('Products:', result.data.products?.length);
|
|
|
952
1594
|
console.log('Orders:', result.data.orders?.length);
|
|
953
1595
|
```
|
|
954
1596
|
|
|
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
1597
|
## License
|
|
1027
1598
|
|
|
1028
1599
|
MIT © Manish Varma
|
|
1029
1600
|
|
|
1030
1601
|
[](https://opensource.org/licenses/MIT)
|
|
1031
|
-
|
|
1032
1602
|
---
|
|
1033
1603
|
|
|
1034
1604
|
**Made with ❤️ for developers integrating with unreliable APIs**
|