/** * Socket.io Client for LocalGreenChain * * Provides a client-side wrapper for Socket.io connections. */ import { io, Socket } from 'socket.io-client'; import type { ClientToServerEvents, ServerToClientEvents, ConnectionStatus, RoomType, TransparencyEventType, TransparencyEvent, ConnectionMetrics, } from './types'; type TypedSocket = Socket; /** * Client configuration options */ export interface SocketClientConfig { url?: string; path?: string; autoConnect?: boolean; auth?: { userId?: string; token?: string; sessionId?: string; }; reconnection?: boolean; reconnectionAttempts?: number; reconnectionDelay?: number; } /** * Event listener types */ export type EventListener = (event: TransparencyEvent) => void; export type StatusListener = (status: ConnectionStatus) => void; export type ErrorListener = (error: { code: string; message: string }) => void; /** * Socket.io client wrapper */ class RealtimeSocketClient { private socket: TypedSocket | null = null; private config: SocketClientConfig; private status: ConnectionStatus = 'disconnected'; private eventListeners: Set = new Set(); private statusListeners: Set = new Set(); private errorListeners: Set = new Set(); private metrics: ConnectionMetrics; private pingInterval: NodeJS.Timeout | null = null; constructor(config: SocketClientConfig = {}) { this.config = { url: typeof window !== 'undefined' ? window.location.origin : '', path: '/api/socket', autoConnect: true, reconnection: true, reconnectionAttempts: 10, reconnectionDelay: 1000, ...config, }; this.metrics = { status: 'disconnected', eventsReceived: 0, reconnectAttempts: 0, rooms: [], }; } /** * Connect to the server */ connect(): void { if (this.socket?.connected) { return; } this.updateStatus('connecting'); this.socket = io(this.config.url!, { path: this.config.path, autoConnect: this.config.autoConnect, auth: this.config.auth, reconnection: this.config.reconnection, reconnectionAttempts: this.config.reconnectionAttempts, reconnectionDelay: this.config.reconnectionDelay, transports: ['websocket', 'polling'], }); this.setupEventHandlers(); } /** * Set up socket event handlers */ private setupEventHandlers(): void { if (!this.socket) return; // Connection events this.socket.on('connect', () => { this.updateStatus('connected'); this.metrics.connectedAt = Date.now(); this.metrics.reconnectAttempts = 0; this.startPingInterval(); }); this.socket.on('disconnect', () => { this.updateStatus('disconnected'); this.stopPingInterval(); }); this.socket.on('connect_error', () => { this.updateStatus('error'); this.metrics.reconnectAttempts++; }); // Server events this.socket.on('connection:established', (data) => { console.log('[SocketClient] Connected:', data.socketId); }); this.socket.on('connection:error', (error) => { this.errorListeners.forEach((listener) => listener(error)); }); // Real-time events this.socket.on('event', (event) => { this.metrics.eventsReceived++; this.metrics.lastEventAt = Date.now(); this.eventListeners.forEach((listener) => listener(event)); }); this.socket.on('event:batch', (events) => { this.metrics.eventsReceived += events.length; this.metrics.lastEventAt = Date.now(); events.forEach((event) => { this.eventListeners.forEach((listener) => listener(event)); }); }); // Room events this.socket.on('room:joined', (room) => { if (!this.metrics.rooms.includes(room)) { this.metrics.rooms.push(room); } }); this.socket.on('room:left', (room) => { this.metrics.rooms = this.metrics.rooms.filter((r) => r !== room); }); // System events this.socket.on('system:message', (message) => { console.log(`[SocketClient] System ${message.type}: ${message.text}`); }); this.socket.on('system:heartbeat', () => { // Heartbeat received - connection is alive }); // Reconnection events this.socket.io.on('reconnect_attempt', () => { this.updateStatus('reconnecting'); this.metrics.reconnectAttempts++; }); this.socket.io.on('reconnect', () => { this.updateStatus('connected'); }); } /** * Start ping interval for latency measurement */ private startPingInterval(): void { this.stopPingInterval(); this.pingInterval = setInterval(() => { if (this.socket?.connected) { const start = Date.now(); this.socket.emit('ping', (serverTime) => { this.metrics.latency = Date.now() - start; }); } }, 10000); // Every 10 seconds } /** * Stop ping interval */ private stopPingInterval(): void { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } } /** * Update connection status and notify listeners */ private updateStatus(status: ConnectionStatus): void { this.status = status; this.metrics.status = status; this.statusListeners.forEach((listener) => listener(status)); } /** * Disconnect from the server */ disconnect(): void { this.stopPingInterval(); if (this.socket) { this.socket.disconnect(); this.socket = null; } this.updateStatus('disconnected'); } /** * Join a room */ joinRoom(room: RoomType): Promise { return new Promise((resolve) => { if (!this.socket?.connected) { resolve(false); return; } this.socket.emit('room:join', room, (success) => { resolve(success); }); }); } /** * Leave a room */ leaveRoom(room: RoomType): Promise { return new Promise((resolve) => { if (!this.socket?.connected) { resolve(false); return; } this.socket.emit('room:leave', room, (success) => { resolve(success); }); }); } /** * Subscribe to specific event types */ subscribeToTypes(types: TransparencyEventType[]): Promise { return new Promise((resolve) => { if (!this.socket?.connected) { resolve(false); return; } this.socket.emit('subscribe:types', types, (success) => { resolve(success); }); }); } /** * Unsubscribe from specific event types */ unsubscribeFromTypes(types: TransparencyEventType[]): Promise { return new Promise((resolve) => { if (!this.socket?.connected) { resolve(false); return; } this.socket.emit('unsubscribe:types', types, (success) => { resolve(success); }); }); } /** * Get recent events */ getRecentEvents(limit: number = 50): Promise { return new Promise((resolve) => { if (!this.socket?.connected) { resolve([]); return; } this.socket.emit('events:recent', limit, (events) => { resolve(events); }); }); } /** * Add an event listener */ onEvent(listener: EventListener): () => void { this.eventListeners.add(listener); return () => this.eventListeners.delete(listener); } /** * Add a status listener */ onStatusChange(listener: StatusListener): () => void { this.statusListeners.add(listener); return () => this.statusListeners.delete(listener); } /** * Add an error listener */ onError(listener: ErrorListener): () => void { this.errorListeners.add(listener); return () => this.errorListeners.delete(listener); } /** * Get current connection status */ getStatus(): ConnectionStatus { return this.status; } /** * Get connection metrics */ getMetrics(): ConnectionMetrics { return { ...this.metrics }; } /** * Check if connected */ isConnected(): boolean { return this.socket?.connected ?? false; } /** * Get socket ID */ getSocketId(): string | undefined { return this.socket?.id; } } // Singleton instance for client-side use let clientInstance: RealtimeSocketClient | null = null; /** * Get the singleton socket client instance */ export function getSocketClient(config?: SocketClientConfig): RealtimeSocketClient { if (!clientInstance) { clientInstance = new RealtimeSocketClient(config); } return clientInstance; } /** * Create a new socket client instance */ export function createSocketClient(config?: SocketClientConfig): RealtimeSocketClient { return new RealtimeSocketClient(config); } export { RealtimeSocketClient }; export default RealtimeSocketClient;