Files
michaelschiemer/resources/js/core/ModuleErrorBoundary.js
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

223 lines
7.6 KiB
JavaScript

/**
* Error Boundary System für Module
* Bietet Schutz vor Module-Crashes und Recovery-Mechanismen
*/
import { Logger } from './logger.js';
export class ModuleErrorBoundary {
constructor() {
this.crashedModules = new Set();
this.recoveryAttempts = new Map();
this.maxRecoveryAttempts = 3;
this.recoveryDelay = 1000; // 1 second
}
/**
* Wraps a module with error handling
* @param {Object} module - The module to wrap
* @param {string} moduleName - Name of the module for logging
* @returns {Proxy} - Protected module instance
*/
wrapModule(module, moduleName) {
if (!module || typeof module !== 'object') {
Logger.warn(`[ErrorBoundary] Cannot wrap non-object module: ${moduleName}`);
return module;
}
return new Proxy(module, {
get: (target, prop, receiver) => {
const originalValue = target[prop];
// If property is not a function, return as-is
if (typeof originalValue !== 'function') {
return originalValue;
}
// Check if property is non-configurable - return original if so
const descriptor = Object.getOwnPropertyDescriptor(target, prop);
if (descriptor && !descriptor.configurable) {
return originalValue;
}
// Return wrapped function with error handling
return (...args) => {
try {
const result = originalValue.apply(target, args);
// Handle async functions
if (result && typeof result.catch === 'function') {
return result.catch(error => {
this.handleModuleError(error, moduleName, prop, args);
return this.getRecoveryValue(moduleName, prop);
});
}
return result;
} catch (error) {
this.handleModuleError(error, moduleName, prop, args);
return this.getRecoveryValue(moduleName, prop);
}
};
},
getOwnPropertyDescriptor: (target, prop) => {
const descriptor = Object.getOwnPropertyDescriptor(target, prop);
// For non-configurable properties, return original descriptor
if (descriptor && !descriptor.configurable) {
return descriptor;
}
return descriptor;
},
has: (target, prop) => {
return prop in target;
},
ownKeys: (target) => {
return Object.getOwnPropertyNames(target);
}
});
}
/**
* Handles module errors with recovery logic
* @param {Error} error - The error that occurred
* @param {string} moduleName - Name of the failing module
* @param {string} method - Method that failed
* @param {Array} args - Arguments passed to the method
*/
handleModuleError(error, moduleName, method, args) {
const errorKey = `${moduleName}.${method}`;
Logger.error(`[ErrorBoundary] Module ${moduleName} crashed in ${method}():`, error);
// Track crashed modules
this.crashedModules.add(moduleName);
// Track recovery attempts
const attempts = this.recoveryAttempts.get(errorKey) || 0;
this.recoveryAttempts.set(errorKey, attempts + 1);
// Emit custom event for external handling
window.dispatchEvent(new CustomEvent('module-error', {
detail: {
moduleName,
method,
error: error.message,
args,
attempts: attempts + 1
}
}));
// Attempt recovery if under limit
if (attempts < this.maxRecoveryAttempts) {
this.scheduleRecovery(moduleName, method, args);
} else {
Logger.error(`[ErrorBoundary] Module ${moduleName} exceeded recovery attempts. Marking as permanently failed.`);
this.markModuleAsPermanentlyFailed(moduleName);
}
}
/**
* Schedules a recovery attempt for a failed module method
* @param {string} moduleName - Name of the module
* @param {string} method - Method to retry
* @param {Array} args - Original arguments
*/
scheduleRecovery(moduleName, method, args) {
setTimeout(() => {
try {
Logger.info(`[ErrorBoundary] Attempting recovery for ${moduleName}.${method}()`);
// This would need module registry integration
// For now, just log the attempt
} catch (recoveryError) {
Logger.error(`[ErrorBoundary] Recovery failed for ${moduleName}.${method}():`, recoveryError);
}
}, this.recoveryDelay);
}
/**
* Returns a safe fallback value for failed module methods
* @param {string} moduleName - Name of the module
* @param {string} method - Method that failed
* @returns {*} - Safe fallback value
*/
getRecoveryValue(moduleName, method) {
// Return safe defaults based on common method names
switch (method) {
case 'init':
case 'destroy':
case 'update':
case 'render':
return Promise.resolve();
case 'getData':
case 'getConfig':
return {};
case 'isEnabled':
case 'isActive':
return false;
default:
return undefined;
}
}
/**
* Marks a module as permanently failed
* @param {string} moduleName - Name of the module
*/
markModuleAsPermanentlyFailed(moduleName) {
window.dispatchEvent(new CustomEvent('module-permanent-failure', {
detail: { moduleName }
}));
}
/**
* Gets health status of all modules
* @returns {Object} - Health status report
*/
getHealthStatus() {
return {
totalCrashedModules: this.crashedModules.size,
crashedModules: Array.from(this.crashedModules),
recoveryAttempts: Object.fromEntries(this.recoveryAttempts),
timestamp: new Date().toISOString()
};
}
/**
* Resets error tracking for a module (useful for hot reload)
* @param {string} moduleName - Name of the module to reset
*/
resetModule(moduleName) {
this.crashedModules.delete(moduleName);
// Remove all recovery attempts for this module
for (const [key] of this.recoveryAttempts) {
if (key.startsWith(`${moduleName}.`)) {
this.recoveryAttempts.delete(key);
}
}
Logger.info(`[ErrorBoundary] Reset error tracking for module: ${moduleName}`);
}
/**
* Clears all error tracking
*/
reset() {
this.crashedModules.clear();
this.recoveryAttempts.clear();
Logger.info('[ErrorBoundary] Reset all error tracking');
}
}
// Global instance
export const moduleErrorBoundary = new ModuleErrorBoundary();
// Global error handlers
window.addEventListener('error', (event) => {
Logger.error('[Global] Unhandled error:', event.error || event.message);
});
window.addEventListener('unhandledrejection', (event) => {
Logger.error('[Global] Unhandled promise rejection:', event.reason);
});