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,42 +31,30 @@ 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
@@ -82,17 +70,20 @@ class SocketService {
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,16 +93,30 @@ 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'));
} }
}); });
});
// Attach event listeners only once per socket instance
this.attachEventListeners();
return this.connectPromise;
}
/**
* Attach socket event listeners — called once per socket instance
*/
private attachEventListeners(): void {
if (this.listenersAttached || !this.socket) return;
this.listenersAttached = true;
// Set up event listeners
this.socket.on('new_message', (message: Message) => { this.socket.on('new_message', (message: Message) => {
this.messageHandlers.forEach((handler) => handler(message)); this.messageHandlers.forEach((handler) => handler(message));
}); });
@@ -143,30 +148,46 @@ class SocketService {
this.socket.on('connection_response', (data: Record<string, unknown>) => { this.socket.on('connection_response', (data: Record<string, unknown>) => {
this.connectionResponseHandlers.forEach((handler) => handler(data)); this.connectionResponseHandlers.forEach((handler) => handler(data));
}); });
});
} }
// Reconnect with a fresh token (used when the previous token expired) // Reconnect with a fresh token (used when the previous token expired)
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.listenersAttached = false;
}
this.isConnecting = false;
this.connectPromise = null;
this.connect(token).catch((err) => { this.connect(token).catch((err) => {
console.error('Socket reconnect with new token failed:', 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);