092_user_interface / src /services /socket.service.js
anotherath's picture
update space and room
57f5158
import { io } from "socket.io-client";
const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || "http://localhost:3000";
class SocketService {
constructor() {
this.socket = null;
this.listeners = new Map();
this._connected = false;
this._activeDMRooms = new Set();
this._activeRooms = new Set();
this._joinedRooms = new Set(); // Rooms that have received 'joinedRoom' ack
}
// ==================== Connection ====================
connect() {
if (this.socket?.connected) return;
const token = localStorage.getItem("access_token");
if (!token) {
return;
}
this.socket = io(`${SOCKET_URL}/chat`, {
// Use auth callback so socket.io reads fresh token on every connect/reconnect
auth: (cb) => {
cb({ token: localStorage.getItem("access_token") });
},
transports: ["websocket", "polling"],
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
});
this.socket.on("connect", () => {
console.log("[Socket] Connected:", this.socket.id);
this._connected = true;
});
this.socket.on("disconnect", (reason) => {
console.log("[Socket] Disconnected:", reason);
this._connected = false;
});
this.socket.on("connect_error", (error) => {
console.error("[Socket] Connect error:", error.message);
// If auth failed due to expired token, try to trigger a token refresh
// by making a dummy API call. The axios interceptor will handle 401.
if (error.message?.includes("jwt expired") || error.message?.includes("auth")) {
this._handleAuthError();
}
});
this.socket.on("reconnect", async (attemptNumber) => {
console.log("[Socket] Reconnected after", attemptNumber, "attempts");
// Re-join all active rooms with fresh token
this._activeDMRooms.forEach((conversationId) => {
this.joinDM(conversationId);
});
// Await room joins to ensure server is ready before sending
const roomJoinPromises = Array.from(this._activeRooms).map((roomId) =>
this.joinRoom(roomId).catch((err) => {
console.warn("[Socket] Failed to rejoin room:", roomId, err);
}),
);
await Promise.all(roomJoinPromises);
});
this.socket.on("reconnect_error", (error) => {
console.error("[Socket] Reconnect error:", error.message);
});
this.socket.on("error", (error) => {
console.error("[Socket] Error:", error);
});
this.socket.on("connected", (data) => {
console.log("[Socket] Server ack:", data);
});
}
// Trigger a token refresh by making a lightweight API call.
// The axios response interceptor will handle 401 and refresh the token.
_handleAuthError() {
console.log("[Socket] Token may be expired, triggering refresh via API...");
// Use a lightweight endpoint to trigger the refresh flow
// The access token will be updated in localStorage by the interceptor
import("./api").then(({ default: api }) => {
api.get("/users/me").catch(() => {
// Expected to fail or succeed; either way, token may have been refreshed
});
});
}
// Reconnect with fresh token. Call this after token refresh succeeds.
reconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
this.connect();
}
disconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
this._connected = false;
this._activeDMRooms.clear();
this._activeRooms.clear();
}
isConnected() {
return this.socket?.connected || false;
}
getId() {
return this.socket?.id || null;
}
// ==================== DM Room Events ====================
joinDM(conversationId) {
if (!conversationId) return;
this._activeDMRooms.add(conversationId);
this.socket?.emit("joinDM", { conversationId });
}
leaveDM(conversationId) {
if (!conversationId) return;
this._activeDMRooms.delete(conversationId);
this.socket?.emit("leaveDM", { conversationId });
}
sendDM(conversationId, content, tempId) {
if (!conversationId) return;
const clientSentAt = Date.now();
const payload = { conversationId, content, clientSentAt };
if (tempId) payload.tempId = tempId;
this.socket?.emit("sendDM", payload);
}
dmTyping(conversationId, isTyping) {
this.socket?.emit("dmTyping", { conversationId, isTyping });
}
markDMRead(conversationId) {
this.socket?.emit("markDMRead", { conversationId });
}
// ==================== Status Events ====================
setStatus(status) {
this.socket?.emit("setStatus", { status });
}
getOnlineUsers() {
this.socket?.emit("getOnlineUsers");
}
// ==================== Notification Events ====================
markNotificationRead(notificationId) {
this.socket?.emit("markNotificationRead", { notificationId });
}
getUnreadCount() {
this.socket?.emit("getUnreadCount");
}
// ==================== Room/Space Events (legacy) ====================
joinRoom(roomId) {
if (!roomId) return Promise.resolve();
this._activeRooms.add(roomId);
return new Promise((resolve) => {
// Fast timeout — don't block UI if server is slow to ack
const timeout = setTimeout(() => {
this.socket?.off("joinedRoom", onJoined);
this._joinedRooms.add(roomId); // Allow sending anyway
console.warn("[Socket] joinRoom timeout, allowing sends for:", roomId);
resolve({ roomId, timeout: true });
}, 1500);
const onJoined = (data) => {
if (data?.roomId === roomId) {
clearTimeout(timeout);
this.socket?.off("joinedRoom", onJoined);
this._joinedRooms.add(roomId);
console.log("[Socket] Joined room:", roomId);
resolve(data);
}
};
this.socket?.on("joinedRoom", onJoined);
this.socket?.emit("joinRoom", { roomId });
});
}
leaveRoom(roomId) {
if (!roomId) return;
this._activeRooms.delete(roomId);
this._joinedRooms.delete(roomId);
this.socket?.emit("leaveRoom", { roomId });
}
sendMessage(data) {
const { roomId } = data;
// Auto-join if not yet acked but is active room (don't block send)
if (roomId && !this._joinedRooms.has(roomId)) {
if (this._activeRooms.has(roomId)) {
console.warn("[Socket] Room not yet acked, allowing send anyway:", roomId);
} else {
console.warn("[Socket] Cannot send message - not in active rooms:", roomId);
return;
}
}
this.socket?.emit("sendMessage", data);
}
updateStatus(status) {
this.socket?.emit("updateStatus", status);
}
emitTyping(roomId) {
if (roomId && !this._activeRooms.has(roomId)) {
console.warn("[Socket] Cannot emit typing - not active room:", roomId);
return;
}
this.socket?.emit("typing", { roomId });
}
emitStopTyping(roomId) {
if (roomId && !this._activeRooms.has(roomId)) {
console.warn("[Socket] Cannot emit stopTyping - not active room:", roomId);
return;
}
this.socket?.emit("stopTyping", { roomId });
}
// ==================== Listener Management ====================
on(event, callback) {
this.socket?.on(event, callback);
}
off(event, callback) {
this.socket?.off(event, callback);
}
offEvent(event) {
this.socket?.off(event);
}
removeAllListeners() {
this.socket?.removeAllListeners();
}
// ==================== DM-specific Listeners ====================
onJoinedDM(callback) {
this.socket?.on("joinedDM", callback);
}
onLeftDM(callback) {
this.socket?.on("leftDM", callback);
}
onNewDM(callback) {
this.socket?.on("newDM", callback);
}
onDmSent(callback) {
this.socket?.on("dmSent", callback);
}
onDmTyping(callback) {
this.socket?.on("dmTyping", callback);
}
onDmRead(callback) {
this.socket?.on("dmRead", callback);
}
onDmMarkedRead(callback) {
this.socket?.on("dmMarkedRead", callback);
}
// ==================== User Status Listeners ====================
onUserStatusChanged(callback) {
this.socket?.on("userStatusChanged", callback);
}
onStatusSet(callback) {
this.socket?.on("statusSet", callback);
}
onOnlineUsers(callback) {
this.socket?.on("onlineUsers", callback);
}
onConnected(callback) {
this.socket?.on("connected", callback);
}
// ==================== Notification Listeners ====================
onNewNotification(callback) {
this.socket?.on("newNotification", callback);
}
onNotificationsMarkedRead(callback) {
this.socket?.on("notificationsMarkedRead", callback);
}
onUnreadCountUpdate(callback) {
this.socket?.on("unreadCount", callback);
}
// ==================== Legacy Listeners ====================
onNewMessage(callback) {
this.socket?.on("newMessage", callback);
}
onMessageSent(callback) {
this.socket?.on("messageSent", callback);
}
onMessageDeleted(callback) {
this.socket?.on("messageDeleted", callback);
}
onMessageUpdated(callback) {
this.socket?.on("messageUpdated", callback);
}
onMessagePinned(callback) {
this.socket?.on("messagePinned", callback);
}
onMessageUnpinned(callback) {
this.socket?.on("messageUnpinned", callback);
}
onReactionAdded(callback) {
this.socket?.on("reactionAdded", callback);
}
onReactionRemoved(callback) {
this.socket?.on("reactionRemoved", callback);
}
onTyping(callback) {
this.socket?.on("typing", callback);
}
onStopTyping(callback) {
this.socket?.on("stopTyping", callback);
}
onUserJoined(callback) {
this.socket?.on("userJoined", callback);
}
onUserLeft(callback) {
this.socket?.on("userLeft", callback);
}
onMemberJoinedSpace(callback) {
this.socket?.on("memberJoinedSpace", callback);
}
onMemberLeftSpace(callback) {
this.socket?.on("memberLeftSpace", callback);
}
onRoomCreated(callback) {
this.socket?.on("roomCreated", callback);
}
onRoomUpdated(callback) {
this.socket?.on("roomUpdated", callback);
}
onRoomDeleted(callback) {
this.socket?.on("roomDeleted", callback);
}
onUserProfileUpdated(callback) {
this.socket?.on("userProfileUpdated", callback);
}
onNotification(callback) {
this.socket?.on("notification", callback);
}
onFileUploadProgress(callback) {
this.socket?.on("fileUploadProgress", callback);
}
onFileUploadComplete(callback) {
this.socket?.on("fileUploadComplete", callback);
}
onFileUploadError(callback) {
this.socket?.on("fileUploadError", callback);
}
}
export default new SocketService();