PatternsReal-Time Architecture
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
- Exponential backoff - Don't hammer the server
- Detect network status - Listen to online/offline events
- Queue messages - Persist to localStorage
- Synchronize on reconnect - Fetch missed updates
- Show status - Users need feedback
- Test failures - Use network simulator
- Timeout requests - Don't wait forever
Networks fail all the time. Build for it from day one, not after user complaints.