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

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