189 lines
5.9 KiB
TypeScript
189 lines
5.9 KiB
TypeScript
import { Component, ErrorInfo, ReactNode } from 'react';
|
|
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { logger } from '@/lib/logger';
|
|
|
|
interface Props {
|
|
children: ReactNode;
|
|
onRetry?: () => void;
|
|
}
|
|
|
|
interface State {
|
|
hasError: boolean;
|
|
error: Error | null;
|
|
retryCount: number;
|
|
isRetrying: boolean;
|
|
}
|
|
|
|
/**
|
|
* Error boundary for game library components
|
|
* Includes automatic retry with exponential backoff for transient failures
|
|
*/
|
|
export class GameLibraryErrorBoundary extends Component<Props, State> {
|
|
private maxRetries = 3;
|
|
private retryTimeouts: number[] = [1000, 3000, 5000]; // Exponential backoff
|
|
|
|
constructor(props: Props) {
|
|
super(props);
|
|
this.state = {
|
|
hasError: false,
|
|
error: null,
|
|
retryCount: 0,
|
|
isRetrying: false
|
|
};
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): Partial<State> {
|
|
return { hasError: true, error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
|
logger.error('GameLibraryErrorBoundary caught an error:', error, errorInfo);
|
|
|
|
// Attempt automatic retry for transient errors
|
|
if (this.isTransientError(error) && this.state.retryCount < this.maxRetries) {
|
|
this.scheduleRetry();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if error might be transient (network, timeout, etc.)
|
|
*/
|
|
private isTransientError(error: Error): boolean {
|
|
const transientKeywords = [
|
|
'network',
|
|
'timeout',
|
|
'fetch',
|
|
'connection',
|
|
'aborted',
|
|
'ECONNREFUSED',
|
|
'ETIMEDOUT'
|
|
];
|
|
|
|
const errorMessage = error.message.toLowerCase();
|
|
return transientKeywords.some(keyword => errorMessage.includes(keyword));
|
|
}
|
|
|
|
/**
|
|
* Schedule automatic retry with exponential backoff
|
|
*/
|
|
private scheduleRetry(): void {
|
|
const delay = this.retryTimeouts[this.state.retryCount] || 5000;
|
|
|
|
this.setState({ isRetrying: true });
|
|
|
|
setTimeout(() => {
|
|
logger.info(`Attempting automatic retry ${this.state.retryCount + 1}/${this.maxRetries}`);
|
|
|
|
this.setState(prevState => ({
|
|
hasError: false,
|
|
error: null,
|
|
retryCount: prevState.retryCount + 1,
|
|
isRetrying: false
|
|
}));
|
|
|
|
// Call custom retry handler if provided
|
|
if (this.props.onRetry) {
|
|
this.props.onRetry();
|
|
}
|
|
}, delay);
|
|
}
|
|
|
|
handleManualRetry = (): void => {
|
|
logger.info('Manual retry triggered');
|
|
|
|
this.setState({
|
|
hasError: false,
|
|
error: null,
|
|
retryCount: 0,
|
|
isRetrying: false
|
|
});
|
|
|
|
if (this.props.onRetry) {
|
|
this.props.onRetry();
|
|
}
|
|
};
|
|
|
|
render(): ReactNode {
|
|
if (this.state.hasError) {
|
|
return (
|
|
<div className="container mx-auto px-4 py-12">
|
|
<div className="max-w-2xl mx-auto bg-background-elevated rounded-lg border border-destructive shadow-lg p-8">
|
|
<div className="flex items-start gap-4 mb-6">
|
|
<div className="p-3 bg-destructive/10 rounded-full flex-shrink-0">
|
|
<AlertTriangle className="w-8 h-8 text-destructive" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<h2 className="text-2xl font-bold text-foreground mb-2">
|
|
Failed to load game library
|
|
</h2>
|
|
<p className="text-muted-foreground">
|
|
{this.state.isRetrying
|
|
? `Retrying automatically (attempt ${this.state.retryCount + 1}/${this.maxRetries})...`
|
|
: 'An error occurred while loading the game library. This might be a temporary issue.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error details */}
|
|
{this.state.error && !this.state.isRetrying && (
|
|
<div className="mb-6 p-4 bg-background-primary rounded-lg border border-border">
|
|
<h3 className="text-sm font-semibold text-destructive mb-2">Error Details:</h3>
|
|
<p className="text-sm text-muted-foreground font-mono">
|
|
{this.state.error.message}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Retry information */}
|
|
{this.state.retryCount > 0 && !this.state.isRetrying && (
|
|
<div className="mb-6 p-4 bg-blue-500/10 rounded-lg border border-blue-500/20">
|
|
<p className="text-sm text-blue-400">
|
|
Attempted {this.state.retryCount} automatic {this.state.retryCount === 1 ? 'retry' : 'retries'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action buttons */}
|
|
{!this.state.isRetrying && (
|
|
<div className="flex gap-3">
|
|
<Button
|
|
variant="default"
|
|
onClick={this.handleManualRetry}
|
|
disabled={this.state.isRetrying}
|
|
>
|
|
<RefreshCw size={18} className="mr-2" />
|
|
Try Again
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Retry progress */}
|
|
{this.state.isRetrying && (
|
|
<div className="flex items-center gap-3 text-muted-foreground">
|
|
<div className="animate-spin rounded-full h-5 w-5 border-2 border-primary border-t-transparent" />
|
|
<span className="text-sm">Retrying...</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Help text */}
|
|
<div className="mt-6 pt-6 border-t border-border">
|
|
<p className="text-sm text-muted-foreground">
|
|
If this problem persists, try:
|
|
</p>
|
|
<ul className="mt-2 space-y-1 text-sm text-muted-foreground list-disc list-inside">
|
|
<li>Checking your internet connection</li>
|
|
<li>Refreshing the page (F5)</li>
|
|
<li>Clearing your browser cache</li>
|
|
<li>Verifying the backend server is running</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|