refactor: optimize socket connection handling, improve messaging cache management, and simplify SpecializationCard UI

This commit is contained in:
pradeepkumar
2026-03-29 03:31:01 +05:30
parent 89fcd060f4
commit 36b94c7b0c
3 changed files with 116 additions and 110 deletions

View File

@@ -29,11 +29,11 @@ export function SpecializationCard({
}; };
return ( return (
<div className="bg-white rounded-[20px] p-6 text-center flex flex-col border-[0.7px] border-[#E58625] relative"> <div className="bg-white rounded-[20px] p-6 text-center flex flex-col border-[0.7px] border-[#E58625]">
<div className="flex justify-center mb-3">{icon}</div> <div className="flex justify-center mb-3">{icon}</div>
<h4 className="font-bold text-[#00293D] text-[14px] leading-[17px] mb-4 font-fractul">{title}</h4> <h4 className="font-bold text-[#00293D] text-[14px] leading-[17px] mb-4 font-fractul">{title}</h4>
<div className="flex-1"> <div className="flex-1">
{items.slice(0, initialItemsToShow).map((item, idx) => ( {displayedItems.map((item, idx) => (
<p key={idx} className="text-[#00293D] text-[14px] leading-[25px] font-normal font-serif text-center"> <p key={idx} className="text-[#00293D] text-[14px] leading-[25px] font-normal font-serif text-center">
{item} {item}
</p> </p>
@@ -56,35 +56,6 @@ export function SpecializationCard({
</button> </button>
</div> </div>
)} )}
{/* Expanded overlay showing all items */}
{isExpanded && hasMoreItems && (
<div className="absolute inset-0 bg-white rounded-[20px] p-6 text-center flex flex-col border-[0.7px] border-[#E58625] z-10 shadow-lg">
<div className="flex justify-center mb-3">{icon}</div>
<h4 className="font-bold text-[#00293D] text-[14px] leading-[17px] mb-4 font-fractul">{title}</h4>
<div className="flex-1 overflow-y-auto">
{items.map((item, idx) => (
<p key={idx} className="text-[#00293D] text-[14px] leading-[25px] font-normal font-serif text-center">
{item}
</p>
))}
</div>
<div className="mt-4 flex justify-center">
<button
onClick={handleToggle}
className="inline-flex items-center justify-center gap-1 h-[20px] px-3 border border-[#e58625] rounded-[20px] text-[#e58625] text-[10px] leading-[22px] font-bold font-serif hover:bg-[#e58625]/5 transition-colors cursor-pointer"
>
Show Less
<Image
src="/assets/icons/chevron-down-icon.svg"
alt="Show less"
width={10}
height={10}
className="transition-transform duration-200 rotate-180"
/>
</button>
</div>
</div>
)}
</div> </div>
); );
} }

View File

@@ -345,7 +345,9 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
async (conversationId: string) => { async (conversationId: string) => {
await messagesService.deleteConversation(conversationId); 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)); setConversations((prev) => prev.filter((c) => c.id !== conversationId));
// Clear current if it was the deleted one // Clear current if it was the deleted one
@@ -592,9 +594,22 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
unsubscribeStatus(); unsubscribeStatus();
unsubscribeRead(); unsubscribeRead();
unsubscribeDelivered(); unsubscribeDelivered();
// Clean up typing timeout to prevent memory leak
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
typingTimeoutRef.current = null;
}
}; };
}, [updateUserStatus]); }, [updateUserStatus]);
// Invalidate cache when session changes (logout/login)
useEffect(() => {
if (!isAuthenticated) {
_cachedConversations = null;
_cachedTimestamp = 0;
}
}, [isAuthenticated]);
// Auto-connect on mount // Auto-connect on mount
useEffect(() => { useEffect(() => {
if (autoConnect && isAuthenticated && !isInitializedRef.current) { if (autoConnect && isAuthenticated && !isInitializedRef.current) {

View File

@@ -31,68 +31,59 @@ class SocketService {
private authErrorHandlers: Set<() => void> = new Set(); private authErrorHandlers: Set<() => void> = new Set();
private isConnecting = false; private isConnecting = false;
private currentToken: string | null = null; private currentToken: string | null = null;
private listenersAttached = false;
private connectPromise: Promise<void> | null = null;
connect(token: string): Promise<void> { connect(token: string): Promise<void> {
return new Promise((resolve, reject) => { this.currentToken = token;
this.currentToken = token;
// Already connected with same token // Already connected with same token
if (this.socket?.connected) { if (this.socket?.connected) {
resolve(); return Promise.resolve();
return; }
}
// Already connecting // Already connecting — return the existing promise
if (this.isConnecting) { if (this.isConnecting && this.connectPromise) {
// Wait for existing connection attempt return this.connectPromise;
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;
}
this.isConnecting = true; this.isConnecting = true;
// Clean up existing socket if any (but not if it's connecting) // Clean up existing socket if any
if (this.socket && !this.socket.connected) { if (this.socket && !this.socket.connected) {
this.socket.removeAllListeners(); this.socket.removeAllListeners();
this.socket.disconnect(); this.socket.disconnect();
this.socket = null; this.socket = null;
} this.listenersAttached = false;
}
// Extract base URL without /api/v1 path for Socket.io connection // Extract base URL without /api/v1 path for Socket.io connection
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
const baseUrl = apiUrl.replace(/\/api\/v1\/?$/, ''); const baseUrl = apiUrl.replace(/\/api\/v1\/?$/, '');
this.socket = io(baseUrl, { this.socket = io(baseUrl, {
auth: { token }, auth: { token },
transports: ['websocket', 'polling'], transports: ['websocket', 'polling'],
reconnection: true, reconnection: true,
reconnectionAttempts: this.maxReconnectAttempts, reconnectionAttempts: this.maxReconnectAttempts,
reconnectionDelay: 1000, reconnectionDelay: 1000,
reconnectionDelayMax: 5000, reconnectionDelayMax: 5000,
}); });
this.socket.on('connect', () => { this.connectPromise = new Promise<void>((resolve, reject) => {
this.socket!.on('connect', () => {
console.log('Socket connected:', this.socket?.id); console.log('Socket connected:', this.socket?.id);
this.isConnecting = false; this.isConnecting = false;
this.connectPromise = null;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.notifyConnectionHandlers(true); this.notifyConnectionHandlers(true);
resolve(); resolve();
}); });
this.socket.on('disconnect', (reason) => { this.socket!.on('disconnect', (reason) => {
console.log('Socket disconnected:', reason); console.log('Socket disconnected:', reason);
this.isConnecting = false; this.isConnecting = false;
this.connectPromise = null;
this.notifyConnectionHandlers(false); this.notifyConnectionHandlers(false);
// "io server disconnect" means the server rejected us (likely auth failure) // "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); console.error('Socket connection error:', error.message);
this.isConnecting = false; this.isConnecting = false;
this.connectPromise = null;
this.reconnectAttempts++; this.reconnectAttempts++;
if (this.reconnectAttempts >= this.maxReconnectAttempts) { if (this.reconnectAttempts >= this.maxReconnectAttempts) {
reject(new Error('Max reconnection attempts reached')); reject(new Error('Max reconnection attempts reached'));
} }
}); });
});
// Set up event listeners // Attach event listeners only once per socket instance
this.socket.on('new_message', (message: Message) => { this.attachEventListeners();
this.messageHandlers.forEach((handler) => handler(message));
});
this.socket.on('typing_start', (data: TypingIndicator) => { return this.connectPromise;
this.typingStartHandlers.forEach((handler) => handler(data)); }
});
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.socket.on('new_message', (message: Message) => {
this.statusHandlers.forEach((handler) => handler(data)); this.messageHandlers.forEach((handler) => handler(message));
}); });
this.socket.on('messages_read', (data: MessagesReadEvent) => { this.socket.on('typing_start', (data: TypingIndicator) => {
this.readHandlers.forEach((handler) => handler(data)); this.typingStartHandlers.forEach((handler) => handler(data));
}); });
this.socket.on('message_delivered', (data: MessageDeliveredEvent) => { this.socket.on('typing_stop', (data: TypingIndicator) => {
this.deliveredHandlers.forEach((handler) => handler(data)); this.typingStopHandlers.forEach((handler) => handler(data));
}); });
this.socket.on('connection_request', (data: Record<string, unknown>) => { this.socket.on('user_status_change', (data: UserStatusChange) => {
this.connectionRequestHandlers.forEach((handler) => handler(data)); this.statusHandlers.forEach((handler) => handler(data));
}); });
this.socket.on('connection_response', (data: Record<string, unknown>) => { this.socket.on('messages_read', (data: MessagesReadEvent) => {
this.connectionResponseHandlers.forEach((handler) => handler(data)); 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<string, unknown>) => {
this.connectionRequestHandlers.forEach((handler) => handler(data));
});
this.socket.on('connection_response', (data: Record<string, unknown>) => {
this.connectionResponseHandlers.forEach((handler) => handler(data));
}); });
} }
@@ -150,23 +154,40 @@ class SocketService {
reconnectWithToken(token: string): void { reconnectWithToken(token: string): void {
this.currentToken = token; this.currentToken = token;
if (this.socket) { if (this.socket) {
// Update the auth token for reconnection // Ensure fully disconnected before reconnecting
this.socket.auth = { token }; this.socket.removeAllListeners();
this.socket.connect(); this.socket.disconnect();
} else { this.socket = null;
this.connect(token).catch((err) => { this.listenersAttached = false;
console.error('Socket reconnect with new token failed:', err);
});
} }
this.isConnecting = false;
this.connectPromise = null;
this.connect(token).catch((err) => {
console.error('Socket reconnect with new token failed:', err);
});
} }
disconnect(): void { disconnect(): void {
if (this.socket) { if (this.socket) {
this.socket.removeAllListeners();
this.socket.disconnect(); this.socket.disconnect();
this.socket = null; this.socket = null;
} }
this.currentToken = null; this.currentToken = null;
this.isConnecting = false; 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 { isConnected(): boolean {
@@ -209,7 +230,6 @@ class SocketService {
return; return;
} }
// Timeout fallback in case acknowledgement never arrives
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
resolve({ success: false, error: 'Socket timeout' }); resolve({ success: false, error: 'Socket timeout' });
}, 10000); }, 10000);