fix: Gitea Traefik routing and connection pool optimization
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
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
- 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
This commit is contained in:
347
resources/js/modules/livecomponent/ErrorBoundary.js
Normal file
347
resources/js/modules/livecomponent/ErrorBoundary.js
Normal file
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user