real time messaging
This commit is contained in:
314
frontend/src/services/socket.ts
Normal file
314
frontend/src/services/socket.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
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 process.env.REACT_APP_BASE_URL || "http://localhost:5001";
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and connect to the socket server
|
||||
* Authentication happens via cookies (sent automatically)
|
||||
*/
|
||||
connect(): Socket {
|
||||
if (this.socket?.connected) {
|
||||
console.log("[Socket] Already connected");
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
console.log("[Socket] Connecting to server...");
|
||||
|
||||
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", () => {
|
||||
console.log("[Socket] Connected successfully", {
|
||||
socketId: this.socket?.id,
|
||||
});
|
||||
this.reconnectAttempts = 0;
|
||||
this.notifyConnectionListeners(true);
|
||||
});
|
||||
|
||||
this.socket.on("disconnect", (reason) => {
|
||||
console.log("[Socket] Disconnected", { reason });
|
||||
this.notifyConnectionListeners(false);
|
||||
});
|
||||
|
||||
this.socket.on("connect_error", (error) => {
|
||||
console.error("[Socket] Connection error", {
|
||||
error: error.message,
|
||||
attempt: this.reconnectAttempts + 1,
|
||||
});
|
||||
this.reconnectAttempts++;
|
||||
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.error("[Socket] Max reconnection attempts reached");
|
||||
this.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on("error", (error) => {
|
||||
console.error("[Socket] Socket error", error);
|
||||
});
|
||||
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the socket server
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.socket) {
|
||||
console.log("[Socket] Disconnecting...");
|
||||
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;
|
||||
}
|
||||
|
||||
console.log("[Socket] Joining conversation", { otherUserId });
|
||||
this.socket.emit("join_conversation", { otherUserId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave a conversation room
|
||||
*/
|
||||
leaveConversation(otherUserId: string): void {
|
||||
if (!this.socket?.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Socket] Leaving conversation", { otherUserId });
|
||||
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;
|
||||
Reference in New Issue
Block a user