297 lines
6.6 KiB
TypeScript
297 lines
6.6 KiB
TypeScript
import { io, Socket } from "socket.io-client";
|
|
|
|
/**
|
|
* Socket event types for type safety
|
|
*/
|
|
export interface SocketEvents {
|
|
// Incoming events (from server)
|
|
new_message: (message: any) => void;
|
|
message_read: (data: {
|
|
messageId: string;
|
|
readAt: string;
|
|
readBy: string;
|
|
}) => void;
|
|
user_typing: (data: {
|
|
userId: string;
|
|
firstName: string;
|
|
isTyping: boolean;
|
|
}) => void;
|
|
conversation_joined: (data: {
|
|
conversationRoom: string;
|
|
otherUserId: string;
|
|
}) => void;
|
|
|
|
// Connection events
|
|
connect: () => void;
|
|
disconnect: (reason: string) => void;
|
|
connect_error: (error: Error) => void;
|
|
error: (error: Error) => void;
|
|
}
|
|
|
|
/**
|
|
* Socket service for managing WebSocket connection
|
|
* Implements singleton pattern to ensure only one socket instance
|
|
*/
|
|
class SocketService {
|
|
private socket: Socket | null = null;
|
|
private reconnectAttempts = 0;
|
|
private maxReconnectAttempts = 5;
|
|
private connectionListeners: Array<(connected: boolean) => void> = [];
|
|
|
|
/**
|
|
* Get the socket instance URL based on environment
|
|
*/
|
|
private getSocketUrl(): string {
|
|
// Use environment variable or default to localhost:5001 (matches backend)
|
|
return import.meta.env.VITE_BASE_URL || "http://localhost:5001";
|
|
}
|
|
|
|
/**
|
|
* Initialize and connect to the socket server
|
|
* Authentication happens via cookies (sent automatically)
|
|
*/
|
|
connect(): Socket {
|
|
if (this.socket?.connected) {
|
|
return this.socket;
|
|
}
|
|
|
|
this.socket = io(this.getSocketUrl(), {
|
|
withCredentials: true, // Send cookies for authentication
|
|
transports: ["websocket", "polling"], // Try WebSocket first, fallback to polling
|
|
path: "/socket.io", // Explicit Socket.io path
|
|
reconnection: true,
|
|
reconnectionAttempts: this.maxReconnectAttempts,
|
|
reconnectionDelay: 1000,
|
|
reconnectionDelayMax: 5000,
|
|
timeout: 20000,
|
|
});
|
|
|
|
// Connection event handlers
|
|
this.socket.on("connect", () => {
|
|
this.reconnectAttempts = 0;
|
|
this.notifyConnectionListeners(true);
|
|
});
|
|
|
|
this.socket.on("disconnect", (reason) => {
|
|
this.notifyConnectionListeners(false);
|
|
});
|
|
|
|
this.socket.on("connect_error", (error) => {
|
|
this.reconnectAttempts++;
|
|
|
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
console.error("[Socket] Max reconnection attempts reached");
|
|
this.disconnect();
|
|
}
|
|
});
|
|
|
|
return this.socket;
|
|
}
|
|
|
|
/**
|
|
* Disconnect from the socket server
|
|
*/
|
|
disconnect(): void {
|
|
if (this.socket) {
|
|
this.socket.disconnect();
|
|
this.socket = null;
|
|
this.notifyConnectionListeners(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current socket instance
|
|
*/
|
|
getSocket(): Socket | null {
|
|
return this.socket;
|
|
}
|
|
|
|
/**
|
|
* Check if socket is connected
|
|
*/
|
|
isConnected(): boolean {
|
|
return this.socket?.connected ?? false;
|
|
}
|
|
|
|
/**
|
|
* Join a conversation room
|
|
*/
|
|
joinConversation(otherUserId: string): void {
|
|
if (!this.socket?.connected) {
|
|
console.warn("[Socket] Not connected, cannot join conversation");
|
|
return;
|
|
}
|
|
|
|
this.socket.emit("join_conversation", { otherUserId });
|
|
}
|
|
|
|
/**
|
|
* Leave a conversation room
|
|
*/
|
|
leaveConversation(otherUserId: string): void {
|
|
if (!this.socket?.connected) {
|
|
return;
|
|
}
|
|
|
|
this.socket.emit("leave_conversation", { otherUserId });
|
|
}
|
|
|
|
/**
|
|
* Emit typing start event
|
|
*/
|
|
emitTypingStart(receiverId: string): void {
|
|
if (!this.socket?.connected) {
|
|
return;
|
|
}
|
|
|
|
this.socket.emit("typing_start", { receiverId });
|
|
}
|
|
|
|
/**
|
|
* Emit typing stop event
|
|
*/
|
|
emitTypingStop(receiverId: string): void {
|
|
if (!this.socket?.connected) {
|
|
return;
|
|
}
|
|
|
|
this.socket.emit("typing_stop", { receiverId });
|
|
}
|
|
|
|
/**
|
|
* Emit mark message as read event
|
|
*/
|
|
emitMarkMessageRead(messageId: string, senderId: string): void {
|
|
if (!this.socket?.connected) {
|
|
return;
|
|
}
|
|
|
|
this.socket.emit("mark_message_read", { messageId, senderId });
|
|
}
|
|
|
|
/**
|
|
* Listen for new messages
|
|
*/
|
|
onNewMessage(callback: (message: any) => void): () => void {
|
|
if (!this.socket) {
|
|
console.warn("[Socket] Socket not initialized");
|
|
return () => {};
|
|
}
|
|
|
|
this.socket.on("new_message", callback);
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
this.socket?.off("new_message", callback);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Listen for message read events
|
|
*/
|
|
onMessageRead(
|
|
callback: (data: {
|
|
messageId: string;
|
|
readAt: string;
|
|
readBy: string;
|
|
}) => void
|
|
): () => void {
|
|
if (!this.socket) {
|
|
console.warn("[Socket] Socket not initialized");
|
|
return () => {};
|
|
}
|
|
|
|
this.socket.on("message_read", callback);
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
this.socket?.off("message_read", callback);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Listen for typing indicators
|
|
*/
|
|
onUserTyping(
|
|
callback: (data: {
|
|
userId: string;
|
|
firstName: string;
|
|
isTyping: boolean;
|
|
}) => void
|
|
): () => void {
|
|
if (!this.socket) {
|
|
console.warn("[Socket] Socket not initialized");
|
|
return () => {};
|
|
}
|
|
|
|
this.socket.on("user_typing", callback);
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
this.socket?.off("user_typing", callback);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Listen for conversation joined event
|
|
*/
|
|
onConversationJoined(
|
|
callback: (data: { conversationRoom: string; otherUserId: string }) => void
|
|
): () => void {
|
|
if (!this.socket) {
|
|
console.warn("[Socket] Socket not initialized");
|
|
return () => {};
|
|
}
|
|
|
|
this.socket.on("conversation_joined", callback);
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
this.socket?.off("conversation_joined", callback);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Add connection status listener
|
|
*/
|
|
addConnectionListener(callback: (connected: boolean) => void): () => void {
|
|
this.connectionListeners.push(callback);
|
|
|
|
// Immediately notify of current status
|
|
callback(this.isConnected());
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
this.connectionListeners = this.connectionListeners.filter(
|
|
(cb) => cb !== callback
|
|
);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Notify all connection listeners of status change
|
|
*/
|
|
private notifyConnectionListeners(connected: boolean): void {
|
|
this.connectionListeners.forEach((callback) => {
|
|
try {
|
|
callback(connected);
|
|
} catch (error) {
|
|
console.error("[Socket] Error in connection listener", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove all event listeners
|
|
*/
|
|
removeAllListeners(): void {
|
|
if (this.socket) {
|
|
this.socket.removeAllListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const socketService = new SocketService();
|
|
export default socketService;
|