Front-end Engineering Lab

Connection Resilience

Handle network failures, reconnections, and offline scenarios gracefully

Networks fail. Your app shouldn't. This guide covers patterns for building resilient real-time connections.

🎯 Goals

What we need:
✅ Automatic reconnection
✅ Exponential backoff
✅ Offline detection
✅ Message queuing
✅ State synchronization
✅ User feedback

🔄 Pattern 1: Smart Reconnection

Reconnect with exponential backoff.

class ResilientWebSocket {
  private ws: WebSocket | null = null;
  private url: string;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private baseDelay = 1000;
  private maxDelay = 30000;
  private reconnectTimer: NodeJS.Timeout | null = null;
  private intentionalClose = false;
  
  constructor(url: string) {
    this.url = url;
    this.connect();
  }
  
  private connect() {
    console.log(`Connecting... (attempt ${this.reconnectAttempts + 1})`);
    
    try {
      this.ws = new WebSocket(this.url);
      
      this.ws.onopen = () => {
        console.log('Connected!');
        this.reconnectAttempts = 0;
        this.onConnected?.();
      };
      
      this.ws.onmessage = (event) => {
        this.onMessage?.(JSON.parse(event.data));
      };
      
      this.ws.onerror = (error) => {
        console.error('WebSocket error:', error);
        this.onError?.(error);
      };
      
      this.ws.onclose = (event) => {
        console.log('Disconnected');
        this.onDisconnected?.();
        
        if (!this.intentionalClose) {
          this.scheduleReconnect();
        }
      };
    } catch (error) {
      console.error('Failed to create WebSocket:', error);
      this.scheduleReconnect();
    }
  }
  
  private scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnect attempts reached');
      this.onMaxReconnectAttemptsReached?.();
      return;
    }
    
    // Exponential backoff with jitter
    const delay = Math.min(
      this.baseDelay * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000,
      this.maxDelay
    );
    
    console.log(`Reconnecting in ${Math.round(delay)}ms...`);
    this.reconnectAttempts++;
    
    this.reconnectTimer = setTimeout(() => {
      this.connect();
    }, delay);
  }
  
  send(data: any) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
      return true;
    }
    return false;
  }
  
  close() {
    this.intentionalClose = true;
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
    }
    this.ws?.close();
  }
  
  isConnected(): boolean {
    return this.ws?.readyState === WebSocket.OPEN;
  }
  
  // Event handlers
  onConnected?: () => void;
  onDisconnected?: () => void;
  onMessage?: (data: any) => void;
  onError?: (error: Event) => void;
  onMaxReconnectAttemptsReached?: () => void;
}

// Usage
const ws = new ResilientWebSocket('wss://api.example.com/ws');

ws.onConnected = () => {
  console.log('Ready to send messages!');
};

ws.onDisconnected = () => {
  console.log('Connection lost, will retry...');
};

ws.onMaxReconnectAttemptsReached = () => {
  alert('Cannot connect to server. Please check your internet connection.');
};

📡 Pattern 2: Network Status Detection

Detect online/offline status.

class NetworkMonitor {
  private isOnline = navigator.onLine;
  private listeners = new Set<(online: boolean) => void>();
  
  constructor() {
    this.setupListeners();
  }
  
  private setupListeners() {
    window.addEventListener('online', () => {
      console.log('Network: online');
      this.isOnline = true;
      this.notifyListeners();
    });
    
    window.addEventListener('offline', () => {
      console.log('Network: offline');
      this.isOnline = false;
      this.notifyListeners();
    });
    
    // Fallback: periodic connectivity check
    setInterval(() => {
      this.checkConnectivity();
    }, 30000); // Every 30 seconds
  }
  
  private async checkConnectivity() {
    try {
      const response = await fetch('/api/ping', {
        method: 'HEAD',
        cache: 'no-cache'
      });
      
      const online = response.ok;
      
      if (online !== this.isOnline) {
        this.isOnline = online;
        this.notifyListeners();
      }
    } catch {
      if (this.isOnline) {
        this.isOnline = false;
        this.notifyListeners();
      }
    }
  }
  
  private notifyListeners() {
    this.listeners.forEach(listener => listener(this.isOnline));
  }
  
  subscribe(listener: (online: boolean) => void): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
  
  getStatus(): boolean {
    return this.isOnline;
  }
}

// React Hook
function useNetworkStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  
  useEffect(() => {
    const monitor = new NetworkMonitor();
    const unsubscribe = monitor.subscribe(setIsOnline);
    
    return unsubscribe;
  }, []);
  
  return isOnline;
}

// Usage
function NetworkIndicator() {
  const isOnline = useNetworkStatus();
  
  if (!isOnline) {
    return (
      <div className="network-offline-banner">
        ⚠️ You're offline. Changes will sync when you're back online.
      </div>
    );
  }
  
  return null;
}

