Add connection retry logic to handle network interruptions gracefully
- 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>
This commit is contained in:
		| @@ -21,11 +21,17 @@ import { Validator } from './validator'; | ||||
| import { StreamingTranslation } from './streamingTranslation'; | ||||
| import { PerformanceMonitor } from './performanceMonitor'; | ||||
| import { SpeakerManager } from './speakerManager'; | ||||
| import { ConnectionManager } from './connectionManager'; | ||||
| import { ConnectionUI } from './connectionUI'; | ||||
| // import { apiClient } from './apiClient'; // Available for cross-origin requests | ||||
|  | ||||
| // Initialize error boundary | ||||
| const errorBoundary = ErrorBoundary.getInstance(); | ||||
|  | ||||
| // Initialize connection management | ||||
| ConnectionManager.getInstance(); // Initialize connection manager | ||||
| const connectionUI = ConnectionUI.getInstance(); | ||||
|  | ||||
| // Configure API client if needed for cross-origin requests | ||||
| // import { apiClient } from './apiClient'; | ||||
| // apiClient.configure({ baseUrl: 'https://api.talk2me.com', credentials: 'include' }); | ||||
| @@ -717,6 +723,13 @@ function initApp(): void { | ||||
|             if (error.message?.includes('Rate limit')) { | ||||
|                 sourceText.innerHTML = `<p class="text-warning">Too many requests. Please wait a moment.</p>`; | ||||
|                 statusIndicator.textContent = 'Rate limit - please wait'; | ||||
|             } else if (error.message?.includes('connection') || error.message?.includes('network')) { | ||||
|                 sourceText.innerHTML = `<p class="text-warning">Connection error. Your request will be processed when connection is restored.</p>`; | ||||
|                 statusIndicator.textContent = 'Connection error - queued'; | ||||
|                 connectionUI.showTemporaryMessage('Request queued for when connection returns', 'warning'); | ||||
|             } else if (!navigator.onLine) { | ||||
|                 sourceText.innerHTML = `<p class="text-warning">You're offline. Request will be sent when connection is restored.</p>`; | ||||
|                 statusIndicator.textContent = 'Offline - request queued'; | ||||
|             } else { | ||||
|                 sourceText.innerHTML = `<p class="text-danger">Failed to transcribe audio. Please try again.</p>`; | ||||
|                 statusIndicator.textContent = 'Transcription failed'; | ||||
| @@ -962,6 +975,10 @@ function initApp(): void { | ||||
|             if (error.message?.includes('Rate limit')) { | ||||
|                 translatedText.innerHTML = `<p class="text-warning">Too many requests. Please wait a moment.</p>`; | ||||
|                 statusIndicator.textContent = 'Rate limit - please wait'; | ||||
|             } else if (error.message?.includes('connection') || error.message?.includes('network')) { | ||||
|                 translatedText.innerHTML = `<p class="text-warning">Connection error. Your translation will be processed when connection is restored.</p>`; | ||||
|                 statusIndicator.textContent = 'Connection error - queued'; | ||||
|                 connectionUI.showTemporaryMessage('Translation queued for when connection returns', 'warning'); | ||||
|             } else if (!navigator.onLine) { | ||||
|                 statusIndicator.textContent = 'Offline - checking cache...'; | ||||
|                 translatedText.innerHTML = `<p class="text-warning">You're offline. Only cached translations are available.</p>`; | ||||
| @@ -1063,6 +1080,18 @@ function initApp(): void { | ||||
|             if (error.message?.includes('Rate limit')) { | ||||
|                 statusIndicator.textContent = 'Too many requests - please wait'; | ||||
|                 alert('Too many requests. Please wait a moment before trying again.'); | ||||
|             } else if (error.message?.includes('connection') || error.message?.includes('network')) { | ||||
|                 statusIndicator.textContent = 'Connection error - audio generation queued'; | ||||
|                 connectionUI.showTemporaryMessage('Audio generation queued for when connection returns', 'warning'); | ||||
|                  | ||||
|                 // Show TTS server alert | ||||
|                 ttsServerAlert.classList.remove('d-none'); | ||||
|                 ttsServerAlert.classList.remove('alert-success'); | ||||
|                 ttsServerAlert.classList.add('alert-warning'); | ||||
|                 ttsServerMessage.textContent = 'Connection error - request queued'; | ||||
|             } else if (!navigator.onLine) { | ||||
|                 statusIndicator.textContent = 'Offline - audio generation unavailable'; | ||||
|                 alert('Audio generation requires an internet connection.'); | ||||
|             } else { | ||||
|                 statusIndicator.textContent = 'TTS failed'; | ||||
|                  | ||||
|   | ||||
							
								
								
									
										321
									
								
								static/js/src/connectionManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								static/js/src/connectionManager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,321 @@ | ||||
| // 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(); | ||||
| } | ||||
							
								
								
									
										325
									
								
								static/js/src/connectionUI.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								static/js/src/connectionUI.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,325 @@ | ||||
| // Connection status UI component | ||||
| import { ConnectionManager, ConnectionState } from './connectionManager'; | ||||
| import { RequestQueueManager } from './requestQueue'; | ||||
|  | ||||
| export class ConnectionUI { | ||||
|     private static instance: ConnectionUI; | ||||
|     private connectionManager: ConnectionManager; | ||||
|     private queueManager: RequestQueueManager; | ||||
|     private statusElement: HTMLElement | null = null; | ||||
|     private retryButton: HTMLButtonElement | null = null; | ||||
|     private offlineMessage: HTMLElement | null = null; | ||||
|      | ||||
|     private constructor() { | ||||
|         this.connectionManager = ConnectionManager.getInstance(); | ||||
|         this.queueManager = RequestQueueManager.getInstance(); | ||||
|         this.createUI(); | ||||
|         this.subscribeToConnectionChanges(); | ||||
|     } | ||||
|      | ||||
|     static getInstance(): ConnectionUI { | ||||
|         if (!ConnectionUI.instance) { | ||||
|             ConnectionUI.instance = new ConnectionUI(); | ||||
|         } | ||||
|         return ConnectionUI.instance; | ||||
|     } | ||||
|      | ||||
|     private createUI(): void { | ||||
|         // Create connection status indicator | ||||
|         this.statusElement = document.createElement('div'); | ||||
|         this.statusElement.id = 'connectionStatus'; | ||||
|         this.statusElement.className = 'connection-status'; | ||||
|         this.statusElement.innerHTML = ` | ||||
|             <span class="connection-icon"></span> | ||||
|             <span class="connection-text">Checking connection...</span> | ||||
|         `; | ||||
|          | ||||
|         // Create offline message banner | ||||
|         this.offlineMessage = document.createElement('div'); | ||||
|         this.offlineMessage.id = 'offlineMessage'; | ||||
|         this.offlineMessage.className = 'offline-message'; | ||||
|         this.offlineMessage.innerHTML = ` | ||||
|             <div class="offline-content"> | ||||
|                 <i class="fas fa-wifi-slash"></i> | ||||
|                 <span class="offline-text">You're offline. Some features may be limited.</span> | ||||
|                 <button class="btn btn-sm btn-outline-light retry-connection"> | ||||
|                     <i class="fas fa-sync"></i> Retry | ||||
|                 </button> | ||||
|                 <div class="queued-info" style="display: none;"> | ||||
|                     <small class="queued-count"></small> | ||||
|                 </div> | ||||
|             </div> | ||||
|         `; | ||||
|         this.offlineMessage.style.display = 'none'; | ||||
|          | ||||
|         // Add to page | ||||
|         document.body.appendChild(this.statusElement); | ||||
|         document.body.appendChild(this.offlineMessage); | ||||
|          | ||||
|         // Get retry button reference | ||||
|         this.retryButton = this.offlineMessage.querySelector('.retry-connection') as HTMLButtonElement; | ||||
|         this.retryButton?.addEventListener('click', () => this.handleRetry()); | ||||
|          | ||||
|         // Add CSS if not already present | ||||
|         if (!document.getElementById('connection-ui-styles')) { | ||||
|             const style = document.createElement('style'); | ||||
|             style.id = 'connection-ui-styles'; | ||||
|             style.textContent = ` | ||||
|                 .connection-status { | ||||
|                     position: fixed; | ||||
|                     bottom: 20px; | ||||
|                     right: 20px; | ||||
|                     background: rgba(0, 0, 0, 0.8); | ||||
|                     color: white; | ||||
|                     padding: 8px 16px; | ||||
|                     border-radius: 20px; | ||||
|                     display: flex; | ||||
|                     align-items: center; | ||||
|                     gap: 8px; | ||||
|                     font-size: 14px; | ||||
|                     z-index: 1000; | ||||
|                     transition: all 0.3s ease; | ||||
|                     opacity: 0; | ||||
|                     transform: translateY(10px); | ||||
|                 } | ||||
|                  | ||||
|                 .connection-status.visible { | ||||
|                     opacity: 1; | ||||
|                     transform: translateY(0); | ||||
|                 } | ||||
|                  | ||||
|                 .connection-status.online { | ||||
|                     background: rgba(40, 167, 69, 0.9); | ||||
|                 } | ||||
|                  | ||||
|                 .connection-status.offline { | ||||
|                     background: rgba(220, 53, 69, 0.9); | ||||
|                 } | ||||
|                  | ||||
|                 .connection-status.connecting { | ||||
|                     background: rgba(255, 193, 7, 0.9); | ||||
|                 } | ||||
|                  | ||||
|                 .connection-icon::before { | ||||
|                     content: '●'; | ||||
|                     display: inline-block; | ||||
|                     animation: pulse 2s infinite; | ||||
|                 } | ||||
|                  | ||||
|                 .connection-status.connecting .connection-icon::before { | ||||
|                     animation: spin 1s linear infinite; | ||||
|                     content: '↻'; | ||||
|                 } | ||||
|                  | ||||
|                 @keyframes pulse { | ||||
|                     0%, 100% { opacity: 1; } | ||||
|                     50% { opacity: 0.5; } | ||||
|                 } | ||||
|                  | ||||
|                 @keyframes spin { | ||||
|                     from { transform: rotate(0deg); } | ||||
|                     to { transform: rotate(360deg); } | ||||
|                 } | ||||
|                  | ||||
|                 .offline-message { | ||||
|                     position: fixed; | ||||
|                     top: 0; | ||||
|                     left: 0; | ||||
|                     right: 0; | ||||
|                     background: #dc3545; | ||||
|                     color: white; | ||||
|                     padding: 12px; | ||||
|                     text-align: center; | ||||
|                     z-index: 1001; | ||||
|                     transform: translateY(-100%); | ||||
|                     transition: transform 0.3s ease; | ||||
|                 } | ||||
|                  | ||||
|                 .offline-message.show { | ||||
|                     transform: translateY(0); | ||||
|                 } | ||||
|                  | ||||
|                 .offline-content { | ||||
|                     display: flex; | ||||
|                     align-items: center; | ||||
|                     justify-content: center; | ||||
|                     gap: 12px; | ||||
|                     flex-wrap: wrap; | ||||
|                 } | ||||
|                  | ||||
|                 .offline-content i { | ||||
|                     font-size: 20px; | ||||
|                 } | ||||
|                  | ||||
|                 .retry-connection { | ||||
|                     border-color: white; | ||||
|                     color: white; | ||||
|                 } | ||||
|                  | ||||
|                 .retry-connection:hover { | ||||
|                     background: white; | ||||
|                     color: #dc3545; | ||||
|                 } | ||||
|                  | ||||
|                 .queued-info { | ||||
|                     margin-left: 12px; | ||||
|                 } | ||||
|                  | ||||
|                 .queued-count { | ||||
|                     opacity: 0.9; | ||||
|                 } | ||||
|                  | ||||
|                 @media (max-width: 768px) { | ||||
|                     .connection-status { | ||||
|                         bottom: 10px; | ||||
|                         right: 10px; | ||||
|                         font-size: 12px; | ||||
|                         padding: 6px 12px; | ||||
|                     } | ||||
|                      | ||||
|                     .offline-content { | ||||
|                         font-size: 14px; | ||||
|                     } | ||||
|                 } | ||||
|             `; | ||||
|             document.head.appendChild(style); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private subscribeToConnectionChanges(): void { | ||||
|         this.connectionManager.subscribe('connection-ui', (state: ConnectionState) => { | ||||
|             this.updateUI(state); | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     private updateUI(state: ConnectionState): void { | ||||
|         if (!this.statusElement || !this.offlineMessage) return; | ||||
|          | ||||
|         const statusText = this.statusElement.querySelector('.connection-text') as HTMLElement; | ||||
|          | ||||
|         // Update status element | ||||
|         this.statusElement.className = `connection-status visible ${state.status}`; | ||||
|          | ||||
|         switch (state.status) { | ||||
|             case 'online': | ||||
|                 statusText.textContent = 'Connected'; | ||||
|                 this.hideOfflineMessage(); | ||||
|                 // Hide status after 3 seconds when online | ||||
|                 setTimeout(() => { | ||||
|                     if (this.connectionManager.getState().status === 'online') { | ||||
|                         this.statusElement?.classList.remove('visible'); | ||||
|                     } | ||||
|                 }, 3000); | ||||
|                 break; | ||||
|                  | ||||
|             case 'offline': | ||||
|                 statusText.textContent = 'Offline'; | ||||
|                 this.showOfflineMessage(); | ||||
|                 this.updateQueuedInfo(); | ||||
|                 break; | ||||
|                  | ||||
|             case 'connecting': | ||||
|                 statusText.textContent = 'Reconnecting...'; | ||||
|                 if (this.retryButton) { | ||||
|                     this.retryButton.disabled = true; | ||||
|                     this.retryButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Connecting...'; | ||||
|                 } | ||||
|                 break; | ||||
|                  | ||||
|             case 'error': | ||||
|                 statusText.textContent = `Connection error${state.retryCount > 0 ? ` (Retry ${state.retryCount})` : ''}`; | ||||
|                 this.showOfflineMessage(); | ||||
|                 this.updateQueuedInfo(); | ||||
|                 if (this.retryButton) { | ||||
|                     this.retryButton.disabled = false; | ||||
|                     this.retryButton.innerHTML = '<i class="fas fa-sync"></i> Retry'; | ||||
|                 } | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private showOfflineMessage(): void { | ||||
|         if (this.offlineMessage) { | ||||
|             this.offlineMessage.style.display = 'block'; | ||||
|             setTimeout(() => { | ||||
|                 this.offlineMessage?.classList.add('show'); | ||||
|             }, 10); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private hideOfflineMessage(): void { | ||||
|         if (this.offlineMessage) { | ||||
|             this.offlineMessage.classList.remove('show'); | ||||
|             setTimeout(() => { | ||||
|                 if (this.offlineMessage) { | ||||
|                     this.offlineMessage.style.display = 'none'; | ||||
|                 } | ||||
|             }, 300); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private updateQueuedInfo(): void { | ||||
|         const queueStatus = this.queueManager.getStatus(); | ||||
|         const queuedByType = this.queueManager.getQueuedByType(); | ||||
|         const queuedInfo = this.offlineMessage?.querySelector('.queued-info') as HTMLElement; | ||||
|         const queuedCount = this.offlineMessage?.querySelector('.queued-count') as HTMLElement; | ||||
|          | ||||
|         if (queuedInfo && queuedCount) { | ||||
|             const totalQueued = queueStatus.queueLength + queueStatus.activeRequests; | ||||
|              | ||||
|             if (totalQueued > 0) { | ||||
|                 queuedInfo.style.display = 'block'; | ||||
|                 const parts = []; | ||||
|                  | ||||
|                 if (queuedByType.transcribe > 0) { | ||||
|                     parts.push(`${queuedByType.transcribe} transcription${queuedByType.transcribe > 1 ? 's' : ''}`); | ||||
|                 } | ||||
|                 if (queuedByType.translate > 0) { | ||||
|                     parts.push(`${queuedByType.translate} translation${queuedByType.translate > 1 ? 's' : ''}`); | ||||
|                 } | ||||
|                 if (queuedByType.tts > 0) { | ||||
|                     parts.push(`${queuedByType.tts} audio generation${queuedByType.tts > 1 ? 's' : ''}`); | ||||
|                 } | ||||
|                  | ||||
|                 queuedCount.textContent = `${totalQueued} request${totalQueued > 1 ? 's' : ''} queued${parts.length > 0 ? ': ' + parts.join(', ') : ''}`; | ||||
|             } else { | ||||
|                 queuedInfo.style.display = 'none'; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private async handleRetry(): Promise<void> { | ||||
|         if (this.retryButton) { | ||||
|             this.retryButton.disabled = true; | ||||
|             this.retryButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Connecting...'; | ||||
|         } | ||||
|          | ||||
|         const success = await this.connectionManager.reconnect(); | ||||
|          | ||||
|         if (!success && this.retryButton) { | ||||
|             this.retryButton.disabled = false; | ||||
|             this.retryButton.innerHTML = '<i class="fas fa-sync"></i> Retry'; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Public method to show temporary connection message | ||||
|     showTemporaryMessage(message: string, type: 'success' | 'error' | 'warning' = 'success'): void { | ||||
|         if (!this.statusElement) return; | ||||
|          | ||||
|         const statusText = this.statusElement.querySelector('.connection-text') as HTMLElement; | ||||
|         const originalClass = this.statusElement.className; | ||||
|         const originalText = statusText.textContent; | ||||
|          | ||||
|         // Update appearance based on type | ||||
|         this.statusElement.className = `connection-status visible ${type === 'success' ? 'online' : type === 'error' ? 'offline' : 'connecting'}`; | ||||
|         statusText.textContent = message; | ||||
|          | ||||
|         // Reset after 3 seconds | ||||
|         setTimeout(() => { | ||||
|             if (this.statusElement && statusText) { | ||||
|                 this.statusElement.className = originalClass; | ||||
|                 statusText.textContent = originalText || ''; | ||||
|             } | ||||
|         }, 3000); | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,6 @@ | ||||
| // Request queue and throttling manager | ||||
| import { ConnectionManager, ConnectionState } from './connectionManager'; | ||||
|  | ||||
| export interface QueuedRequest { | ||||
|     id: string; | ||||
|     type: 'transcribe' | 'translate' | 'tts'; | ||||
| @@ -18,6 +20,8 @@ export class RequestQueueManager { | ||||
|     private maxRetries = 3; | ||||
|     private retryDelay = 1000; // Base retry delay in ms | ||||
|     private isProcessing = false; | ||||
|     private connectionManager: ConnectionManager; | ||||
|     private isPaused = false; | ||||
|  | ||||
|     // Rate limiting | ||||
|     private requestHistory: number[] = []; | ||||
| @@ -25,6 +29,13 @@ export class RequestQueueManager { | ||||
|     private maxRequestsPerSecond = 2; | ||||
|  | ||||
|     private constructor() { | ||||
|         this.connectionManager = ConnectionManager.getInstance(); | ||||
|          | ||||
|         // Subscribe to connection state changes | ||||
|         this.connectionManager.subscribe('request-queue', (state: ConnectionState) => { | ||||
|             this.handleConnectionStateChange(state); | ||||
|         }); | ||||
|          | ||||
|         // Start processing queue | ||||
|         this.startProcessing(); | ||||
|     } | ||||
| @@ -118,8 +129,14 @@ export class RequestQueueManager { | ||||
|     } | ||||
|  | ||||
|     private async processQueue(): Promise<void> { | ||||
|         // Check if we can process more requests | ||||
|         if (this.activeRequests.size >= this.maxConcurrent || this.queue.length === 0) { | ||||
|         // Check if we're paused or can't process more requests | ||||
|         if (this.isPaused || this.activeRequests.size >= this.maxConcurrent || this.queue.length === 0) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Check if we're online | ||||
|         if (!this.connectionManager.isOnline()) { | ||||
|             console.log('Queue processing paused - offline'); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -131,25 +148,33 @@ export class RequestQueueManager { | ||||
|         this.activeRequests.set(request.id, request); | ||||
|  | ||||
|         try { | ||||
|             // Execute request | ||||
|             const result = await request.request(); | ||||
|             // Execute request with connection manager retry logic | ||||
|             const result = await this.connectionManager.retryRequest( | ||||
|                 request.request, | ||||
|                 { | ||||
|                     retries: this.maxRetries - request.retryCount, | ||||
|                     delay: this.calculateRetryDelay(request.retryCount + 1), | ||||
|                     onRetry: (attempt, error) => { | ||||
|                         console.log(`Retry ${attempt} for ${request.type}: ${error.message}`); | ||||
|                     } | ||||
|                 } | ||||
|             ); | ||||
|              | ||||
|             request.resolve(result); | ||||
|             console.log(`Request completed: ${request.type}`); | ||||
|         } catch (error) { | ||||
|             console.error(`Request failed: ${request.type}`, error); | ||||
|             console.error(`Request failed after retries: ${request.type}`, error); | ||||
|              | ||||
|             // Check if we should retry | ||||
|             if (request.retryCount < this.maxRetries && this.shouldRetry(error)) { | ||||
|             // Check if it's a connection error and we should queue for later | ||||
|             if (this.isConnectionError(error) && request.retryCount < this.maxRetries) { | ||||
|                 request.retryCount++; | ||||
|                 const delay = this.calculateRetryDelay(request.retryCount); | ||||
|                 console.log(`Retrying request ${request.type} in ${delay}ms (attempt ${request.retryCount})`); | ||||
|                 console.log(`Re-queuing ${request.type} due to connection error`); | ||||
|                  | ||||
|                 // Re-queue with delay | ||||
|                 setTimeout(() => { | ||||
|                     this.addToQueue(request); | ||||
|                 }, delay); | ||||
|                 // Re-queue with higher priority | ||||
|                 request.priority = Math.max(request.priority + 1, 10); | ||||
|                 this.addToQueue(request); | ||||
|             } else { | ||||
|                 // Max retries reached or non-retryable error | ||||
|                 // Non-recoverable error or max retries reached | ||||
|                 request.reject(error); | ||||
|             } | ||||
|         } finally { | ||||
| @@ -158,23 +183,8 @@ export class RequestQueueManager { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private shouldRetry(error: any): boolean { | ||||
|         // Retry on network errors or 5xx status codes | ||||
|         if (error.message?.includes('network') || error.message?.includes('Network')) { | ||||
|             return true; | ||||
|         } | ||||
|          | ||||
|         if (error.status >= 500 && error.status < 600) { | ||||
|             return true; | ||||
|         } | ||||
|          | ||||
|         // Don't retry on client errors (4xx) | ||||
|         if (error.status >= 400 && error.status < 500) { | ||||
|             return false; | ||||
|         } | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|     // Note: shouldRetry logic is now handled by ConnectionManager | ||||
|     // Keeping for reference but not used directly | ||||
|  | ||||
|     private calculateRetryDelay(retryCount: number): number { | ||||
|         // Exponential backoff with jitter | ||||
| @@ -258,4 +268,66 @@ export class RequestQueueManager { | ||||
|             this.maxRequestsPerSecond = settings.maxRequestsPerSecond; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Handle connection state changes | ||||
|     private handleConnectionStateChange(state: ConnectionState): void { | ||||
|         console.log(`Connection state changed: ${state.status}`); | ||||
|          | ||||
|         if (state.status === 'offline' || state.status === 'error') { | ||||
|             // Pause processing when offline | ||||
|             this.isPaused = true; | ||||
|              | ||||
|             // Notify queued requests about offline status | ||||
|             if (this.queue.length > 0) { | ||||
|                 console.log(`${this.queue.length} requests queued while offline`); | ||||
|             } | ||||
|         } else if (state.status === 'online') { | ||||
|             // Resume processing when back online | ||||
|             this.isPaused = false; | ||||
|             console.log('Connection restored, resuming queue processing'); | ||||
|              | ||||
|             // Process any queued requests | ||||
|             if (this.queue.length > 0) { | ||||
|                 console.log(`Processing ${this.queue.length} queued requests`); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Check if error is connection-related | ||||
|     private isConnectionError(error: any): boolean { | ||||
|         const errorMessage = error.message?.toLowerCase() || ''; | ||||
|         const connectionErrors = [ | ||||
|             'network', | ||||
|             'fetch', | ||||
|             'connection', | ||||
|             'timeout', | ||||
|             'offline', | ||||
|             'cors' | ||||
|         ]; | ||||
|          | ||||
|         return connectionErrors.some(e => errorMessage.includes(e)); | ||||
|     } | ||||
|      | ||||
|     // Pause queue processing | ||||
|     pause(): void { | ||||
|         this.isPaused = true; | ||||
|         console.log('Request queue paused'); | ||||
|     } | ||||
|      | ||||
|     // Resume queue processing | ||||
|     resume(): void { | ||||
|         this.isPaused = false; | ||||
|         console.log('Request queue resumed'); | ||||
|     } | ||||
|      | ||||
|     // Get number of queued requests by type | ||||
|     getQueuedByType(): { transcribe: number; translate: number; tts: number } { | ||||
|         const counts = { transcribe: 0, translate: 0, tts: 0 }; | ||||
|          | ||||
|         this.queue.forEach(request => { | ||||
|             counts[request.type]++; | ||||
|         }); | ||||
|          | ||||
|         return counts; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user