Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 57 additions & 19 deletions src/components/providers/SocketProvider.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -30,6 +30,10 @@ export function SocketProvider({ children, config, configKey, autoConnect = true
const [socket, setSocket] = useState<Socket | null>(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);
Expand All @@ -48,40 +52,78 @@ 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);

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,
Expand All @@ -95,11 +137,7 @@ export function SocketProvider({ children, config, configKey, autoConnect = true
once,
};

return (
<SocketContext.Provider value={value}>
{children}
</SocketContext.Provider>
);
return <SocketContext.Provider value={value}>{children}</SocketContext.Provider>;
}

export function useSocket(): SocketContextType {
Expand Down
2 changes: 1 addition & 1 deletion src/services/__tests__/socketManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,4 @@ describe('initializeSocketManager', () => {
expect(manager2).toBe(manager3);
expect(manager1).not.toBe(manager2);
});
});
});
82 changes: 82 additions & 0 deletions src/services/socketManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
Loading