📬 Pattern 3: Persistent Message Queue

Queue messages when offline.

interface QueuedMessage {
  id: string;
  data: any;
  timestamp: number;
  retries: number;
  priority: number;
}

class MessageQueue {
  private queue: QueuedMessage[] = [];
  private maxSize = 1000;
  private maxRetries = 3;
  private storageKey = 'message-queue';
  
  constructor() {
    this.loadFromStorage();
  }
  
  add(data: any, priority: number = 0): string {
    const message: QueuedMessage = {
      id: `${Date.now()}-${Math.random()}`,
      data,
      timestamp: Date.now(),
      retries: 0,
      priority
    };
    
    this.queue.push(message);
    this.sortByPriority();
    
    // Enforce max size
    if (this.queue.length > this.maxSize) {
      this.queue.shift(); // Remove oldest
    }
    
    this.saveToStorage();
    
    return message.id;
  }
  
  async flush(sendFn: (data: any) => Promise<boolean>) {
    const messages = [...this.queue];
    
    for (const message of messages) {
      try {
        const success = await sendFn(message.data);
        
        if (success) {
          this.remove(message.id);
        } else {
          this.incrementRetries(message.id);
        }
      } catch (error) {
        console.error('Failed to send message:', error);
        this.incrementRetries(message.id);
      }
    }
  }
  
  private incrementRetries(id: string) {
    const message = this.queue.find(m => m.id === id);
    
    if (message) {
      message.retries++;
      
      if (message.retries >= this.maxRetries) {
        console.error('Message exceeded max retries, dropping:', message);
        this.remove(id);
      } else {
        this.saveToStorage();
      }
    }
  }
  
  private remove(id: string) {
    this.queue = this.queue.filter(m => m.id !== id);
    this.saveToStorage();
  }
  
  private sortByPriority() {
    this.queue.sort((a, b) => {
      if (a.priority !== b.priority) {
        return b.priority - a.priority; // Higher priority first
      }
      return a.timestamp - b.timestamp; // Older first
    });
  }
  
  private saveToStorage() {
    try {
      localStorage.setItem(this.storageKey, JSON.stringify(this.queue));
    } catch (error) {
      console.error('Failed to save queue to storage:', error);
    }
  }
  
  private loadFromStorage() {
    try {
      const stored = localStorage.getItem(this.storageKey);
      if (stored) {
        this.queue = JSON.parse(stored);
      }
    } catch (error) {
      console.error('Failed to load queue from storage:', error);
    }
  }
  
  getSize(): number {
    return this.queue.length;
  }
  
  clear() {
    this.queue = [];
    this.saveToStorage();
  }
}

// Integration with WebSocket
class ResilientWebSocketWithQueue extends ResilientWebSocket {
  private messageQueue = new MessageQueue();
  
  send(data: any, priority: number = 0): string {
    if (this.isConnected()) {
      const success = super.send(data);
      if (success) {
        return 'sent';
      }
    }
    
    // Queue if not connected
    const messageId = this.messageQueue.add(data, priority);
    console.log(`Message queued (${this.messageQueue.getSize()} in queue)`);
    return messageId;
  }
  
  override onConnected = () => {
    console.log('Connected, flushing queue...');
    
    this.messageQueue.flush(async (data) => {
      return super.send(data);
    });
  };
}

🔄 Pattern 4: State Synchronization

Sync state after reconnection.

interface SyncState {
  lastSyncTimestamp: number;
  pendingChanges: any[];
  version: number;
}

class StateSynchronizer {
  private ws: WebSocket;
  private state: SyncState = {
    lastSyncTimestamp: 0,
    pendingChanges: [],
    version: 0
  };
  
  constructor(ws: WebSocket) {
    this.ws = ws;
  }
  
  async synchronize() {
    console.log('Synchronizing state...');
    
    // Request server state
    this.ws.send(JSON.stringify({
      type: 'sync_request',
      lastSyncTimestamp: this.state.lastSyncTimestamp,
      version: this.state.version
    }));
  }
  
  handleSyncResponse(data: {
    serverVersion: number;
    changes: any[];
    timestamp: number;
  }) {
    console.log('Received sync response');
    
    // Apply server changes
    data.changes.forEach(change => {
      this.applyChange(change);
    });
    
    // Send pending local changes
    this.state.pendingChanges.forEach(change => {
      this.ws.send(JSON.stringify({
        type: 'change',
        change
      }));
    });
    
    // Clear pending changes
    this.state.pendingChanges = [];
    
    // Update state
    this.state.lastSyncTimestamp = data.timestamp;
    this.state.version = data.serverVersion;
    
    console.log('Synchronization complete');
  }
  
