@onlineapps/conn-infra-error-handler 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +0 -0
- package/README.md +225 -0
- package/onlineapps-conn-infra-error-handler-1.0.0.tgz +0 -0
- package/package.json +5 -4
- package/src/index.js +208 -577
- package/{test → tests}/component/error-handling-flow.test.js +56 -34
- package/{test → tests}/unit/ErrorHandlerConnector.test.js +106 -100
package/src/index.js
CHANGED
|
@@ -1,76 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module @onlineapps/conn-infra-error-handler
|
|
3
|
-
* @description Unified error handling connector
|
|
4
|
-
*
|
|
3
|
+
* @description Unified error handling connector - wrapper around error-handler-core for business services
|
|
4
|
+
*
|
|
5
|
+
* This connector wraps @onlineapps/error-handler-core and integrates with conn-base-monitoring
|
|
6
|
+
* for business services using ServiceWrapper.
|
|
5
7
|
*
|
|
6
8
|
* @see {@link https://github.com/onlineapps/oa-drive/tree/main/shared/connector/conn-infra-error-handler|GitHub Repository}
|
|
9
|
+
* @see {@link /docs/standards/UNIFIED_ERROR_HANDLING.md|Unified Error Handling Standard}
|
|
7
10
|
* @author OA Drive Team
|
|
8
11
|
* @license MIT
|
|
9
12
|
* @since 1.0.0
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
// Error types enum
|
|
15
|
-
const ErrorTypes = {
|
|
16
|
-
TRANSIENT: 'TRANSIENT', // Retry with backoff
|
|
17
|
-
BUSINESS: 'BUSINESS', // Don't retry, return error
|
|
18
|
-
FATAL: 'FATAL', // Stop workflow
|
|
19
|
-
VALIDATION: 'VALIDATION', // Input validation error
|
|
20
|
-
TIMEOUT: 'TIMEOUT', // Operation timeout
|
|
21
|
-
RATE_LIMIT: 'RATE_LIMIT', // Rate limiting
|
|
22
|
-
UNKNOWN: 'UNKNOWN' // Unclassified error
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Standard error codes
|
|
26
|
-
const ErrorCodes = {
|
|
27
|
-
// Network errors
|
|
28
|
-
ECONNREFUSED: ErrorTypes.TRANSIENT,
|
|
29
|
-
ENOTFOUND: ErrorTypes.TRANSIENT,
|
|
30
|
-
ETIMEDOUT: ErrorTypes.TRANSIENT,
|
|
31
|
-
ECONNRESET: ErrorTypes.TRANSIENT,
|
|
32
|
-
EPIPE: ErrorTypes.TRANSIENT,
|
|
33
|
-
|
|
34
|
-
// HTTP status codes
|
|
35
|
-
408: ErrorTypes.TIMEOUT,
|
|
36
|
-
429: ErrorTypes.RATE_LIMIT,
|
|
37
|
-
500: ErrorTypes.TRANSIENT,
|
|
38
|
-
502: ErrorTypes.TRANSIENT,
|
|
39
|
-
503: ErrorTypes.TRANSIENT,
|
|
40
|
-
504: ErrorTypes.TIMEOUT,
|
|
41
|
-
|
|
42
|
-
// Business errors
|
|
43
|
-
400: ErrorTypes.VALIDATION,
|
|
44
|
-
401: ErrorTypes.BUSINESS,
|
|
45
|
-
403: ErrorTypes.BUSINESS,
|
|
46
|
-
404: ErrorTypes.BUSINESS,
|
|
47
|
-
409: ErrorTypes.BUSINESS,
|
|
48
|
-
422: ErrorTypes.VALIDATION
|
|
49
|
-
};
|
|
15
|
+
const { ErrorHandlerCore } = require('@onlineapps/error-handler-core');
|
|
16
|
+
const { ErrorTypes, ErrorCodes } = require('@onlineapps/error-handler-core');
|
|
50
17
|
|
|
51
18
|
/**
|
|
52
|
-
* Error handling connector
|
|
19
|
+
* Error handling connector for business services
|
|
20
|
+
* Wraps error-handler-core and integrates with conn-base-monitoring
|
|
53
21
|
*
|
|
54
22
|
* @class ErrorHandlerConnector
|
|
55
23
|
*
|
|
56
|
-
* @example <caption>Basic Usage</caption>
|
|
57
|
-
* const
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
24
|
+
* @example <caption>Basic Usage in ServiceWrapper</caption>
|
|
25
|
+
* const { ServiceWrapper } = require('@onlineapps/service-wrapper');
|
|
26
|
+
*
|
|
27
|
+
* const wrapper = new ServiceWrapper({
|
|
28
|
+
* serviceName: 'my-service',
|
|
29
|
+
* monitoring: { enabled: true },
|
|
30
|
+
* errorHandling: {
|
|
31
|
+
* maxRetries: 3,
|
|
32
|
+
* retryDelay: 1000
|
|
33
|
+
* }
|
|
64
34
|
* });
|
|
65
|
-
*
|
|
66
|
-
*
|
|
35
|
+
*
|
|
36
|
+
* // Error handler is automatically available as wrapper.errorHandler
|
|
37
|
+
*
|
|
38
|
+
* @example <caption>Direct Usage</caption>
|
|
39
|
+
* const { ErrorHandlerConnector } = require('@onlineapps/conn-infra-error-handler');
|
|
40
|
+
*
|
|
67
41
|
* const errorHandler = new ErrorHandlerConnector({
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
42
|
+
* serviceName: 'my-service',
|
|
43
|
+
* monitoring: monitoringInstance, // conn-base-monitoring instance
|
|
44
|
+
* handling: {
|
|
45
|
+
* maxRetries: 3,
|
|
46
|
+
* retryDelay: 1000
|
|
47
|
+
* }
|
|
74
48
|
* });
|
|
75
49
|
*/
|
|
76
50
|
class ErrorHandlerConnector {
|
|
@@ -78,55 +52,66 @@ class ErrorHandlerConnector {
|
|
|
78
52
|
* Creates a new ErrorHandlerConnector instance
|
|
79
53
|
*
|
|
80
54
|
* @constructor
|
|
81
|
-
* @param {Object}
|
|
82
|
-
* @param {
|
|
83
|
-
* @param {
|
|
84
|
-
* @param {
|
|
85
|
-
* @param {
|
|
86
|
-
* @param {
|
|
87
|
-
* @param {number} [config.
|
|
88
|
-
* @param {number} [config.
|
|
89
|
-
* @param {number} [config.
|
|
90
|
-
* @param {
|
|
91
|
-
* @param {
|
|
55
|
+
* @param {Object} config - Configuration options
|
|
56
|
+
* @param {string} config.serviceName - Service name (required)
|
|
57
|
+
* @param {string} [config.serviceVersion] - Service version
|
|
58
|
+
* @param {string} [config.environment] - Environment
|
|
59
|
+
* @param {Object} config.monitoring - Monitoring instance (conn-base-monitoring, required)
|
|
60
|
+
* @param {Object} [config.handling] - Error handling configuration
|
|
61
|
+
* @param {number} [config.handling.maxRetries=3] - Maximum retry attempts
|
|
62
|
+
* @param {number} [config.handling.retryDelay=1000] - Initial retry delay in ms
|
|
63
|
+
* @param {number} [config.handling.retryMultiplier=2] - Backoff multiplier
|
|
64
|
+
* @param {number} [config.handling.maxRetryDelay=30000] - Maximum retry delay
|
|
65
|
+
* @param {boolean} [config.handling.circuitBreakerEnabled=true] - Enable circuit breaker
|
|
66
|
+
* @param {number} [config.handling.circuitTimeout=10000] - Circuit breaker timeout
|
|
67
|
+
* @param {number} [config.handling.errorThreshold=50] - Error threshold percentage
|
|
68
|
+
* @param {number} [config.handling.resetTimeout=30000] - Circuit reset timeout
|
|
69
|
+
* @param {boolean} [config.handling.dlqEnabled=true] - Enable DLQ routing
|
|
70
|
+
* @param {Object} [config.handling.mqClient] - MQ client for DLQ
|
|
71
|
+
* @param {boolean} [config.handling.compensationEnabled=true] - Enable compensation
|
|
92
72
|
*
|
|
93
73
|
* @example <caption>Full Configuration</caption>
|
|
94
74
|
* const errorHandler = new ErrorHandlerConnector({
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
75
|
+
* serviceName: 'my-service',
|
|
76
|
+
* serviceVersion: '1.0.0',
|
|
77
|
+
* environment: 'production',
|
|
78
|
+
* monitoring: monitoringInstance,
|
|
79
|
+
* handling: {
|
|
80
|
+
* maxRetries: 5,
|
|
81
|
+
* retryDelay: 500,
|
|
82
|
+
* retryMultiplier: 1.5,
|
|
83
|
+
* maxRetryDelay: 20000,
|
|
84
|
+
* circuitBreakerEnabled: true,
|
|
85
|
+
* errorThreshold: 60,
|
|
86
|
+
* dlqEnabled: true,
|
|
87
|
+
* mqClient: mqClientInstance,
|
|
88
|
+
* compensationEnabled: true
|
|
89
|
+
* }
|
|
102
90
|
* });
|
|
103
91
|
*/
|
|
104
92
|
constructor(config = {}) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
this.circuitBreakers = new Map();
|
|
128
|
-
|
|
129
|
-
// Error statistics
|
|
93
|
+
if (!config.serviceName) {
|
|
94
|
+
throw new Error('serviceName is required');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!config.monitoring) {
|
|
98
|
+
throw new Error('monitoring instance is required (conn-base-monitoring)');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Initialize error-handler-core with monitoring instance
|
|
102
|
+
// monitoring instance from conn-base-monitoring wraps monitoring-core
|
|
103
|
+
this.core = new ErrorHandlerCore({
|
|
104
|
+
serviceName: config.serviceName,
|
|
105
|
+
serviceVersion: config.serviceVersion,
|
|
106
|
+
environment: config.environment,
|
|
107
|
+
monitoring: config.monitoring, // conn-base-monitoring instance
|
|
108
|
+
handling: config.handling || {}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Store config for compatibility
|
|
112
|
+
this.config = config;
|
|
113
|
+
|
|
114
|
+
// Statistics (for backward compatibility)
|
|
130
115
|
this.stats = {
|
|
131
116
|
errors: 0,
|
|
132
117
|
retries: 0,
|
|
@@ -134,285 +119,171 @@ class ErrorHandlerConnector {
|
|
|
134
119
|
circuitBreaks: 0,
|
|
135
120
|
byType: {}
|
|
136
121
|
};
|
|
137
|
-
|
|
122
|
+
|
|
138
123
|
// Initialize error type stats
|
|
139
124
|
Object.values(ErrorTypes).forEach(type => {
|
|
140
125
|
this.stats.byType[type] = 0;
|
|
141
126
|
});
|
|
142
127
|
}
|
|
143
|
-
|
|
128
|
+
|
|
144
129
|
/**
|
|
145
130
|
* Classify error into type for appropriate handling
|
|
131
|
+
* Delegates to error-handler-core
|
|
146
132
|
*
|
|
147
133
|
* @method classifyError
|
|
148
134
|
* @param {Error} error - Error to classify
|
|
149
|
-
* @param {string} [error.code] - Error code (e.g., ECONNREFUSED)
|
|
150
|
-
* @param {number} [error.statusCode] - HTTP status code
|
|
151
|
-
* @param {string} [error.type] - Explicit error type
|
|
152
135
|
* @returns {string} Error type from ErrorTypes enum
|
|
153
136
|
*
|
|
154
|
-
* @example
|
|
137
|
+
* @example
|
|
155
138
|
* const type = errorHandler.classifyError(new Error('ECONNREFUSED'));
|
|
156
139
|
* // Returns: 'TRANSIENT'
|
|
157
|
-
*
|
|
158
|
-
* @example <caption>HTTP Error</caption>
|
|
159
|
-
* const error = new Error('Not Found');
|
|
160
|
-
* error.statusCode = 404;
|
|
161
|
-
* const type = errorHandler.classifyError(error);
|
|
162
|
-
* // Returns: 'BUSINESS'
|
|
163
|
-
*
|
|
164
|
-
* @example <caption>Timeout Error</caption>
|
|
165
|
-
* const type = errorHandler.classifyError(new Error('Operation timeout'));
|
|
166
|
-
* // Returns: 'TIMEOUT'
|
|
167
140
|
*/
|
|
168
141
|
classifyError(error) {
|
|
169
|
-
|
|
170
|
-
if (error.code && ErrorCodes[error.code]) {
|
|
171
|
-
return ErrorCodes[error.code];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Check by HTTP status
|
|
175
|
-
if (error.statusCode && ErrorCodes[error.statusCode]) {
|
|
176
|
-
return ErrorCodes[error.statusCode];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Check by error message patterns
|
|
180
|
-
const message = error.message?.toLowerCase() || '';
|
|
181
|
-
|
|
182
|
-
if (message.includes('timeout')) return ErrorTypes.TIMEOUT;
|
|
183
|
-
if (message.includes('rate limit')) return ErrorTypes.RATE_LIMIT;
|
|
184
|
-
if (message.includes('validation')) return ErrorTypes.VALIDATION;
|
|
185
|
-
if (message.includes('unauthorized') || message.includes('forbidden')) {
|
|
186
|
-
return ErrorTypes.BUSINESS;
|
|
187
|
-
}
|
|
188
|
-
if (message.includes('connection') || message.includes('network')) {
|
|
189
|
-
return ErrorTypes.TRANSIENT;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Check if error has explicit type
|
|
193
|
-
if (error.type && ErrorTypes[error.type]) {
|
|
194
|
-
return error.type;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return ErrorTypes.UNKNOWN;
|
|
142
|
+
return this.core.classifier.classify(error);
|
|
198
143
|
}
|
|
199
|
-
|
|
144
|
+
|
|
200
145
|
/**
|
|
201
146
|
* Determine if error should be retried
|
|
147
|
+
* Delegates to error-handler-core
|
|
202
148
|
*
|
|
203
149
|
* @method shouldRetry
|
|
204
150
|
* @param {Error} error - Error to check
|
|
205
151
|
* @param {number} [attempts=0] - Current attempt count
|
|
206
152
|
* @returns {boolean} True if should retry
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* const error = new Error('Connection refused');
|
|
210
|
-
* if (errorHandler.shouldRetry(error, 1)) {
|
|
211
|
-
* // Retry the operation
|
|
212
|
-
* }
|
|
213
153
|
*/
|
|
214
154
|
shouldRetry(error, attempts = 0) {
|
|
215
|
-
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const errorType = this.classifyError(error);
|
|
220
|
-
|
|
221
|
-
// Only retry transient and timeout errors
|
|
222
|
-
return [
|
|
223
|
-
ErrorTypes.TRANSIENT,
|
|
224
|
-
ErrorTypes.TIMEOUT,
|
|
225
|
-
ErrorTypes.RATE_LIMIT
|
|
226
|
-
].includes(errorType);
|
|
155
|
+
return this.core.classifier.shouldRetry(error, attempts, this.core.retryHandler.maxRetries);
|
|
227
156
|
}
|
|
228
|
-
|
|
157
|
+
|
|
229
158
|
/**
|
|
230
159
|
* Calculate exponential backoff delay
|
|
160
|
+
* Delegates to error-handler-core
|
|
231
161
|
*
|
|
232
162
|
* @method calculateBackoff
|
|
233
163
|
* @param {number} attempts - Current attempt number (1-based)
|
|
234
164
|
* @returns {number} Delay in milliseconds
|
|
235
|
-
*
|
|
236
|
-
* @example
|
|
237
|
-
* const delay = errorHandler.calculateBackoff(3);
|
|
238
|
-
* // With default config: 1000 * 2^2 = 4000ms
|
|
239
165
|
*/
|
|
240
166
|
calculateBackoff(attempts) {
|
|
241
|
-
|
|
242
|
-
return Math.min(delay, this.maxRetryDelay);
|
|
167
|
+
return this.core.retryHandler.calculateBackoff(attempts);
|
|
243
168
|
}
|
|
244
|
-
|
|
169
|
+
|
|
245
170
|
/**
|
|
246
171
|
* Execute function with automatic retry on failure
|
|
172
|
+
* Delegates to error-handler-core
|
|
247
173
|
*
|
|
248
174
|
* @async
|
|
249
175
|
* @method executeWithRetry
|
|
250
176
|
* @param {Function} fn - Async function to execute
|
|
251
177
|
* @param {Object} [options={}] - Retry options
|
|
252
178
|
* @param {number} [options.maxRetries] - Override max retries
|
|
253
|
-
* @param {Array<string>} [options.retryOn=[]] - Additional error codes to retry
|
|
254
179
|
* @param {Function} [options.onRetry] - Callback on retry (error, attempt, delay)
|
|
255
180
|
* @returns {Promise<*>} Function result
|
|
256
181
|
*
|
|
257
182
|
* @throws {Error} Last error if all retries fail
|
|
258
183
|
*
|
|
259
|
-
* @example
|
|
184
|
+
* @example
|
|
260
185
|
* const result = await errorHandler.executeWithRetry(async () => {
|
|
261
186
|
* return await apiClient.fetchData();
|
|
262
187
|
* });
|
|
263
|
-
*
|
|
264
|
-
* @example <caption>With Options</caption>
|
|
265
|
-
* const result = await errorHandler.executeWithRetry(
|
|
266
|
-
* async () => await riskyOperation(),
|
|
267
|
-
* {
|
|
268
|
-
* maxRetries: 5,
|
|
269
|
-
* retryOn: ['CUSTOM_ERROR'],
|
|
270
|
-
* onRetry: (error, attempt, delay) => {
|
|
271
|
-
* console.log(`Retry ${attempt} after ${delay}ms`);
|
|
272
|
-
* }
|
|
273
|
-
* }
|
|
274
|
-
* );
|
|
275
|
-
*
|
|
276
|
-
* @example <caption>Database Operation</caption>
|
|
277
|
-
* const data = await errorHandler.executeWithRetry(async () => {
|
|
278
|
-
* const conn = await db.connect();
|
|
279
|
-
* try {
|
|
280
|
-
* return await conn.query('SELECT * FROM users');
|
|
281
|
-
* } finally {
|
|
282
|
-
* conn.close();
|
|
283
|
-
* }
|
|
284
|
-
* });
|
|
285
188
|
*/
|
|
286
189
|
async executeWithRetry(fn, options = {}) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
} catch (error) {
|
|
296
|
-
lastError = error;
|
|
297
|
-
this.stats.errors++;
|
|
298
|
-
this.stats.byType[this.classifyError(error)]++;
|
|
299
|
-
|
|
300
|
-
// Check if should retry
|
|
301
|
-
const shouldRetry = this.shouldRetry(error, attempt) ||
|
|
302
|
-
retryOn.includes(error.code);
|
|
303
|
-
|
|
304
|
-
if (!shouldRetry || attempt === maxAttempts) {
|
|
305
|
-
throw error;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Calculate and wait backoff
|
|
309
|
-
const delay = this.calculateBackoff(attempt);
|
|
310
|
-
this.stats.retries++;
|
|
311
|
-
|
|
312
|
-
if (options.onRetry) {
|
|
313
|
-
options.onRetry(error, attempt, delay);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
await this.sleep(delay);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
throw lastError;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Get or create circuit breaker for operation
|
|
325
|
-
* @param {string} name - Operation name
|
|
326
|
-
* @param {Function} fn - Function to protect
|
|
327
|
-
* @param {object} options - Circuit breaker options
|
|
328
|
-
* @returns {CircuitBreaker} Circuit breaker instance
|
|
329
|
-
*/
|
|
330
|
-
getCircuitBreaker(name, fn, options = {}) {
|
|
331
|
-
if (!this.circuitBreakerEnabled) {
|
|
332
|
-
// Return pass-through if disabled
|
|
333
|
-
return { fire: () => fn() };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (!this.circuitBreakers.has(name)) {
|
|
337
|
-
const breaker = new CircuitBreaker(fn, {
|
|
338
|
-
...this.circuitBreakerOptions,
|
|
339
|
-
...options
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// Track circuit breaker events
|
|
343
|
-
breaker.on('open', () => {
|
|
344
|
-
this.stats.circuitBreaks++;
|
|
345
|
-
console.warn(`Circuit breaker opened: ${name}`);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
breaker.on('halfOpen', () => {
|
|
349
|
-
console.info(`Circuit breaker half-open: ${name}`);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
this.circuitBreakers.set(name, breaker);
|
|
190
|
+
this.stats.errors++;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
return await this.core.retryHandler.executeWithRetry(fn, options);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const errorType = this.classifyError(error);
|
|
196
|
+
this.stats.byType[errorType] = (this.stats.byType[errorType] || 0) + 1;
|
|
197
|
+
throw error;
|
|
353
198
|
}
|
|
354
|
-
|
|
355
|
-
return this.circuitBreakers.get(name);
|
|
356
199
|
}
|
|
357
|
-
|
|
200
|
+
|
|
358
201
|
/**
|
|
359
202
|
* Execute function with circuit breaker protection
|
|
203
|
+
* Delegates to error-handler-core
|
|
360
204
|
*
|
|
361
205
|
* @async
|
|
362
206
|
* @method executeWithCircuitBreaker
|
|
363
207
|
* @param {string} name - Operation name for circuit breaker
|
|
364
208
|
* @param {Function} fn - Async function to execute
|
|
365
209
|
* @param {Object} [options={}] - Circuit breaker options
|
|
366
|
-
* @param {number} [options.timeout] - Operation timeout
|
|
367
|
-
* @param {number} [options.errorThresholdPercentage] - Error threshold
|
|
368
210
|
* @returns {Promise<*>} Function result
|
|
369
211
|
*
|
|
370
212
|
* @throws {Error} If circuit is open or operation fails
|
|
371
213
|
*
|
|
372
|
-
* @
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* try {
|
|
377
|
-
* const data = await errorHandler.executeWithCircuitBreaker(
|
|
378
|
-
* 'user-api',
|
|
379
|
-
* async () => await userAPI.getUser(id)
|
|
380
|
-
* );
|
|
381
|
-
* } catch (error) {
|
|
382
|
-
* if (error.message.includes('circuit is open')) {
|
|
383
|
-
* // Service is down, use fallback
|
|
384
|
-
* }
|
|
385
|
-
* }
|
|
386
|
-
*
|
|
387
|
-
* @example <caption>With Custom Options</caption>
|
|
388
|
-
* const result = await errorHandler.executeWithCircuitBreaker(
|
|
389
|
-
* 'payment-gateway',
|
|
390
|
-
* async () => await processPayment(order),
|
|
391
|
-
* {
|
|
392
|
-
* timeout: 5000,
|
|
393
|
-
* errorThresholdPercentage: 30
|
|
394
|
-
* }
|
|
214
|
+
* @example
|
|
215
|
+
* const data = await errorHandler.executeWithCircuitBreaker(
|
|
216
|
+
* 'user-api',
|
|
217
|
+
* async () => await userAPI.getUser(id)
|
|
395
218
|
* );
|
|
396
219
|
*/
|
|
397
220
|
async executeWithCircuitBreaker(name, fn, options = {}) {
|
|
398
|
-
const breaker = this.getCircuitBreaker(name, fn, options);
|
|
399
|
-
|
|
400
221
|
try {
|
|
401
|
-
return await
|
|
222
|
+
return await this.core.circuitBreaker.executeWithCircuitBreaker(name, fn, options);
|
|
402
223
|
} catch (error) {
|
|
403
224
|
this.stats.errors++;
|
|
404
|
-
this.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
225
|
+
const errorType = this.classifyError(error);
|
|
226
|
+
this.stats.byType[errorType] = (this.stats.byType[errorType] || 0) + 1;
|
|
227
|
+
|
|
228
|
+
if (error.message && error.message.includes('circuit is open')) {
|
|
229
|
+
this.stats.circuitBreaks++;
|
|
408
230
|
}
|
|
409
|
-
|
|
231
|
+
|
|
410
232
|
throw error;
|
|
411
233
|
}
|
|
412
234
|
}
|
|
413
|
-
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Log error using unified schema
|
|
238
|
+
* Delegates to error-handler-core
|
|
239
|
+
*
|
|
240
|
+
* @async
|
|
241
|
+
* @method logError
|
|
242
|
+
* @param {Object} errorData - Error data
|
|
243
|
+
* @param {string} errorData.moduleName - Module name
|
|
244
|
+
* @param {string} errorData.operation - Operation name
|
|
245
|
+
* @param {Error} errorData.error - Error object
|
|
246
|
+
* @param {Object} [errorData.context] - Additional context
|
|
247
|
+
* @returns {Promise<Object>} Unified error log entry
|
|
248
|
+
*/
|
|
249
|
+
async logError(errorData) {
|
|
250
|
+
return await this.core.logError(errorData);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Handle error: classify + log + decide action
|
|
255
|
+
* Delegates to error-handler-core
|
|
256
|
+
*
|
|
257
|
+
* @async
|
|
258
|
+
* @method handleError
|
|
259
|
+
* @param {Object} errorData - Error data
|
|
260
|
+
* @param {string} errorData.moduleName - Module name
|
|
261
|
+
* @param {string} errorData.operation - Operation name
|
|
262
|
+
* @param {Error} errorData.error - Error object
|
|
263
|
+
* @param {Object} [errorData.context] - Additional context
|
|
264
|
+
* @returns {Promise<Object>} Handling result
|
|
265
|
+
*/
|
|
266
|
+
async handleError(errorData) {
|
|
267
|
+
this.stats.errors++;
|
|
268
|
+
const result = await this.core.handleError(errorData);
|
|
269
|
+
|
|
270
|
+
const errorType = result.errorType;
|
|
271
|
+
this.stats.byType[errorType] = (this.stats.byType[errorType] || 0) + 1;
|
|
272
|
+
|
|
273
|
+
if (result.action === 'retry') {
|
|
274
|
+
this.stats.retries++;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (result.compensated) {
|
|
278
|
+
this.stats.compensations++;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
414
284
|
/**
|
|
415
285
|
* Register compensation handler for an operation
|
|
286
|
+
* Delegates to error-handler-core
|
|
416
287
|
*
|
|
417
288
|
* @method registerCompensation
|
|
418
289
|
* @param {string} operation - Operation name
|
|
@@ -421,21 +292,16 @@ class ErrorHandlerConnector {
|
|
|
421
292
|
*
|
|
422
293
|
* @example
|
|
423
294
|
* errorHandler.registerCompensation('create-order', async (context) => {
|
|
424
|
-
* // Rollback order creation
|
|
425
295
|
* await orderService.cancel(context.orderId);
|
|
426
296
|
* });
|
|
427
|
-
*
|
|
428
|
-
* errorHandler.registerCompensation('charge-payment', async (context) => {
|
|
429
|
-
* // Refund payment
|
|
430
|
-
* await paymentService.refund(context.chargeId);
|
|
431
|
-
* });
|
|
432
297
|
*/
|
|
433
298
|
registerCompensation(operation, handler) {
|
|
434
|
-
this.
|
|
299
|
+
this.core.registerCompensation(operation, handler);
|
|
435
300
|
}
|
|
436
|
-
|
|
301
|
+
|
|
437
302
|
/**
|
|
438
303
|
* Execute compensation for failed operation
|
|
304
|
+
* Delegates to error-handler-core
|
|
439
305
|
*
|
|
440
306
|
* @async
|
|
441
307
|
* @method executeCompensation
|
|
@@ -444,82 +310,37 @@ class ErrorHandlerConnector {
|
|
|
444
310
|
* @returns {Promise<*>} Compensation result or null if no handler
|
|
445
311
|
*
|
|
446
312
|
* @throws {Error} If compensation fails
|
|
447
|
-
*
|
|
448
|
-
* @example
|
|
449
|
-
* try {
|
|
450
|
-
* await createOrder(data);
|
|
451
|
-
* } catch (error) {
|
|
452
|
-
* await errorHandler.executeCompensation('create-order', {
|
|
453
|
-
* orderId: data.orderId,
|
|
454
|
-
* userId: data.userId
|
|
455
|
-
* });
|
|
456
|
-
* }
|
|
457
313
|
*/
|
|
458
314
|
async executeCompensation(operation, context) {
|
|
459
|
-
if (!this.compensationEnabled) {
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const handler = this.compensationHandlers.get(operation);
|
|
464
|
-
|
|
465
|
-
if (!handler) {
|
|
466
|
-
console.warn(`No compensation handler for: ${operation}`);
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
315
|
try {
|
|
471
|
-
this.
|
|
472
|
-
|
|
473
|
-
|
|
316
|
+
const result = await this.core.compensation.execute(operation, context);
|
|
317
|
+
if (result !== null) {
|
|
318
|
+
this.stats.compensations++;
|
|
319
|
+
}
|
|
474
320
|
return result;
|
|
475
321
|
} catch (error) {
|
|
476
|
-
console.error(`Compensation failed for ${operation}:`, error);
|
|
477
322
|
throw error;
|
|
478
323
|
}
|
|
479
324
|
}
|
|
480
|
-
|
|
325
|
+
|
|
481
326
|
/**
|
|
482
327
|
* Route failed message to dead letter queue
|
|
328
|
+
* Delegates to error-handler-core
|
|
483
329
|
*
|
|
484
330
|
* @async
|
|
485
331
|
* @method routeToDLQ
|
|
486
|
-
* @param {Object} mqClient - Message queue client
|
|
332
|
+
* @param {Object} mqClient - Message queue client (optional, can be in config)
|
|
487
333
|
* @param {Object} message - Original message that failed
|
|
488
334
|
* @param {Error} error - Error that occurred
|
|
489
335
|
* @returns {Promise<void>}
|
|
490
|
-
*
|
|
491
|
-
* @example
|
|
492
|
-
* try {
|
|
493
|
-
* await processMessage(message);
|
|
494
|
-
* } catch (error) {
|
|
495
|
-
* if (!errorHandler.shouldRetry(error)) {
|
|
496
|
-
* await errorHandler.routeToDLQ(mqClient, message, error);
|
|
497
|
-
* }
|
|
498
|
-
* }
|
|
499
336
|
*/
|
|
500
337
|
async routeToDLQ(mqClient, message, error) {
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
message: error.message,
|
|
505
|
-
stack: error.stack,
|
|
506
|
-
code: error.code,
|
|
507
|
-
type: this.classifyError(error),
|
|
508
|
-
timestamp: new Date().toISOString()
|
|
509
|
-
},
|
|
510
|
-
originalQueue: message.queue || 'unknown',
|
|
511
|
-
retryCount: message.retryCount || 0
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
const dlqName = `${message.queue || 'unknown'}.dlq`;
|
|
515
|
-
|
|
516
|
-
await mqClient.publish(dlqName, dlqMessage, {
|
|
517
|
-
persistent: true
|
|
338
|
+
const errorType = this.classifyError(error);
|
|
339
|
+
await this.core.dlqRouter.routeToDLQ(message, error, errorType, {
|
|
340
|
+
queue: message.queue || 'unknown'
|
|
518
341
|
});
|
|
519
|
-
|
|
520
|
-
console.info(`Message routed to DLQ: ${dlqName}`);
|
|
521
342
|
}
|
|
522
|
-
|
|
343
|
+
|
|
523
344
|
/**
|
|
524
345
|
* Create standardized error response
|
|
525
346
|
*
|
|
@@ -527,17 +348,10 @@ class ErrorHandlerConnector {
|
|
|
527
348
|
* @param {Error} error - Original error
|
|
528
349
|
* @param {Object} [context={}] - Additional context
|
|
529
350
|
* @returns {ErrorResponse} Formatted error response
|
|
530
|
-
*
|
|
531
|
-
* @example
|
|
532
|
-
* const response = errorHandler.createErrorResponse(
|
|
533
|
-
* new Error('Database connection failed'),
|
|
534
|
-
* { operation: 'user-fetch', userId: 123 }
|
|
535
|
-
* );
|
|
536
|
-
* // Returns standardized error object
|
|
537
351
|
*/
|
|
538
352
|
createErrorResponse(error, context = {}) {
|
|
539
353
|
const errorType = this.classifyError(error);
|
|
540
|
-
|
|
354
|
+
|
|
541
355
|
return {
|
|
542
356
|
success: false,
|
|
543
357
|
error: {
|
|
@@ -550,161 +364,27 @@ class ErrorHandlerConnector {
|
|
|
550
364
|
}
|
|
551
365
|
};
|
|
552
366
|
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Wrap function with error handling capabilities
|
|
556
|
-
*
|
|
557
|
-
* @method wrap
|
|
558
|
-
* @param {Function} fn - Function to wrap
|
|
559
|
-
* @param {Object} [options={}] - Wrapping options
|
|
560
|
-
* @param {string} [options.name] - Operation name
|
|
561
|
-
* @param {boolean} [options.circuitBreaker=true] - Use circuit breaker
|
|
562
|
-
* @param {boolean} [options.retry=true] - Use retry logic
|
|
563
|
-
* @param {number} [options.maxRetries] - Max retry attempts
|
|
564
|
-
* @returns {Function} Wrapped function
|
|
565
|
-
*
|
|
566
|
-
* @example <caption>Wrap API Function</caption>
|
|
567
|
-
* const safeApiCall = errorHandler.wrap(
|
|
568
|
-
* async (id) => await api.getUser(id),
|
|
569
|
-
* { name: 'get-user', maxRetries: 3 }
|
|
570
|
-
* );
|
|
571
|
-
*
|
|
572
|
-
* const user = await safeApiCall(123);
|
|
573
|
-
*
|
|
574
|
-
* @example <caption>Wrap Database Function</caption>
|
|
575
|
-
* const safeQuery = errorHandler.wrap(
|
|
576
|
-
* async (sql, params) => await db.query(sql, params),
|
|
577
|
-
* { circuitBreaker: false, retry: true }
|
|
578
|
-
* );
|
|
579
|
-
*/
|
|
580
|
-
wrap(fn, options = {}) {
|
|
581
|
-
const name = options.name || fn.name || 'wrapped';
|
|
582
|
-
const useCircuitBreaker = options.circuitBreaker !== false;
|
|
583
|
-
const useRetry = options.retry !== false;
|
|
584
|
-
|
|
585
|
-
return async (...args) => {
|
|
586
|
-
const execute = async () => {
|
|
587
|
-
if (useRetry) {
|
|
588
|
-
return await this.executeWithRetry(() => fn(...args), options);
|
|
589
|
-
}
|
|
590
|
-
return await fn(...args);
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
if (useCircuitBreaker) {
|
|
594
|
-
return await this.executeWithCircuitBreaker(name, execute, options);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
return await execute();
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* Handle workflow error with compensation support
|
|
603
|
-
*
|
|
604
|
-
* @async
|
|
605
|
-
* @method handleWorkflowError
|
|
606
|
-
* @param {Object} workflow - Workflow context
|
|
607
|
-
* @param {Error} error - Error that occurred
|
|
608
|
-
* @param {string} failedStep - Step that failed
|
|
609
|
-
* @returns {Promise<WorkflowErrorResult>} Error handling result
|
|
610
|
-
*
|
|
611
|
-
* @example
|
|
612
|
-
* const result = await errorHandler.handleWorkflowError(
|
|
613
|
-
* workflowContext,
|
|
614
|
-
* error,
|
|
615
|
-
* 'payment-processing'
|
|
616
|
-
* );
|
|
617
|
-
*
|
|
618
|
-
* if (result.shouldRetry) {
|
|
619
|
-
* // Retry the step
|
|
620
|
-
* } else if (result.compensated) {
|
|
621
|
-
* // Compensation was executed
|
|
622
|
-
* }
|
|
623
|
-
*/
|
|
624
|
-
async handleWorkflowError(workflow, error, failedStep) {
|
|
625
|
-
const errorType = this.classifyError(error);
|
|
626
|
-
|
|
627
|
-
const result = {
|
|
628
|
-
handled: false,
|
|
629
|
-
compensated: false,
|
|
630
|
-
shouldContinue: false,
|
|
631
|
-
shouldRetry: false,
|
|
632
|
-
error: this.createErrorResponse(error, { step: failedStep })
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
// Determine action based on error type
|
|
636
|
-
switch (errorType) {
|
|
637
|
-
case ErrorTypes.TRANSIENT:
|
|
638
|
-
case ErrorTypes.TIMEOUT:
|
|
639
|
-
result.shouldRetry = true;
|
|
640
|
-
result.handled = true;
|
|
641
|
-
break;
|
|
642
|
-
|
|
643
|
-
case ErrorTypes.BUSINESS:
|
|
644
|
-
case ErrorTypes.VALIDATION:
|
|
645
|
-
// Try compensation
|
|
646
|
-
if (this.compensationEnabled && workflow.compensationSteps) {
|
|
647
|
-
try {
|
|
648
|
-
await this.executeCompensation(failedStep, workflow);
|
|
649
|
-
result.compensated = true;
|
|
650
|
-
result.handled = true;
|
|
651
|
-
} catch (compError) {
|
|
652
|
-
console.error('Compensation failed:', compError);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
break;
|
|
656
|
-
|
|
657
|
-
case ErrorTypes.FATAL:
|
|
658
|
-
// Stop workflow immediately
|
|
659
|
-
result.shouldContinue = false;
|
|
660
|
-
result.handled = true;
|
|
661
|
-
break;
|
|
662
|
-
|
|
663
|
-
default:
|
|
664
|
-
// Unknown error, let it propagate
|
|
665
|
-
break;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
return result;
|
|
669
|
-
}
|
|
670
|
-
|
|
367
|
+
|
|
671
368
|
/**
|
|
672
369
|
* Get error handler statistics
|
|
673
370
|
*
|
|
674
371
|
* @method getStats
|
|
675
372
|
* @returns {ErrorStats} Current statistics
|
|
676
|
-
*
|
|
677
|
-
* @example
|
|
678
|
-
* const stats = errorHandler.getStats();
|
|
679
|
-
* console.log(`Total errors: ${stats.errors}`);
|
|
680
|
-
* console.log(`Retry success rate: ${stats.retries / stats.errors * 100}%`);
|
|
681
|
-
* console.log(`Circuit breakers:`, stats.circuitBreakers);
|
|
682
373
|
*/
|
|
683
374
|
getStats() {
|
|
684
|
-
const circuitBreakerStats =
|
|
685
|
-
|
|
686
|
-
this.circuitBreakers.forEach((breaker, name) => {
|
|
687
|
-
circuitBreakerStats[name] = {
|
|
688
|
-
state: breaker.opened ? 'open' : breaker.halfOpen ? 'half-open' : 'closed',
|
|
689
|
-
stats: breaker.stats
|
|
690
|
-
};
|
|
691
|
-
});
|
|
692
|
-
|
|
375
|
+
const circuitBreakerStats = this.core.getAllCircuitBreakerStates();
|
|
376
|
+
|
|
693
377
|
return {
|
|
694
378
|
...this.stats,
|
|
695
379
|
circuitBreakers: circuitBreakerStats
|
|
696
380
|
};
|
|
697
381
|
}
|
|
698
|
-
|
|
382
|
+
|
|
699
383
|
/**
|
|
700
384
|
* Reset all statistics
|
|
701
385
|
*
|
|
702
386
|
* @method resetStats
|
|
703
387
|
* @returns {void}
|
|
704
|
-
*
|
|
705
|
-
* @example
|
|
706
|
-
* errorHandler.resetStats();
|
|
707
|
-
* // All counters reset to 0
|
|
708
388
|
*/
|
|
709
389
|
resetStats() {
|
|
710
390
|
this.stats = {
|
|
@@ -714,51 +394,34 @@ class ErrorHandlerConnector {
|
|
|
714
394
|
circuitBreaks: 0,
|
|
715
395
|
byType: {}
|
|
716
396
|
};
|
|
717
|
-
|
|
397
|
+
|
|
718
398
|
Object.values(ErrorTypes).forEach(type => {
|
|
719
399
|
this.stats.byType[type] = 0;
|
|
720
400
|
});
|
|
721
401
|
}
|
|
722
|
-
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get circuit breaker state
|
|
405
|
+
*
|
|
406
|
+
* @method getCircuitBreakerState
|
|
407
|
+
* @param {string} name - Circuit breaker name
|
|
408
|
+
* @returns {string} State: 'open' | 'half-open' | 'closed'
|
|
409
|
+
*/
|
|
410
|
+
getCircuitBreakerState(name) {
|
|
411
|
+
return this.core.getCircuitBreakerState(name);
|
|
412
|
+
}
|
|
413
|
+
|
|
723
414
|
/**
|
|
724
|
-
*
|
|
725
|
-
*
|
|
415
|
+
* Get all circuit breaker states
|
|
416
|
+
*
|
|
417
|
+
* @method getAllCircuitBreakerStates
|
|
418
|
+
* @returns {Object} Map of circuit breaker states
|
|
726
419
|
*/
|
|
727
|
-
|
|
728
|
-
return
|
|
420
|
+
getAllCircuitBreakerStates() {
|
|
421
|
+
return this.core.getAllCircuitBreakerStates();
|
|
729
422
|
}
|
|
730
423
|
}
|
|
731
424
|
|
|
732
|
-
/**
|
|
733
|
-
* @typedef {Object} ErrorResponse
|
|
734
|
-
* @property {boolean} success - Always false for errors
|
|
735
|
-
* @property {Object} error - Error details
|
|
736
|
-
* @property {string} error.message - Error message
|
|
737
|
-
* @property {string} error.code - Error code
|
|
738
|
-
* @property {string} error.type - Error type from ErrorTypes
|
|
739
|
-
* @property {boolean} error.retryable - Whether error is retryable
|
|
740
|
-
* @property {string} error.timestamp - ISO timestamp
|
|
741
|
-
*/
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* @typedef {Object} WorkflowErrorResult
|
|
745
|
-
* @property {boolean} handled - Whether error was handled
|
|
746
|
-
* @property {boolean} compensated - Whether compensation was executed
|
|
747
|
-
* @property {boolean} shouldContinue - Whether workflow should continue
|
|
748
|
-
* @property {boolean} shouldRetry - Whether step should be retried
|
|
749
|
-
* @property {ErrorResponse} error - Error response
|
|
750
|
-
*/
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* @typedef {Object} ErrorStats
|
|
754
|
-
* @property {number} errors - Total errors
|
|
755
|
-
* @property {number} retries - Total retries
|
|
756
|
-
* @property {number} compensations - Total compensations
|
|
757
|
-
* @property {number} circuitBreaks - Circuit breaker trips
|
|
758
|
-
* @property {Object} byType - Errors by type
|
|
759
|
-
* @property {Object} circuitBreakers - Circuit breaker states
|
|
760
|
-
*/
|
|
761
|
-
|
|
762
425
|
// Export main class as default
|
|
763
426
|
module.exports = ErrorHandlerConnector;
|
|
764
427
|
|
|
@@ -780,12 +443,6 @@ module.exports.ErrorCodes = ErrorCodes;
|
|
|
780
443
|
* @function create
|
|
781
444
|
* @param {Object} config - Configuration object
|
|
782
445
|
* @returns {ErrorHandlerConnector} New error handler instance
|
|
783
|
-
*
|
|
784
|
-
* @example
|
|
785
|
-
* const errorHandler = ErrorHandlerConnector.create({
|
|
786
|
-
* maxRetries: 5,
|
|
787
|
-
* circuitBreakerEnabled: true
|
|
788
|
-
* });
|
|
789
446
|
*/
|
|
790
447
|
module.exports.create = (config) => new ErrorHandlerConnector(config);
|
|
791
448
|
|
|
@@ -794,29 +451,3 @@ module.exports.create = (config) => new ErrorHandlerConnector(config);
|
|
|
794
451
|
* @constant {string}
|
|
795
452
|
*/
|
|
796
453
|
module.exports.VERSION = '1.0.0';
|
|
797
|
-
|
|
798
|
-
// Export mock for testing
|
|
799
|
-
module.exports.MockErrorHandler = class MockErrorHandler {
|
|
800
|
-
constructor() {
|
|
801
|
-
this.stats = { errors: 0, retries: 0, compensations: 0 };
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
classifyError(error) {
|
|
805
|
-
return error.type || ErrorTypes.UNKNOWN;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
shouldRetry(error) {
|
|
809
|
-
return error.retryable !== false;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
async executeWithRetry(fn) {
|
|
813
|
-
try {
|
|
814
|
-
return await fn();
|
|
815
|
-
} catch (error) {
|
|
816
|
-
this.stats.errors++;
|
|
817
|
-
throw error;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
getStats() { return this.stats; }
|
|
822
|
-
};
|