@onlineapps/error-handler-core 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/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @onlineapps/error-handler-core
2
+
3
+ Core error handling functionality for OA Drive microservices. Used by both business services (via `conn-infra-error-handler` wrapper) and infrastructure services (directly).
4
+
5
+ ## Features
6
+
7
+ - **Error Classification** - Automatic error type detection (TRANSIENT, BUSINESS, FATAL, etc.)
8
+ - **Retry Logic** - Exponential backoff for transient errors
9
+ - **Circuit Breaker** - Protection against cascading failures
10
+ - **DLQ Routing** - Dead letter queue routing for permanent errors
11
+ - **Compensation** - Rollback operations for failed workflows
12
+ - **Unified Logging** - Structured error logging via monitoring-core
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @onlineapps/error-handler-core
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Basic Setup
23
+
24
+ ```javascript
25
+ const { init: initMonitoring } = require('@onlineapps/monitoring-core');
26
+ const { ErrorHandlerCore } = require('@onlineapps/error-handler-core');
27
+
28
+ // Initialize monitoring-core
29
+ const monitoring = await initMonitoring({
30
+ serviceName: 'my-service',
31
+ mode: 'light'
32
+ });
33
+
34
+ // Initialize error handler
35
+ const errorHandler = new ErrorHandlerCore({
36
+ serviceName: 'my-service',
37
+ serviceVersion: '1.0.0',
38
+ environment: process.env.NODE_ENV,
39
+ monitoring: monitoring, // monitoring-core instance (required)
40
+ handling: {
41
+ maxRetries: 3,
42
+ retryDelay: 1000,
43
+ retryMultiplier: 2,
44
+ circuitBreakerEnabled: true,
45
+ dlqEnabled: true,
46
+ mqClient: mqClient // Optional, for DLQ routing
47
+ }
48
+ });
49
+ ```
50
+
51
+ ### Log Error
52
+
53
+ ```javascript
54
+ await errorHandler.logError({
55
+ moduleName: 'MyModule',
56
+ operation: 'myOperation',
57
+ error: new Error('Something failed'),
58
+ context: { userId: 123 }
59
+ });
60
+ ```
61
+
62
+ ### Handle Error
63
+
64
+ ```javascript
65
+ try {
66
+ await criticalOperation();
67
+ } catch (error) {
68
+ const result = await errorHandler.handleError({
69
+ moduleName: 'MyModule',
70
+ operation: 'criticalOperation',
71
+ error: error,
72
+ context: { operationId: 'op-123' }
73
+ });
74
+
75
+ // result.action: 'retry' | 'dlq' | 'throw' | 'continue' | 'compensate'
76
+ if (result.action === 'retry') {
77
+ await sleep(result.retryDelay);
78
+ // Retry operation
79
+ }
80
+ }
81
+ ```
82
+
83
+ ### Execute with Retry
84
+
85
+ ```javascript
86
+ const result = await errorHandler.executeWithRetry({
87
+ moduleName: 'MQClient',
88
+ operation: 'publish',
89
+ fn: async () => await mqClient.publish(queue, message),
90
+ context: { queue, messageId }
91
+ });
92
+ ```
93
+
94
+ ### Execute with Circuit Breaker
95
+
96
+ ```javascript
97
+ const result = await errorHandler.executeWithCircuitBreaker({
98
+ moduleName: 'ExternalAPI',
99
+ operation: 'getUser',
100
+ circuitName: 'user-api',
101
+ fn: async () => await userAPI.getUser(id),
102
+ context: { userId: id }
103
+ });
104
+ ```
105
+
106
+ ## API Reference
107
+
108
+ See [UNIFIED_ERROR_HANDLING.md](/docs/standards/UNIFIED_ERROR_HANDLING.md) for complete API documentation.
109
+
110
+ ## Architecture
111
+
112
+ - **Core Library** - Used directly by infrastructure services
113
+ - **Wrapper** - `conn-infra-error-handler` wraps this for business services
114
+ - **Dependencies** - Uses `monitoring-core` for logging
115
+
116
+ ## Related Documentation
117
+
118
+ - [Unified Error Handling Standard](/docs/standards/UNIFIED_ERROR_HANDLING.md)
119
+ - [Error Handling Architecture](/docs/standards/ERROR_HANDLING_ARCHITECTURE.md)
120
+ - [Error Handling Standard](/docs/standards/ERROR_HANDLING.md)
121
+
122
+ ---
123
+
124
+ *Version: 1.0.0 | License: MIT*
125
+
package/jest.config.js ADDED
@@ -0,0 +1,23 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ coveragePathIgnorePatterns: [
4
+ '/node_modules/',
5
+ '/tests/'
6
+ ],
7
+ testMatch: [
8
+ '**/tests/**/*.test.js'
9
+ ],
10
+ collectCoverageFrom: [
11
+ 'src/**/*.js',
12
+ '!src/**/*.test.js'
13
+ ],
14
+ coverageThreshold: {
15
+ global: {
16
+ branches: 70,
17
+ functions: 70,
18
+ lines: 70,
19
+ statements: 70
20
+ }
21
+ }
22
+ };
23
+
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@onlineapps/error-handler-core",
3
+ "version": "1.0.0",
4
+ "description": "Core error handling functionality for OA Drive microservices - classification, retry, circuit breaker, DLQ routing",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "test:coverage": "jest --coverage",
9
+ "test:watch": "jest --watch",
10
+ "test:unit": "jest tests/unit",
11
+ "test:component": "jest tests/component"
12
+ },
13
+ "keywords": [
14
+ "error-handling",
15
+ "retry",
16
+ "circuit-breaker",
17
+ "dlq",
18
+ "microservices"
19
+ ],
20
+ "author": "OnlineApps",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@onlineapps/monitoring-core": "^1.0.0",
24
+ "opossum": "^7.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "jest": "^29.7.0"
28
+ },
29
+ "publishConfig": {
30
+ "registry": "https://registry.npmjs.org/",
31
+ "access": "public"
32
+ }
33
+ }
34
+
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Circuit Breaker
3
+ * Protects against cascading failures
4
+ */
5
+
6
+ const CircuitBreaker = require('opossum');
7
+
8
+ class CircuitBreakerManager {
9
+ constructor(config = {}) {
10
+ this.enabled = config.circuitBreakerEnabled !== false;
11
+ this.options = {
12
+ timeout: config.timeout || 10000,
13
+ errorThresholdPercentage: config.errorThreshold || 50,
14
+ resetTimeout: config.resetTimeout || 30000,
15
+ rollingCountTimeout: config.rollingCountTimeout || 10000,
16
+ rollingCountBuckets: config.rollingCountBuckets || 10,
17
+ ...config.circuitBreakerOptions
18
+ };
19
+
20
+ // Registry of circuit breakers
21
+ this.breakers = new Map();
22
+ }
23
+
24
+ /**
25
+ * Get or create circuit breaker for operation
26
+ *
27
+ * @param {string} name - Operation name
28
+ * @param {Function} fn - Function to protect
29
+ * @param {Object} options - Circuit breaker options
30
+ * @returns {CircuitBreaker} Circuit breaker instance
31
+ */
32
+ getCircuitBreaker(name, fn, options = {}) {
33
+ if (!this.enabled) {
34
+ // Return pass-through if disabled
35
+ return {
36
+ fire: () => fn(),
37
+ opened: false,
38
+ halfOpen: false
39
+ };
40
+ }
41
+
42
+ if (!this.breakers.has(name)) {
43
+ const breaker = new CircuitBreaker(fn, {
44
+ ...this.options,
45
+ ...options
46
+ });
47
+
48
+ this.breakers.set(name, breaker);
49
+ }
50
+
51
+ return this.breakers.get(name);
52
+ }
53
+
54
+ /**
55
+ * Execute function with circuit breaker protection
56
+ *
57
+ * @param {string} name - Operation name
58
+ * @param {Function} fn - Async function to execute
59
+ * @param {Object} options - Circuit breaker options
60
+ * @returns {Promise<*>} Function result
61
+ * @throws {Error} If circuit is open or operation fails
62
+ */
63
+ async executeWithCircuitBreaker(name, fn, options = {}) {
64
+ const breaker = this.getCircuitBreaker(name, fn, options);
65
+
66
+ try {
67
+ return await breaker.fire();
68
+ } catch (error) {
69
+ if (error.code === 'EOPENBREAKER') {
70
+ throw new Error(`Service unavailable: ${name} circuit is open`);
71
+ }
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Get circuit breaker state
78
+ *
79
+ * @param {string} name - Circuit breaker name
80
+ * @returns {string} State: 'open' | 'half-open' | 'closed'
81
+ */
82
+ getState(name) {
83
+ if (!this.breakers.has(name)) {
84
+ return 'closed';
85
+ }
86
+
87
+ const breaker = this.breakers.get(name);
88
+ if (breaker.opened) return 'open';
89
+ if (breaker.halfOpen) return 'half-open';
90
+ return 'closed';
91
+ }
92
+
93
+ /**
94
+ * Get all circuit breaker states
95
+ * @returns {Object} Map of circuit breaker states
96
+ */
97
+ getAllStates() {
98
+ const states = {};
99
+ this.breakers.forEach((breaker, name) => {
100
+ states[name] = {
101
+ state: this.getState(name),
102
+ stats: breaker.stats || {}
103
+ };
104
+ });
105
+ return states;
106
+ }
107
+ }
108
+
109
+ module.exports = CircuitBreakerManager;
110
+
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Compensation Handler
3
+ * Executes rollback operations for failed workflow steps
4
+ */
5
+
6
+ class CompensationHandler {
7
+ constructor(config = {}) {
8
+ this.enabled = config.compensationEnabled !== false;
9
+ this.handlers = new Map();
10
+ }
11
+
12
+ /**
13
+ * Register compensation handler for operation
14
+ *
15
+ * @param {string} operation - Operation name
16
+ * @param {Function} handler - Compensation handler function
17
+ */
18
+ register(operation, handler) {
19
+ if (typeof handler !== 'function') {
20
+ throw new Error(`Compensation handler for ${operation} must be a function`);
21
+ }
22
+
23
+ this.handlers.set(operation, handler);
24
+ }
25
+
26
+ /**
27
+ * Execute compensation for failed operation
28
+ *
29
+ * @param {string} operation - Operation that failed
30
+ * @param {Object} context - Operation context for compensation
31
+ * @returns {Promise<*>} Compensation result or null if no handler
32
+ * @throws {Error} If compensation fails
33
+ */
34
+ async execute(operation, context) {
35
+ if (!this.enabled) {
36
+ return null;
37
+ }
38
+
39
+ const handler = this.handlers.get(operation);
40
+
41
+ if (!handler) {
42
+ return null; // No compensation handler registered
43
+ }
44
+
45
+ try {
46
+ return await handler(context);
47
+ } catch (error) {
48
+ throw new Error(`Compensation failed for ${operation}: ${error.message}`);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Execute compensations in reverse order
54
+ * Used for workflow rollback
55
+ *
56
+ * @param {Array<string>} operations - Operations executed (in order)
57
+ * @param {Object} context - Shared context for all compensations
58
+ * @returns {Promise<Array>} Results of all compensations
59
+ */
60
+ async executeReverse(operations, context) {
61
+ if (!this.enabled || !operations || operations.length === 0) {
62
+ return [];
63
+ }
64
+
65
+ const results = [];
66
+
67
+ // Execute in reverse order
68
+ for (let i = operations.length - 1; i >= 0; i--) {
69
+ const operation = operations[i];
70
+ try {
71
+ const result = await this.execute(operation, context);
72
+ if (result !== null) {
73
+ results.push({ operation, success: true, result });
74
+ }
75
+ } catch (error) {
76
+ results.push({ operation, success: false, error: error.message });
77
+ // Continue with other compensations even if one fails
78
+ }
79
+ }
80
+
81
+ return results;
82
+ }
83
+
84
+ /**
85
+ * Check if compensation handler exists for operation
86
+ *
87
+ * @param {string} operation - Operation name
88
+ * @returns {boolean} True if handler exists
89
+ */
90
+ hasHandler(operation) {
91
+ return this.handlers.has(operation);
92
+ }
93
+
94
+ /**
95
+ * Get all registered operations
96
+ * @returns {Array<string>} List of operation names with handlers
97
+ */
98
+ getRegisteredOperations() {
99
+ return Array.from(this.handlers.keys());
100
+ }
101
+ }
102
+
103
+ module.exports = CompensationHandler;
104
+
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Dead Letter Queue Router
3
+ * Routes failed messages to DLQ for manual inspection
4
+ */
5
+
6
+ class DLQRouter {
7
+ constructor(config = {}) {
8
+ this.enabled = config.dlqEnabled !== false;
9
+ this.mqClient = config.mqClient || null;
10
+ this.dlqNamePattern = config.dlqNamePattern || '{queue}.dlq';
11
+ }
12
+
13
+ /**
14
+ * Route message to DLQ
15
+ *
16
+ * @param {Object} message - Original message
17
+ * @param {Error} error - Error that occurred
18
+ * @param {string} errorType - Classified error type
19
+ * @param {Object} context - Additional context
20
+ * @returns {Promise<void>}
21
+ */
22
+ async routeToDLQ(message, error, errorType, context = {}) {
23
+ if (!this.enabled || !this.mqClient) {
24
+ return; // DLQ disabled or no MQ client
25
+ }
26
+
27
+ const originalQueue = message.queue || context.queue || 'unknown';
28
+ const dlqName = this._generateDLQName(originalQueue);
29
+
30
+ const dlqMessage = {
31
+ // Original message
32
+ ...message,
33
+
34
+ // Error details
35
+ error: {
36
+ message: error.message,
37
+ stack: error.stack,
38
+ code: error.code || null,
39
+ type: errorType,
40
+ timestamp: new Date().toISOString()
41
+ },
42
+
43
+ // Routing metadata
44
+ originalQueue: originalQueue,
45
+ retryCount: message.retryCount || context.retryCount || 0,
46
+ routedAt: new Date().toISOString(),
47
+ routingReason: this._determineRoutingReason(errorType, context)
48
+ };
49
+
50
+ try {
51
+ await this.mqClient.publish(dlqName, dlqMessage, {
52
+ persistent: true
53
+ });
54
+ } catch (dlqError) {
55
+ // If DLQ routing fails, log but don't throw
56
+ // (we don't want DLQ failures to break error handling)
57
+ console.error(`[DLQRouter] Failed to route to DLQ ${dlqName}:`, dlqError.message);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Generate DLQ name from original queue name
63
+ * @private
64
+ */
65
+ _generateDLQName(originalQueue) {
66
+ if (this.dlqNamePattern.includes('{queue}')) {
67
+ return this.dlqNamePattern.replace('{queue}', originalQueue);
68
+ }
69
+ return this.dlqNamePattern;
70
+ }
71
+
72
+ /**
73
+ * Determine routing reason
74
+ * @private
75
+ */
76
+ _determineRoutingReason(errorType, context) {
77
+ if (context.retryCount >= context.maxRetries) {
78
+ return 'max_retries_exceeded';
79
+ }
80
+
81
+ if (['BUSINESS', 'VALIDATION'].includes(errorType)) {
82
+ return 'permanent_error';
83
+ }
84
+
85
+ return 'error_handling_decision';
86
+ }
87
+
88
+ /**
89
+ * Check if DLQ routing is available
90
+ * @returns {boolean} True if DLQ is enabled and MQ client available
91
+ */
92
+ isAvailable() {
93
+ return this.enabled && this.mqClient !== null;
94
+ }
95
+ }
96
+
97
+ module.exports = DLQRouter;
98
+
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Error Classifier
3
+ * Classifies errors into types for appropriate handling strategy
4
+ */
5
+
6
+ // Error types enum
7
+ const ErrorTypes = {
8
+ TRANSIENT: 'TRANSIENT', // Network, temporary failures - RETRY
9
+ BUSINESS: 'BUSINESS', // Business logic errors - DLQ
10
+ FATAL: 'FATAL', // Critical failures - THROW immediately
11
+ VALIDATION: 'VALIDATION', // Input validation - DLQ
12
+ TIMEOUT: 'TIMEOUT', // Operation timeout - RETRY
13
+ RATE_LIMIT: 'RATE_LIMIT', // Rate limiting - RETRY with backoff
14
+ UNKNOWN: 'UNKNOWN' // Unclassified - LOG and THROW
15
+ };
16
+
17
+ // Error code mappings
18
+ const ErrorCodes = {
19
+ // Network errors → TRANSIENT
20
+ ECONNREFUSED: ErrorTypes.TRANSIENT,
21
+ ENOTFOUND: ErrorTypes.TRANSIENT,
22
+ ETIMEDOUT: ErrorTypes.TRANSIENT,
23
+ ECONNRESET: ErrorTypes.TRANSIENT,
24
+ EPIPE: ErrorTypes.TRANSIENT,
25
+
26
+ // HTTP status codes
27
+ 408: ErrorTypes.TIMEOUT,
28
+ 429: ErrorTypes.RATE_LIMIT,
29
+ 500: ErrorTypes.TRANSIENT,
30
+ 502: ErrorTypes.TRANSIENT,
31
+ 503: ErrorTypes.TRANSIENT,
32
+ 504: ErrorTypes.TIMEOUT,
33
+
34
+ // Business errors
35
+ 400: ErrorTypes.VALIDATION,
36
+ 401: ErrorTypes.BUSINESS,
37
+ 403: ErrorTypes.BUSINESS,
38
+ 404: ErrorTypes.BUSINESS,
39
+ 409: ErrorTypes.BUSINESS,
40
+ 422: ErrorTypes.VALIDATION
41
+ };
42
+
43
+ class ErrorClassifier {
44
+ /**
45
+ * Classify error into type for appropriate handling
46
+ *
47
+ * @param {Error} error - Error to classify
48
+ * @returns {string} Error type from ErrorTypes enum
49
+ */
50
+ classify(error) {
51
+ if (!error) {
52
+ return ErrorTypes.UNKNOWN;
53
+ }
54
+
55
+ // 1. Check explicit type
56
+ if (error.type && ErrorTypes[error.type]) {
57
+ return error.type;
58
+ }
59
+
60
+ // 2. Check error code
61
+ if (error.code && ErrorCodes[error.code]) {
62
+ return ErrorCodes[error.code];
63
+ }
64
+
65
+ // 3. Check HTTP status
66
+ if (error.statusCode && ErrorCodes[error.statusCode]) {
67
+ return ErrorCodes[error.statusCode];
68
+ }
69
+
70
+ // 4. Check message patterns
71
+ const message = (error.message || '').toLowerCase();
72
+
73
+ if (message.includes('timeout')) {
74
+ return ErrorTypes.TIMEOUT;
75
+ }
76
+
77
+ if (message.includes('rate limit') || message.includes('rate_limit')) {
78
+ return ErrorTypes.RATE_LIMIT;
79
+ }
80
+
81
+ if (message.includes('validation') || message.includes('invalid')) {
82
+ return ErrorTypes.VALIDATION;
83
+ }
84
+
85
+ if (message.includes('fatal') || message.includes('critical')) {
86
+ return ErrorTypes.FATAL;
87
+ }
88
+
89
+ if (message.includes('connection') || message.includes('network') || message.includes('econn')) {
90
+ return ErrorTypes.TRANSIENT;
91
+ }
92
+
93
+ // 5. Default
94
+ return ErrorTypes.UNKNOWN;
95
+ }
96
+
97
+ /**
98
+ * Determine if error should be retried
99
+ *
100
+ * @param {Error} error - Error to check
101
+ * @param {number} attempts - Current attempt count
102
+ * @param {number} maxRetries - Maximum retry attempts
103
+ * @returns {boolean} True if should retry
104
+ */
105
+ shouldRetry(error, attempts = 0, maxRetries = 3) {
106
+ if (attempts >= maxRetries) {
107
+ return false;
108
+ }
109
+
110
+ const errorType = this.classify(error);
111
+
112
+ // Only retry transient, timeout, and rate limit errors
113
+ return [
114
+ ErrorTypes.TRANSIENT,
115
+ ErrorTypes.TIMEOUT,
116
+ ErrorTypes.RATE_LIMIT
117
+ ].includes(errorType);
118
+ }
119
+
120
+ /**
121
+ * Get error types enum
122
+ * @returns {Object} ErrorTypes enum
123
+ */
124
+ static getErrorTypes() {
125
+ return ErrorTypes;
126
+ }
127
+
128
+ /**
129
+ * Get error codes mapping
130
+ * @returns {Object} ErrorCodes mapping
131
+ */
132
+ static getErrorCodes() {
133
+ return ErrorCodes;
134
+ }
135
+ }
136
+
137
+ module.exports = {
138
+ ErrorClassifier,
139
+ ErrorTypes,
140
+ ErrorCodes
141
+ };
142
+