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