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:
Adolfo Delorenzo 2025-06-03 00:00:03 -06:00
parent b08574efe5
commit 17e0f2f03d
7 changed files with 993 additions and 31 deletions

173
CONNECTION_RETRY.md Normal file
View File

@ -0,0 +1,173 @@
# Connection Retry Logic Documentation
This document explains the connection retry and network interruption handling features in Talk2Me.
## Overview
Talk2Me implements robust connection retry logic to handle network interruptions gracefully. When a connection is lost or a request fails due to network issues, the application automatically queues requests and retries them when the connection is restored.
## Features
### 1. Automatic Connection Monitoring
- Monitors browser online/offline events
- Periodic health checks to the server (every 5 seconds when offline)
- Visual connection status indicator
- Automatic detection when returning from sleep/hibernation
### 2. Request Queuing
- Failed requests are automatically queued during network interruptions
- Requests maintain their priority and are processed in order
- Queue persists across connection failures
- Visual indication of queued requests
### 3. Exponential Backoff Retry
- Failed requests are retried with exponential backoff
- Initial retry delay: 1 second
- Maximum retry delay: 30 seconds
- Backoff multiplier: 2x
- Maximum retries: 3 attempts
### 4. Connection Status UI
- Real-time connection status indicator (bottom-right corner)
- Offline banner with retry button
- Queue status showing pending requests by type
- Temporary status messages for important events
## User Experience
### When Connection is Lost
1. **Visual Indicators**:
- Connection status shows "Offline" or "Connection error"
- Red banner appears at top of screen
- Queued request count is displayed
2. **Request Handling**:
- New requests are automatically queued
- User sees "Connection error - queued" message
- Requests will be sent when connection returns
3. **Manual Retry**:
- Users can click "Retry" button in offline banner
- Forces immediate connection check
### When Connection is Restored
1. **Automatic Recovery**:
- Connection status changes to "Connecting..."
- Queued requests are processed automatically
- Success message shown briefly
2. **Request Processing**:
- Queued requests maintain their order
- Higher priority requests (transcription) processed first
- Progress indicators show processing status
## Configuration
The connection retry logic can be configured programmatically:
```javascript
// In app.ts or initialization code
connectionManager.configure({
maxRetries: 3, // Maximum retry attempts
initialDelay: 1000, // Initial retry delay (ms)
maxDelay: 30000, // Maximum retry delay (ms)
backoffMultiplier: 2, // Exponential backoff multiplier
timeout: 10000, // Request timeout (ms)
onlineCheckInterval: 5000 // Health check interval (ms)
});
```
## Request Priority
Requests are prioritized as follows:
1. **Transcription** (Priority: 8) - Highest priority
2. **Translation** (Priority: 5) - Normal priority
3. **TTS/Audio** (Priority: 3) - Lower priority
## Error Types
### Retryable Errors
- Network errors
- Connection timeouts
- Server errors (5xx)
- CORS errors (in some cases)
### Non-Retryable Errors
- Client errors (4xx)
- Authentication errors
- Rate limit errors
- Invalid request errors
## Best Practices
1. **For Users**:
- Wait for queued requests to complete before closing the app
- Use the manual retry button if automatic recovery fails
- Check the connection status indicator for current state
2. **For Developers**:
- All fetch requests should go through RequestQueueManager
- Use appropriate request priorities
- Handle both online and offline scenarios in UI
- Provide clear feedback about connection status
## Technical Implementation
### Key Components
1. **ConnectionManager** (`connectionManager.ts`):
- Monitors connection state
- Implements retry logic with exponential backoff
- Provides connection state subscriptions
2. **RequestQueueManager** (`requestQueue.ts`):
- Queues failed requests
- Integrates with ConnectionManager
- Handles request prioritization
3. **ConnectionUI** (`connectionUI.ts`):
- Displays connection status
- Shows offline banner
- Updates queue information
### Integration Example
```typescript
// Automatic integration through RequestQueueManager
const queue = RequestQueueManager.getInstance();
const data = await queue.enqueue<ResponseType>(
'translate', // Request type
async () => {
// Your fetch request
const response = await fetch('/api/translate', options);
return response.json();
},
5 // Priority (1-10, higher = more important)
);
```
## Troubleshooting
### Connection Not Detected
- Check browser permissions for network status
- Ensure health endpoint (/health) is accessible
- Verify no firewall/proxy blocking
### Requests Not Retrying
- Check browser console for errors
- Verify request type is retryable
- Check if max retries exceeded
### Queue Not Processing
- Manually trigger retry with button
- Check if requests are timing out
- Verify server is responding
## Future Enhancements
- Persistent queue storage (survive page refresh)
- Configurable retry strategies per request type
- Network speed detection and adaptation
- Progressive web app offline mode

View File

@ -78,6 +78,16 @@ export CORS_ORIGINS="https://yourdomain.com,https://app.yourdomain.com"
export ADMIN_CORS_ORIGINS="https://admin.yourdomain.com"
```
## Connection Retry & Offline Support
Talk2Me handles network interruptions gracefully with automatic retry logic:
- Automatic request queuing during connection loss
- Exponential backoff retry with configurable parameters
- Visual connection status indicators
- Priority-based request processing
See [CONNECTION_RETRY.md](CONNECTION_RETRY.md) for detailed documentation.
## Mobile Support
The interface is fully responsive and designed to work well on mobile devices.

View File

@ -5,9 +5,14 @@ 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' });
@ -596,6 +601,15 @@ function initApp() {
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';
@ -783,6 +797,11 @@ function initApp() {
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>`;
@ -872,6 +891,19 @@ function initApp() {
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';
// Show TTS server alert

View File

@ -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';

View 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();
}

View 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);
}
}

View File

@ -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;
}
}