Files
rentall-app/frontend/src/services/socket.ts

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