- Implement ConnectionManager with exponential backoff retry strategy - Add automatic connection monitoring and health checks - Update RequestQueueManager to integrate with connection state - Create ConnectionUI component for visual connection status - Queue requests during offline periods and process when online - Add comprehensive error handling for network-related failures - Create detailed documentation for connection retry features - Support manual retry and automatic recovery Features: - Real-time connection status indicator - Offline banner with retry button - Request queue visualization - Priority-based request processing - Configurable retry parameters 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
321 lines
10 KiB
TypeScript
321 lines
10 KiB
TypeScript
// Connection management with retry logic
|
|
export interface ConnectionConfig {
|
|
maxRetries: number;
|
|
initialDelay: number;
|
|
maxDelay: number;
|
|
backoffMultiplier: number;
|
|
timeout: number;
|
|
onlineCheckInterval: number;
|
|
}
|
|
|
|
export interface RetryOptions {
|
|
retries?: number;
|
|
delay?: number;
|
|
onRetry?: (attempt: number, error: Error) => void;
|
|
}
|
|
|
|
export type ConnectionStatus = 'online' | 'offline' | 'connecting' | 'error';
|
|
|
|
export interface ConnectionState {
|
|
status: ConnectionStatus;
|
|
lastError?: Error;
|
|
retryCount: number;
|
|
lastOnlineTime?: Date;
|
|
}
|
|
|
|
export class ConnectionManager {
|
|
private static instance: ConnectionManager;
|
|
private config: ConnectionConfig;
|
|
private state: ConnectionState;
|
|
private listeners: Map<string, (state: ConnectionState) => void> = new Map();
|
|
private onlineCheckTimer?: number;
|
|
private reconnectTimer?: number;
|
|
|
|
private constructor() {
|
|
this.config = {
|
|
maxRetries: 3,
|
|
initialDelay: 1000, // 1 second
|
|
maxDelay: 30000, // 30 seconds
|
|
backoffMultiplier: 2,
|
|
timeout: 10000, // 10 seconds
|
|
onlineCheckInterval: 5000 // 5 seconds
|
|
};
|
|
|
|
this.state = {
|
|
status: navigator.onLine ? 'online' : 'offline',
|
|
retryCount: 0
|
|
};
|
|
|
|
this.setupEventListeners();
|
|
this.startOnlineCheck();
|
|
}
|
|
|
|
static getInstance(): ConnectionManager {
|
|
if (!ConnectionManager.instance) {
|
|
ConnectionManager.instance = new ConnectionManager();
|
|
}
|
|
return ConnectionManager.instance;
|
|
}
|
|
|
|
// Configure connection settings
|
|
configure(config: Partial<ConnectionConfig>): void {
|
|
this.config = { ...this.config, ...config };
|
|
}
|
|
|
|
// Setup browser online/offline event listeners
|
|
private setupEventListeners(): void {
|
|
window.addEventListener('online', () => {
|
|
console.log('Browser online event detected');
|
|
this.updateState({ status: 'online', retryCount: 0 });
|
|
this.checkServerConnection();
|
|
});
|
|
|
|
window.addEventListener('offline', () => {
|
|
console.log('Browser offline event detected');
|
|
this.updateState({ status: 'offline' });
|
|
});
|
|
|
|
// Listen for visibility changes to check connection when tab becomes active
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (!document.hidden && this.state.status === 'offline') {
|
|
this.checkServerConnection();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Start periodic online checking
|
|
private startOnlineCheck(): void {
|
|
this.onlineCheckTimer = window.setInterval(() => {
|
|
if (this.state.status === 'offline' || this.state.status === 'error') {
|
|
this.checkServerConnection();
|
|
}
|
|
}, this.config.onlineCheckInterval);
|
|
}
|
|
|
|
// Check actual server connection
|
|
async checkServerConnection(): Promise<boolean> {
|
|
if (!navigator.onLine) {
|
|
this.updateState({ status: 'offline' });
|
|
return false;
|
|
}
|
|
|
|
this.updateState({ status: 'connecting' });
|
|
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
|
|
const response = await fetch('/health', {
|
|
method: 'GET',
|
|
signal: controller.signal,
|
|
cache: 'no-cache'
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (response.ok) {
|
|
this.updateState({
|
|
status: 'online',
|
|
retryCount: 0,
|
|
lastOnlineTime: new Date()
|
|
});
|
|
return true;
|
|
} else {
|
|
throw new Error(`Server returned status ${response.status}`);
|
|
}
|
|
} catch (error) {
|
|
this.updateState({
|
|
status: 'error',
|
|
lastError: error as Error
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Retry a failed request with exponential backoff
|
|
async retryRequest<T>(
|
|
request: () => Promise<T>,
|
|
options: RetryOptions = {}
|
|
): Promise<T> {
|
|
const {
|
|
retries = this.config.maxRetries,
|
|
delay = this.config.initialDelay,
|
|
onRetry
|
|
} = options;
|
|
|
|
let lastError: Error;
|
|
|
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
try {
|
|
// Check if we're online before attempting
|
|
if (!navigator.onLine) {
|
|
throw new Error('No internet connection');
|
|
}
|
|
|
|
// Add timeout to request
|
|
const result = await this.withTimeout(request(), this.config.timeout);
|
|
|
|
// Success - reset retry count
|
|
if (this.state.retryCount > 0) {
|
|
this.updateState({ retryCount: 0 });
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
|
|
// Don't retry if offline
|
|
if (!navigator.onLine) {
|
|
this.updateState({ status: 'offline' });
|
|
throw new Error('Request failed: No internet connection');
|
|
}
|
|
|
|
// Don't retry on client errors (4xx)
|
|
if (this.isClientError(error)) {
|
|
throw error;
|
|
}
|
|
|
|
// Call retry callback if provided
|
|
if (onRetry && attempt < retries) {
|
|
onRetry(attempt + 1, lastError);
|
|
}
|
|
|
|
// If we have retries left, wait and try again
|
|
if (attempt < retries) {
|
|
const backoffDelay = Math.min(
|
|
delay * Math.pow(this.config.backoffMultiplier, attempt),
|
|
this.config.maxDelay
|
|
);
|
|
|
|
console.log(`Retry attempt ${attempt + 1}/${retries} after ${backoffDelay}ms`);
|
|
|
|
// Update retry count in state
|
|
this.updateState({ retryCount: attempt + 1 });
|
|
|
|
await this.delay(backoffDelay);
|
|
}
|
|
}
|
|
}
|
|
|
|
// All retries exhausted
|
|
this.updateState({
|
|
status: 'error',
|
|
lastError: lastError!
|
|
});
|
|
|
|
throw new Error(`Request failed after ${retries} retries: ${lastError!.message}`);
|
|
}
|
|
|
|
// Add timeout to a promise
|
|
private withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
|
|
return Promise.race([
|
|
promise,
|
|
new Promise<T>((_, reject) => {
|
|
setTimeout(() => reject(new Error('Request timeout')), timeout);
|
|
})
|
|
]);
|
|
}
|
|
|
|
// Check if error is a client error (4xx)
|
|
private isClientError(error: any): boolean {
|
|
if (error.response && error.response.status >= 400 && error.response.status < 500) {
|
|
return true;
|
|
}
|
|
|
|
// Check for specific error messages that shouldn't be retried
|
|
const message = error.message?.toLowerCase() || '';
|
|
const noRetryErrors = ['unauthorized', 'forbidden', 'bad request', 'not found'];
|
|
|
|
return noRetryErrors.some(e => message.includes(e));
|
|
}
|
|
|
|
// Delay helper
|
|
private delay(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
// Update connection state
|
|
private updateState(updates: Partial<ConnectionState>): void {
|
|
this.state = { ...this.state, ...updates };
|
|
this.notifyListeners();
|
|
}
|
|
|
|
// Subscribe to connection state changes
|
|
subscribe(id: string, callback: (state: ConnectionState) => void): void {
|
|
this.listeners.set(id, callback);
|
|
// Immediately call with current state
|
|
callback(this.state);
|
|
}
|
|
|
|
// Unsubscribe from connection state changes
|
|
unsubscribe(id: string): void {
|
|
this.listeners.delete(id);
|
|
}
|
|
|
|
// Notify all listeners of state change
|
|
private notifyListeners(): void {
|
|
this.listeners.forEach(callback => callback(this.state));
|
|
}
|
|
|
|
// Get current connection state
|
|
getState(): ConnectionState {
|
|
return { ...this.state };
|
|
}
|
|
|
|
// Check if currently online
|
|
isOnline(): boolean {
|
|
return this.state.status === 'online';
|
|
}
|
|
|
|
// Manual reconnect attempt
|
|
async reconnect(): Promise<boolean> {
|
|
console.log('Manual reconnect requested');
|
|
return this.checkServerConnection();
|
|
}
|
|
|
|
// Cleanup
|
|
destroy(): void {
|
|
if (this.onlineCheckTimer) {
|
|
clearInterval(this.onlineCheckTimer);
|
|
}
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer);
|
|
}
|
|
this.listeners.clear();
|
|
}
|
|
}
|
|
|
|
// Helper function for retrying fetch requests
|
|
export async function fetchWithRetry(
|
|
url: string,
|
|
options: RequestInit = {},
|
|
retryOptions: RetryOptions = {}
|
|
): Promise<Response> {
|
|
const connectionManager = ConnectionManager.getInstance();
|
|
|
|
return connectionManager.retryRequest(async () => {
|
|
const response = await fetch(url, options);
|
|
|
|
if (!response.ok && response.status >= 500) {
|
|
// Server error - throw to trigger retry
|
|
throw new Error(`Server error: ${response.status}`);
|
|
}
|
|
|
|
return response;
|
|
}, retryOptions);
|
|
}
|
|
|
|
// Helper function for retrying JSON requests
|
|
export async function fetchJSONWithRetry<T>(
|
|
url: string,
|
|
options: RequestInit = {},
|
|
retryOptions: RetryOptions = {}
|
|
): Promise<T> {
|
|
const response = await fetchWithRetry(url, options, retryOptions);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
} |