@emmvish/stable-request 2.8.4 → 3.0.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.
Files changed (138) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1153 -2319
  3. package/dist/constants/index.d.ts +0 -10
  4. package/dist/constants/index.d.ts.map +1 -1
  5. package/dist/constants/index.js +0 -113
  6. package/dist/constants/index.js.map +1 -1
  7. package/dist/core/index.d.ts +0 -5
  8. package/dist/core/index.d.ts.map +1 -1
  9. package/dist/core/index.js +0 -5
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/stable-request.d.ts.map +1 -1
  12. package/dist/core/stable-request.js +22 -7
  13. package/dist/core/stable-request.js.map +1 -1
  14. package/dist/enums/index.d.ts +0 -37
  15. package/dist/enums/index.d.ts.map +1 -1
  16. package/dist/enums/index.js +0 -43
  17. package/dist/enums/index.js.map +1 -1
  18. package/dist/index.d.ts +4 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +18 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/types/index.d.ts +100 -1135
  23. package/dist/types/index.d.ts.map +1 -1
  24. package/dist/utilities/index.d.ts +0 -18
  25. package/dist/utilities/index.d.ts.map +1 -1
  26. package/dist/utilities/index.js +0 -18
  27. package/dist/utilities/index.js.map +1 -1
  28. package/dist/utilities/infrastructure-persistence.d.ts +0 -1
  29. package/dist/utilities/infrastructure-persistence.d.ts.map +1 -1
  30. package/dist/utilities/infrastructure-persistence.js +12 -15
  31. package/dist/utilities/infrastructure-persistence.js.map +1 -1
  32. package/dist/utilities/metrics-aggregator.d.ts +2 -13
  33. package/dist/utilities/metrics-aggregator.d.ts.map +1 -1
  34. package/dist/utilities/metrics-aggregator.js +9 -251
  35. package/dist/utilities/metrics-aggregator.js.map +1 -1
  36. package/dist/utilities/metrics-validator.d.ts +6 -76
  37. package/dist/utilities/metrics-validator.d.ts.map +1 -1
  38. package/dist/utilities/metrics-validator.js +12 -181
  39. package/dist/utilities/metrics-validator.js.map +1 -1
  40. package/dist/utilities/validate-trial-mode-probabilities.js +2 -2
  41. package/dist/utilities/validate-trial-mode-probabilities.js.map +1 -1
  42. package/package.json +20 -24
  43. package/dist/core/stable-api-gateway.d.ts +0 -4
  44. package/dist/core/stable-api-gateway.d.ts.map +0 -1
  45. package/dist/core/stable-api-gateway.js +0 -136
  46. package/dist/core/stable-api-gateway.js.map +0 -1
  47. package/dist/core/stable-function.d.ts +0 -11
  48. package/dist/core/stable-function.d.ts.map +0 -1
  49. package/dist/core/stable-function.js +0 -340
  50. package/dist/core/stable-function.js.map +0 -1
  51. package/dist/core/stable-scheduler.d.ts +0 -71
  52. package/dist/core/stable-scheduler.d.ts.map +0 -1
  53. package/dist/core/stable-scheduler.js +0 -768
  54. package/dist/core/stable-scheduler.js.map +0 -1
  55. package/dist/core/stable-workflow-graph.d.ts +0 -3
  56. package/dist/core/stable-workflow-graph.d.ts.map +0 -1
  57. package/dist/core/stable-workflow-graph.js +0 -5
  58. package/dist/core/stable-workflow-graph.js.map +0 -1
  59. package/dist/core/stable-workflow.d.ts +0 -3
  60. package/dist/core/stable-workflow.d.ts.map +0 -1
  61. package/dist/core/stable-workflow.js +0 -362
  62. package/dist/core/stable-workflow.js.map +0 -1
  63. package/dist/stable-runner/index.d.ts +0 -2
  64. package/dist/stable-runner/index.d.ts.map +0 -1
  65. package/dist/stable-runner/index.js +0 -324
  66. package/dist/stable-runner/index.js.map +0 -1
  67. package/dist/utilities/concurrency-limiter.d.ts +0 -46
  68. package/dist/utilities/concurrency-limiter.d.ts.map +0 -1
  69. package/dist/utilities/concurrency-limiter.js +0 -172
  70. package/dist/utilities/concurrency-limiter.js.map +0 -1
  71. package/dist/utilities/execute-branch-workflow.d.ts +0 -3
  72. package/dist/utilities/execute-branch-workflow.d.ts.map +0 -1
  73. package/dist/utilities/execute-branch-workflow.js +0 -730
  74. package/dist/utilities/execute-branch-workflow.js.map +0 -1
  75. package/dist/utilities/execute-concurrently.d.ts +0 -3
  76. package/dist/utilities/execute-concurrently.d.ts.map +0 -1
  77. package/dist/utilities/execute-concurrently.js +0 -258
  78. package/dist/utilities/execute-concurrently.js.map +0 -1
  79. package/dist/utilities/execute-gateway-item.d.ts +0 -6
  80. package/dist/utilities/execute-gateway-item.d.ts.map +0 -1
  81. package/dist/utilities/execute-gateway-item.js +0 -127
  82. package/dist/utilities/execute-gateway-item.js.map +0 -1
  83. package/dist/utilities/execute-non-linear-workflow.d.ts +0 -3
  84. package/dist/utilities/execute-non-linear-workflow.d.ts.map +0 -1
  85. package/dist/utilities/execute-non-linear-workflow.js +0 -483
  86. package/dist/utilities/execute-non-linear-workflow.js.map +0 -1
  87. package/dist/utilities/execute-phase.d.ts +0 -3
  88. package/dist/utilities/execute-phase.d.ts.map +0 -1
  89. package/dist/utilities/execute-phase.js +0 -129
  90. package/dist/utilities/execute-phase.js.map +0 -1
  91. package/dist/utilities/execute-sequentially.d.ts +0 -3
  92. package/dist/utilities/execute-sequentially.d.ts.map +0 -1
  93. package/dist/utilities/execute-sequentially.js +0 -49
  94. package/dist/utilities/execute-sequentially.js.map +0 -1
  95. package/dist/utilities/execute-with-timeout.d.ts +0 -6
  96. package/dist/utilities/execute-with-timeout.d.ts.map +0 -1
  97. package/dist/utilities/execute-with-timeout.js +0 -28
  98. package/dist/utilities/execute-with-timeout.js.map +0 -1
  99. package/dist/utilities/execute-workflow-graph.d.ts +0 -3
  100. package/dist/utilities/execute-workflow-graph.d.ts.map +0 -1
  101. package/dist/utilities/execute-workflow-graph.js +0 -429
  102. package/dist/utilities/execute-workflow-graph.js.map +0 -1
  103. package/dist/utilities/extract-common-request-config-options.d.ts +0 -3
  104. package/dist/utilities/extract-common-request-config-options.d.ts.map +0 -1
  105. package/dist/utilities/extract-common-request-config-options.js +0 -12
  106. package/dist/utilities/extract-common-request-config-options.js.map +0 -1
  107. package/dist/utilities/fn-exec.d.ts +0 -3
  108. package/dist/utilities/fn-exec.d.ts.map +0 -1
  109. package/dist/utilities/fn-exec.js +0 -66
  110. package/dist/utilities/fn-exec.js.map +0 -1
  111. package/dist/utilities/function-cache-manager.d.ts +0 -32
  112. package/dist/utilities/function-cache-manager.d.ts.map +0 -1
  113. package/dist/utilities/function-cache-manager.js +0 -172
  114. package/dist/utilities/function-cache-manager.js.map +0 -1
  115. package/dist/utilities/prepare-api-function-options.d.ts +0 -3
  116. package/dist/utilities/prepare-api-function-options.d.ts.map +0 -1
  117. package/dist/utilities/prepare-api-function-options.js +0 -51
  118. package/dist/utilities/prepare-api-function-options.js.map +0 -1
  119. package/dist/utilities/prepare-api-request-data.d.ts +0 -3
  120. package/dist/utilities/prepare-api-request-data.d.ts.map +0 -1
  121. package/dist/utilities/prepare-api-request-data.js +0 -15
  122. package/dist/utilities/prepare-api-request-data.js.map +0 -1
  123. package/dist/utilities/prepare-api-request-options.d.ts +0 -3
  124. package/dist/utilities/prepare-api-request-options.d.ts.map +0 -1
  125. package/dist/utilities/prepare-api-request-options.js +0 -22
  126. package/dist/utilities/prepare-api-request-options.js.map +0 -1
  127. package/dist/utilities/rate-limiter.d.ts +0 -49
  128. package/dist/utilities/rate-limiter.d.ts.map +0 -1
  129. package/dist/utilities/rate-limiter.js +0 -197
  130. package/dist/utilities/rate-limiter.js.map +0 -1
  131. package/dist/utilities/validate-workflow-graph.d.ts +0 -7
  132. package/dist/utilities/validate-workflow-graph.d.ts.map +0 -1
  133. package/dist/utilities/validate-workflow-graph.js +0 -235
  134. package/dist/utilities/validate-workflow-graph.js.map +0 -1
  135. package/dist/utilities/workflow-graph-builder.d.ts +0 -37
  136. package/dist/utilities/workflow-graph-builder.d.ts.map +0 -1
  137. package/dist/utilities/workflow-graph-builder.js +0 -225
  138. package/dist/utilities/workflow-graph-builder.js.map +0 -1
package/README.md CHANGED
@@ -1,2520 +1,1354 @@
1
1
  # @emmvish/stable-request
2
2
 
