From 36b94c7b0c2d470e67f5f28aba2a6624d4743b6f Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Sun, 29 Mar 2026 03:31:01 +0530 Subject: [PATCH] refactor: optimize socket connection handling, improve messaging cache management, and simplify SpecializationCard UI --- src/components/profile/SpecializationCard.tsx | 33 +--- src/hooks/useMessaging.ts | 17 +- src/services/socket.service.ts | 176 ++++++++++-------- 3 files changed, 116 insertions(+), 110 deletions(-) diff --git a/src/components/profile/SpecializationCard.tsx b/src/components/profile/SpecializationCard.tsx index 1e45e02..c8209a6 100644 --- a/src/components/profile/SpecializationCard.tsx +++ b/src/components/profile/SpecializationCard.tsx @@ -29,11 +29,11 @@ export function SpecializationCard({ }; return ( -
+
{icon}

{title}

- {items.slice(0, initialItemsToShow).map((item, idx) => ( + {displayedItems.map((item, idx) => (

{item}

@@ -56,35 +56,6 @@ export function SpecializationCard({
)} - {/* Expanded overlay showing all items */} - {isExpanded && hasMoreItems && ( -
-
{icon}
-

{title}

-
- {items.map((item, idx) => ( -

- {item} -

- ))} -
-
- -
-
- )}
); } diff --git a/src/hooks/useMessaging.ts b/src/hooks/useMessaging.ts index 0cad3a5..9900e5f 100644 --- a/src/hooks/useMessaging.ts +++ b/src/hooks/useMessaging.ts @@ -345,7 +345,9 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet async (conversationId: string) => { await messagesService.deleteConversation(conversationId); - // Remove from list + // Remove from list and invalidate cache + _cachedConversations = null; + _cachedTimestamp = 0; setConversations((prev) => prev.filter((c) => c.id !== conversationId)); // Clear current if it was the deleted one @@ -592,9 +594,22 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet unsubscribeStatus(); unsubscribeRead(); unsubscribeDelivered(); + // Clean up typing timeout to prevent memory leak + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + typingTimeoutRef.current = null; + } }; }, [updateUserStatus]); + // Invalidate cache when session changes (logout/login) + useEffect(() => { + if (!isAuthenticated) { + _cachedConversations = null; + _cachedTimestamp = 0; + } + }, [isAuthenticated]); + // Auto-connect on mount useEffect(() => { if (autoConnect && isAuthenticated && !isInitializedRef.current) { diff --git a/src/services/socket.service.ts b/src/services/socket.service.ts index ab8c7e2..cce83a0 100644 --- a/src/services/socket.service.ts +++ b/src/services/socket.service.ts @@ -31,68 +31,59 @@ class SocketService { private authErrorHandlers: Set<() => void> = new Set(); private isConnecting = false; private currentToken: string | null = null; + private listenersAttached = false; + private connectPromise: Promise | null = null; connect(token: string): Promise { - return new Promise((resolve, reject) => { - this.currentToken = token; + this.currentToken = token; - // Already connected with same token - if (this.socket?.connected) { - resolve(); - return; - } + // Already connected with same token + if (this.socket?.connected) { + return Promise.resolve(); + } - // Already connecting - if (this.isConnecting) { - // Wait for existing connection attempt - const checkConnection = setInterval(() => { - if (this.socket?.connected) { - clearInterval(checkConnection); - resolve(); - } - }, 100); - setTimeout(() => { - clearInterval(checkConnection); - if (!this.socket?.connected) { - reject(new Error('Connection timeout')); - } - }, 10000); - return; - } + // Already connecting — return the existing promise + if (this.isConnecting && this.connectPromise) { + return this.connectPromise; + } - this.isConnecting = true; + this.isConnecting = true; - // Clean up existing socket if any (but not if it's connecting) - if (this.socket && !this.socket.connected) { - this.socket.removeAllListeners(); - this.socket.disconnect(); - this.socket = null; - } + // Clean up existing socket if any + if (this.socket && !this.socket.connected) { + this.socket.removeAllListeners(); + this.socket.disconnect(); + this.socket = null; + this.listenersAttached = false; + } - // Extract base URL without /api/v1 path for Socket.io connection - const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; - const baseUrl = apiUrl.replace(/\/api\/v1\/?$/, ''); + // Extract base URL without /api/v1 path for Socket.io connection + const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; + const baseUrl = apiUrl.replace(/\/api\/v1\/?$/, ''); - this.socket = io(baseUrl, { - auth: { token }, - transports: ['websocket', 'polling'], - reconnection: true, - reconnectionAttempts: this.maxReconnectAttempts, - reconnectionDelay: 1000, - reconnectionDelayMax: 5000, - }); + this.socket = io(baseUrl, { + auth: { token }, + transports: ['websocket', 'polling'], + reconnection: true, + reconnectionAttempts: this.maxReconnectAttempts, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + }); - this.socket.on('connect', () => { + this.connectPromise = new Promise((resolve, reject) => { + this.socket!.on('connect', () => { console.log('Socket connected:', this.socket?.id); this.isConnecting = false; + this.connectPromise = null; this.reconnectAttempts = 0; this.notifyConnectionHandlers(true); resolve(); }); - this.socket.on('disconnect', (reason) => { + this.socket!.on('disconnect', (reason) => { console.log('Socket disconnected:', reason); this.isConnecting = false; + this.connectPromise = null; this.notifyConnectionHandlers(false); // "io server disconnect" means the server rejected us (likely auth failure) @@ -102,47 +93,60 @@ class SocketService { } }); - this.socket.on('connect_error', (error) => { + this.socket!.on('connect_error', (error) => { console.error('Socket connection error:', error.message); this.isConnecting = false; + this.connectPromise = null; this.reconnectAttempts++; if (this.reconnectAttempts >= this.maxReconnectAttempts) { reject(new Error('Max reconnection attempts reached')); } }); + }); - // Set up event listeners - this.socket.on('new_message', (message: Message) => { - this.messageHandlers.forEach((handler) => handler(message)); - }); + // Attach event listeners only once per socket instance + this.attachEventListeners(); - this.socket.on('typing_start', (data: TypingIndicator) => { - this.typingStartHandlers.forEach((handler) => handler(data)); - }); + return this.connectPromise; + } - this.socket.on('typing_stop', (data: TypingIndicator) => { - this.typingStopHandlers.forEach((handler) => handler(data)); - }); + /** + * Attach socket event listeners — called once per socket instance + */ + private attachEventListeners(): void { + if (this.listenersAttached || !this.socket) return; + this.listenersAttached = true; - this.socket.on('user_status_change', (data: UserStatusChange) => { - this.statusHandlers.forEach((handler) => handler(data)); - }); + this.socket.on('new_message', (message: Message) => { + this.messageHandlers.forEach((handler) => handler(message)); + }); - this.socket.on('messages_read', (data: MessagesReadEvent) => { - this.readHandlers.forEach((handler) => handler(data)); - }); + this.socket.on('typing_start', (data: TypingIndicator) => { + this.typingStartHandlers.forEach((handler) => handler(data)); + }); - this.socket.on('message_delivered', (data: MessageDeliveredEvent) => { - this.deliveredHandlers.forEach((handler) => handler(data)); - }); + this.socket.on('typing_stop', (data: TypingIndicator) => { + this.typingStopHandlers.forEach((handler) => handler(data)); + }); - this.socket.on('connection_request', (data: Record) => { - this.connectionRequestHandlers.forEach((handler) => handler(data)); - }); + this.socket.on('user_status_change', (data: UserStatusChange) => { + this.statusHandlers.forEach((handler) => handler(data)); + }); - this.socket.on('connection_response', (data: Record) => { - this.connectionResponseHandlers.forEach((handler) => handler(data)); - }); + this.socket.on('messages_read', (data: MessagesReadEvent) => { + this.readHandlers.forEach((handler) => handler(data)); + }); + + this.socket.on('message_delivered', (data: MessageDeliveredEvent) => { + this.deliveredHandlers.forEach((handler) => handler(data)); + }); + + this.socket.on('connection_request', (data: Record) => { + this.connectionRequestHandlers.forEach((handler) => handler(data)); + }); + + this.socket.on('connection_response', (data: Record) => { + this.connectionResponseHandlers.forEach((handler) => handler(data)); }); } @@ -150,23 +154,40 @@ class SocketService { reconnectWithToken(token: string): void { this.currentToken = token; if (this.socket) { - // Update the auth token for reconnection - this.socket.auth = { token }; - this.socket.connect(); - } else { - this.connect(token).catch((err) => { - console.error('Socket reconnect with new token failed:', err); - }); + // Ensure fully disconnected before reconnecting + this.socket.removeAllListeners(); + this.socket.disconnect(); + this.socket = null; + this.listenersAttached = false; } + this.isConnecting = false; + this.connectPromise = null; + this.connect(token).catch((err) => { + console.error('Socket reconnect with new token failed:', err); + }); } disconnect(): void { if (this.socket) { + this.socket.removeAllListeners(); this.socket.disconnect(); this.socket = null; } this.currentToken = null; this.isConnecting = false; + this.connectPromise = null; + this.listenersAttached = false; + // Clear all handler sets to prevent leaks + this.messageHandlers.clear(); + this.typingStartHandlers.clear(); + this.typingStopHandlers.clear(); + this.statusHandlers.clear(); + this.readHandlers.clear(); + this.deliveredHandlers.clear(); + this.connectionRequestHandlers.clear(); + this.connectionResponseHandlers.clear(); + this.connectionHandlers.clear(); + this.authErrorHandlers.clear(); } isConnected(): boolean { @@ -209,7 +230,6 @@ class SocketService { return; } - // Timeout fallback in case acknowledgement never arrives const timeout = setTimeout(() => { resolve({ success: false, error: 'Socket timeout' }); }, 10000);