/** * WebSocket service for real-time blockchain updates. * Handles connection management, reconnection, and event subscriptions. */ export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting'; export interface BlockEvent { type: 'new_block'; hash: string; blueScore: number; timestamp: number; txCount: number; isChainBlock: boolean; } export interface StatsEvent { type: 'stats_update'; blockCount: number; virtualDaaScore: number; difficulty: number; mempoolSize: number; hashrate: number; hashrateHuman: string; } export interface TipEvent { type: 'tip_update'; tips: string[]; tipCount: number; } export interface MempoolEvent { type: 'mempool_tx'; txId: string; fee: number; mass: number; } export type WebSocketEvent = BlockEvent | StatsEvent | TipEvent | MempoolEvent; type EventCallback = (event: T) => void; type StatusCallback = (status: WebSocketStatus) => void; class WebSocketService { private ws: WebSocket | null = null; private url: string; private subscriptions: Map> = new Map(); private statusListeners: Set = new Set(); private status: WebSocketStatus = 'disconnected'; private reconnectAttempts = 0; private maxReconnectAttempts = 10; private reconnectDelay = 1000; private reconnectTimer: ReturnType | null = null; private mockMode = false; private mockInterval: ReturnType | null = null; constructor() { // Default WebSocket URL - can be overridden const wsProtocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsHost = typeof window !== 'undefined' ? window.location.host : 'localhost:3000'; this.url = `${wsProtocol}//${wsHost}/ws`; // Check if mock mode should be enabled this.mockMode = this.shouldUseMock(); } private shouldUseMock(): boolean { if (typeof window !== 'undefined') { if (localStorage.getItem('useMockApi') === 'true') return true; } return import.meta.env.VITE_USE_MOCK === 'true'; } setUrl(url: string) { this.url = url; } enableMock(enable: boolean) { this.mockMode = enable; if (enable && this.status === 'connected') { this.startMockUpdates(); } else if (!enable) { this.stopMockUpdates(); } } connect(): void { if (this.mockMode) { this.setStatus('connected'); this.startMockUpdates(); return; } if (this.ws?.readyState === WebSocket.OPEN) { return; } this.setStatus('connecting'); try { this.ws = new WebSocket(this.url); this.ws.onopen = () => { this.setStatus('connected'); this.reconnectAttempts = 0; this.reconnectDelay = 1000; console.log('[WS] Connected to', this.url); }; this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data) as WebSocketEvent; this.emit(data); } catch (e) { console.error('[WS] Failed to parse message:', e); } }; this.ws.onerror = (error) => { console.error('[WS] Error:', error); }; this.ws.onclose = (event) => { console.log('[WS] Disconnected:', event.code, event.reason); this.setStatus('disconnected'); this.scheduleReconnect(); }; } catch (error) { console.error('[WS] Failed to connect:', error); this.setStatus('disconnected'); this.scheduleReconnect(); } } disconnect(): void { this.stopMockUpdates(); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.close(1000, 'Client disconnect'); this.ws = null; } this.setStatus('disconnected'); } private scheduleReconnect(): void { if (this.mockMode) return; if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.log('[WS] Max reconnect attempts reached'); return; } this.reconnectAttempts++; this.setStatus('reconnecting'); const delay = Math.min(this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1), 30000); console.log(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); this.reconnectTimer = setTimeout(() => { this.connect(); }, delay); } private setStatus(status: WebSocketStatus): void { this.status = status; this.statusListeners.forEach(callback => callback(status)); } getStatus(): WebSocketStatus { return this.status; } onStatusChange(callback: StatusCallback): () => void { this.statusListeners.add(callback); return () => this.statusListeners.delete(callback); } subscribe(eventType: T['type'], callback: EventCallback): () => void { if (!this.subscriptions.has(eventType)) { this.subscriptions.set(eventType, new Set()); } this.subscriptions.get(eventType)!.add(callback as EventCallback); // Return unsubscribe function return () => { this.subscriptions.get(eventType)?.delete(callback as EventCallback); }; } private emit(event: WebSocketEvent): void { const callbacks = this.subscriptions.get(event.type); if (callbacks) { callbacks.forEach(callback => callback(event)); } // Also emit to wildcard subscribers const wildcardCallbacks = this.subscriptions.get('*'); if (wildcardCallbacks) { wildcardCallbacks.forEach(callback => callback(event)); } } // Mock data simulation for development private startMockUpdates(): void { if (this.mockInterval) return; let blockCounter = 125847; let mempoolSize = 42; this.mockInterval = setInterval(() => { // Simulate new block every 1 second blockCounter++; const blockEvent: BlockEvent = { type: 'new_block', hash: this.generateMockHash(blockCounter), blueScore: blockCounter, timestamp: Date.now(), txCount: Math.floor(Math.random() * 50) + 1, isChainBlock: true, }; this.emit(blockEvent); // Stats update with each block mempoolSize = Math.max(0, mempoolSize + Math.floor(Math.random() * 10) - 5); const statsEvent: StatsEvent = { type: 'stats_update', blockCount: blockCounter, virtualDaaScore: blockCounter, difficulty: 1234567890 + Math.random() * 100000000, mempoolSize, hashrate: 45.6e9 + Math.random() * 5e9, hashrateHuman: `${(45.6 + Math.random() * 5).toFixed(2)} GH/s`, }; this.emit(statsEvent); // Occasional tip updates if (Math.random() > 0.7) { const tipEvent: TipEvent = { type: 'tip_update', tips: [ this.generateMockHash(blockCounter), this.generateMockHash(blockCounter - 1), this.generateMockHash(blockCounter - 2), ], tipCount: 3, }; this.emit(tipEvent); } }, 1000); } private stopMockUpdates(): void { if (this.mockInterval) { clearInterval(this.mockInterval); this.mockInterval = null; } } private generateMockHash(seed: number): string { const chars = '0123456789abcdef'; let hash = ''; for (let i = 0; i < 64; i++) { const charIndex = (seed * 31 + i * 7 + Math.floor(i / 8) * seed) % 16; hash += chars[Math.abs(charIndex)]; } return hash; } } // Singleton instance export const wsService = new WebSocketService(); export default wsService;