Files
michaelschiemer/resources/js/modules/livecomponent/ErrorBoundary.js
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
2025-11-09 14:46:15 +01:00

348 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<boolean>} 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<boolean>} 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();
}
}