/** * Error Boundary for LiveComponents * * Provides automatic error handling, retry mechanisms, and error recovery * for LiveComponent operations. */ // Note: LiveComponentError type is defined in types/livecomponent.d.ts // This is a runtime implementation, so we don't need to import the type export class ErrorBoundary { constructor(liveComponentManager) { this.manager = liveComponentManager; this.retryStrategies = new Map(); this.errorHandlers = new Map(); this.maxRetries = 3; this.retryDelays = [1000, 2000, 5000]; // Progressive backoff } /** * Handle error from component action * * @param {string} componentId - Component ID * @param {string} action - Action method name * @param {Error|LiveComponentError} error - Error object * @param {Object} context - Additional context * @returns {Promise} True if error was handled, false otherwise */ async handleError(componentId, action, error, context = {}) { console.error(`[ErrorBoundary] Error in ${componentId}.${action}:`, error); // Convert to standardized error format const standardizedError = this.standardizeError(error, componentId, action); // Check for custom error handler const handler = this.errorHandlers.get(componentId); if (handler) { try { const handled = await handler(standardizedError, context); if (handled) { return true; } } catch (handlerError) { console.error('[ErrorBoundary] Error handler failed:', handlerError); } } // Check if error is retryable if (this.isRetryable(standardizedError)) { const retried = await this.retryOperation(componentId, action, context, standardizedError); if (retried) { return true; } } // Show error to user this.showError(componentId, standardizedError); // Dispatch error event this.dispatchErrorEvent(componentId, standardizedError); return false; } /** * Standardize error format * * @param {Error|LiveComponentError|Object} error - Error object * @param {string} componentId - Component ID * @param {string} action - Action method name * @returns {LiveComponentError} Standardized error */ standardizeError(error, componentId, action) { // If already standardized if (error && typeof error === 'object' && 'code' in error && 'message' in error) { return { code: error.code, message: error.message, details: error.details || {}, componentId: error.componentId || componentId, action: error.action || action, timestamp: error.timestamp || Date.now() }; } // If Error object if (error instanceof Error) { return { code: 'INTERNAL_ERROR', message: error.message, details: { stack: error.stack, name: error.name }, componentId, action, timestamp: Date.now() }; } // If string if (typeof error === 'string') { return { code: 'INTERNAL_ERROR', message: error, componentId, action, timestamp: Date.now() }; } // Default return { code: 'INTERNAL_ERROR', message: 'An unknown error occurred', details: { original: error }, componentId, action, timestamp: Date.now() }; } /** * Check if error is retryable * * @param {LiveComponentError} error - Standardized error * @returns {boolean} True if error is retryable */ isRetryable(error) { const retryableCodes = [ 'RATE_LIMIT_EXCEEDED', 'STATE_CONFLICT', 'INTERNAL_ERROR' ]; return retryableCodes.includes(error.code); } /** * Retry operation with exponential backoff * * @param {string} componentId - Component ID * @param {string} action - Action method name * @param {Object} context - Operation context * @param {LiveComponentError} error - Error that occurred * @returns {Promise} True if retry succeeded */ async retryOperation(componentId, action, context, error) { const retryKey = `${componentId}:${action}`; const retryCount = this.retryStrategies.get(retryKey) || 0; if (retryCount >= this.maxRetries) { console.warn(`[ErrorBoundary] Max retries exceeded for ${retryKey}`); this.retryStrategies.delete(retryKey); return false; } // Calculate delay (progressive backoff) const delay = this.retryDelays[retryCount] || this.retryDelays[this.retryDelays.length - 1]; console.log(`[ErrorBoundary] Retrying ${retryKey} in ${delay}ms (attempt ${retryCount + 1}/${this.maxRetries})`); // Update retry count this.retryStrategies.set(retryKey, retryCount + 1); // Wait before retry await new Promise(resolve => setTimeout(resolve, delay)); try { // Retry the operation const result = await this.manager.executeAction( componentId, action, context.params || {}, context.fragments || null ); // Success - clear retry count this.retryStrategies.delete(retryKey); console.log(`[ErrorBoundary] Retry succeeded for ${retryKey}`); return true; } catch (retryError) { // Retry failed - will be handled by next retry or error handler console.warn(`[ErrorBoundary] Retry failed for ${retryKey}:`, retryError); return false; } } /** * Show error to user * * @param {string} componentId - Component ID * @param {LiveComponentError} error - Standardized error */ showError(componentId, error) { const config = this.manager.components.get(componentId); if (!config) return; // Remove existing error const existingError = config.element.querySelector('.livecomponent-error-boundary'); if (existingError) { existingError.remove(); } // Create error element const errorEl = document.createElement('div'); errorEl.className = 'livecomponent-error-boundary'; errorEl.style.cssText = ` padding: 1rem; background: #fee; color: #c00; border: 1px solid #faa; border-radius: 4px; margin-bottom: 1rem; `; // Error message const messageEl = document.createElement('div'); messageEl.style.fontWeight = 'bold'; messageEl.textContent = error.message; errorEl.appendChild(messageEl); // Error code (if not generic) if (error.code !== 'INTERNAL_ERROR') { const codeEl = document.createElement('div'); codeEl.style.fontSize = '0.875rem'; codeEl.style.marginTop = '0.5rem'; codeEl.style.color = '#666'; codeEl.textContent = `Error Code: ${error.code}`; errorEl.appendChild(codeEl); } // Retry button (if retryable) if (this.isRetryable(error)) { const retryBtn = document.createElement('button'); retryBtn.textContent = 'Retry'; retryBtn.style.cssText = ` margin-top: 0.5rem; padding: 0.5rem 1rem; background: #c00; color: white; border: none; border-radius: 4px; cursor: pointer; `; retryBtn.addEventListener('click', async () => { errorEl.remove(); await this.retryOperation(componentId, error.action || '', {}, error); }); errorEl.appendChild(retryBtn); } // Close button const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; closeBtn.style.cssText = ` float: right; background: transparent; border: none; font-size: 1.5rem; cursor: pointer; color: #c00; `; closeBtn.addEventListener('click', () => { errorEl.remove(); }); errorEl.appendChild(closeBtn); // Insert at top of component config.element.insertAdjacentElement('afterbegin', errorEl); // Auto-remove after 10 seconds setTimeout(() => { if (errorEl.parentNode) { errorEl.remove(); } }, 10000); } /** * Dispatch error event * * @param {string} componentId - Component ID * @param {LiveComponentError} error - Standardized error */ dispatchErrorEvent(componentId, error) { // Dispatch custom DOM event const event = new CustomEvent('livecomponent:error', { detail: { componentId, error }, bubbles: true }); const config = this.manager.components.get(componentId); if (config) { config.element.dispatchEvent(event); } // Also dispatch on document document.dispatchEvent(event); } /** * Register custom error handler for component * * @param {string} componentId - Component ID * @param {Function} handler - Error handler function */ registerErrorHandler(componentId, handler) { if (typeof handler !== 'function') { throw new Error('Error handler must be a function'); } this.errorHandlers.set(componentId, handler); } /** * Clear error handler for component * * @param {string} componentId - Component ID */ clearErrorHandler(componentId) { this.errorHandlers.delete(componentId); } /** * Clear retry strategy for component/action * * @param {string} componentId - Component ID * @param {string} action - Action method name */ clearRetryStrategy(componentId, action) { const retryKey = `${componentId}:${action}`; this.retryStrategies.delete(retryKey); } /** * Reset all retry strategies */ resetRetryStrategies() { this.retryStrategies.clear(); } }