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 (
<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>
<h4 className="font-bold text-[#00293D] text-[14px] leading-[17px] mb-4 font-fractul">{title}</h4>
<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">
{item}
</p>
@@ -56,35 +56,6 @@ export function SpecializationCard({
</button>
</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>
);
}

View File

@@ -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) {

View File

@@ -31,42 +31,30 @@ class SocketService {
private authErrorHandlers: Set<() => void> = new Set();
private isConnecting = false;
private currentToken: string | null = null;
private listenersAttached = false;
private connectPromise: Promise<void> | null = null;
connect(token: string): Promise<void> {
return new Promise((resolve, reject) => {
this.currentToken = token;
// Already connected with same token
if (this.socket?.connected) {
resolve();
return;
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;
// Clean up existing socket if any (but not if it's connecting)
// 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
@@ -82,17 +70,20 @@ class SocketService {
reconnectionDelayMax: 5000,
});
this.socket.on('connect', () => {
this.connectPromise = new Promise<void>((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,16 +93,30 @@ 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'));
}
});
});
// 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.messageHandlers.forEach((handler) => handler(message));
});
@@ -143,30 +148,46 @@ class SocketService {
this.socket.on('connection_response', (data: Record<string, unknown>) => {
this.connectionResponseHandlers.forEach((handler) => handler(data));
});
});
}
// Reconnect with a fresh token (used when the previous token expired)
reconnectWithToken(token: string): void {
this.currentToken = token;
if (this.socket) {
// Update the auth token for reconnection
this.socket.auth = { token };
this.socket.connect();
} else {
// 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);