refactor: optimize socket connection handling, improve messaging cache management, and simplify SpecializationCard UI
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user