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,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<void> | null = null;
connect(token: string): Promise<void> {
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<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,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<string, unknown>) => {
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<string, unknown>) => {
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<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 {
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);