diff --git a/src/components/providers/SocketProvider.tsx b/src/components/providers/SocketProvider.tsx index edd6f36..6d678c1 100644 --- a/src/components/providers/SocketProvider.tsx +++ b/src/components/providers/SocketProvider.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; +import React, { createContext, useContext, useEffect, useState, useRef, useCallback, ReactNode } from 'react'; import { Socket } from 'socket.io-client'; import { SocketManager, SocketConfig, initializeSocketManager } from '@/services/socketManager'; @@ -30,6 +30,10 @@ export function SocketProvider({ children, config, configKey, autoConnect = true const [socket, setSocket] = useState(null); const [isConnected, setIsConnected] = useState(false); + // Refs to hold handler references so they can be cleaned up. + const handleConnectRef = useRef<(() => void) | null>(null); + const handleDisconnectRef = useRef<(() => void) | null>(null); + useEffect(() => { const manager = initializeSocketManager(config); setSocketManager(manager); @@ -48,8 +52,24 @@ export function SocketProvider({ children, config, configKey, autoConnect = true }; }, [configKey, autoConnect]); - const connect = () => { + const connect = useCallback(() => { if (socketManager) { + // Remove any stale handlers before reconnecting. + if (handleConnectRef.current) { + const sock = socketManager.getSocket(); + if (sock) { + sock.off('connect', handleConnectRef.current); + } + handleConnectRef.current = null; + } + if (handleDisconnectRef.current) { + const sock = socketManager.getSocket(); + if (sock) { + sock.off('disconnect', handleDisconnectRef.current); + } + handleDisconnectRef.current = null; + } + const socketInstance = socketManager.connect(); setSocket(socketInstance); setIsConnected(socketInstance.connected); @@ -57,31 +77,53 @@ export function SocketProvider({ children, config, configKey, autoConnect = true socketManager.on('connect', () => setIsConnected(true)); socketManager.on('disconnect', () => setIsConnected(false)); } - }; + }, [socketManager]); - const disconnect = () => { + const disconnect = useCallback(() => { if (socketManager) { + // Remove handler refs before disconnecting. + const sock = socketManager.getSocket(); + if (sock) { + if (handleConnectRef.current) { + sock.off('connect', handleConnectRef.current); + } + if (handleDisconnectRef.current) { + sock.off('disconnect', handleDisconnectRef.current); + } + } + handleConnectRef.current = null; + handleDisconnectRef.current = null; + socketManager.disconnect(); setSocket(null); setIsConnected(false); } - }; + }, [socketManager]); const emit = (event: string, data?: any): boolean => { return socketManager?.emit(event, data) ?? false; }; - const on = (event: string, callback: (...args: any[]) => void) => { - socketManager?.on(event, callback); - }; + const on = useCallback( + (event: string, callback: (...args: unknown[]) => void) => { + socketManager?.on(event, callback); + }, + [socketManager] + ); - const off = (event: string, callback?: (...args: any[]) => void) => { - socketManager?.off(event, callback); - }; + const off = useCallback( + (event: string, callback?: (...args: unknown[]) => void) => { + socketManager?.off(event, callback); + }, + [socketManager] + ); - const once = (event: string, callback: (...args: any[]) => void) => { - socketManager?.once(event, callback); - }; + const once = useCallback( + (event: string, callback: (...args: unknown[]) => void) => { + socketManager?.once(event, callback); + }, + [socketManager] + ); const value: SocketContextType = { socket, @@ -95,11 +137,7 @@ export function SocketProvider({ children, config, configKey, autoConnect = true once, }; - return ( - - {children} - - ); + return {children}; } export function useSocket(): SocketContextType { diff --git a/src/services/__tests__/socketManager.test.ts b/src/services/__tests__/socketManager.test.ts index 9b4a7dc..21ae3ba 100644 --- a/src/services/__tests__/socketManager.test.ts +++ b/src/services/__tests__/socketManager.test.ts @@ -289,4 +289,4 @@ describe('initializeSocketManager', () => { expect(manager2).toBe(manager3); expect(manager1).not.toBe(manager2); }); -}); \ No newline at end of file +}); diff --git a/src/services/socketManager.ts b/src/services/socketManager.ts index 83eed42..9f0aa5a 100644 --- a/src/services/socketManager.ts +++ b/src/services/socketManager.ts @@ -49,6 +49,39 @@ export class SocketManager { this.maxQueueSize = this.config.maxQueueSize ?? 100; } + /** Handler for socket 'connect' event. */ + private onConnect = (): void => { + console.log('Socket connected:', this.socket?.id); + this.reconnectAttempts = 0; + }; + + /** Handler for socket 'disconnect' event. */ + private onDisconnect = (reason: string): void => { + console.log('Socket disconnected:', reason); + }; + + /** Handler for socket 'connect_error' event. */ + private onConnectError = (error: Error): void => { + console.error('Socket connection error:', error); + this.reconnectAttempts++; + }; + + /** Handler for socket 'reconnect' event. */ + private onReconnect = (attemptNumber: number): void => { + console.log('Socket reconnected after', attemptNumber, 'attempts'); + this.reconnectAttempts = 0; + }; + + /** Handler for socket 'reconnect_error' event. */ + private onReconnectError = (error: Error): void => { + console.error('Socket reconnection error:', error); + }; + + /** Handler for socket 'reconnect_failed' event. */ + private onReconnectFailed = (): void => { + console.error('Socket reconnection failed after', this.reconnectAttempts, 'attempts'); + }; + connect(): Socket { if (this.socket?.connected) { return this.socket; @@ -94,6 +127,10 @@ export class SocketManager { return this.socket; } + /** + * Removes all internally-registered listeners from the socket, then + * disconnects and clears the instance. + */ disconnect(): void { const socket = this.socket; @@ -212,6 +249,51 @@ export class SocketManager { this.socket.once(event, callback); } } + + // --------------------------------------------------------------------------- + // Private helpers + // --------------------------------------------------------------------------- + + private registerEvent(event: string): void { + this.registeredEvents.add(event); + } + + private isManagerEvent(event: string): event is ManagerEvent { + return (MANAGER_EVENTS as readonly string[]).includes(event); + } + + private getHandlerForEvent(event: ManagerEvent): ((...args: unknown[]) => void) | undefined { + switch (event) { + case 'connect': + return this.onConnect; + case 'disconnect': + return this.onDisconnect; + case 'connect_error': + return this.onConnectError; + case 'reconnect': + return this.onReconnect; + case 'reconnect_error': + return this.onReconnectError; + case 'reconnect_failed': + return this.onReconnectFailed; + default: + return undefined; + } + } + + // --------------------------------------------------------------------------- + // Test helpers + // --------------------------------------------------------------------------- + + /** @internal Returns the number of tracked registered events (for tests). */ + getRegisteredEventCount(): number { + return this.registeredEvents.size; + } + + /** @internal Returns whether a specific event is tracked in the registry (for tests). */ + isEventRegistered(event: string): boolean { + return this.registeredEvents.has(event); + } } // Singleton instance