  addPendingChange(change: any) {
    this.state.pendingChanges.push(change);
  }
  
  private applyChange(change: any) {
    // Apply change to local state
    console.log('Applying change:', change);
  }
}

// React Hook
function useSynchronizedState(ws: WebSocket) {
  const [syncing, setSyncing] = useState(false);
  const synchronizer = useRef(new StateSynchronizer(ws));
  
  useEffect(() => {
    const handleOnline = () => {
      setSyncing(true);
      synchronizer.current.synchronize();
    };
    
    const handleSyncComplete = () => {
      setSyncing(false);
    };
    
    window.addEventListener('online', handleOnline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
    };
  }, [ws]);
  
  return { syncing, synchronizer: synchronizer.current };
}

🔔 Pattern 5: User Feedback

Show connection status to users.

function ConnectionStatus() {
  const [status, setStatus] = useState<'connected' | 'connecting' | 'disconnected'>('connecting');
  const [queueSize, setQueueSize] = useState(0);
  
  return (
    <div className={`connection-status ${status}`}>
      {status === 'connected' && (
        <div className="status-connected">
          🟢 Connected
        </div>
      )}
      
      {status === 'connecting' && (
        <div className="status-connecting">
          🟡 Connecting...
          <div className="spinner" />
        </div>
      )}
      
      {status === 'disconnected' && (
        <div className="status-disconnected">
          🔴 Disconnected
          {queueSize > 0 && (
            <span>({queueSize} messages queued)</span>
          )}
          <button onClick={() => window.location.reload()}>
            Retry
          </button>
        </div>
      )}
    </div>
  );
}

// Toast notifications
function ConnectionToast() {
  const isOnline = useNetworkStatus();
  
  useEffect(() => {
    if (!isOnline) {
      toast.warning('You\'re offline. Changes will sync when you\'re back online.', {
        duration: Infinity,
        id: 'offline-toast'
      });
    } else {
      toast.dismiss('offline-toast');
      toast.success('You\'re back online!');
    }
  }, [isOnline]);
  
  return null;
}

🧪 Pattern 6: Testing Resilience

Simulate network failures.

class NetworkSimulator {
  private originalFetch: typeof fetch;
  private originalWebSocket: typeof WebSocket;
  
  constructor() {
    this.originalFetch = window.fetch;
    this.originalWebSocket = window.WebSocket;
  }
  
  // Simulate offline mode
  goOffline() {
    console.log('🔌 Simulating offline mode');
    
    window.fetch = () => {
      return Promise.reject(new Error('Network request failed'));
    };
    
    window.dispatchEvent(new Event('offline'));
  }
  
  // Restore connection
  goOnline() {
    console.log('🔌 Simulating online mode');
    
    window.fetch = this.originalFetch;
    window.dispatchEvent(new Event('online'));
  }
  
  // Simulate slow connection
  simulateSlowConnection(delayMs: number = 3000) {
    console.log(`🐌 Simulating ${delayMs}ms latency`);
    
    window.fetch = async (...args) => {
      await new Promise(resolve => setTimeout(resolve, delayMs));
      return this.originalFetch(...args);
    };
  }
  
  // Simulate packet loss
  simulatePacketLoss(lossRate: number = 0.3) {
    console.log(`📉 Simulating ${lossRate * 100}% packet loss`);
    
    window.fetch = (...args) => {
      if (Math.random() < lossRate) {
        return Promise.reject(new Error('Packet lost'));
      }
      return this.originalFetch(...args);
    };
  }
  
  reset() {
    window.fetch = this.originalFetch;
    window.WebSocket = this.originalWebSocket;
  }
}

// Dev tools panel
function DevNetworkTools() {
  const simulator = useRef(new NetworkSimulator());
  
  return (
    <div className="dev-tools">
      <button onClick={() => simulator.current.goOffline()}>
        Go Offline
      </button>
      <button onClick={() => simulator.current.goOnline()}>
        Go Online
      </button>
      <button onClick={() => simulator.current.simulateSlowConnection(3000)}>
        Slow 3G
      </button>
      <button onClick={() => simulator.current.simulatePacketLoss(0.3)}>
        30% Packet Loss
      </button>
      <button onClick={() => simulator.current.reset()}>
        Reset
      </button>
    </div>
  );
}

📚 Key Takeaways

  1. Exponential backoff - Don't hammer the server
  2. Detect network status - Listen to online/offline events
  3. Queue messages - Persist to localStorage
  4. Synchronize on reconnect - Fetch missed updates
  5. Show status - Users need feedback
  6. Test failures - Use network simulator
  7. Timeout requests - Don't wait forever

Networks fail all the time. Build for it from day one, not after user complaints.

On this page