3
- A stability-first production-grade TypeScript framework for resilient API integrations, batch processing, and orchestrating complex workflows with deterministic error handling, type safety, and comprehensive observability.
4
-
5
- ## Table of Contents
6
-
7
- - [Overview](#overview)
8
- - [Core Concepts](#core-concepts)
9
- - [Core Modules](#core-modules)
10
- - [stableRequest](#stablerequest)
11
- - [stableFunction](#stablefunction)
12
- - [stableApiGateway](#stableapigateway)
13
- - [stableWorkflow](#stableworkflow)
14
- - [stableWorkflowGraph](#stableworkflowgraph)
15
- - [StableScheduler](#stablescheduler)
16
- - [StableBuffer](#stablebuffer)
17
- - [Stable Runner](#stable-runner)
18
- - [Resilience Mechanisms](#resilience-mechanisms)
19
- - [Retry Strategies](#retry-strategies)
20
- - [Circuit Breaker](#circuit-breaker)
21
- - [Caching](#caching)
22
- - [Rate Limiting](#rate-limiting)
23
- - [Concurrency Limiting](#concurrency-limiting)
24
- - [Workflow Patterns](#workflow-patterns)
25
- - [Sequential & Concurrent Phases](#sequential--concurrent-phases)
26
- - [Non-Linear Workflows](#non-linear-workflows)
27
- - [Branched Workflows](#branched-workflows)
28
- - [Graph-based Workflow Patterns](#graph-based-workflow-patterns)
29
- - [Graph-Based Workflows with Mixed Items](#graph-based-workflows-with-mixed-items)
30
- - [Parallel Phase Execution](#parallel-phase-execution)
31
- - [Merge Points](#merge-points)
32
- - [Linear Helper](#linear-helper)
33
- - [Branch Racing in Graphs](#branch-racing-in-graphs)
34
- - [Configuration & State](#configuration--state)
35
- - [Config Cascading](#config-cascading)
36
- - [Shared & State Buffers](#shared--state-buffers)
37
- - [Hooks & Observability](#hooks--observability)
38
- - [Pre-Execution Hooks](#pre-execution-hooks)
39
- - [Analysis Hooks](#analysis-hooks)
40
- - [Handler Hooks](#handler-hooks)
41
- - [Decision Hooks](#decision-hooks)
42
- - [Metrics & Logging](#metrics--logging)
43
- - [Advanced Features](#advanced-features)
44
- - [Trial Mode](#trial-mode)
45
- - [State Persistence](#state-persistence)
46
- - [Mixed Request & Function Phases](#mixed-request--function-phases)
47
- - [Best Practices](#best-practices)
3
+ > ⚠️ **Maintenance Mode Notice**: This library is now in maintenance mode. For the full-featured execution engine with workflows, schedulers, API gateways, and more, please use [**stable-infra**](https://npmjs.com/package/@emmvish/stable-infra) - the natural evolution of stable-request. If you wish to continue using stable-request for workflows / gateway / scheduling, then, refer to its docs in version 2.8.5.
48
4
 
49
- ---
5
+ A resilient HTTP request framework for Node.js with built-in intelligent retry strategies, circuit breakers, caching, state persistence, and comprehensive observability.
6
+ I created this framework when I was integrating with **unreliable, flaky APIs** and needed a simple solution for retrying the requests. While such libraries do exist already, I needed something more... an intelligent, fully customizable and stable framework that would not throw errors randomly, but rather, give me only the most important information on why my requests were failing or succeeding, with metrics and type-safety.
50
7
 
51
- ## Overview
8
+ ## 🚀 Try stable-infra Instead
52
9
 
53
- **@emmvish/stable-request** evolved from a focused library for resilient API calls to a comprehensive execution framework. Originally addressing **API integration** challenges via `stableRequest`, it expanded to include:
10
+ **stable-request** has evolved into **stable-infra** - a complete execution infrastructure that includes:
54
11
 
55
- 1. **Batch orchestration** via `stableApiGateway` for processing groups of mixed requests/functions
56
- 2. **Phased workflows** via `stableWorkflow` for array-based multi-phase execution with dynamic control flow
57
- 3. **Graph-based workflows** via `stableWorkflowGraph` for DAG execution with higher parallelism
58
- 4. **Generic function execution** via `stableFunction`, inheriting all resilience guards
59
- 5. **Queue based scheduling** via `StableScheduler`, with option to preserve scheduler state and recover from saved state
60
- 6. **Transactional shared state** via `StableBuffer`, a concurrency-safe buffer you can pass as `commonBuffer` or `sharedBuffer`
12
+ - Everything in `stable-request` and more updates
13
+ - `stableWorkflow` - Multi-phase workflow orchestration with branching
14
+ - `stableApiGateway` - Batch request execution with grouping
15
+ - `stableFunction` - Resilient function execution with retries
16
+ - `stableWorkflowGraph` - DAG-based workflow execution
17
+ - `StableScheduler` - Job scheduling with cron, intervals, and timestamps
18
+ - `StableBuffer` - A safe shared-state buffer for all the stable modules
19
+ - `stableRunner` - CLI runner for all execution types
61
20
 
62
- All seven core modules support the same resilience stack: retries, jitter, circuit breaking, caching, rate/concurrency limits, config cascading, shared buffers, trial mode, comprehensive hooks, and metrics. This uniformity makes it trivial to compose requests and functions in any topology. Finally, `Stable Runner` executes jobs from config.
21
+ **[Get started with stable-infra →](https://npmjs.com/package/@emmvish/stable-infra)**
63
22
 
64
23
  ---
65
24
 
66
- ## Core Concepts
25
+ ## Installation
67
26
 
68
- ### Resilience as Default
27
+ ```bash
28
+ npm install stable-request
29
+ ```
69
30
 
70
- Every execution—whether a single request, a pure function, or an entire workflow—inherits built-in resilience:
31
+ ## Why stable-request?
71
32
 
72
- - **Retries** with configurable backoff strategies (FIXED, LINEAR, EXPONENTIAL)
73
- - **Jitter** to prevent thundering herd
74
- - **Circuit breaker** to fail fast and protect downstream systems
75
- - **Caching** for idempotent read operations
76
- - **Rate & concurrency limits** to respect external constraints
77
- - **Metrics guardrails** to validate execution against thresholds with automatic anomaly detection
33
+ Traditional retry libraries blindly retry on network failures or non-2xx HTTP status codes. But in the real world, **HTTP 200 doesn't always mean success**:
78
34
 
79
- ### Type Safety
35
+ - An API returns `200 OK` with `{ "status": "pending" }` - you need to retry until it's `"completed"`
36
+ - ✅ A payment gateway returns `200 OK` but the transaction is still processing
37
+ - ✅ A search API returns `200 OK` with empty results due to eventual consistency
38
+ - ✅ An external API returns `200 OK` with `{ "error": "rate_limited" }` in the body
39
+ - ✅ You need to validate response data against business rules before accepting it
80
40
 
81
- All examples in this guide use TypeScript generics for type-safe request/response data and function arguments/returns. Analyzers validate shapes at runtime; TypeScript ensures compile-time safety.
41
+ **stable-request** lets you inject business intelligence into every stage of the request lifecycle through **hooks** - making your HTTP requests truly resilient to both infrastructure and business-level failures.
82
42
 
83
- ### Config Cascading
43
+ ## Features
84
44
 
85
- Global defaults group overrides → phase overrides → branch overrides → item overrides. Lower levels always win, preventing repetition while maintaining expressiveness.
45
+ ### 🔄 Configurable Retry Strategies
86
46
 
87
- ### Shared State
47
+ Automatically retry failed requests with customizable backoff strategies:
88
48
 
89
- Workflows and gateways support `sharedBuffer` for passing computed state across phases/branches/items without global state.
49
+ ```typescript
50
+ import { stableRequest, RETRY_STRATEGIES, REQUEST_METHODS } from 'stable-request';
51
+ import type { STABLE_REQUEST_RESULT } from 'stable-request';
90
52
 
91
- ---
53
+ interface ApiResponse {
54
+ data: string[];
55
+ total: number;
56
+ }
57
+
58
+ (async () => {
59
+ const result: STABLE_REQUEST_RESULT<ApiResponse> = await stableRequest<void, ApiResponse>({
60
+ reqData: {
61
+ hostname: 'api.example.com',
62
+ path: '/data',
63
+ method: REQUEST_METHODS.GET
64
+ },
65
+ resReq: true,
66
+ attempts: 5,
67
+ wait: 1000,
68
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL, // FIXED, LINEAR, or EXPONENTIAL
69
+ jitter: 0.2, // Add ±20% randomness to delays
70
+ maxAllowedWait: 30000 // Cap maximum wait time
71
+ });
92
72
 
93
- ## Core Modules
73
+ if (result.success) {
74
+ console.log('Data:', result.data);
75
+ }
76
+ })();
77
+ ```
94
78
 
95
- ### stableRequest
79
+ ### ⚡ Circuit Breaker Pattern
96
80
 
97
- Single API call with resilience, type-safe request and response types.
81
+ Protect your services from cascading failures:
98
82
 
99
83
  ```typescript
100
- import { stableRequest, REQUEST_METHODS, VALID_REQUEST_PROTOCOLS } from '@emmvish/stable-request';
84
+ import { stableRequest, REQUEST_METHODS } from 'stable-request';
85
+ import type { STABLE_REQUEST_RESULT, CircuitBreakerConfig } from 'stable-request';
101
86
 
102
- interface GetUserRequest {
103
- // Empty for GET requests with no body
87
+ interface ApiResponse {
88
+ status: string;
104
89
  }
105
90
 
106
- interface User {
107
- id: number;
108
- name: string;
109
- }
91
+ (async () => {
92
+ const circuitBreakerConfig: CircuitBreakerConfig = {
93
+ failureThresholdPercentage: 50, // Open circuit at 50% failure rate
94
+ minimumRequests: 10, // Minimum requests before evaluation
95
+ recoveryTimeoutMs: 30000, // Time before attempting recovery
96
+ halfOpenMaxRequests: 5, // Requests allowed in half-open state
97
+ trackIndividualAttempts: true // Track each retry attempt
98
+ };
99
+
100
+ const result: STABLE_REQUEST_RESULT<ApiResponse> = await stableRequest<void, ApiResponse>({
101
+ reqData: {
102
+ hostname: 'api.example.com',
103
+ path: '/data',
104
+ method: REQUEST_METHODS.GET
105
+ },
106
+ resReq: true,
107
+ circuitBreaker: circuitBreakerConfig
108
+ });
109
+ })();
110
+ ```
110
111
 
111
- const result = await stableRequest<GetUserRequest, User>({
112
- reqData: {
113
- method: REQUEST_METHODS.GET,
114
- protocol: VALID_REQUEST_PROTOCOLS.HTTPS,
115
- hostname: 'api.example.com',
116
- path: '/users/1'
117
- },
118
- resReq: true,
119
- attempts: 3,
120
- wait: 500,
121
- jitter: 100,
122
- cache: { enabled: true, ttl: 5000 },
123
- rateLimit: { maxRequests: 10, windowMs: 1000 },
124
- maxConcurrentRequests: 5,
125
- responseAnalyzer: ({ data }) => {
126
- return typeof data === 'object' && data !== null && 'id' in data;
127
- },
128
- handleSuccessfulAttemptData: ({ successfulAttemptData }) => {
129
- console.log(`User loaded: ${successfulAttemptData.data.name}`);
130
- }
131
- });
112
+ ### 💾 Intelligent Response Caching
132
113
 
133
- if (result.success) {
134
- console.log(result.data.name, result.metrics.totalAttempts);
135
- } else {
136
- console.error(result.error);
114
+ Cache responses with full HTTP cache-control support:
115
+
116
+ ```typescript
117
+ import { stableRequest, REQUEST_METHODS } from 'stable-request';
118
+ import type { STABLE_REQUEST_RESULT, CacheConfig } from 'stable-request';
119
+
120
+ interface ApiResponse {
121
+ items: { id: number; name: string }[];
137
122
  }
138
- ```
139
123
 
140
- **Key responsibilities:**
141
- - Execute a single HTTP request with automatic retry and backoff
142
- - Validate response shape via analyzer; retry if invalid
143
- - Cache successful responses with TTL
144
- - Apply rate and concurrency limits
145
- - Throw or gracefully suppress errors via finalErrorAnalyzer
146
- - Collect attempt metrics and infra dashboards (circuit breaker, cache, rate limiter state)
124
+ (async () => {
125
+ const cacheConfig: CacheConfig = {
126
+ enabled: true,
127
+ ttl: 300000, // 5 minutes default TTL
128
+ maxSize: 100, // Maximum cache entries
129
+ respectCacheControl: true, // Honor HTTP cache headers
130
+ cacheableStatusCodes: [200, 203, 204, 206, 300, 301],
131
+ excludeMethods: [REQUEST_METHODS.POST, REQUEST_METHODS.PUT, REQUEST_METHODS.PATCH, REQUEST_METHODS.DELETE]
132
+ };
133
+
134
+ const result: STABLE_REQUEST_RESULT<ApiResponse> = await stableRequest<void, ApiResponse>({
135
+ reqData: {
136
+ hostname: 'api.example.com',
137
+ path: '/data',
138
+ method: REQUEST_METHODS.GET
139
+ },
140
+ resReq: true,
141
+ cache: cacheConfig
142
+ });
143
+ })();
144
+ ```
147
145
 
148
- ### stableFunction
146
+ ### 🔒 StableBuffer - Thread-Safe State Management
149
147
 
150
- Generic async/sync function execution with identical resilience.
148
+ Manage shared state safely across concurrent operations:
151
149
 
152
150
  ```typescript
153
- import { stableFunction, RETRY_STRATEGIES } from '@emmvish/stable-request';
154
-
155
- type ComputeArgs = [number, number];
156
- type ComputeResult = number;
157
-
158
- const multiply = (a: number, b: number) => a * b;
159
-
160
- const result = await stableFunction<ComputeArgs, ComputeResult>({
161
- fn: multiply,
162
- args: [5, 3],
163
- returnResult: true,
164
- attempts: 2,
165
- wait: 100,
166
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
167
- responseAnalyzer: ({ data }) => data > 0,
168
- cache: { enabled: true, ttl: 10000 }
169
- });
151
+ import { StableBuffer } from 'stable-request';
152
+ import type { StableBufferOptions, StableBufferTransactionLog, StableBufferState } from 'stable-request';
170
153
 
171
- if (result.success) {
172
- console.log('Result:', result.data); // 15
154
+ interface BufferState extends StableBufferState {
155
+ counter: number;
156
+ items: { id: number; timestamp: number }[];
173
157
  }
174
- ```
175
158
 
176
- **Key responsibilities:**
177
- - Execute any async or sync function with typed arguments and return
178
- - Support argument-based cache key generation
179
- - Retry on error or analyzer rejection
180
- - Enforce success criteria via analyzer
181
- - Optionally suppress exceptions
159
+ (async () => {
160
+ const bufferOptions: StableBufferOptions = {
161
+ initialState: { counter: 0, items: [] } as BufferState,
162
+ transactionTimeoutMs: 5000,
163
+ logTransaction: async (log: StableBufferTransactionLog): Promise<void> => {
164
+ // Persist transaction logs for replay/audit
165
+ await saveToDatabase(log);
166
+ }
167
+ };
168
+
169
+ const buffer = new StableBuffer(bufferOptions);
170
+
171
+ // Safe concurrent updates
172
+ await buffer.run(async (state): Promise<void> => {
173
+ const typedState = state as BufferState;
174
+ typedState.counter += 1;
175
+ typedState.items.push({ id: typedState.counter, timestamp: Date.now() });
176
+ });
177
+
178
+ // Read state (returns a clone)
179
+ const currentState = buffer.read() as BufferState;
180
+ })();
181
+ ```
182
182
 
183
- ### stableApiGateway
183
+ ### 📜 Transaction Logs & State Replay
184
184
 
185
- Batch orchestration of mixed requests and functions.
185
+ Replay transactions for recovery or auditing:
186
186
 
187
187
  ```typescript
188
- import {
189
- stableApiGateway,
190
- REQUEST_METHODS,
191
- VALID_REQUEST_PROTOCOLS,
192
- RequestOrFunction
193
- } from '@emmvish/stable-request';
194
- import type { API_GATEWAY_ITEM } from '@emmvish/stable-request';
195
-
196
- // Define request types
197
- interface ApiRequestData {
198
- filters?: Record<string, any>;
188
+ import { replayStableBufferTransactions } from 'stable-request';
189
+ import type { StableBufferTransactionLog, StableBufferReplayResult, StableBufferReplayHandler } from 'stable-request';
190
+
191
+ interface OrderState {
192
+ orders: string[];
193
+ inventory: Record<string, number>;
199
194
  }
200
195
 
201
- interface ApiResponse {
202
- id: number;
203
- value: string;
196
+ interface OrderHookParams {
197
+ orderId: string;
204
198
  }
205
199
 
206
- // Define function types
207
- type TransformArgs = [ApiResponse[], number];
208
- type TransformResult = {
209
- transformed: ApiResponse[];
210
- count: number;
211
- };
200
+ interface InventoryHookParams {
201
+ sku: string;
202
+ quantity: number;
203
+ }
212
204
 
213
- type ValidateArgs = [TransformResult];
214
- type ValidateResult = boolean;
205
+ (async () => {
206
+ // Load saved transaction logs
207
+ const logs: StableBufferTransactionLog[] = await loadTransactionLogsFromDB();
215
208
 
216
- const items: API_GATEWAY_ITEM<ApiRequestData, ApiResponse, TransformArgs | ValidateArgs, TransformResult | ValidateResult>[] = [
217
- {
218
- type: RequestOrFunction.REQUEST,
219
- request: {
220
- id: 'fetch-data',
221
- requestOptions: {
222
- reqData: {
223
- method: REQUEST_METHODS.GET,
224
- protocol: VALID_REQUEST_PROTOCOLS.HTTPS,
225
- hostname: 'api.example.com',
226
- path: '/data'
227
- },
228
- resReq: true,
229
- attempts: 3
230
- }
231
- }
232
- },
233
- {
234
- type: RequestOrFunction.FUNCTION,
235
- function: {
236
- id: 'transform-data',
237
- functionOptions: {
238
- fn: (data: ApiResponse[], threshold: number): TransformResult => ({
239
- transformed: data.filter(item => item.id > threshold),
240
- count: data.length
241
- }),
242
- args: [[], 10] as TransformArgs,
243
- returnResult: true,
244
- attempts: 2,
245
- cache: { enabled: true, ttl: 5000 }
246
- }
247
- }
248
- },
249
- {
250
- type: RequestOrFunction.FUNCTION,
251
- function: {
252
- id: 'validate-result',
253
- functionOptions: {
254
- fn: (result: TransformResult): ValidateResult => result.count > 0,
255
- args: [{ transformed: [], count: 0 }] as ValidateArgs,
256
- returnResult: true
257
- }
209
+ // Define replay handlers
210
+ const handlers: Record<string, StableBufferReplayHandler> = {
211
+ 'processOrder': async (state, log): Promise<void> => {
212
+ const typedState = state as OrderState;
213
+ const params = log.hookParams as OrderHookParams;
214
+ typedState.orders.push(params.orderId);
215
+ },
216
+ 'updateInventory': async (state, log): Promise<void> => {
217
+ const typedState = state as OrderState;
218
+ const params = log.hookParams as InventoryHookParams;
219
+ typedState.inventory[params.sku] -= params.quantity;
258
220
  }
259
- }
260
- ];
261
-
262
- const responses = await stableApiGateway<ApiRequestData, ApiResponse>(items, {
263
- concurrentExecution: true,
264
- stopOnFirstError: false,
265
- sharedBuffer: {},
266
- commonAttempts: 2,
267
- commonWait: 300,
268
- maxConcurrentRequests: 3
269
- });
221
+ };
270
222
 
271
- // Access individual responses
272
- responses.forEach((resp, i) => {
273
- console.log(`Item ${i}: success=${resp.success}`);
274
- });
223
+ // Replay with custom handlers
224
+ const result: StableBufferReplayResult = await replayStableBufferTransactions({
225
+ logs,
226
+ handlers,
227
+ initialState: { orders: [], inventory: {} } as OrderState,
228
+ dedupe: true, // Skip duplicate transactions
229
+ sort: true // Order by timestamp
230
+ });
275
231
 
276
- // Access aggregate metrics
277
- console.log(`Success rate: ${responses.metrics.successRate.toFixed(2)}%`);
278
- console.log(`Execution time: ${responses.metrics.executionTime}ms`);
279
- console.log(`Throughput: ${responses.metrics.throughput.toFixed(2)} req/s`);
280
- console.log(`Average duration: ${responses.metrics.averageRequestDuration.toFixed(2)}ms`);
232
+ console.log('Replayed state:', result.buffer.read() as OrderState);
233
+ })();
281
234
  ```
282
235
 
283
- #### Request/Function Racing
236
+ ### 💾 State Persistence
284
237
 
285
- Enable racing to accept the first successful request or function and cancel others, useful for redundant API calls or failover scenarios.
238
+ Persist and restore state across executions:
286
239
 
287
240
  ```typescript
288
- const responses = await stableApiGateway(items, {
289
- concurrentExecution: true,
290
- enableRacing: true, // First successful item wins, others cancelled
291
- maxConcurrentRequests: 10
292
- });
241
+ import { stableRequest, StableBuffer, PersistenceStage, REQUEST_METHODS } from 'stable-request';
242
+ import type { STABLE_REQUEST_RESULT, StatePersistenceConfig, StatePersistenceOptions } from 'stable-request';
293
243
 
294
- // responses contains only the winning result
295
- // Losing items marked as cancelled with appropriate error
296
- ```
244
+ interface ApiResponse {
245
+ data: string;
246
+ }
247
+
248
+ interface BufferState {
249
+ lastFetched: string | null;
250
+ }
297
251
 
298
- **Key responsibilities:**
299
- - Execute a batch of requests and functions concurrently or sequentially
300
- - Apply global, group-level, and item-level config overrides
301
- - Maintain shared buffer across items for state passing
302
- - Stop on first error or continue despite failures
303
- - Collect per-item and aggregate metrics (success rates, execution time, throughput)
304
- - Support request grouping with group-specific config
305
- - Track infrastructure metrics (circuit breaker, cache, rate limiter, concurrency)
252
+ (async () => {
253
+ const buffer = new StableBuffer({
254
+ initialState: { lastFetched: null } as BufferState
255
+ });
256
+
257
+ const statePersistence: StatePersistenceConfig = {
258
+ persistenceFunction: async (options: StatePersistenceOptions): Promise<Record<string, any>> => {
259
+ const { executionContext, buffer, persistenceStage } = options;
260
+ if (persistenceStage === PersistenceStage.BEFORE_HOOK) {
261
+ return await loadStateFromDB(executionContext);
262
+ } else {
263
+ await saveStateToDB(executionContext, buffer);
264
+ return buffer;
265
+ }
266
+ },
267
+ loadBeforeHooks: true,
268
+ storeAfterHooks: true
269
+ };
306
270
 
307
- ### stableWorkflow
271
+ const result: STABLE_REQUEST_RESULT<ApiResponse> = await stableRequest<void, ApiResponse>({
272
+ reqData: {
273
+ hostname: 'api.example.com',
274
+ path: '/data',
275
+ method: REQUEST_METHODS.GET
276
+ },
277
+ resReq: true,
278
+ commonBuffer: buffer,
279
+ statePersistence
280
+ });
281
+ })();
282
+ ```
308
283
 
309
- Phased array-based workflows with sequential/concurrent phases, mixed items, and non-linear control flow.
284
+ ### 🪝 Lifecycle Hooks
310
285
 
311
- You can start a workflow from a specific phase using `startPhaseIndex` (0-based). When starting inside a concurrent group (`markConcurrentPhase`), execution aligns to the group’s first phase.
286
+ Tap into every stage of request execution:
312
287
 
313
288
  ```typescript
314
- import { stableWorkflow, PHASE_DECISION_ACTIONS, RequestOrFunction, REQUEST_METHODS } from '@emmvish/stable-request';
315
- import type { STABLE_WORKFLOW_PHASE, API_GATEWAY_ITEM } from '@emmvish/stable-request';
316
-
317
- // Define types for requests
318
- interface FetchRequestData {}
319
- interface FetchResponse {
320
- users: Array<{ id: number; name: string }>;
321
- posts: Array<{ id: number; title: string }>;
322
- }
289
+ import { stableRequest, REQUEST_METHODS } from 'stable-request';
290
+ import type {
291
+ STABLE_REQUEST_RESULT,
292
+ STABLE_REQUEST,
293
+ PreExecutionHookOptions,
294
+ ResponseAnalysisHookOptions,
295
+ HandleErrorHookOptions,
296
+ HandleSuccessfulAttemptDataHookOptions,
297
+ FinalErrorAnalysisHookOptions
298
+ } from 'stable-request';
323
299
 
324
- // Define types for functions
325
- type ProcessArgs = [FetchResponse];
326
- type ProcessResult = {
327
- merged: Array<{ userId: number; userName: string; postTitle: string }>;
328
- };
329
-
330
- type AuditArgs = [ProcessResult, string];
331
- type AuditResult = { logged: boolean; timestamp: string };
300
+ interface ApiResponse {
301
+ status: 'success' | 'pending' | 'failed';
302
+ data?: unknown;
303
+ }
332
304
 
333
- const phases: STABLE_WORKFLOW_PHASE<FetchRequestData, FetchResponse, ProcessArgs | AuditArgs, ProcessResult | AuditResult>[] = [
334
- {
335
- id: 'fetch-data',
336
- requests: [
337
- {
338
- id: 'get-users-posts',
339
- requestOptions: {
305
+ (async () => {
306
+ const result: STABLE_REQUEST_RESULT<ApiResponse> = await stableRequest<void, ApiResponse>({
307
+ reqData: {
308
+ hostname: 'api.example.com',
309
+ path: '/data',
310
+ method: REQUEST_METHODS.GET
311
+ },
312
+ resReq: true, // Return response data
313
+
314
+ // Pre-execution hook
315
+ preExecution: {
316
+ preExecutionHook: async (options: PreExecutionHookOptions<void, ApiResponse>): Promise<Partial<STABLE_REQUEST<void, ApiResponse>>> => {
317
+ const { inputParams, commonBuffer, stableRequestOptions } = options;
318
+ // Modify options before execution
319
+ return {
340
320
  reqData: {
341
- hostname: 'api.example.com',
342
- path: '/users-and-posts'
343
- },
344
- resReq: true,
345
- attempts: 3
346
- }
347
- }
348
- ]
349
- },
350
- {
351
- id: 'process-and-audit',
352
- markConcurrentPhase: true,
353
- items: [
354
- {
355
- type: RequestOrFunction.FUNCTION,
356
- function: {
357
- id: 'process-data',
358
- functionOptions: {
359
- fn: (data: FetchResponse): ProcessResult => ({
360
- merged: data.users.map((user, idx) => ({
361
- userId: user.id,
362
- userName: user.name,
363
- postTitle: data.posts[idx]?.title || 'No post'
364
- }))
365
- }),
366
- args: [{ users: [], posts: [] }] as ProcessArgs,
367
- returnResult: true
321
+ ...stableRequestOptions.reqData,
322
+ headers: { 'X-Custom': 'value' }
368
323
  }
369
- }
324
+ };
370
325
  },
371
- {
372
- type: RequestOrFunction.FUNCTION,
373
- function: {
374
- id: 'audit-processing',
375
- functionOptions: {
376
- fn: async (result: ProcessResult, auditId: string): Promise<AuditResult> => {
377
- console.log(`Audit ${auditId}:`, result);
378
- return { logged: true, timestamp: new Date().toISOString() };
379
- },
380
- args: [{ merged: [] }, 'audit-123'] as AuditArgs,
381
- returnResult: true
382
- }
383
- }
384
- }
385
- ],
386
- phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
387
- if (!phaseResult.success) {
388
- return { action: PHASE_DECISION_ACTIONS.TERMINATE };
389
- }
390
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
326
+ applyPreExecutionConfigOverride: true
327
+ },
328
+
329
+ // Response validation
330
+ responseAnalyzer: async (options: ResponseAnalysisHookOptions<void, ApiResponse>): Promise<boolean> => {
331
+ const { data, trialMode, commonBuffer } = options;
332
+ return data.status === 'success'; // Return true if response is valid
333
+ },
334
+
335
+ // Error handling
336
+ logAllErrors: true,
337
+ handleErrors: async (options: HandleErrorHookOptions<void>): Promise<void> => {
338
+ const { reqData, errorLog, commonBuffer } = options;
339
+ await logToMonitoring(errorLog);
340
+ },
341
+
342
+ // Success tracking
343
+ logAllSuccessfulAttempts: true,
344
+ handleSuccessfulAttemptData: async (options: HandleSuccessfulAttemptDataHookOptions<void, ApiResponse>): Promise<void> => {
345
+ const { successfulAttemptData } = options;
346
+ await trackMetric('request_success', successfulAttemptData);
347
+ },
348
+
349
+ // Final error analysis
350
+ finalErrorAnalyzer: async (options: FinalErrorAnalysisHookOptions<void>): Promise<boolean> => {
351
+ const { error, commonBuffer } = options;
352
+ return error.message.includes('temporary'); // Return true if handled
391
353
  }
392
- },
393
- {
394
- id: 'finalize',
395
- requests: [
396
- {
397
- id: 'store-result',
398
- requestOptions: {
399
- reqData: {
400
- hostname: 'api.example.com',
401
- path: '/store',
402
- method: REQUEST_METHODS.POST
403
- },
404
- resReq: false
405
- }
406
- }
407
- ]
408
- }
409
- ];
410
-
411
- const result = await stableWorkflow(phases, {
412
- workflowId: 'data-pipeline',
413
- concurrentPhaseExecution: false, // Phases sequential
414
- enableNonLinearExecution: true,
415
- sharedBuffer: { userId: '123' },
416
- commonAttempts: 2,
417
- commonWait: 200,
418
- handlePhaseCompletion: ({ phaseResult, workflowId }) => {
419
- console.log(`Phase ${phaseResult.phaseId} complete in workflow ${workflowId}`);
420
- }
421
- });
354
+ });
355
+ })();
356
+ ```
357
+
358
+ ---
422
359
 
423
- console.log(`Workflow succeeded: ${result.success}, phases: ${result.totalPhases}`);
360
+ ## 🎣 Hook Reference
361
+
362
+ stable-request provides **5 hooks** that let you inject business logic into the request lifecycle. Each hook serves a specific purpose and receives contextual information to make intelligent decisions.
363
+
364
+ ### Hook Execution Flow
365
+
366
+ ```
367
+ ┌─────────────────────────────────────────────────────────────────────────┐
368
+ │ stableRequest() called │
369
+ └─────────────────────────────────────────────────────────────────────────┘
370
+
371
+
372
+ ┌─────────────────────────────────────────────────────────────────────────┐
373
+ │ 1. preExecutionHook │
374
+ │ • Modify request config, inject auth tokens, validate inputs │
375
+ │ • Can override any stableRequest option │
376
+ └─────────────────────────────────────────────────────────────────────────┘
377
+
378
+
379
+ ┌───────────────────────────────┐
380
+ │ Execute HTTP Request │
381
+ │ (attempt 1 of N) │
382
+ └───────────────────────────────┘
383
+ │ │
384
+ Success (2xx) Network/HTTP Error
385
+ │ │
386
+ ▼ ▼
387
+ ┌──────────────────────────────────┐ ┌─────────────────────────────────────┐
388
+ │ 2. responseAnalyzer │ │ 3. handleErrors │
389
+ │ • Validate business logic │ │ • Log error, alert, track │
390
+ │ • Return false = retry │ │ • Called for each failed attempt│
391
+ └──────────────────────────────────┘ └─────────────────────────────────────┘
392
+ │ │ │
393
+ Return true Return false │
394
+ (valid) (invalid = retry) │
395
+ │ │ │
396
+ │ └──────────────────────────────┤
397
+ │ │
398
+ ▼ ▼
399
+ ┌──────────────────────────────────┐ ┌─────────────────────────────────┐
400
+ │ 4. handleSuccessfulAttemptData │ │ (Retry with backoff if │
401
+ │ • Track successful attempts │ │ attempts remaining) │
402
+ │ • Audit logging │ └─────────────────────────────────┘
403
+ └──────────────────────────────────┘ │
404
+ │ │
405
+ │ All attempts exhausted
406
+ │ │
407
+ ▼ ▼
408
+ ┌─────────────────────────────────────────────────────────────────────────┐
409
+ │ Return Result │
410
+ │ │ │
411
+ │ ┌───────────────┴───────────────┐ │
412
+ │ success: true success: false │
413
+ │ │ │
414
+ │ ▼ │
415
+ │ ┌─────────────────────────────────────┐ │
416
+ │ │ 5. finalErrorAnalyzer │ │
417
+ │ │ • Analyze final failure │ │
418
+ │ │ • Determine if error is fatal │ │
419
+ │ │ • Control throwOnFailedError │ │
420
+ │ └─────────────────────────────────────┘ │
421
+ └─────────────────────────────────────────────────────────────────────────┘
424
422
  ```
425
423
 
426
- **Key responsibilities:**
427
- - Execute phases sequentially or concurrently
428
- - Support mixed requests and functions per phase
429
- - Enable non-linear flow (CONTINUE, SKIP, REPLAY, JUMP, TERMINATE)
430
- - Maintain shared buffer across all phases
431
- - Apply phase-level and request-level config cascading
432
- - Support branching with parallel/sequential branches
433
- - Collect per-phase metrics and workflow aggregates
424
+ ---
434
425
 
435
- ### stableWorkflowGraph
426
+ ### 1. `preExecutionHook` - Pre-Request Setup
436
427
 
437
- DAG-based execution for higher parallelism and explicit phase dependencies.
428
+ Called **once** before any attempt is made. Use it to modify request configuration, inject dynamic values, or validate preconditions.
438
429
 
439
430
  ```typescript
440
- import { stableWorkflowGraph, WorkflowGraphBuilder } from '@emmvish/stable-request';
441
-
442
- const graph = new WorkflowGraphBuilder()
443
- .addPhase('fetch-posts', {
444
- requests: [{
445
- id: 'get-posts',
446
- requestOptions: {
447
- reqData: { hostname: 'api.example.com', path: '/posts' },
448
- resReq: true
449
- }
450
- }]
451
- })
452
- .addPhase('fetch-users', {
453
- requests: [{
454
- id: 'get-users',
455
- requestOptions: {
456
- reqData: { hostname: 'api.example.com', path: '/users' },
457
- resReq: true
458
- }
459
- }]
460
- })
461
- .addParallelGroup('fetch-all', ['fetch-posts', 'fetch-users'])
462
- .addPhase('aggregate', {
463
- functions: [{
464
- id: 'combine',
465
- functionOptions: {
466
- fn: () => ({ posts: [], users: [] }),
467
- args: [],
468
- returnResult: true
469
- }
470
- }]
471
- })
472
- .addMergePoint('sync', ['fetch-all'])
473
- .connectSequence('fetch-all', 'sync', 'aggregate')
474
- .setEntryPoint('fetch-all')
475
- .build();
476
-
477
- const result = await stableWorkflowGraph(graph, {
478
- workflowId: 'data-aggregation'
479
- });
431
+ import { stableRequest, REQUEST_METHODS } from 'stable-request';
432
+ import type {
433
+ STABLE_REQUEST_RESULT,
434
+ STABLE_REQUEST,
435
+ PreExecutionHookOptions,
436
+ RequestPreExecutionOptions
437
+ } from 'stable-request';
480
438
 
481
- console.log(`Graph workflow success: ${result.success}`);
482
- ```
439
+ interface OrderRequest {
440
+ productId: string;
441
+ quantity: number;
442
+ }
483
443
 
484
- **Key responsibilities:**
485
- - Define phases as DAG nodes with explicit dependency edges
486
- - Execute independent phases in parallel automatically
487
- - Support parallel groups, merge points, and conditional routing
488
- - Validate graph structure (cycle detection, reachability, orphan detection)
489
- - Provide deterministic execution order
490
- - Offer higher parallelism than phased workflows for complex topologies
444
+ interface OrderResponse {
445
+ orderId: string;
446
+ status: string;
447
+ }
448
+
449
+ (async () => {
450
+ const preExecutionConfig: RequestPreExecutionOptions<OrderRequest, OrderResponse> = {
451
+ preExecutionHook: async (options: PreExecutionHookOptions<OrderRequest, OrderResponse>): Promise<Partial<STABLE_REQUEST<OrderRequest, OrderResponse>>> => {
452
+ const { inputParams, commonBuffer, stableRequestOptions, transactionLogs } = options;
453
+
454
+ // Inject fresh auth token
455
+ const token: string = await getAuthToken();
456
+
457
+ // Validate business preconditions
458
+ if (!commonBuffer?.userId) {
459
+ throw new Error('User ID required');
460
+ }
461
+
462
+ // Return partial config to merge (if applyPreExecutionConfigOverride is true)
463
+ return {
464
+ reqData: {
465
+ ...stableRequestOptions.reqData,
466
+ headers: {
467
+ ...stableRequestOptions.reqData.headers,
468
+ 'Authorization': `Bearer ${token}`,
469
+ 'X-User-Id': commonBuffer.userId
470
+ }
471
+ }
472
+ };
473
+ },
474
+ preExecutionHookParams: { customData: 'value' }, // Passed as inputParams
475
+ applyPreExecutionConfigOverride: true, // Merge returned config
476
+ continueOnPreExecutionHookFailure: false // Fail fast if hook throws
477
+ };
478
+
479
+ const result: STABLE_REQUEST_RESULT<OrderResponse> = await stableRequest<OrderRequest, OrderResponse>({
480
+ reqData: {
481
+ hostname: 'api.example.com',
482
+ path: '/orders',
483
+ method: REQUEST_METHODS.POST
484
+ },
485
+ resReq: true,
486
+ preExecution: preExecutionConfig
487
+ });
488
+ })();
489
+ ```
491
490
 
492
- ### StableScheduler
491
+ **💡 Use cases:**
492
+ - Inject fresh authentication tokens
493
+ - Add request signing/HMAC
494
+ - Validate preconditions before making the request
495
+ - Dynamically modify endpoints based on state
496
+ - Load configuration from external sources
493
497
 
494
- Queue-based scheduler for cron/interval/timestamp execution with concurrency limits and recoverable state via custom persistence handlers.
498
+ ---
495
499
 
496
- **Key responsibilities:**
497
- - Enforce max-parallel job execution
498
- - Schedule jobs with cron, interval, or timestamp(s)
499
- - Persist and restore scheduler state via user-provided handlers
500
+ ### 2. `responseAnalyzer` - Business Logic Validation
500
501
 
501
- ### StableBuffer
502
+ Called after **each successful HTTP response** (2xx status). This is where you validate that the response meets your business requirements.
502
503
 
503
- Transactional, concurrency-safe shared state. It’s opt-in: pass a `StableBuffer` instance as `commonBuffer` or `sharedBuffer` to serialize updates across concurrent executions.
504
+ > **Key insight:** Return `true` if the response is acceptable, `false` to trigger a retry.
504
505
 
505
- Key features:
506
- - Serialized transactions via FIFO queue
507
- - Snapshot reads with `read()`
508
- - Optional transaction timeouts
509
- - Optional transaction logging with `logTransaction`
506
+ ```typescript
507
+ import { stableRequest, StableBuffer, REQUEST_METHODS, RETRY_STRATEGIES } from 'stable-request';
508
+ import type {
509
+ STABLE_REQUEST_RESULT,
510
+ ResponseAnalysisHookOptions,
511
+ HookParams
512
+ } from 'stable-request';
510
513
 
511
- ```ts
512
- import { StableBuffer } from '@emmvish/stable-request';
514
+ interface PaymentRequest {
515
+ cardToken: string;
516
+ amount: number;
517
+ }
513
518
 
514
- const buffer = new StableBuffer({
515
- initialState: { counter: 0 },
516
- transactionTimeoutMs: 500,
517
- logTransaction: (log) => {
518
- // persist log.transactionId, log.activity, log.hookName, log.stateBefore, log.stateAfter
519
- }
520
- });
519
+ interface PaymentResponse {
520
+ status: 'pending' | 'processing' | 'completed' | 'failed';
521
+ transactionId?: string;
522
+ receiptUrl?: string;
523
+ amount?: number;
524
+ error?: string;
525
+ errorCode?: string;
526
+ }
521
527
 
522
- await buffer.run(
523
- (state) => { state.counter += 1; },
524
- { activity: 'workflow-phase', hookName: 'phase-1', workflowId: 'wf-1' }
525
- );
528
+ (async () => {
529
+ // Create buffer to track state across retries
530
+ const buffer = new StableBuffer({
531
+ initialState: { expectedAmount: 100, transactionId: null }
532
+ });
533
+
534
+ const hookParams: HookParams = {
535
+ responseAnalyzerParams: { expectedStatus: 'completed' } // Passed as params
536
+ };
537
+
538
+ const result: STABLE_REQUEST_RESULT<PaymentResponse> = await stableRequest<PaymentRequest, PaymentResponse>({
539
+ reqData: {
540
+ hostname: 'payment.api.com',
541
+ path: '/charge',
542
+ method: REQUEST_METHODS.POST,
543
+ body: { cardToken: 'tok_xxx', amount: 100 }
544
+ },
545
+ resReq: true, // Must be true to receive response data
546
+ attempts: 5,
547
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
548
+ commonBuffer: buffer,
549
+
550
+ responseAnalyzer: async (options: ResponseAnalysisHookOptions<PaymentRequest, PaymentResponse>): Promise<boolean> => {
551
+ const { reqData, data, trialMode, params, commonBuffer, executionContext } = options;
552
+
553
+ // Example 1: Wait for async processing to complete
554
+ if (data.status === 'pending' || data.status === 'processing') {
555
+ console.log('Payment still processing, will retry...');
556
+ return false; // Retry
557
+ }
558
+
559
+ // Example 2: Validate response has required data
560
+ if (!data.transactionId || !data.receiptUrl) {
561
+ console.log('Incomplete response, will retry...');
562
+ return false; // Retry
563
+ }
564
+
565
+ // Example 3: Check for soft errors in response body
566
+ if (data.error || data.errorCode) {
567
+ console.log(`Soft error: ${data.errorCode}, will retry...`);
568
+ return false; // Retry
569
+ }
570
+
571
+ // Example 4: Validate against business rules
572
+ if (data.amount !== commonBuffer?.expectedAmount) {
573
+ console.log('Amount mismatch, will retry...');
574
+ return false; // Retry
575
+ }
576
+
577
+ // Success - accept this response
578
+ if (commonBuffer) {
579
+ commonBuffer.transactionId = data.transactionId;
580
+ }
581
+ return true;
582
+ },
583
+
584
+ hookParams
585
+ });
586
+ })();
526
587
  ```
527
588
 
528
- Replay utility (transaction logs → deterministic state replay):
589
+ **💡 Use cases:**
590
+ - Poll until async operation completes (`status: pending` → `status: completed`)
591
+ - Validate response data integrity
592
+ - Check for soft errors in response body (APIs that return 200 with error payloads)
593
+ - Ensure eventual consistency (retry until data propagates)
594
+ - Validate business invariants
529
595
 
530
- ```ts
531
- import { replayStableBufferTransactions } from '@emmvish/stable-request';
596
+ ---
532
597
 
533
- const replay = await replayStableBufferTransactions({
534
- logs, // StableBufferTransactionLog[]
535
- handlers: {
536
- 'phase-1': (state) => { state.counter += 1; }
537
- },
538
- initialState: { counter: 0 }
539
- });
598
+ ### 3. `handleErrors` - Error Observation & Logging
540
599
 
541
- console.log(replay.buffer.getState());
542
- ```
600
+ Called after **each failed attempt** (network error, non-2xx status, or `responseAnalyzer` returning `false`). Use for observability - this hook doesn't affect retry behavior.
543
601
 
544
- ---
602
+ > **Note:** Only called when `logAllErrors: true`
545
603
 
546
- ## Stable Runner
604
+ ```typescript
605
+ import { stableRequest, StableBuffer, REQUEST_METHODS, RETRY_STRATEGIES } from 'stable-request';
606
+ import type {
607
+ STABLE_REQUEST_RESULT,
608
+ HandleErrorHookOptions,
609
+ ERROR_LOG,
610
+ HookParams
611
+ } from 'stable-request';
547
612
 
548
- Config-driven runner that executes core module jobs from JSON/ESM configs and can use StableScheduler for scheduled jobs.
613
+ interface ApiResponse {
614
+ data: unknown;
615
+ }
549
616
 
550
- ---
617
+ interface ErrorHistoryEntry {
618
+ timestamp: string;
619
+ attempt: string;
620
+ error: string;
621
+ statusCode: number;
622
+ }
551
623
 
552
- ## Resilience Mechanisms
624
+ (async () => {
625
+ // Create buffer to track errors
626
+ const buffer = new StableBuffer({
627
+ initialState: { errorHistory: [] as ErrorHistoryEntry[] }
628
+ });
629
+
630
+ const hookParams: HookParams = {
631
+ handleErrorsParams: { alertChannel: '#api-errors' }
632
+ };
633
+
634
+ const result: STABLE_REQUEST_RESULT<ApiResponse> = await stableRequest<void, ApiResponse>({
635
+ reqData: {
636
+ hostname: 'api.example.com',
637
+ path: '/data',
638
+ method: REQUEST_METHODS.GET
639
+ },
640
+ resReq: true,
641
+ attempts: 3,
642
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
643
+ logAllErrors: true, // Required to trigger handleErrors
644
+ commonBuffer: buffer,
645
+
646
+ handleErrors: async (options: HandleErrorHookOptions<void>): Promise<void> => {
647
+ const { reqData, errorLog, maxSerializableChars, params, commonBuffer, executionContext } = options;
648
+
649
+ // errorLog contains detailed error information
650
+ const { timestamp, attempt, error, type, isRetryable, executionTime, statusCode }: ERROR_LOG = errorLog;
651
+
652
+ // Log to monitoring system
653
+ await sendToDatadog({
654
+ level: 'error',
655
+ message: error,
656
+ tags: {
657
+ attempt,
658
+ statusCode,
659
+ isRetryable,
660
+ workflowId: executionContext?.workflowId,
661
+ requestId: executionContext?.requestId
662
+ },
663
+ executionTime
664
+ });
665
+
666
+ // Track in shared buffer for analysis
667
+ if (commonBuffer) {
668
+ commonBuffer.errorHistory = commonBuffer.errorHistory || [];
669
+ commonBuffer.errorHistory.push({
670
+ timestamp,
671
+ attempt,
672
+ error,
673
+ statusCode
674
+ });
675
+ }
676
+
677
+ // Alert on specific error types
678
+ if (statusCode === 503) {
679
+ await sendSlackAlert(`Service unavailable: ${reqData.url}`);
680
+ }
681
+ },
682
+
683
+ hookParams
684
+ });
685
+ })();
686
+ ```
687
+
688
+ **Error Log Structure:**
689
+ ```typescript
690
+ interface ERROR_LOG {
691
+ timestamp: string; // ISO timestamp of the error
692
+ attempt: string; // e.g., "2/5" (attempt 2 of 5)
693
+ error: string; // Error message
694
+ type: 'HTTP_ERROR' | 'INVALID_CONTENT'; // HTTP error or responseAnalyzer rejection
695
+ isRetryable: boolean; // Whether this error qualifies for retry
696
+ executionTime: number; // Time taken for this attempt (ms)
697
+ statusCode: number; // HTTP status code (0 for network errors)
698
+ }
699
+ ```
553
700
 
554
- ### Execution Timeouts
701
+ **💡 Use cases:**
702
+ - Send errors to monitoring (Datadog, New Relic, Sentry)
703
+ - Track error patterns for circuit breaker decisions
704
+ - Alert on specific error types
705
+ - Build error history for debugging
706
+ - Audit logging
555
707
 
556
- Set maximum execution time for functions to prevent indefinite hangs. Timeouts are enforced at multiple levels with proper inheritance.
708
+ ---
557
709
 
558
- You can also set a **workflow/gateway-level `maxTimeout`** to cap total execution time (applies to `stableWorkflow`, `stableWorkflowGraph`, and `stableApiGateway`).
710
+ ### 4. `handleSuccessfulAttemptData` - Success Observation
559
711
 
560
- #### Function-Level Timeout
712
+ Called after **each successful attempt** (HTTP 2xx + `responseAnalyzer` returns `true`). Use for observability and tracking.
561
713
 
562
- Set timeout directly on a function:
714
+ > **Note:** Only called when `logAllSuccessfulAttempts: true`. Most useful with `performAllAttempts: true` for polling scenarios.
563
715
 
564
716
  ```typescript
565
- import { stableFunction } from '@emmvish/stable-request';
566
-
567
- const result = await stableFunction({
568
- fn: async () => {
569
- // Long-running operation
570
- await processLargeDataset();
571
- return 'success';
572
- },
573
- args: [],
574
- returnResult: true,
575
- executionTimeout: 5000, // 5 seconds max
576
- attempts: 3,
577
- });
717
+ import { stableRequest, StableBuffer, REQUEST_METHODS, RETRY_STRATEGIES } from 'stable-request';
718
+ import type {
719
+ STABLE_REQUEST_RESULT,
720
+ HandleSuccessfulAttemptDataHookOptions,
721
+ SUCCESSFUL_ATTEMPT_DATA
722
+ } from 'stable-request';
578
723
 
579
- if (!result.success && result.error?.includes('timeout')) {
580
- console.log('Function timed out');
724
+ interface HealthResponse {
725
+ status: 'healthy' | 'degraded' | 'unhealthy';
726
+ uptime: number;
581
727
  }
582
- ```
583
728
 
584
- #### Gateway-Level Timeout
729
+ interface ResponseHistoryEntry {
730
+ attempt: string;
731
+ timestamp: string;
732
+ status: string;
733
+ latency: number;
734
+ }
585
735
 
586
- Apply timeout to all functions in a gateway:
736
+ (async () => {
737
+ // Create buffer to track response history
738
+ const buffer = new StableBuffer({
739
+ initialState: { responseHistory: [] as ResponseHistoryEntry[] }
740
+ });
741
+
742
+ const result: STABLE_REQUEST_RESULT<HealthResponse> = await stableRequest<void, HealthResponse>({
743
+ reqData: {
744
+ hostname: 'api.example.com',
745
+ path: '/health',
746
+ method: REQUEST_METHODS.GET
747
+ },
748
+ resReq: true,
749
+ attempts: 10,
750
+ retryStrategy: RETRY_STRATEGIES.LINEAR,
751
+ performAllAttempts: true, // Continue even after success
752
+ logAllSuccessfulAttempts: true, // Required to trigger this hook
753
+ commonBuffer: buffer,
754
+
755
+ handleSuccessfulAttemptData: async (options: HandleSuccessfulAttemptDataHookOptions<void, HealthResponse>): Promise<void> => {
756
+ const { reqData, successfulAttemptData, maxSerializableChars, params, commonBuffer } = options;
757
+ const { attempt, timestamp, data, executionTime, statusCode }: SUCCESSFUL_ATTEMPT_DATA<HealthResponse> = successfulAttemptData;
758
+
759
+ // Track latency metrics
760
+ await sendMetric('api_latency', executionTime, {
761
+ endpoint: reqData.url,
762
+ attempt
763
+ });
764
+
765
+ // Build response history (useful for polling scenarios)
766
+ if (commonBuffer) {
767
+ commonBuffer.responseHistory = commonBuffer.responseHistory || [];
768
+ commonBuffer.responseHistory.push({
769
+ attempt,
770
+ timestamp,
771
+ status: data.status,
772
+ latency: executionTime
773
+ });
774
+ }
775
+
776
+ // Log successful recovery
777
+ if (attempt !== '1/10') {
778
+ console.log(`Recovered on attempt ${attempt} after ${executionTime}ms`);
779
+ }
780
+ }
781
+ });
782
+ })();
783
+ ```
587
784
 
785
+ **Successful Attempt Data Structure:**
588
786
  ```typescript
589
- import { stableApiGateway, RequestOrFunction } from '@emmvish/stable-request';
590
-
591
- const results = await stableApiGateway(
592
- [
593
- {
594
- type: RequestOrFunction.FUNCTION,
595
- function: {
596
- id: 'task1',
597
- functionOptions: {
598
- fn: async () => await task1(),
599
- args: [],
600
- // No timeout specified - inherits from gateway
601
- },
602
- },
603
- },
604
- {
605
- type: RequestOrFunction.FUNCTION,
606
- function: {
607
- id: 'task2',
608
- functionOptions: {
609
- fn: async () => await task2(),
610
- args: [],
611
- executionTimeout: 10000, // Override gateway timeout
612
- },
613
- },
614
- },
615
- ],
616
- {
617
- commonExecutionTimeout: 3000, // Default 3s for all functions
618
- }
619
- );
787
+ interface SUCCESSFUL_ATTEMPT_DATA<ResponseDataType> {
788
+ attempt: string; // e.g., "3/5"
789
+ timestamp: string; // ISO timestamp
790
+ data: ResponseDataType; // Response data
791
+ executionTime: number; // Time taken (ms)
792
+ statusCode: number; // HTTP status code
793
+ }
620
794
  ```
621
795
 
622
- #### Request Group Timeout
796
+ **💡 Use cases:**
797
+ - Track latency percentiles
798
+ - Monitor recovery patterns (which attempts succeed?)
799
+ - Build response history for polling workflows
800
+ - Celebrate successful retries in observability dashboards
623
801
 
624
- Different timeouts for different groups:
802
+ ---
625
803
 
626
- ```typescript
627
- const results = await stableApiGateway(
628
- [
629
- {
630
- type: RequestOrFunction.FUNCTION,
631
- function: {
632
- id: 'critical',
633
- groupId: 'criticalOps',
634
- functionOptions: { fn: criticalOp, args: [] },
635
- },
636
- },
637
- {
638
- type: RequestOrFunction.FUNCTION,
639
- function: {
640
- id: 'background',
641
- groupId: 'backgroundOps',
642
- functionOptions: { fn: backgroundOp, args: [] },
643
- },
644
- },
645
- ],
646
- {
647
- requestGroups: [
648
- {
649
- id: 'criticalOps',
650
- commonConfig: {
651
- commonExecutionTimeout: 1000, // Strict 1s timeout
652
- },
653
- },
654
- {
655
- id: 'backgroundOps',
656
- commonConfig: {
657
- commonExecutionTimeout: 30000, // Lenient 30s timeout
658
- },
659
- },
660
- ],
661
- }
662
- );
663
- ```
804
+ ### 5. `finalErrorAnalyzer` - Final Failure Analysis
664
805
 
665
- #### Workflow Phase Timeout
806
+ Called **once** when all retry attempts have been exhausted and the request has failed. This is your last chance to analyze the failure and decide how to handle it.
666
807
 
667
- Apply timeout at phase level in workflows:
808
+ > **Key insight:** Return `true` if you've handled the error gracefully, `false` to let it propagate. Works with `throwOnFailedErrorAnalysis` option.
668
809
 
669
810
  ```typescript
670
- import { stableWorkflow } from '@emmvish/stable-request';
671
-
672
- const result = await stableWorkflow(
673
- [
674
- {
675
- id: 'initialization',
676
- functions: [
677
- {
678
- id: 'init',
679
- functionOptions: {
680
- fn: initializeSystem,
681
- args: [],
682
- },
683
- },
684
- ],
685
- commonConfig: {
686
- commonExecutionTimeout: 5000, // 5s for initialization
687
- },
811
+ import { stableRequest, StableBuffer, REQUEST_METHODS, RETRY_STRATEGIES } from 'stable-request';
812
+ import type {
813
+ STABLE_REQUEST_RESULT,
814
+ FinalErrorAnalysisHookOptions,
815
+ HookParams,
816
+ ERROR_LOG
817
+ } from 'stable-request';
818
+
819
+ interface CriticalResponse {
820
+ result: string;
821
+ }
822
+
823
+ (async () => {
824
+ // Create buffer with fallback state
825
+ const buffer = new StableBuffer({
826
+ initialState: {
827
+ errorHistory: [] as ERROR_LOG[],
828
+ isMaintenanceMode: false,
829
+ cachedResponse: null as CriticalResponse | null,
830
+ useFallback: false
831
+ }
832
+ });
833
+
834
+ const hookParams: HookParams = {
835
+ finalErrorAnalyzerParams: { allowFailure: false }
836
+ };
837
+
838
+ const result: STABLE_REQUEST_RESULT<CriticalResponse> = await stableRequest<void, CriticalResponse>({
839
+ reqData: {
840
+ hostname: 'api.example.com',
841
+ path: '/critical-operation',
842
+ method: REQUEST_METHODS.POST
688
843
  },
689
- {
690
- id: 'processing',
691
- functions: [
692
- {
693
- id: 'process',
694
- functionOptions: {
695
- fn: processData,
696
- args: [],
697
- },
698
- },
699
- ],
700
- commonConfig: {
701
- commonExecutionTimeout: 30000, // 30s for processing
702
- },
844
+ resReq: true,
845
+ attempts: 5,
846
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
847
+ throwOnFailedErrorAnalysis: true, // Throw if finalErrorAnalyzer returns false
848
+ commonBuffer: buffer,
849
+
850
+ finalErrorAnalyzer: async (options: FinalErrorAnalysisHookOptions<void>): Promise<boolean> => {
851
+ const { reqData, error, trialMode, params, commonBuffer, executionContext } = options;
852
+
853
+ // Log comprehensive failure report
854
+ await logFinalFailure({
855
+ request: reqData,
856
+ error: error.message,
857
+ errorHistory: commonBuffer?.errorHistory,
858
+ context: executionContext
859
+ });
860
+
861
+ // Check if this is a known/acceptable failure
862
+ if (error.message.includes('MAINTENANCE_MODE')) {
863
+ if (commonBuffer) commonBuffer.isMaintenanceMode = true;
864
+ return true; // Handled - won't throw
865
+ }
866
+
867
+ // Check if we should fall back to cached data
868
+ if (commonBuffer?.cachedResponse) {
869
+ commonBuffer.useFallback = true;
870
+ return true; // Handled - use fallback
871
+ }
872
+
873
+ // Check if this is a non-critical operation
874
+ if (params?.allowFailure) {
875
+ return true; // Handled - operation is optional
876
+ }
877
+
878
+ // Unhandled critical failure
879
+ await sendPagerDutyAlert({
880
+ severity: 'critical',
881
+ message: `Critical API failure after 5 attempts`,
882
+ context: executionContext
883
+ });
884
+
885
+ return false; // Not handled - will throw if throwOnFailedErrorAnalysis is true
703
886
  },
704
- ],
705
- {
706
- commonExecutionTimeout: 10000, // Default for phases without specific timeout
707
- }
708
- );
887
+
888
+ hookParams
889
+ });
890
+ })();
709
891
  ```
710
892
 
711
- #### Timeout Precedence
893
+ **💡 Use cases:**
894
+ - Comprehensive failure reporting
895
+ - Determine if failure is recoverable vs. fatal
896
+ - Trigger fallback mechanisms
897
+ - Escalate to PagerDuty/on-call
898
+ - Mark operation as gracefully degraded
899
+ - Decide whether to throw or return error result
712
900
 
713
- Timeouts follow the configuration cascade pattern:
901
+ ---
714
902
 
715
- **Function > Group > Phase/Branch > Gateway**
903
+ ### Hook Parameters Summary
904
+
905
+ All hooks receive contextual information through their options parameter:
906
+
907
+ | Parameter | Description | Available In |
908
+ |-----------|-------------|--------------|
909
+ | `reqData` | Axios request configuration | responseAnalyzer, handleErrors, handleSuccessfulAttemptData, finalErrorAnalyzer |
910
+ | `data` | Response data | responseAnalyzer |
911
+ | `error` | Error object | finalErrorAnalyzer |
912
+ | `errorLog` | Detailed error information | handleErrors |
913
+ | `successfulAttemptData` | Success details | handleSuccessfulAttemptData |
914
+ | `trialMode` | Trial mode configuration | responseAnalyzer, finalErrorAnalyzer |
915
+ | `params` | Custom params from hookParams | All hooks |
916
+ | `preExecutionResult` | Return value from preExecutionHook | responseAnalyzer, handleErrors, handleSuccessfulAttemptData, finalErrorAnalyzer |
917
+ | `commonBuffer` | Shared state buffer | All hooks |
918
+ | `executionContext` | Workflow/phase/request IDs | All hooks |
919
+ | `transactionLogs` | Historical transaction logs | All hooks |
920
+ | `inputParams` | preExecutionHookParams | preExecutionHook |
921
+ | `stableRequestOptions` | Full stableRequest config | preExecutionHook |
716
922
 
717
- - Function-level `executionTimeout` always wins
718
- - If not set, inherits from request group's `commonExecutionTimeout`
719
- - If not set, inherits from phase/branch's `commonExecutionTimeout`
720
- - If not set, inherits from gateway's `commonExecutionTimeout`
721
- - If not set, no timeout is applied
923
+ ---
722
924
 
723
- #### Timeout Behavior
925
+ ### 📊 Comprehensive Metrics
724
926
 
725
- - Timeout applies to **entire function execution** including all retry attempts
726
- - When timeout is exceeded, function returns failed result with timeout error
727
- - Timeout does NOT stop execution mid-flight (no AbortController)
728
- - Metrics are still collected even when timeout occurs
729
- - Use with retries: timeout encompasses all attempts, not per-attempt
927
+ Get detailed metrics for every request:
730
928
 
731
929
  ```typescript
732
- const result = await stableFunction({
733
- fn: slowFunction,
734
- args: [],
735
- attempts: 5,
736
- wait: 1000,
737
- executionTimeout: 3000, // Total time for all 5 attempts
738
- });
930
+ import { stableRequest, REQUEST_METHODS, RETRY_STRATEGIES } from 'stable-request';
931
+ import type {
932
+ STABLE_REQUEST_RESULT,
933
+ MetricsGuardrails,
934
+ StableRequestMetrics
935
+ } from 'stable-request';
739
936
 
740
- // If each attempt takes 800ms:
741
- // - Attempt 1: 800ms
742
- // - Attempt 2: starts at 1800ms (after 1s wait)
743
- // - Attempt 3: would start at 3600ms → TIMEOUT at 3000ms
744
- ```
745
-
746
- ### Retry Strategies
747
-
748
- When a request or function fails and is retryable, retry with configurable backoff.
937
+ interface ApiResponse {
938
+ data: string[];
939
+ }
749
940
 
750
- #### FIXED Strategy
941
+ (async () => {
942
+ const metricsGuardrails: MetricsGuardrails = {
943
+ request: {
944
+ totalAttempts: { max: 5 },
945
+ totalExecutionTime: { max: 10000 },
946
+ failedAttempts: { max: 2 }
947
+ }
948
+ };
751
949
 
752
- Constant wait between retries.
950
+ const result: STABLE_REQUEST_RESULT<ApiResponse> = await stableRequest<void, ApiResponse>({
951
+ reqData: {
952
+ hostname: 'api.example.com',
953
+ path: '/data',
954
+ method: REQUEST_METHODS.GET
955
+ },
956
+ resReq: true,
957
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
958
+ logAllErrors: true,
959
+ logAllSuccessfulAttempts: true,
960
+ metricsGuardrails
961
+ });
753
962
 
754
- ```typescript
755
- import { stableRequest, RETRY_STRATEGIES } from '@emmvish/stable-request';
756
-
757
- interface DataRequest {}
758
- interface DataResponse { data: any; }
759
-
760
- const result = await stableRequest<DataRequest, DataResponse>({
761
- reqData: { hostname: 'api.example.com', path: '/data' },
762
- resReq: true,
763
- attempts: 4,
764
- wait: 500,
765
- retryStrategy: RETRY_STRATEGIES.FIXED
766
- // Retries at: 500ms, 1000ms, 1500ms
767
- });
963
+ const metrics: StableRequestMetrics | undefined = result.metrics;
964
+ console.log(metrics);
965
+ // {
966
+ // totalAttempts: 3,
967
+ // successfulAttempts: 1,
968
+ // failedAttempts: 2,
969
+ // totalExecutionTime: 4532,
970
+ // averageAttemptTime: 1510,
971
+ // infrastructureMetrics: {
972
+ // circuitBreaker: { state: 'CLOSED', failurePercentage: 10, ... },
973
+ // cache: { hitRate: 45.5, missRate: 54.5, ... }
974
+ // },
975
+ // validation: { isValid: true, anomalies: [] }
976
+ // }
977
+ })();
768
978
  ```
769
979
 
770
- #### LINEAR Strategy
980
+ ### 🧪 Trial Mode (Chaos Engineering)
771
981
 
772
- Wait increases linearly with attempt number.
982
+ Test your error handling without hitting real endpoints:
773
983
 
774
984
  ```typescript
775
- const result = await stableRequest<DataRequest, DataResponse>({
776
- reqData: { hostname: 'api.example.com', path: '/data' },
777
- resReq: true,
778
- attempts: 4,
779
- wait: 100,
780
- retryStrategy: RETRY_STRATEGIES.LINEAR
781
- // Retries at: 100ms, 200ms, 300ms (wait * attempt)
782
- });
783
- ```
784
-
785
- #### EXPONENTIAL Strategy
985
+ import { stableRequest, REQUEST_METHODS } from 'stable-request';
986
+ import type { STABLE_REQUEST_RESULT, TRIAL_MODE_OPTIONS } from 'stable-request';
786
987
 
787
- Wait increases exponentially; useful for heavily loaded services.
988
+ interface ApiResponse {
989
+ data: unknown;
990
+ }
788
991
 
789
- ```typescript
790
- const result = await stableRequest<DataRequest, DataResponse>({
791
- reqData: { hostname: 'api.example.com', path: '/data' },
792
- resReq: true,
793
- attempts: 4,
794
- wait: 100,
795
- maxAllowedWait: 10000,
796
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
797
- // Retries at: 100ms, 200ms, 400ms (wait * 2^(attempt-1))
798
- // Capped at maxAllowedWait
799
- });
992
+ (async () => {
993
+ const trialMode: TRIAL_MODE_OPTIONS = {
994
+ enabled: true,
995
+ reqFailureProbability: 0.3, // 30% chance of request failure
996
+ retryFailureProbability: 0.2 // 20% chance retry is not allowed
997
+ };
998
+
999
+ const result: STABLE_REQUEST_RESULT<ApiResponse> = await stableRequest<void, ApiResponse>({
1000
+ reqData: {
1001
+ hostname: 'api.example.com',
1002
+ path: '/data',
1003
+ method: REQUEST_METHODS.GET
1004
+ },
1005
+ resReq: true,
1006
+ trialMode
1007
+ });
1008
+ })();
800
1009
  ```
801
1010
 
802
- #### Jitter
1011
+ ### 🎯 Execution Context
803
1012
 
804
- Add random milliseconds to prevent synchronization.
1013
+ Track requests across distributed systems:
805
1014
 
806
1015
  ```typescript
807
- const result = await stableRequest<DataRequest, DataResponse>({
808
- reqData: { hostname: 'api.example.com', path: '/data' },
809
- resReq: true,
810
- attempts: 3,
811
- wait: 500,
812
- jitter: 200, // Add 0-200ms randomness
813
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL
814
- });
815
- ```
816
-
817
- #### Perform All Attempts
1016
+ import { stableRequest, REQUEST_METHODS } from 'stable-request';
1017
+ import type { STABLE_REQUEST_RESULT, ExecutionContext } from 'stable-request';
818
1018
 
819
- Collect all outcomes instead of failing on first error.
1019
+ interface ApiResponse {
1020
+ data: unknown;
1021
+ }
820
1022
 
821
- ```typescript
822
- const result = await stableRequest<DataRequest, DataResponse>({
823
- reqData: { hostname: 'api.example.com', path: '/data' },
824
- resReq: true,
825
- attempts: 3,
826
- performAllAttempts: true
827
- // All 3 attempts execute; check result.successfulAttempts
828
- });
1023
+ (async () => {
1024
+ const executionContext: ExecutionContext = {
1025
+ workflowId: 'order-processing-123',
1026
+ phaseId: 'payment-validation',
1027
+ requestId: 'req-456'
1028
+ };
1029
+
1030
+ const result: STABLE_REQUEST_RESULT<ApiResponse> = await stableRequest<void, ApiResponse>({
1031
+ reqData: {
1032
+ hostname: 'api.example.com',
1033
+ path: '/data',
1034
+ method: REQUEST_METHODS.GET
1035
+ },
1036
+ resReq: true,
1037
+ executionContext
1038
+ });
1039
+ // Logs will include: [Workflow: order-processing-123] [Phase: payment-validation] [Request: req-456]
1040
+ })();
829
1041
  ```
830
1042
 
831
- ### Circuit Breaker
1043
+ ## StableBuffer API
832
1044
 
833
- Prevent cascading failures by failing fast when a dependency becomes unhealthy.
1045
+ ### Constructor Options
834
1046
 
835
1047
  ```typescript
836
- import { stableApiGateway, CircuitBreaker } from '@emmvish/stable-request';
837
-
838
- interface FlakyRequest {}
839
- interface FlakyResponse { status: string; }
1048
+ import { StableBuffer } from 'stable-request';
1049
+ import type { StableBufferOptions, StableBufferTransactionLog, MetricsGuardrailsStableBuffer, StableBufferState } from 'stable-request';
840
1050
 
841
- const breaker = new CircuitBreaker({
842
- failureThresholdPercentage: 50,
843
- minimumRequests: 10,
844
- recoveryTimeoutMs: 30000,
845
- successThresholdPercentage: 80,
846
- halfOpenMaxRequests: 5
847
- });
1051
+ const bufferOptions: StableBufferOptions = {
1052
+ initialState: {}, // Starting state
1053
+ clone: (state: StableBufferState): StableBufferState => ({ ...state }), // Custom cloning function
1054
+ transactionTimeoutMs: 5000, // Transaction timeout
1055
+ logTransaction: async (log: StableBufferTransactionLog): Promise<void> => {}, // Transaction logger
1056
+ metricsGuardrails: { // Validation rules
1057
+ totalTransactions: { max: 1000 },
1058
+ averageQueueWaitMs: { max: 100 }
1059
+ }
1060
+ };
848
1061
 
849
- const requests = [
850
- { id: 'req-1', requestOptions: { reqData: { path: '/flaky' }, resReq: true } },
851
- { id: 'req-2', requestOptions: { reqData: { path: '/flaky' }, resReq: true } }
852
- ];
1062
+ const buffer = new StableBuffer(bufferOptions);
1063
+ ```
853
1064
 
854
- const responses = await stableApiGateway<FlakyRequest, FlakyResponse>(requests, {
855
- circuitBreaker: breaker
856
- });
1065
+ ### Methods
857
1066
 
858
- // Circuit breaker states:
859
- // CLOSED: Normal operation (accept all requests)
860
- // OPEN: Too many failures; reject immediately
861
- // HALF_OPEN: Testing recovery; allow limited requests
862
- ```
1067
+ ```typescript
1068
+ import { StableBuffer } from 'stable-request';
1069
+ import type { StableBufferMetrics, StableBufferState } from 'stable-request';
863
1070
 
864
- **State Transitions:**
1071
+ interface BufferState extends StableBufferState {
1072
+ value: string;
1073
+ counter: number;
1074
+ newState?: boolean;
1075
+ }
865
1076
 
866
- - **CLOSED OPEN:** Failure rate exceeds threshold after minimum requests
867
- - **OPEN HALF_OPEN:** Recovery timeout elapsed; attempt recovery
868
- - **HALF_OPEN → CLOSED:** Success rate exceeds recovery threshold
869
- - **HALF_OPEN → OPEN:** Success rate below recovery threshold; reopen
1077
+ const buffer = new StableBuffer({
1078
+ initialState: { value: '', counter: 0 } as BufferState
1079
+ });
1080
+
1081
+ (async () => {
1082
+ // Read current state (cloned)
1083
+ const state = buffer.read() as BufferState;
1084
+
1085
+ // Get direct state reference
1086
+ const directState = buffer.getState() as BufferState;
1087
+
1088
+ // Set entire state
1089
+ buffer.setState({ value: '', counter: 0, newState: true } as BufferState);
1090
+
1091
+ // Run transaction
1092
+ const result: string = await buffer.run(async (state): Promise<string> => {
1093
+ const typedState = state as BufferState;
1094
+ typedState.value = 'updated';
1095
+ return typedState.value;
1096
+ });
1097
+
1098
+ // Update state (no return value)
1099
+ await buffer.run(async (state): Promise<void> => {
1100
+ const typedState = state as BufferState;
1101
+ typedState.counter += 1;
1102
+ });
1103
+
1104
+ // Get metrics
1105
+ const metrics: StableBufferMetrics = buffer.getMetrics();
1106
+ })();
1107
+ ```
1108
+
1109
+ ### Transaction Logs
1110
+
1111
+ Each transaction generates a log entry:
1112
+
1113
+ ```typescript
1114
+ interface StableBufferTransactionLog {
1115
+ transactionId: string;
1116
+ queuedAt: string;
1117
+ startedAt: string;
1118
+ finishedAt: string;
1119
+ durationMs: number;
1120
+ queueWaitMs: number;
1121
+ success: boolean;
1122
+ errorMessage?: string;
1123
+ stateBefore: Record<string, any>;
1124
+ stateAfter: Record<string, any>;
1125
+ activity?: string;
1126
+ hookName?: string;
1127
+ hookParams?: any;
1128
+ workflowId?: string;
1129
+ branchId?: string;
1130
+ phaseId?: string;
1131
+ requestId?: string;
1132
+ }
1133
+ ```
870
1134
 
871
- ### Caching
1135
+ ## Infrastructure Persistence
872
1136
 
873
- Cache responses to avoid redundant calls.
1137
+ Persist circuit breaker and cache state for recovery:
874
1138
 
875
1139
  ```typescript
876
- import { stableRequest, CacheManager } from '@emmvish/stable-request';
1140
+ import { stableRequest, StableBuffer, REQUEST_METHODS } from 'stable-request';
1141
+ import type {
1142
+ STABLE_REQUEST_RESULT,
1143
+ CircuitBreakerConfig,
1144
+ CacheConfig,
1145
+ CircuitBreakerPersistedState,
1146
+ CacheManagerPersistedState,
1147
+ InfrastructurePersistence
1148
+ } from 'stable-request';
877
1149
 
878
- interface UserRequest {}
879
- interface UserResponse {
880
- id: number;
881
- name: string;
882
- email: string;
883
- }
884
-
885
- const cache = new CacheManager({
886
- enabled: true,
887
- ttl: 5000 // 5 seconds
888
- });
889
-
890
- // First call: cache miss, hits API
891
- const result1 = await stableRequest<UserRequest, UserResponse>({
892
- reqData: { hostname: 'api.example.com', path: '/user/1' },
893
- resReq: true,
894
- cache
895
- });
896
-
897
- // Second call within 5s: cache hit, returns cached response
898
- const result2 = await stableRequest<UserRequest, UserResponse>({
899
- reqData: { hostname: 'api.example.com', path: '/user/1' },
900
- resReq: true,
901
- cache
902
- });
903
-
904
- // Respects Cache-Control headers if enabled
905
- const cache2 = new CacheManager({
906
- enabled: true,
907
- ttl: 60000,
908
- respectCacheControl: true // Uses max-age, no-cache, no-store
909
- });
910
- ```
911
-
912
- **Function Caching:**
913
-
914
- Arguments become cache key; identical args hit cache.
915
-
916
- ```typescript
917
- import { stableFunction } from '@emmvish/stable-request';
918
-
919
- const expensive = (x: number) => x * x * x; // Cubic calculation
920
-
921
- const result1 = await stableFunction({
922
- fn: expensive,
923
- args: [5],
924
- returnResult: true,
925
- cache: { enabled: true, ttl: 10000 }
926
- });
927
-
928
- const result2 = await stableFunction({
929
- fn: expensive,
930
- args: [5], // Same args → cache hit
931
- returnResult: true,
932
- cache: { enabled: true, ttl: 10000 }
933
- });
934
- ```
935
-
936
- ### Rate Limiting
937
-
938
- Enforce max requests per time window.
939
-
940
- ```typescript
941
- import { stableApiGateway } from '@emmvish/stable-request';
942
-
943
- interface ItemRequest {}
944
- interface ItemResponse {
945
- id: number;
946
- data: any;
947
- }
948
-
949
- const requests = Array.from({ length: 20 }, (_, i) => ({
950
- id: `req-${i}`,
951
- requestOptions: {
952
- reqData: { path: `/item/${i}` },
953
- resReq: true
954
- }
955
- }));
956
-
957
- const responses = await stableApiGateway<ItemRequest, ItemResponse>(requests, {
958
- concurrentExecution: true,
959
- rateLimit: {
960
- maxRequests: 5,
961
- windowMs: 1000 // 5 requests per second
962
- }
963
- // Requests queued until window allows; prevents overwhelming API
964
- });
965
- ```
966
-
967
- ### Concurrency Limiting
968
-
969
- Limit concurrent in-flight requests.
970
-
971
- ```typescript
972
- import { stableApiGateway } from '@emmvish/stable-request';
973
-
974
- interface ItemRequest {}
975
- interface ItemResponse {
976
- id: number;
977
- data: any;
978
- }
979
-
980
- const requests = Array.from({ length: 50 }, (_, i) => ({
981
- id: `req-${i}`,
982
- requestOptions: {
983
- reqData: { path: `/item/${i}` },
984
- resReq: true,
985
- attempts: 1
986
- }
987
- }));
988
-
989
- const responses = await stableApiGateway<ItemRequest, ItemResponse>(requests, {
990
- concurrentExecution: true,
991
- maxConcurrentRequests: 5 // Only 5 requests in-flight at a time
992
- // Others queued and executed as slots free
993
- });
994
- ```
995
-
996
- ---
997
-
998
- ## Workflow Patterns
999
-
1000
- ### Sequential & Concurrent Phases
1001
-
1002
- #### Sequential (Default)
1003
-
1004
- Each phase waits for the previous to complete.
1005
-
1006
- ```typescript
1007
- import { stableWorkflow } from '@emmvish/stable-request';
1008
- import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1009
-
1010
- const phases: STABLE_WORKFLOW_PHASE[] = [
1011
- {
1012
- id: 'phase-1',
1013
- requests: [{ id: 'r1', requestOptions: { reqData: { path: '/p1' }, resReq: true } }]
1014
- },
1015
- {
1016
- id: 'phase-2',
1017
- requests: [{ id: 'r2', requestOptions: { reqData: { path: '/p2' }, resReq: true } }]
1018
- },
1019
- {
1020
- id: 'phase-3',
1021
- requests: [{ id: 'r3', requestOptions: { reqData: { path: '/p3' }, resReq: true } }]
1022
- }
1023
- ];
1024
-
1025
- const result = await stableWorkflow(phases, {
1026
- workflowId: 'sequential-phases',
1027
- concurrentPhaseExecution: false // Phase-1 → Phase-2 → Phase-3
1028
- });
1029
- ```
1030
-
1031
- #### Concurrent Phases
1032
-
1033
- Multiple phases run in parallel.
1034
-
1035
- ```typescript
1036
- const phases: STABLE_WORKFLOW_PHASE[] = [
1037
- {
1038
- id: 'fetch-users',
1039
- requests: [{ id: 'get-users', requestOptions: { reqData: { path: '/users' }, resReq: true } }]
1040
- },
1041
- {
1042
- id: 'fetch-posts',
1043
- requests: [{ id: 'get-posts', requestOptions: { reqData: { path: '/posts' }, resReq: true } }]
1044
- },
1045
- {
1046
- id: 'fetch-comments',
1047
- requests: [{ id: 'get-comments', requestOptions: { reqData: { path: '/comments' }, resReq: true } }]
1048
- }
1049
- ];
1050
-
1051
- const result = await stableWorkflow(phases, {
1052
- workflowId: 'parallel-phases',
1053
- concurrentPhaseExecution: true // All 3 phases in parallel
1054
- });
1055
- ```
1056
-
1057
- #### Mixed Phases
1058
-
1059
- Combine sequential and concurrent phases in one workflow.
1060
-
1061
- ```typescript
1062
- const phases: STABLE_WORKFLOW_PHASE[] = [
1063
- {
1064
- id: 'init', // Sequential
1065
- requests: [{ id: 'setup', requestOptions: { reqData: { path: '/init' }, resReq: true } }]
1066
- },
1067
- {
1068
- id: 'fetch-a',
1069
- markConcurrentPhase: true, // Concurrent with next
1070
- requests: [{ id: 'data-a', requestOptions: { reqData: { path: '/a' }, resReq: true } }]
1071
- },
1072
- {
1073
- id: 'fetch-b',
1074
- markConcurrentPhase: true, // Concurrent with fetch-a
1075
- requests: [{ id: 'data-b', requestOptions: { reqData: { path: '/b' }, resReq: true } }]
1076
- },
1077
- {
1078
- id: 'finalize', // Sequential after fetch-a/b complete
1079
- requests: [{ id: 'done', requestOptions: { reqData: { path: '/finalize' }, resReq: true } }]
1080
- }
1081
- ];
1082
-
1083
- const result = await stableWorkflow(phases, {
1084
- concurrentPhaseExecution: false // Respects markConcurrentPhase per phase
1085
- });
1086
- ```
1087
-
1088
- ### Non-Linear Workflows
1089
-
1090
- Use decision hooks to dynamically control phase flow.
1091
-
1092
- #### CONTINUE
1093
-
1094
- Standard flow to next sequential phase.
1095
-
1096
- ```typescript
1097
- const phases: STABLE_WORKFLOW_PHASE[] = [
1098
- {
1099
- id: 'check-status',
1100
- requests: [{ id: 'api', requestOptions: { reqData: { path: '/status' }, resReq: true } }],
1101
- phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
1102
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1103
- }
1104
- },
1105
- {
1106
- id: 'process', // Executes after check-status
1107
- requests: [{ id: 'process-data', requestOptions: { reqData: { path: '/process' }, resReq: true } }]
1108
- }
1109
- ];
1110
-
1111
- const result = await stableWorkflow(phases, {
1112
- enableNonLinearExecution: true
1113
- });
1114
- ```
1115
-
1116
- #### SKIP
1117
-
1118
- Skip the next phase; execute the one after.
1119
-
1120
- ```typescript
1121
- const phases: STABLE_WORKFLOW_PHASE[] = [
1122
- {
1123
- id: 'phase-1',
1124
- requests: [{ id: 'r1', requestOptions: { reqData: { path: '/p1' }, resReq: true } }],
1125
- phaseDecisionHook: async () => ({
1126
- action: PHASE_DECISION_ACTIONS.SKIP
1127
- })
1128
- },
1129
- {
1130
- id: 'phase-2', // Skipped
1131
- requests: [{ id: 'r2', requestOptions: { reqData: { path: '/p2' }, resReq: true } }]
1132
- },
1133
- {
1134
- id: 'phase-3', // Executes
1135
- requests: [{ id: 'r3', requestOptions: { reqData: { path: '/p3' }, resReq: true } }]
1136
- }
1137
- ];
1138
-
1139
- const result = await stableWorkflow(phases, {
1140
- enableNonLinearExecution: true
1141
- });
1142
-
1143
- // Execution: phase-1 → phase-3
1144
- ```
1145
-
1146
- #### JUMP
1147
-
1148
- Jump to a specific phase by ID.
1149
-
1150
- ```typescript
1151
- const phases: STABLE_WORKFLOW_PHASE[] = [
1152
- {
1153
- id: 'phase-1',
1154
- requests: [{ id: 'r1', requestOptions: { reqData: { path: '/p1' }, resReq: true } }],
1155
- phaseDecisionHook: async () => ({
1156
- action: PHASE_DECISION_ACTIONS.JUMP,
1157
- targetPhaseId: 'recovery'
1158
- })
1159
- },
1160
- {
1161
- id: 'phase-2', // Skipped
1162
- requests: [{ id: 'r2', requestOptions: { reqData: { path: '/p2' }, resReq: true } }]
1163
- },
1164
- {
1165
- id: 'recovery',
1166
- requests: [{ id: 'recover', requestOptions: { reqData: { path: '/recovery' }, resReq: true } }]
1167
- }
1168
- ];
1169
-
1170
- const result = await stableWorkflow(phases, {
1171
- enableNonLinearExecution: true
1172
- });
1173
-
1174
- // Execution: phase-1 → recovery
1175
- ```
1176
-
1177
- #### REPLAY
1178
-
1179
- Re-execute current phase; useful for polling.
1180
-
1181
- ```typescript
1182
- const phases: STABLE_WORKFLOW_PHASE[] = [
1183
- {
1184
- id: 'wait-for-job',
1185
- allowReplay: true,
1186
- maxReplayCount: 5,
1187
- requests: [
1188
- {
1189
- id: 'check-job',
1190
- requestOptions: { reqData: { path: '/job/status' }, resReq: true, attempts: 1 }
1191
- }
1192
- ],
1193
- phaseDecisionHook: async ({ phaseResult, executionHistory }) => {
1194
- const lastResponse = phaseResult.responses?.[0];
1195
- if ((lastResponse as any)?.data?.status === 'pending' && executionHistory.length < 5) {
1196
- return { action: PHASE_DECISION_ACTIONS.REPLAY };
1197
- }
1198
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1199
- }
1200
- },
1201
- {
1202
- id: 'process-result',
1203
- requests: [{ id: 'process', requestOptions: { reqData: { path: '/process' }, resReq: true } }]
1204
- }
1205
- ];
1206
-
1207
- const result = await stableWorkflow(phases, {
1208
- enableNonLinearExecution: true,
1209
- maxWorkflowIterations: 100
1210
- });
1211
-
1212
- // Polls up to 5 times before continuing
1213
- ```
1214
-
1215
- #### TERMINATE
1216
-
1217
- Stop workflow early.
1218
-
1219
- ```typescript
1220
- const phases: STABLE_WORKFLOW_PHASE[] = [
1221
- {
1222
- id: 'validate',
1223
- requests: [{ id: 'validate-input', requestOptions: { reqData: { path: '/validate' }, resReq: true } }],
1224
- phaseDecisionHook: async ({ phaseResult, sharedBuffer }) => {
1225
- if (!phaseResult.success) {
1226
- return { action: PHASE_DECISION_ACTIONS.TERMINATE };
1227
- }
1228
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1229
- }
1230
- },
1231
- {
1232
- id: 'phase-2', // Won't execute if validation fails
1233
- requests: [{ id: 'r2', requestOptions: { reqData: { path: '/p2' }, resReq: true } }]
1234
- }
1235
- ];
1236
-
1237
- const result = await stableWorkflow(phases, {
1238
- enableNonLinearExecution: true
1239
- });
1240
-
1241
- console.log(result.terminatedEarly); // true if TERMINATE triggered
1242
- ```
1243
-
1244
- ### Branched Workflows
1245
-
1246
- Execute multiple independent branches with shared state.
1247
-
1248
- ```typescript
1249
- import { stableWorkflow } from '@emmvish/stable-request';
1250
- import type { STABLE_WORKFLOW_BRANCH } from '@emmvish/stable-request';
1251
-
1252
- const branches: STABLE_WORKFLOW_BRANCH[] = [
1253
- {
1254
- id: 'branch-payment',
1255
- phases: [
1256
- {
1257
- id: 'process-payment',
1258
- requests: [
1259
- {
1260
- id: 'charge-card',
1261
- requestOptions: {
1262
- reqData: { path: '/payment/charge' },
1263
- resReq: true
1264
- }
1265
- }
1266
- ]
1267
- }
1268
- ]
1269
- },
1270
- {
1271
- id: 'branch-notification',
1272
- phases: [
1273
- {
1274
- id: 'send-email',
1275
- requests: [
1276
- {
1277
- id: 'send',
1278
- requestOptions: {
1279
- reqData: { path: '/notify/email' },
1280
- resReq: false
1281
- }
1282
- }
1283
- ]
1284
- }
1285
- ]
1286
- }
1287
- ];
1288
-
1289
- const result = await stableWorkflow([], {
1290
- workflowId: 'checkout',
1291
- enableBranchExecution: true,
1292
- branches,
1293
- sharedBuffer: { orderId: '12345' },
1294
- markConcurrentBranch: true // Branches run in parallel
1295
- });
1296
-
1297
- // Both branches access/modify sharedBuffer
1298
- ```
1299
-
1300
- #### Branch Racing
1301
-
1302
- When multiple branches execute concurrently, enable racing to accept the first successful branch and cancel others.
1303
-
1304
- ```typescript
1305
- const result = await stableWorkflow([], {
1306
- workflowId: 'payment-racing',
1307
- enableBranchExecution: true,
1308
- enableBranchRacing: true, // First successful branch wins
1309
- branches: [
1310
- {
1311
- id: 'payment-provider-a',
1312
- phases: [/* ... */]
1313
- },
1314
- {
1315
- id: 'payment-provider-b',
1316
- phases: [/* ... */]
1317
- }
1318
- ],
1319
- markConcurrentBranch: true
1320
- });
1321
-
1322
- // Only winning branch's execution history recorded
1323
- // Losing branches marked as cancelled
1324
- ```
1325
-
1326
- ## Graph-based Workflow Patterns
1327
-
1328
- **Key responsibilities:**
1329
- - Define phases as DAG nodes with explicit dependency edges
1330
- - Execute independent phases in parallel automatically
1331
- - Support parallel groups, merge points, and conditional routing
1332
- - Validate graph structure (cycle detection, reachability, orphan detection)
1333
- - Provide deterministic execution order
1334
- - Offer higher parallelism than phased workflows for complex topologies
1335
-
1336
- ### Graph-Based Workflows with Mixed Items
1337
-
1338
- For complex topologies with explicit dependencies, use DAG execution mixing requests and functions.
1339
-
1340
- ```typescript
1341
- import { stableWorkflowGraph, WorkflowGraphBuilder, RequestOrFunction } from '@emmvish/stable-request';
1342
- import type { API_GATEWAY_ITEM } from '@emmvish/stable-request';
1343
-
1344
- // Request types
1345
- interface PostsRequest {}
1346
- interface PostsResponse { posts: Array<{ id: number; title: string }> };
1347
-
1348
- interface UsersRequest {}
1349
- interface UsersResponse { users: Array<{ id: number; name: string }> };
1350
-
1351
- // Function types
1352
- type AggregateArgs = [PostsResponse, UsersResponse];
1353
- type AggregateResult = {
1354
- combined: Array<{ userId: number; userName: string; postCount: number }>;
1355
- };
1356
-
1357
- type AnalyzeArgs = [AggregateResult];
1358
- type AnalyzeResult = { totalPosts: number; activeUsers: number };
1359
-
1360
- const graph = new WorkflowGraphBuilder<
1361
- PostsRequest | UsersRequest,
1362
- PostsResponse | UsersResponse,
1363
- AggregateArgs | AnalyzeArgs,
1364
- AggregateResult | AnalyzeResult
1365
- >()
1366
- .addPhase('fetch-posts', {
1367
- requests: [{
1368
- id: 'get-posts',
1369
- requestOptions: {
1370
- reqData: { path: '/posts' },
1371
- resReq: true
1372
- }
1373
- }]
1374
- })
1375
- .addPhase('fetch-users', {
1376
- requests: [{
1377
- id: 'get-users',
1378
- requestOptions: {
1379
- reqData: { path: '/users' },
1380
- resReq: true
1381
- }
1382
- }]
1383
- })
1384
- .addParallelGroup('fetch-all', ['fetch-posts', 'fetch-users'])
1385
- .addPhase('aggregate', {
1386
- functions: [{
1387
- id: 'combine-data',
1388
- functionOptions: {
1389
- fn: (posts: PostsResponse, users: UsersResponse): AggregateResult => ({
1390
- combined: users.users.map(user => ({
1391
- userId: user.id,
1392
- userName: user.name,
1393
- postCount: posts.posts.filter(p => p.id === user.id).length
1394
- }))
1395
- }),
1396
- args: [{ posts: [] }, { users: [] }] as AggregateArgs,
1397
- returnResult: true
1398
- }
1399
- }]
1400
- })
1401
- .addPhase('analyze', {
1402
- functions: [{
1403
- id: 'analyze-data',
1404
- functionOptions: {
1405
- fn: (aggregated: AggregateResult): AnalyzeResult => ({
1406
- totalPosts: aggregated.combined.reduce((sum, u) => sum + u.postCount, 0),
1407
- activeUsers: aggregated.combined.filter(u => u.postCount > 0).length
1408
- }),
1409
- args: [{ combined: [] }] as AnalyzeArgs,
1410
- returnResult: true
1411
- }
1412
- }]
1413
- })
1414
- .addMergePoint('sync', ['fetch-all'])
1415
- .connectSequence('fetch-all', 'sync', 'aggregate', 'analyze')
1416
- .setEntryPoint('fetch-all')
1417
- .build();
1418
-
1419
- const result = await stableWorkflowGraph(graph, {
1420
- workflowId: 'data-aggregation'
1421
- });
1422
-
1423
- console.log(`Graph workflow success: ${result.success}`);
1424
- ```
1425
-
1426
- ### Parallel Phase Execution
1427
-
1428
- Execute multiple phases concurrently within a group.
1429
-
1430
- ```typescript
1431
- import { stableWorkflowGraph, WorkflowGraphBuilder } from '@emmvish/stable-request';
1432
-
1433
- const graph = new WorkflowGraphBuilder()
1434
- .addPhase('fetch-users', {
1435
- requests: [{
1436
- id: 'users',
1437
- requestOptions: { reqData: { path: '/users' }, resReq: true }
1438
- }]
1439
- })
1440
- .addPhase('fetch-posts', {
1441
- requests: [{
1442
- id: 'posts',
1443
- requestOptions: { reqData: { path: '/posts' }, resReq: true }
1444
- }]
1445
- })
1446
- .addPhase('fetch-comments', {
1447
- requests: [{
1448
- id: 'comments',
1449
- requestOptions: { reqData: { path: '/comments' }, resReq: true }
1450
- }]
1451
- })
1452
- .addParallelGroup('data-fetch', ['fetch-users', 'fetch-posts', 'fetch-comments'])
1453
- .setEntryPoint('data-fetch')
1454
- .build();
1455
-
1456
- const result = await stableWorkflowGraph(graph, {
1457
- workflowId: 'data-aggregation'
1458
- });
1459
-
1460
- // All 3 phases run concurrently
1461
- ```
1462
-
1463
- ### Merge Points
1464
-
1465
- Synchronize multiple predecessor phases.
1466
-
1467
- ```typescript
1468
- const graph = new WorkflowGraphBuilder()
1469
- .addPhase('fetch-a', {
1470
- requests: [{ id: 'a', requestOptions: { reqData: { path: '/a' }, resReq: true } }]
1471
- })
1472
- .addPhase('fetch-b', {
1473
- requests: [{ id: 'b', requestOptions: { reqData: { path: '/b' }, resReq: true } }]
1474
- })
1475
- .addMergePoint('sync', ['fetch-a', 'fetch-b'])
1476
- .addPhase('aggregate', {
1477
- functions: [{
1478
- id: 'combine',
1479
- functionOptions: {
1480
- fn: () => 'combined',
1481
- args: [],
1482
- returnResult: true
1483
- }
1484
- }]
1485
- })
1486
- .connectSequence('fetch-a', 'sync')
1487
- .connectSequence('fetch-b', 'sync')
1488
- .connectSequence('sync', 'aggregate')
1489
- .setEntryPoint('fetch-a')
1490
- .build();
1491
-
1492
- const result = await stableWorkflowGraph(graph, {
1493
- workflowId: 'parallel-sync'
1494
- });
1495
-
1496
- // fetch-a and fetch-b run in parallel
1497
- // aggregate waits for both to complete
1498
- ```
1499
-
1500
- ### Linear Helper
1501
-
1502
- Convenience function for sequential phase chains.
1503
-
1504
- ```typescript
1505
- import { createLinearWorkflowGraph } from '@emmvish/stable-request';
1506
-
1507
- const phases = [
1508
- {
1509
- id: 'init',
1510
- requests: [{ id: 'setup', requestOptions: { reqData: { path: '/init' }, resReq: true } }]
1511
- },
1512
- {
1513
- id: 'process',
1514
- requests: [{ id: 'do-work', requestOptions: { reqData: { path: '/work' }, resReq: true } }]
1515
- },
1516
- {
1517
- id: 'finalize',
1518
- requests: [{ id: 'cleanup', requestOptions: { reqData: { path: '/cleanup' }, resReq: true } }]
1519
- }
1520
- ];
1521
-
1522
- const graph = createLinearWorkflowGraph(phases);
1523
-
1524
- const result = await stableWorkflowGraph(graph, {
1525
- workflowId: 'linear-workflow'
1526
- });
1527
- ```
1528
-
1529
- ### Branch Racing in Graphs
1530
-
1531
- Enable branch racing in workflow graphs to accept the first successful branch node when multiple branches are executed in parallel.
1532
-
1533
- ```typescript
1534
- import { stableWorkflowGraph, WorkflowGraphBuilder } from '@emmvish/stable-request';
1535
-
1536
- const branch1 = {
1537
- id: 'provider-a',
1538
- phases: [{ /* ... */ }]
1539
- };
1540
-
1541
- const branch2 = {
1542
- id: 'provider-b',
1543
- phases: [{ /* ... */ }]
1544
- };
1545
-
1546
- const graph = new WorkflowGraphBuilder()
1547
- .addBranch('provider-a', branch1)
1548
- .addBranch('provider-b', branch2)
1549
- .addParallelGroup('race', ['provider-a', 'provider-b'])
1550
- .setEntryPoint('race')
1551
- .build();
1552
-
1553
- const result = await stableWorkflowGraph(graph, {
1554
- workflowId: 'provider-racing',
1555
- enableBranchRacing: true // First successful branch wins
1556
- });
1557
-
1558
- // Only winning branch's results recorded
1559
- // Losing branch marked as cancelled
1560
- ```
1561
-
1562
- ---
1563
-
1564
- ## Configuration & State
1565
-
1566
- ### Config Cascading
1567
-
1568
- Define defaults globally; override at group, phase, branch, or item level.
1569
-
1570
- ```typescript
1571
- import { stableWorkflow } from '@emmvish/stable-request';
1572
- import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1573
-
1574
- const phases: STABLE_WORKFLOW_PHASE[] = [
1575
- {
1576
- id: 'phase-1',
1577
- attempts: 5, // Override global attempts for this phase
1578
- wait: 1000,
1579
- requests: [
1580
- {
1581
- id: 'req-1',
1582
- requestOptions: {
1583
- reqData: { path: '/data' },
1584
- resReq: true,
1585
- attempts: 2 // Override phase attempts for this item
1586
- }
1587
- }
1588
- ]
1589
- }
1590
- ];
1591
-
1592
- const result = await stableWorkflow(phases, {
1593
- workflowId: 'cascade-demo',
1594
- commonAttempts: 1, // Global default
1595
- commonWait: 500,
1596
- retryStrategy: 'LINEAR' // Global default
1597
- // Final config per item: merge common → phase → request
1598
- });
1599
- ```
1600
-
1601
- Hierarchy: global → group → phase → branch → item. Lower levels override.
1602
-
1603
- ### Shared & State Buffers
1604
-
1605
- Pass mutable state across phases, branches, and items. For concurrency-safe shared state, pass a `StableBuffer` instance instead of a plain object.
1606
-
1607
- #### Shared Buffer (Workflow/Gateway)
1608
-
1609
- ```typescript
1610
- import { stableWorkflow } from '@emmvish/stable-request';
1611
- import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1612
-
1613
- const phases: STABLE_WORKFLOW_PHASE[] = [
1614
- {
1615
- id: 'fetch',
1616
- requests: [
1617
- {
1618
- id: 'user-data',
1619
- requestOptions: {
1620
- reqData: { path: '/users/1' },
1621
- resReq: true,
1622
- handleSuccessfulAttemptData: ({ successfulAttemptData, stableRequestOptions }) => {
1623
- // Mutate shared buffer
1624
- const sharedBuffer = (stableRequestOptions as any).sharedBuffer;
1625
- sharedBuffer.userId = (successfulAttemptData.data as any).id;
1626
- }
1627
- }
1628
- }
1629
- ]
1630
- },
1631
- {
1632
- id: 'use-shared-data',
1633
- requests: [
1634
- {
1635
- id: 'dependent-call',
1636
- requestOptions: {
1637
- reqData: { path: '/user-posts' },
1638
- resReq: true,
1639
- preExecution: {
1640
- preExecutionHook: async ({ stableRequestOptions, commonBuffer }) => {
1641
- const sharedBuffer = (stableRequestOptions as any).sharedBuffer;
1642
- console.log(`Using userId: ${sharedBuffer.userId}`);
1643
- }
1644
- }
1645
- }
1646
- }
1647
- ]
1648
- }
1649
- ];
1650
-
1651
- const result = await stableWorkflow(phases, {
1652
- workflowId: 'shared-state-demo',
1653
- sharedBuffer: {} // Mutable across phases
1654
- });
1655
- ```
1656
-
1657
- #### Common Buffer (Request Level)
1658
-
1659
- ```typescript
1660
- import { stableRequest, PersistenceStage } from '@emmvish/stable-request';
1661
-
1662
- const commonBuffer = { transactionId: null };
1663
-
1664
- const result = await stableRequest({
1665
- reqData: { path: '/transaction/start' },
1666
- resReq: true,
1667
- commonBuffer,
1668
- preExecution: {
1669
- preExecutionHook: async ({ commonBuffer, stableRequestOptions }) => {
1670
- // commonBuffer writable here
1671
- commonBuffer.userId = '123';
1672
- }
1673
- },
1674
- handleSuccessfulAttemptData: ({ successfulAttemptData }) => {
1675
- // commonBuffer readable in handlers
1676
- console.log(`Transaction for user ${commonBuffer.userId} done`);
1677
- }
1678
- });
1679
- ```
1680
-
1681
- ---
1682
-
1683
- ## Hooks & Observability
1684
-
1685
- ### Pre-Execution Hooks
1686
-
1687
- Modify config or state before execution.
1688
-
1689
- ```typescript
1690
- import { stableRequest } from '@emmvish/stable-request';
1691
-
1692
- interface SecureRequest {}
1693
- interface SecureResponse {
1694
- data: any;
1695
- token?: string;
1696
- }
1697
-
1698
- const result = await stableRequest<SecureRequest, SecureResponse>({
1699
- reqData: { path: '/secure-data' },
1700
- resReq: true,
1701
- preExecution: {
1702
- preExecutionHook: async ({ inputParams, commonBuffer, stableRequestOptions }) => {
1703
- // Dynamically fetch auth token
1704
- const token = await getAuthToken();
1705
-
1706
- // Return partial config override
1707
- return {
1708
- reqData: {
1709
- headers: { Authorization: `Bearer ${token}` }
1710
- }
1711
- };
1712
- },
1713
- preExecutionHookParams: { context: 'auth-fetch' },
1714
- applyPreExecutionConfigOverride: true,
1715
- continueOnPreExecutionHookFailure: false
1716
- }
1717
- });
1718
- ```
1719
-
1720
- ### Analysis Hooks
1721
-
1722
- Validate responses and errors.
1723
-
1724
- #### Response Analyzer
1725
-
1726
- ```typescript
1727
- import { stableRequest } from '@emmvish/stable-request';
1728
-
1729
- interface ResourceRequest {}
1730
1150
  interface ApiResponse {
1731
- id: number;
1732
- status: 'active' | 'inactive';
1733
- }
1734
-
1735
- const result = await stableRequest<ResourceRequest, ApiResponse>({
1736
- reqData: { path: '/resource' },
1737
- resReq: true,
1738
- responseAnalyzer: ({ data, reqData, trialMode }) => {
1739
- // Return true to accept, false to retry
1740
- if (!data || typeof data !== 'object') return false;
1741
- if (!('id' in data)) return false;
1742
- if ((data as any).status !== 'active') return false;
1743
- return true;
1744
- }
1745
- });
1746
- ```
1747
-
1748
- #### Error Analyzer
1749
-
1750
- Decide whether to suppress error gracefully.
1751
-
1752
- ```typescript
1753
- import { stableRequest } from '@emmvish/stable-request';
1754
-
1755
- interface FeatureRequest {}
1756
- interface FeatureResponse {
1757
- enabled: boolean;
1758
- data?: any;
1759
- }
1760
-
1761
- const result = await stableRequest<FeatureRequest, FeatureResponse>({
1762
- reqData: { path: '/optional-feature' },
1763
- resReq: true,
1764
- finalErrorAnalyzer: ({ error, reqData, trialMode }) => {
1765
- // Return true to suppress error and return failure result
1766
- // Return false to throw error
1767
- if (error.code === 'ECONNREFUSED') {
1768
- console.warn('Service unavailable, continuing with fallback');
1769
- return true; // Suppress, don't throw
1770
- }
1771
- return false; // Throw
1772
- }
1773
- });
1774
-
1775
- if (result.success) {
1776
- console.log('Got data:', result.data);
1777
- } else {
1778
- console.log('Service offline, but we continue');
1779
- }
1780
- ```
1781
-
1782
- ### Handler Hooks
1783
-
1784
- Custom logging and processing.
1785
-
1786
- #### Success Handler
1787
-
1788
- ```typescript
1789
- import { stableRequest } from '@emmvish/stable-request';
1790
-
1791
- interface DataRequest {}
1792
- interface DataResponse {
1793
- id: number;
1794
- value: string;
1795
- }
1796
-
1797
- const result = await stableRequest<DataRequest, DataResponse>({
1798
- reqData: { path: '/data' },
1799
- resReq: true,
1800
- logAllSuccessfulAttempts: true,
1801
- handleSuccessfulAttemptData: ({
1802
- successfulAttemptData,
1803
- reqData,
1804
- maxSerializableChars,
1805
- executionContext
1806
- }) => {
1807
- // Custom logging, metrics, state updates
1808
- console.log(
1809
- `Success in context ${executionContext.workflowId}`,
1810
- `data:`,
1811
- successfulAttemptData.data
1812
- );
1813
- }
1814
- });
1815
- ```
1816
-
1817
- #### Error Handler
1818
-
1819
- ```typescript
1820
- const result = await stableRequest<DataRequest, DataResponse>({
1821
- reqData: { path: '/data' },
1822
- resReq: true,
1823
- logAllErrors: true,
1824
- handleErrors: ({ errorLog, reqData, executionContext }) => {
1825
- // Custom error logging, alerting, retry logic
1826
- console.error(
1827
- `Error in ${executionContext.workflowId}:`,
1828
- errorLog.errorMessage,
1829
- `Retryable: ${errorLog.isRetryable}`
1830
- );
1831
- }
1832
- });
1833
- ```
1834
-
1835
- #### Phase Handlers (Workflow)
1836
-
1837
- ```typescript
1838
- import { stableWorkflow } from '@emmvish/stable-request';
1839
- import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1840
-
1841
- const phases: STABLE_WORKFLOW_PHASE[] = [
1842
- {
1843
- id: 'phase-1',
1844
- requests: [{ id: 'r1', requestOptions: { reqData: { path: '/data' }, resReq: true } }]
1845
- }
1846
- ];
1847
-
1848
- const result = await stableWorkflow(phases, {
1849
- workflowId: 'wf-handlers',
1850
- handlePhaseCompletion: ({ phaseResult, workflowId }) => {
1851
- console.log(`Phase ${phaseResult.phaseId} complete in ${workflowId}`);
1852
- },
1853
- handlePhaseError: ({ phaseResult, error, workflowId }) => {
1854
- console.error(`Phase ${phaseResult.phaseId} failed:`, error);
1855
- },
1856
- handlePhaseDecision: ({ decision, phaseResult }) => {
1857
- console.log(`Phase decision: ${decision.action}`);
1858
- }
1859
- });
1860
- ```
1861
-
1862
- ### Decision Hooks
1863
-
1864
- Dynamically determine workflow flow.
1865
-
1866
- ```typescript
1867
- import { stableWorkflow, PHASE_DECISION_ACTIONS } from '@emmvish/stable-request';
1868
- import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1869
-
1870
- const phases: STABLE_WORKFLOW_PHASE[] = [
1871
- {
1872
- id: 'fetch-data',
1873
- requests: [{ id: 'api', requestOptions: { reqData: { path: '/data' }, resReq: true } }],
1874
- phaseDecisionHook: async ({ phaseResult, sharedBuffer, executionHistory }) => {
1875
- if (!phaseResult.success) {
1876
- return { action: PHASE_DECISION_ACTIONS.TERMINATE };
1877
- }
1878
- if (phaseResult.responses[0].data?.needsRetry) {
1879
- return { action: PHASE_DECISION_ACTIONS.REPLAY };
1880
- }
1881
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
1882
- }
1883
- }
1884
- ];
1885
-
1886
- const result = await stableWorkflow(phases, {
1887
- enableNonLinearExecution: true
1888
- });
1889
- ```
1890
-
1891
- ### Metrics & Logging
1892
-
1893
- Automatic metrics collection across all execution modes.
1894
-
1895
- #### Request Metrics
1896
-
1897
- ```typescript
1898
- import { stableRequest } from '@emmvish/stable-request';
1899
-
1900
- interface DataRequest {}
1901
- interface DataResponse { data: any; }
1902
-
1903
- const result = await stableRequest<DataRequest, DataResponse>({
1904
- reqData: { path: '/data' },
1905
- resReq: true,
1906
- attempts: 3
1907
- });
1908
-
1909
- console.log(result.metrics); // {
1910
- // totalAttempts: 2,
1911
- // successfulAttempts: 1,
1912
- // failedAttempts: 1,
1913
- // totalExecutionTime: 450,
1914
- // averageAttemptTime: 225,
1915
- // infrastructureMetrics: {
1916
- // circuitBreaker: { /* state, stats, config */ },
1917
- // cache: { /* hits, misses, size */ },
1918
- // rateLimiter: { /* limit, current rate */ },
1919
- // concurrencyLimiter: { /* limit, in-flight */ }
1920
- // },
1921
- // validation: {
1922
- // isValid: true,
1923
- // anomalies: [],
1924
- // validatedAt: '2026-01-20T...'
1925
- // }
1926
- // }
1927
- ```
1928
-
1929
- #### API Gateway Metrics
1930
-
1931
- ```typescript
1932
- import { stableApiGateway } from '@emmvish/stable-request';
1933
- import type { API_GATEWAY_REQUEST } from '@emmvish/stable-request';
1934
-
1935
- interface ApiRequest {}
1936
- interface ApiResponse { data: any; }
1937
-
1938
- const requests: API_GATEWAY_REQUEST<ApiRequest, ApiResponse>[] = [
1939
- { id: 'req-1', requestOptions: { reqData: { path: '/data/1' }, resReq: true } },
1940
- { id: 'req-2', requestOptions: { reqData: { path: '/data/2' }, resReq: true } },
1941
- { id: 'req-3', requestOptions: { reqData: { path: '/data/3' }, resReq: true } }
1942
- ];
1943
-
1944
- const result = await stableApiGateway<ApiRequest, ApiResponse>(requests, {
1945
- concurrentExecution: true,
1946
- maxConcurrentRequests: 5
1947
- });
1948
-
1949
- console.log(result.metrics); // {
1950
- // totalRequests: 3,
1951
- // successfulRequests: 3,
1952
- // failedRequests: 0,
1953
- // successRate: 100,
1954
- // failureRate: 0,
1955
- // executionTime: 450, // Total execution time in ms
1956
- // timestamp: '2026-01-20T...', // ISO 8601 completion timestamp
1957
- // throughput: 6.67, // Requests per second
1958
- // averageRequestDuration: 150, // Average time per request in ms
1959
- // requestGroups: [/* per-group stats */],
1960
- // infrastructureMetrics: {
1961
- // circuitBreaker: { /* state, stats, config */ },
1962
- // cache: { /* hit rate, size, utilization */ },
1963
- // rateLimiter: { /* throttle rate, queue length */ },
1964
- // concurrencyLimiter: { /* utilization, queue */ }
1965
- // },
1966
- // validation: {
1967
- // isValid: true,
1968
- // anomalies: [],
1969
- // validatedAt: '2026-01-20T...'
1970
- // }
1971
- // }
1972
- ```
1973
-
1974
- #### Workflow Metrics
1975
-
1976
- ```typescript
1977
- import { stableWorkflow } from '@emmvish/stable-request';
1978
- import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
1979
-
1980
- const phases: STABLE_WORKFLOW_PHASE[] = [
1981
- { id: 'p1', requests: [{ id: 'r1', requestOptions: { reqData: { path: '/a' }, resReq: true } }] },
1982
- { id: 'p2', requests: [{ id: 'r2', requestOptions: { reqData: { path: '/b' }, resReq: true } }] }
1983
- ];
1984
-
1985
- const result = await stableWorkflow(phases, {
1986
- workflowId: 'wf-metrics'
1987
- });
1988
-
1989
- console.log(result); // {
1990
- // workflowId: 'wf-metrics',
1991
- // success: true,
1992
- // totalPhases: 2,
1993
- // completedPhases: 2,
1994
- // totalRequests: 2,
1995
- // successfulRequests: 2,
1996
- // failedRequests: 0,
1997
- // workflowExecutionTime: 1200,
1998
- // phases: [
1999
- // { phaseId: 'p1', success: true, responses: [...], validation: {...}, ... },
2000
- // { phaseId: 'p2', success: true, responses: [...], validation: {...}, ... }
2001
- // ],
2002
- // validation: {
2003
- // isValid: true,
2004
- // anomalies: [],
2005
- // validatedAt: '2026-01-20T...'
2006
- // }
2007
- // }
2008
- ```
2009
-
2010
- #### Structured Error Logs
2011
-
2012
- ```typescript
2013
- const result = await stableRequest<DataRequest, DataResponse>({
2014
- reqData: { path: '/flaky' },
2015
- resReq: true,
2016
- attempts: 3,
2017
- logAllErrors: true,
2018
- handleErrors: ({ errorLog }) => {
2019
- console.log(errorLog); // {
2020
- // attempt: '1/3',
2021
- // type: 'NetworkError',
2022
- // error: 'ECONNREFUSED',
2023
- // isRetryable: true,
2024
- // timestamp: 1234567890
2025
- // }
2026
- }
2027
- });
2028
-
2029
- if (result.errorLogs) {
2030
- console.log(`${result.errorLogs.length} errors logged`);
1151
+ data: unknown;
2031
1152
  }
2032
- ```
2033
1153
 
2034
- ---
2035
-
2036
- ## Advanced Features
2037
-
2038
- ### Trial Mode
2039
-
2040
- Dry-run workflows without side effects; simulate failures.
2041
-
2042
- ```typescript
2043
- import { stableWorkflow } from '@emmvish/stable-request';
2044
- import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
2045
-
2046
- const phases: STABLE_WORKFLOW_PHASE[] = [
2047
- {
2048
- id: 'process',
2049
- requests: [
2050
- {
2051
- id: 'api-call',
2052
- requestOptions: {
2053
- reqData: { path: '/payment/charge' },
2054
- resReq: true,
2055
- trialMode: {
2056
- enabled: true,
2057
- requestFailureProbability: 0.3 // 30% simulated failure rate
2058
- }
2059
- }
2060
- }
2061
- ]
2062
- }
2063
- ];
2064
-
2065
- const result = await stableWorkflow(phases, {
2066
- workflowId: 'payment-trial',
2067
- trialMode: {
1154
+ (async () => {
1155
+ const sharedBuffer = new StableBuffer({
1156
+ initialState: {}
1157
+ });
1158
+
1159
+ const circuitBreakerPersistence: InfrastructurePersistence<CircuitBreakerPersistedState> = {
1160
+ load: async (): Promise<CircuitBreakerPersistedState | null> => await loadCircuitBreakerState(),
1161
+ store: async (state: CircuitBreakerPersistedState): Promise<void> => await saveCircuitBreakerState(state),
1162
+ buffer: sharedBuffer // Use StableBuffer for coordination
1163
+ };
1164
+
1165
+ const circuitBreakerConfig: CircuitBreakerConfig = {
1166
+ failureThresholdPercentage: 50,
1167
+ minimumRequests: 10,
1168
+ recoveryTimeoutMs: 30000,
1169
+ persistence: circuitBreakerPersistence
1170
+ };
1171
+
1172
+ const cachePersistence: InfrastructurePersistence<CacheManagerPersistedState> = {
1173
+ load: async (): Promise<CacheManagerPersistedState | null> => await loadCacheState(),
1174
+ store: async (state: CacheManagerPersistedState): Promise<void> => await saveCacheState(state)
1175
+ };
1176
+
1177
+ const cacheConfig: CacheConfig = {
2068
1178
  enabled: true,
2069
- functionFailureProbability: 0.2
2070
- }
2071
- });
2072
-
2073
- // Requests/functions execute but failures are simulated
2074
- // Real API calls happen; real side effects occur only if enabled
2075
- // Useful for testing retry logic, decision hooks, workflow topology
2076
- ```
2077
-
2078
- ### State Persistence
2079
-
2080
- Persist state across retry attempts for distributed tracing.
2081
-
2082
- The `persistenceFunction` receives a `persistenceStage` parameter (`PersistenceStage.BEFORE_HOOK` or `PersistenceStage.AFTER_HOOK`) to indicate when it is called.
2083
-
2084
- ```typescript
2085
- import { stableRequest, PersistenceStage } from '@emmvish/stable-request';
2086
-
2087
- interface DataRequest {}
2088
- interface DataResponse { data: any; }
2089
-
2090
- const result = await stableRequest<DataRequest, DataResponse>({
2091
- reqData: { path: '/data' },
2092
- resReq: true,
2093
- attempts: 3,
2094
- statePersistence: {
2095
- persistenceFunction: async ({ executionContext, buffer, params, persistenceStage }) => {
2096
- const key = `${executionContext.workflowId}:${executionContext.requestId}`;
2097
- if (persistenceStage === PersistenceStage.BEFORE_HOOK || params?.operation === 'load') {
2098
- // Load state for recovery
2099
- return await loadFromDatabase(key);
2100
- }
2101
- // Save state to database or distributed cache
2102
- await saveToDatabase({ key, state: buffer });
2103
- return buffer;
1179
+ persistence: cachePersistence
1180
+ };
1181
+
1182
+ const result: STABLE_REQUEST_RESULT<ApiResponse> = await stableRequest<void, ApiResponse>({
1183
+ reqData: {
1184
+ hostname: 'api.example.com',
1185
+ path: '/data',
1186
+ method: REQUEST_METHODS.GET
2104
1187
  },
2105
- persistenceParams: { operation: 'save' },
2106
- loadBeforeHooks: true,
2107
- storeAfterHooks: true
2108
- }
2109
- });
2110
- ```
2111
-
2112
- ### Mixed Request & Function Phases
2113
-
2114
- Combine API calls and computations in single phases with full type safety.
2115
-
2116
- ```typescript
2117
- import { stableWorkflow, RequestOrFunction } from '@emmvish/stable-request';
2118
- import type { STABLE_WORKFLOW_PHASE, API_GATEWAY_ITEM } from '@emmvish/stable-request';
2119
-
2120
- // Request types
2121
- interface ProductRequest {}\ninterface ProductResponse {
2122
- id: number;
1188
+ resReq: true,
1189
+ circuitBreaker: circuitBreakerConfig,
1190
+ cache: cacheConfig
1191
+ });
1192
+ })();
1193
+ ```
1194
+
1195
+ ## Complete Example
1196
+
1197
+ ```typescript
1198
+ import {
1199
+ stableRequest,
1200
+ StableBuffer,
1201
+ RETRY_STRATEGIES,
1202
+ REQUEST_METHODS
1203
+ } from 'stable-request';
1204
+ import type {
1205
+ STABLE_REQUEST_RESULT,
1206
+ StableBufferTransactionLog,
1207
+ CircuitBreakerConfig,
1208
+ CacheConfig,
1209
+ MetricsGuardrails,
1210
+ ExecutionContext,
1211
+ ResponseAnalysisHookOptions,
1212
+ HandleErrorHookOptions,
1213
+ ERROR_LOG
1214
+ } from 'stable-request';
1215
+
1216
+ // Define response and request types
1217
+ interface UserRequest {
2123
1218
  name: string;
2124
- price: number;
2125
- }
2126
-
2127
- interface InventoryRequest {}
2128
- interface InventoryResponse {
2129
- productId: number;
2130
- stock: number;
2131
1219
  }
2132
1220
 
2133
- // Function types
2134
- type EnrichArgs = [ProductResponse[], InventoryResponse[]];
2135
- type EnrichResult = Array<{
2136
- id: number;
2137
- name: string;
2138
- price: number;
2139
- stock: number;
2140
- inStock: boolean;
2141
- }>;
2142
-
2143
- type CalculateArgs = [EnrichResult];
2144
- type CalculateResult = {
2145
- totalValue: number;
2146
- lowStockItems: number;
2147
- };
2148
-
2149
- type NotifyArgs = [CalculateResult, string];
2150
- type NotifyResult = { notified: boolean };
2151
-
2152
- const phase: STABLE_WORKFLOW_PHASE<
2153
- ProductRequest | InventoryRequest,
2154
- ProductResponse | InventoryResponse,
2155
- EnrichArgs | CalculateArgs | NotifyArgs,
2156
- EnrichResult | CalculateResult | NotifyResult
2157
- > = {
2158
- id: 'mixed-phase',
2159
- items: [
2160
- {
2161
- type: RequestOrFunction.REQUEST,
2162
- request: {
2163
- id: 'fetch-products',
2164
- requestOptions: {
2165
- reqData: { path: '/products' },
2166
- resReq: true
2167
- }
2168
- }
2169
- },
2170
- {
2171
- type: RequestOrFunction.REQUEST,
2172
- request: {
2173
- id: 'fetch-inventory',
2174
- requestOptions: {
2175
- reqData: { path: '/inventory' },
2176
- resReq: true
2177
- }
2178
- }
2179
- },
2180
- {
2181
- type: RequestOrFunction.FUNCTION,
2182
- function: {
2183
- id: 'enrich-products',
2184
- functionOptions: {
2185
- fn: (products: ProductResponse[], inventory: InventoryResponse[]): EnrichResult => {
2186
- return products.map(product => {
2187
- const inv = inventory.find(i => i.productId === product.id);
2188
- return {
2189
- ...product,
2190
- stock: inv?.stock || 0,
2191
- inStock: (inv?.stock || 0) > 0
2192
- };
2193
- });
2194
- },
2195
- args: [[], []] as EnrichArgs,
2196
- returnResult: true,
2197
- cache: { enabled: true, ttl: 30000 }
2198
- }
2199
- }
2200
- },
2201
- {
2202
- type: RequestOrFunction.FUNCTION,
2203
- function: {
2204
- id: 'calculate-metrics',
2205
- functionOptions: {
2206
- fn: (enriched: EnrichResult): CalculateResult => ({
2207
- totalValue: enriched.reduce((sum, p) => sum + (p.price * p.stock), 0),
2208
- lowStockItems: enriched.filter(p => p.stock < 10 && p.stock > 0).length
2209
- }),
2210
- args: [[]] as CalculateArgs,
2211
- returnResult: true
2212
- }
2213
- }
2214
- },
2215
- {
2216
- type: RequestOrFunction.FUNCTION,
2217
- function: {
2218
- id: 'notify-if-needed',
2219
- functionOptions: {
2220
- fn: async (metrics: CalculateResult, channel: string): Promise<NotifyResult> => {
2221
- if (metrics.lowStockItems > 5) {
2222
- console.log(`Sending alert to ${channel}: ${metrics.lowStockItems} items low`);
2223
- return { notified: true };
2224
- }
2225
- return { notified: false };
2226
- },
2227
- args: [{ totalValue: 0, lowStockItems: 0 }, 'slack'] as NotifyArgs,
2228
- returnResult: true,
2229
- attempts: 3,
2230
- wait: 1000
2231
- }
2232
- }
2233
- }
2234
- ]
2235
- };
2236
-
2237
- const result = await stableWorkflow([phase], {
2238
- workflowId: 'mixed-execution',
2239
- sharedBuffer: {}
2240
- });
2241
- ```
2242
-
2243
- ---
2244
-
2245
- ## Best Practices
2246
-
2247
- ### 1. Start Conservative, Override When Needed
2248
-
2249
- Define global defaults; override only where necessary.
2250
-
2251
- ```typescript
2252
- await stableWorkflow(phases, {
2253
- // Global defaults (conservative)
2254
- commonAttempts: 3,
2255
- commonWait: 500,
2256
- retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
2257
-
2258
- // Override for specific phase
2259
- phases: [
2260
- {
2261
- id: 'fast-phase',
2262
- attempts: 1, // Override: no retries
2263
- requests: [...]
2264
- }
2265
- ]
2266
- });
2267
- ```
2268
-
2269
- ### 2. Validate Responses
2270
-
2271
- Use analyzers to ensure data shape and freshness.
2272
-
2273
- ```typescript
2274
- interface DataRequest {}
2275
- interface ApiResponse {
2276
- id: number;
2277
- lastUpdated: string;
2278
- }
2279
-
2280
- const result = await stableRequest<DataRequest, ApiResponse>({
2281
- reqData: { path: '/data' },
2282
- resReq: true,
2283
- responseAnalyzer: ({ data }) => {
2284
- if (!data || typeof data !== 'object') return false;
2285
- if (!('id' in data && 'lastUpdated' in data)) return false;
2286
- const age = Date.now() - new Date((data as any).lastUpdated).getTime();
2287
- if (age > 60000) return false; // Data older than 1 minute
2288
- return true;
2289
- }
2290
- });
2291
- ```
2292
-
2293
- ### 3. Cache Idempotent Reads Aggressively
2294
-
2295
- Reduce latency and load on dependencies.
2296
-
2297
- ```typescript
2298
- interface UserRequest {}
2299
1221
  interface UserResponse {
2300
- id: number;
1222
+ id: string;
2301
1223
  name: string;
1224
+ createdAt: string;
2302
1225
  }
2303
1226
 
2304
- const userCache = new CacheManager({
2305
- enabled: true,
2306
- ttl: 30000, // 30 seconds
2307
- respectCacheControl: true
2308
- });
2309
-
2310
- await stableRequest<UserRequest, UserResponse>({
2311
- reqData: { path: '/users/1' },
2312
- resReq: true,
2313
- cache: userCache
2314
- });
2315
-
2316
- await stableRequest<UserRequest, UserResponse>({
2317
- reqData: { path: '/users/1' },
2318
- resReq: true,
2319
- cache: userCache // Cached within 30s
2320
- });
2321
- ```
2322
-
2323
- ### 4. Use Circuit Breaker for Unstable Services
2324
-
2325
- Protect against cascading failures.
2326
-
2327
- ```typescript
2328
- interface ServiceRequest {}
2329
- interface ServiceResponse { status: string; data: any; }
2330
-
2331
- const unstabledServiceBreaker = new CircuitBreaker({
2332
- failureThresholdPercentage: 40,
2333
- minimumRequests: 5,
2334
- recoveryTimeoutMs: 30000,
2335
- successThresholdPercentage: 80
2336
- });
1227
+ (async () => {
1228
+ // Create shared buffer for state management
1229
+ const buffer = new StableBuffer({
1230
+ initialState: {
1231
+ requestCount: 0,
1232
+ errors: [] as ERROR_LOG[]
1233
+ },
1234
+ logTransaction: async (log: StableBufferTransactionLog): Promise<void> => {
1235
+ await persistTransactionLog(log);
1236
+ }
1237
+ });
2337
1238
 
2338
- await stableApiGateway<ServiceRequest, ServiceResponse>(requests, {
2339
- circuitBreaker: unstabledServiceBreaker
2340
- });
2341
- ```
1239
+ // Define typed configurations
1240
+ const circuitBreakerConfig: CircuitBreakerConfig = {
1241
+ failureThresholdPercentage: 50,
1242
+ minimumRequests: 5,
1243
+ recoveryTimeoutMs: 30000
1244
+ };
2342
1245
 
2343
- ### 6. Define Metrics Guardrails for SLA Monitoring
1246
+ const cacheConfig: CacheConfig = {
1247
+ enabled: true,
1248
+ ttl: 60000
1249
+ };
2344
1250
 
2345
- Enforce performance and reliability SLAs with automatic validation.
1251
+ const executionContext: ExecutionContext = {
1252
+ workflowId: 'user-creation',
1253
+ requestId: 'create-user-001'
1254
+ };
2346
1255
 
2347
- ```typescript
2348
- import { stableWorkflow, MetricsGuardrails } from '@emmvish/stable-request';
2349
- import type { STABLE_WORKFLOW_PHASE } from '@emmvish/stable-request';
2350
-
2351
- interface ApiRequest {}
2352
- interface ApiResponse { data: any; }
2353
-
2354
- const phases: STABLE_WORKFLOW_PHASE<ApiRequest, ApiResponse>[] = [
2355
- {
2356
- id: 'critical-phase',
2357
- requests: [{ id: 'req-1', requestOptions: { reqData: { path: '/critical' }, resReq: true } }],
2358
- metricsGuardrails: {
2359
- phase: {
2360
- executionTime: { max: 3000 }, // SLA: <3s
2361
- requestSuccessRate: { min: 99.5 } // SLA: 99.5% success
2362
- }
2363
- }
2364
- }
2365
- ];
2366
-
2367
- const result = await stableWorkflow(phases, {
2368
- workflowId: 'sla-monitored',
2369
- metricsGuardrails: {
2370
- workflow: {
2371
- executionTime: { max: 10000 }, // Workflow SLA: <10s
2372
- requestSuccessRate: { min: 99 } // Workflow SLA: 99% success
1256
+ const metricsGuardrails: MetricsGuardrails = {
1257
+ request: {
1258
+ totalExecutionTime: { max: 15000 },
1259
+ failedAttempts: { max: 2 }
2373
1260
  }
1261
+ };
1262
+
1263
+ // Make a resilient request
1264
+ const result: STABLE_REQUEST_RESULT<UserResponse> = await stableRequest<UserRequest, UserResponse>({
1265
+ reqData: {
1266
+ hostname: 'api.example.com',
1267
+ path: '/users',
1268
+ method: REQUEST_METHODS.POST,
1269
+ body: { name: 'John Doe' },
1270
+ headers: { 'Content-Type': 'application/json' },
1271
+ timeout: 10000
1272
+ },
1273
+ resReq: true,
1274
+ attempts: 3,
1275
+ wait: 1000,
1276
+ retryStrategy: RETRY_STRATEGIES.EXPONENTIAL,
1277
+ jitter: 0.2,
1278
+
1279
+ commonBuffer: buffer,
1280
+
1281
+ circuitBreaker: circuitBreakerConfig,
1282
+ cache: cacheConfig,
1283
+
1284
+ responseAnalyzer: async (options: ResponseAnalysisHookOptions<UserRequest, UserResponse>): Promise<boolean> => {
1285
+ const { data, commonBuffer } = options;
1286
+ if (commonBuffer) commonBuffer.requestCount += 1;
1287
+ return data.id !== undefined;
1288
+ },
1289
+
1290
+ logAllErrors: true,
1291
+ handleErrors: async (options: HandleErrorHookOptions<UserRequest>): Promise<void> => {
1292
+ const { errorLog, commonBuffer } = options;
1293
+ if (commonBuffer) commonBuffer.errors.push(errorLog);
1294
+ },
1295
+
1296
+ executionContext,
1297
+ metricsGuardrails
1298
+ });
1299
+
1300
+ if (result.success) {
1301
+ console.log('User created:', result.data);
1302
+ } else {
1303
+ console.error('Failed:', result.error);
2374
1304
  }
2375
- });
2376
-
2377
- // Automatic SLA violation detection
2378
- if (result.validation && !result.validation.isValid) {
2379
- const criticalAnomalies = result.validation.anomalies.filter(a => a.severity === 'CRITICAL');
2380
- if (criticalAnomalies.length > 0) {
2381
- // Trigger alerts for SLA violations
2382
- console.error('SLA violated:', criticalAnomalies);
2383
- }
2384
- }
2385
- ```
2386
-
2387
- ### 6. Apply Rate & Concurrency Limits
2388
-
2389
- Respect external quotas and capacity.
2390
-
2391
- ```typescript
2392
- interface ApiRequest {}
2393
- interface ApiResponse { result: any; }
2394
-
2395
- // API allows 100 req/second, use 80% headroom
2396
- const rateLimit = { maxRequests: 80, windowMs: 1000 };
2397
-
2398
- // Database connection pool has 10 slots, use 5
2399
- const maxConcurrent = 5;
2400
1305
 
2401
- await stableApiGateway<ApiRequest, ApiResponse>(requests, {
2402
- rateLimit,
2403
- maxConcurrentRequests: maxConcurrent
2404
- });
1306
+ console.log('Metrics:', result.metrics);
1307
+ console.log('Buffer state:', buffer.read());
1308
+ })();
2405
1309
  ```
2406
1310
 
2407
- ### 6. Use Shared Buffers for Cross-Phase Coordination
1311
+ ## TypeScript Support
2408
1312
 
2409
- Avoid global state; pass computed data cleanly.
1313
+ This library is written in TypeScript and includes full type definitions:
2410
1314
 
2411
1315
  ```typescript
2412
- const sharedBuffer = {};
2413
-
2414
- await stableWorkflow(phases, {
2415
- sharedBuffer,
2416
- // Phase 1 writes userId to sharedBuffer
2417
- // Phase 2 reads userId from sharedBuffer
2418
- // Phase 3 uses both
2419
- });
1316
+ import type {
1317
+ STABLE_REQUEST,
1318
+ STABLE_REQUEST_RESULT,
1319
+ StableBufferOptions,
1320
+ StableBufferTransactionLog,
1321
+ CircuitBreakerConfig,
1322
+ CacheConfig,
1323
+ MetricsGuardrails
1324
+ } from 'stable-request';
2420
1325
  ```
2421
1326
 
2422
- ### 7. Log Selectively with Max Serialization Cap
1327
+ ## Migration to stable-infra
2423
1328
 
2424
- Prevent noisy logs from large payloads.
1329
+ If you need workflows, schedulers, or advanced orchestration, migrating to stable-infra is straightforward:
2425
1330
 
2426
1331
  ```typescript
2427
- interface DataRequest {}
2428
- interface DataResponse { data: any; }
2429
-
2430
- await stableRequest<DataRequest, DataResponse>({
2431
- reqData: { path: '/data' },
2432
- resReq: true,
2433
- maxSerializableChars: 500, // Truncate logs to 500 chars
2434
- handleSuccessfulAttemptData: ({ successfulAttemptData, maxSerializableChars }) => {
2435
- console.log(safelyStringify(successfulAttemptData, maxSerializableChars));
2436
- }
2437
- });
2438
- ```
2439
-
2440
- ### 8. Use Non-Linear Workflows for Polling
2441
-
2442
- REPLAY action simplifies polling logic.
1332
+ // stable-request
1333
+ import { stableRequest } from 'stable-request';
2443
1334
 
2444
- ```typescript
2445
- const phases: STABLE_WORKFLOW_PHASE[] = [
2446
- {
2447
- id: 'wait-for-job',
2448
- allowReplay: true,
2449
- maxReplayCount: 10,
2450
- requests: [
2451
- {
2452
- id: 'check-status',
2453
- requestOptions: {
2454
- reqData: { path: '/jobs/123' },
2455
- resReq: true,
2456
- attempts: 1
2457
- }
2458
- }
2459
- ],
2460
- phaseDecisionHook: async ({ phaseResult }) => {
2461
- const status = (phaseResult.responses[0].data as any)?.status;
2462
- if (status === 'pending') {
2463
- return { action: PHASE_DECISION_ACTIONS.REPLAY };
2464
- }
2465
- return { action: PHASE_DECISION_ACTIONS.CONTINUE };
2466
- }
2467
- }
2468
- ];
1335
+ // stable-infra (same API, more features)
1336
+ import { stableRequest } from 'stable-infra';
2469
1337
 
2470
- await stableWorkflow(phases, {
2471
- enableNonLinearExecution: true
2472
- });
2473
- ```
2474
-
2475
- ### 9. Use Graph Workflows for Complex Parallelism
2476
-
2477
- DAGs make dependencies explicit and enable maximum parallelism.
2478
-
2479
- ```typescript
2480
- // Clearer than 6 phases with conditional concurrency markers
2481
- const graph = new WorkflowGraphBuilder()
2482
- .addParallelGroup('fetch', ['fetch-users', 'fetch-posts', 'fetch-comments'])
2483
- .addMergePoint('sync', ['fetch'])
2484
- .addPhase('aggregate', {...})
2485
- .connectSequence('fetch', 'sync', 'aggregate')
2486
- .build();
2487
-
2488
- await stableWorkflowGraph(graph);
1338
+ // Plus you get access to:
1339
+ import {
1340
+ stableWorkflow,
1341
+ stableApiGateway,
1342
+ stableFunction,
1343
+ stableScheduler,
1344
+ stableWorkflowGraph
1345
+ } from 'stable-infra';
2489
1346
  ```
2490
1347
 
2491
- ### 10. Prefer Dry-Run (Trial Mode) Before Production
1348
+ ## License
2492
1349
 
2493
- Test workflows and retry logic safely.
2494
-
2495
- ```typescript
2496
- await stableWorkflow(phases, {
2497
- workflowId: 'payment-pipeline',
2498
- trialMode: { enabled: true }, // Dry-run before production
2499
- handlePhaseCompletion: ({ phaseResult }) => {
2500
- console.log(`Trial phase: ${phaseResult.phaseId}, success=${phaseResult.success}`);
2501
- }
2502
- });
2503
-
2504
- // If satisfied, deploy with trialMode: { enabled: false }
2505
- ```
1350
+ MIT
2506
1351
 
2507
1352
  ---
2508
1353
 
2509
- ## Summary
2510
-
2511
- @emmvish/stable-request provides a unified, type-safe framework for resilient execution:
2512
-
2513
- - **Single calls** via `stableRequest` (APIs) or `stableFunction` (pure functions)
2514
- - **Batch orchestration** via `stableApiGateway` (concurrent/sequential mixed items)
2515
- - **Phased workflows** via `stableWorkflow` (array-based, non-linear, branched)
2516
- - **Graph workflows** via `stableWorkflowGraph` (DAG, explicit parallelism)
2517
-
2518
- All modes inherit robust resilience (retries, jitter, circuit breaking, caching, rate/concurrency limits), config cascading, shared state, hooks, and metrics. Use together or independently; compose freely.
2519
-
2520
- Build resilient, observable, type-safe systems with confidence.
1354
+ **stable-request** is now in maintenance mode. For new projects, please use [**stable-infra**](https://npmjs.com/package/@emmvish/stable-infra).