@onlineapps/conn-infra-error-handler 1.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.
package/src/index.js ADDED
@@ -0,0 +1,822 @@
1
+ /**
2
+ * @module @onlineapps/conn-infra-error-handler
3
+ * @description Unified error handling connector providing retry strategies, circuit breaker pattern,
4
+ * and compensation mechanisms for OA Drive microservices.
5
+ *
6
+ * @see {@link https://github.com/onlineapps/oa-drive/tree/main/shared/connector/conn-infra-error-handler|GitHub Repository}
7
+ * @author OA Drive Team
8
+ * @license MIT
9
+ * @since 1.0.0
10
+ */
11
+
12
+ const CircuitBreaker = require('opossum');
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
+ };
50
+
51
+ /**
52
+ * Error handling connector with retry, circuit breaker, and compensation
53
+ *
54
+ * @class ErrorHandlerConnector
55
+ *
56
+ * @example <caption>Basic Usage</caption>
57
+ * const errorHandler = new ErrorHandlerConnector({
58
+ * maxRetries: 3,
59
+ * retryDelay: 1000
60
+ * });
61
+ *
62
+ * const result = await errorHandler.executeWithRetry(async () => {
63
+ * return await riskyOperation();
64
+ * });
65
+ *
66
+ * @example <caption>With Circuit Breaker</caption>
67
+ * const errorHandler = new ErrorHandlerConnector({
68
+ * circuitBreakerEnabled: true,
69
+ * errorThreshold: 50
70
+ * });
71
+ *
72
+ * await errorHandler.executeWithCircuitBreaker('api-call', async () => {
73
+ * return await apiClient.call();
74
+ * });
75
+ */
76
+ class ErrorHandlerConnector {
77
+ /**
78
+ * Creates a new ErrorHandlerConnector instance
79
+ *
80
+ * @constructor
81
+ * @param {Object} [config={}] - Configuration options
82
+ * @param {number} [config.maxRetries=3] - Maximum retry attempts
83
+ * @param {number} [config.retryDelay=1000] - Initial retry delay in ms
84
+ * @param {number} [config.retryMultiplier=2] - Backoff multiplier
85
+ * @param {number} [config.maxRetryDelay=30000] - Maximum retry delay
86
+ * @param {boolean} [config.circuitBreakerEnabled=true] - Enable circuit breaker
87
+ * @param {number} [config.circuitTimeout=10000] - Circuit breaker timeout
88
+ * @param {number} [config.errorThreshold=50] - Error threshold percentage
89
+ * @param {number} [config.resetTimeout=30000] - Circuit reset timeout
90
+ * @param {boolean} [config.compensationEnabled=true] - Enable compensation
91
+ * @param {Object} [config.circuitBreakerOptions] - Additional circuit breaker options
92
+ *
93
+ * @example <caption>Full Configuration</caption>
94
+ * const errorHandler = new ErrorHandlerConnector({
95
+ * maxRetries: 5,
96
+ * retryDelay: 500,
97
+ * retryMultiplier: 1.5,
98
+ * maxRetryDelay: 20000,
99
+ * circuitBreakerEnabled: true,
100
+ * errorThreshold: 60,
101
+ * compensationEnabled: true
102
+ * });
103
+ */
104
+ constructor(config = {}) {
105
+ // Retry configuration
106
+ this.maxRetries = config.maxRetries || 3;
107
+ this.retryDelay = config.retryDelay || 1000;
108
+ this.retryMultiplier = config.retryMultiplier || 2;
109
+ this.maxRetryDelay = config.maxRetryDelay || 30000;
110
+
111
+ // Circuit breaker configuration
112
+ this.circuitBreakerEnabled = config.circuitBreakerEnabled !== false;
113
+ this.circuitBreakerOptions = {
114
+ timeout: config.circuitTimeout || 10000,
115
+ errorThresholdPercentage: config.errorThreshold || 50,
116
+ resetTimeout: config.resetTimeout || 30000,
117
+ rollingCountTimeout: config.rollingCountTimeout || 10000,
118
+ rollingCountBuckets: config.rollingCountBuckets || 10,
119
+ ...config.circuitBreakerOptions
120
+ };
121
+
122
+ // Compensation configuration
123
+ this.compensationEnabled = config.compensationEnabled !== false;
124
+ this.compensationHandlers = new Map();
125
+
126
+ // Circuit breakers registry
127
+ this.circuitBreakers = new Map();
128
+
129
+ // Error statistics
130
+ this.stats = {
131
+ errors: 0,
132
+ retries: 0,
133
+ compensations: 0,
134
+ circuitBreaks: 0,
135
+ byType: {}
136
+ };
137
+
138
+ // Initialize error type stats
139
+ Object.values(ErrorTypes).forEach(type => {
140
+ this.stats.byType[type] = 0;
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Classify error into type for appropriate handling
146
+ *
147
+ * @method classifyError
148
+ * @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
+ * @returns {string} Error type from ErrorTypes enum
153
+ *
154
+ * @example <caption>Network Error</caption>
155
+ * const type = errorHandler.classifyError(new Error('ECONNREFUSED'));
156
+ * // 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
+ */
168
+ classifyError(error) {
169
+ // Check by error code
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;
198
+ }
199
+
200
+ /**
201
+ * Determine if error should be retried
202
+ *
203
+ * @method shouldRetry
204
+ * @param {Error} error - Error to check
205
+ * @param {number} [attempts=0] - Current attempt count
206
+ * @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
+ */
214
+ shouldRetry(error, attempts = 0) {
215
+ if (attempts >= this.maxRetries) {
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);
227
+ }
228
+
229
+ /**
230
+ * Calculate exponential backoff delay
231
+ *
232
+ * @method calculateBackoff
233
+ * @param {number} attempts - Current attempt number (1-based)
234
+ * @returns {number} Delay in milliseconds
235
+ *
236
+ * @example
237
+ * const delay = errorHandler.calculateBackoff(3);
238
+ * // With default config: 1000 * 2^2 = 4000ms
239
+ */
240
+ calculateBackoff(attempts) {
241
+ const delay = this.retryDelay * Math.pow(this.retryMultiplier, attempts - 1);
242
+ return Math.min(delay, this.maxRetryDelay);
243
+ }
244
+
245
+ /**
246
+ * Execute function with automatic retry on failure
247
+ *
248
+ * @async
249
+ * @method executeWithRetry
250
+ * @param {Function} fn - Async function to execute
251
+ * @param {Object} [options={}] - Retry options
252
+ * @param {number} [options.maxRetries] - Override max retries
253
+ * @param {Array<string>} [options.retryOn=[]] - Additional error codes to retry
254
+ * @param {Function} [options.onRetry] - Callback on retry (error, attempt, delay)
255
+ * @returns {Promise<*>} Function result
256
+ *
257
+ * @throws {Error} Last error if all retries fail
258
+ *
259
+ * @example <caption>Simple Retry</caption>
260
+ * const result = await errorHandler.executeWithRetry(async () => {
261
+ * return await apiClient.fetchData();
262
+ * });
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
+ */
286
+ async executeWithRetry(fn, options = {}) {
287
+ const maxAttempts = options.maxRetries || this.maxRetries;
288
+ const retryOn = options.retryOn || [];
289
+
290
+ let lastError;
291
+
292
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
293
+ try {
294
+ return await fn();
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);
353
+ }
354
+
355
+ return this.circuitBreakers.get(name);
356
+ }
357
+
358
+ /**
359
+ * Execute function with circuit breaker protection
360
+ *
361
+ * @async
362
+ * @method executeWithCircuitBreaker
363
+ * @param {string} name - Operation name for circuit breaker
364
+ * @param {Function} fn - Async function to execute
365
+ * @param {Object} [options={}] - Circuit breaker options
366
+ * @param {number} [options.timeout] - Operation timeout
367
+ * @param {number} [options.errorThresholdPercentage] - Error threshold
368
+ * @returns {Promise<*>} Function result
369
+ *
370
+ * @throws {Error} If circuit is open or operation fails
371
+ *
372
+ * @fires CircuitBreaker#open - When circuit opens
373
+ * @fires CircuitBreaker#halfOpen - When circuit enters half-open state
374
+ *
375
+ * @example <caption>API Call Protection</caption>
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
+ * }
395
+ * );
396
+ */
397
+ async executeWithCircuitBreaker(name, fn, options = {}) {
398
+ const breaker = this.getCircuitBreaker(name, fn, options);
399
+
400
+ try {
401
+ return await breaker.fire();
402
+ } catch (error) {
403
+ this.stats.errors++;
404
+ this.stats.byType[this.classifyError(error)]++;
405
+
406
+ if (error.code === 'EOPENBREAKER') {
407
+ throw new Error(`Service unavailable: ${name} circuit is open`);
408
+ }
409
+
410
+ throw error;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Register compensation handler for an operation
416
+ *
417
+ * @method registerCompensation
418
+ * @param {string} operation - Operation name
419
+ * @param {Function} handler - Compensation handler function
420
+ * @returns {void}
421
+ *
422
+ * @example
423
+ * errorHandler.registerCompensation('create-order', async (context) => {
424
+ * // Rollback order creation
425
+ * await orderService.cancel(context.orderId);
426
+ * });
427
+ *
428
+ * errorHandler.registerCompensation('charge-payment', async (context) => {
429
+ * // Refund payment
430
+ * await paymentService.refund(context.chargeId);
431
+ * });
432
+ */
433
+ registerCompensation(operation, handler) {
434
+ this.compensationHandlers.set(operation, handler);
435
+ }
436
+
437
+ /**
438
+ * Execute compensation for failed operation
439
+ *
440
+ * @async
441
+ * @method executeCompensation
442
+ * @param {string} operation - Operation that failed
443
+ * @param {Object} context - Operation context for compensation
444
+ * @returns {Promise<*>} Compensation result or null if no handler
445
+ *
446
+ * @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
+ */
458
+ 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
+ try {
471
+ this.stats.compensations++;
472
+ const result = await handler(context);
473
+ console.info(`Compensation executed for: ${operation}`);
474
+ return result;
475
+ } catch (error) {
476
+ console.error(`Compensation failed for ${operation}:`, error);
477
+ throw error;
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Route failed message to dead letter queue
483
+ *
484
+ * @async
485
+ * @method routeToDLQ
486
+ * @param {Object} mqClient - Message queue client
487
+ * @param {Object} message - Original message that failed
488
+ * @param {Error} error - Error that occurred
489
+ * @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
+ */
500
+ async routeToDLQ(mqClient, message, error) {
501
+ const dlqMessage = {
502
+ ...message,
503
+ error: {
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
518
+ });
519
+
520
+ console.info(`Message routed to DLQ: ${dlqName}`);
521
+ }
522
+
523
+ /**
524
+ * Create standardized error response
525
+ *
526
+ * @method createErrorResponse
527
+ * @param {Error} error - Original error
528
+ * @param {Object} [context={}] - Additional context
529
+ * @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
+ */
538
+ createErrorResponse(error, context = {}) {
539
+ const errorType = this.classifyError(error);
540
+
541
+ return {
542
+ success: false,
543
+ error: {
544
+ message: error.message,
545
+ code: error.code || 'UNKNOWN_ERROR',
546
+ type: errorType,
547
+ retryable: this.shouldRetry(error),
548
+ timestamp: new Date().toISOString(),
549
+ ...context
550
+ }
551
+ };
552
+ }
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
+
671
+ /**
672
+ * Get error handler statistics
673
+ *
674
+ * @method getStats
675
+ * @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
+ */
683
+ 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
+
693
+ return {
694
+ ...this.stats,
695
+ circuitBreakers: circuitBreakerStats
696
+ };
697
+ }
698
+
699
+ /**
700
+ * Reset all statistics
701
+ *
702
+ * @method resetStats
703
+ * @returns {void}
704
+ *
705
+ * @example
706
+ * errorHandler.resetStats();
707
+ * // All counters reset to 0
708
+ */
709
+ resetStats() {
710
+ this.stats = {
711
+ errors: 0,
712
+ retries: 0,
713
+ compensations: 0,
714
+ circuitBreaks: 0,
715
+ byType: {}
716
+ };
717
+
718
+ Object.values(ErrorTypes).forEach(type => {
719
+ this.stats.byType[type] = 0;
720
+ });
721
+ }
722
+
723
+ /**
724
+ * Sleep utility
725
+ * @private
726
+ */
727
+ async sleep(ms) {
728
+ return new Promise(resolve => setTimeout(resolve, ms));
729
+ }
730
+ }
731
+
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
+ // Export main class as default
763
+ module.exports = ErrorHandlerConnector;
764
+
765
+ /**
766
+ * Error type enumeration
767
+ * @enum {string}
768
+ */
769
+ module.exports.ErrorTypes = ErrorTypes;
770
+
771
+ /**
772
+ * Standard error code mappings
773
+ * @enum {string}
774
+ */
775
+ module.exports.ErrorCodes = ErrorCodes;
776
+
777
+ /**
778
+ * Factory function to create error handler instance
779
+ *
780
+ * @function create
781
+ * @param {Object} config - Configuration object
782
+ * @returns {ErrorHandlerConnector} New error handler instance
783
+ *
784
+ * @example
785
+ * const errorHandler = ErrorHandlerConnector.create({
786
+ * maxRetries: 5,
787
+ * circuitBreakerEnabled: true
788
+ * });
789
+ */
790
+ module.exports.create = (config) => new ErrorHandlerConnector(config);
791
+
792
+ /**
793
+ * Current version
794
+ * @constant {string}
795
+ */
796
+ 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
+ };