"use client"; import { useEffect, useRef, useState } from "react"; import io from "socket.io-client"; import { ENV } from "../core/env"; import { Modules, rehydrate } from "../core"; import { NotificationInterface } from "../features/notification/data"; type Socket = ReturnType; interface UseSocketOptions { token: string; } interface SocketLike { emit: (event: string, ...args: any[]) => void; on: (event: string, callback: (...args: any[]) => void) => void; off: (event: string, callback?: (...args: any[]) => void) => void; connected: boolean; } interface UseSocketReturn { socket: SocketLike | null; isConnected: boolean; messages: any[]; socketNotifications: NotificationInterface[]; sendMessage: (event: string, data: any) => void; removeMessage: (index: number) => void; removeSocketNotification: (index: number) => void; clearMessages: () => void; clearSocketNotifications: () => void; } export function useSocket({ token }: UseSocketOptions): UseSocketReturn { const _errorCount = useRef(0); const shouldConnect = useRef(true); const _hookInstanceId = useRef(Math.random().toString(36).substring(2, 11)); const [socket, setSocket] = useState(null); const [isConnected, setIsConnected] = useState(false); const [messages, setMessages] = useState([]); const [socketNotifications, setSocketNotifications] = useState([]); const socketRef = useRef(null); useEffect(() => { if (!token) return; const globalSocketKey = `__socket_${ENV.API_URL.replace(/[^a-zA-Z0-9]/g, "_")}`; if (typeof window !== "undefined") { const _allSocketKeys = Object.keys(window).filter((key) => key.startsWith("__socket_")); const existingSocket = (window as any)[globalSocketKey]; if (existingSocket) { if (existingSocket.connected) { // CRITICAL: Clear existing listeners to prevent accumulation existingSocket.removeAllListeners(); socketRef.current = existingSocket; setSocket(existingSocket); setIsConnected(existingSocket.connected); } else { existingSocket.disconnect(); delete (window as any)[globalSocketKey]; } } } let currentSocket: any; const isReusing = socketRef.current && socketRef.current.connected; if (isReusing) { currentSocket = socketRef.current; } else { // React StrictMode protection: Prevent double-mounting issues if (!shouldConnect.current) { return; } shouldConnect.current = false; try { currentSocket = io(ENV.API_URL, { auth: { token }, transports: ["websocket"], timeout: 20000, }); if (typeof window !== "undefined") { if (currentSocket.id && currentSocket.id !== "undefined") { (window as any)[globalSocketKey] = currentSocket; } } socketRef.current = currentSocket; setSocket(currentSocket); } catch { return () => {}; // Return empty cleanup function on error } } const handleConnect = () => { setIsConnected(true); }; const handleDisconnect = () => { setIsConnected(false); }; const handleMessage = (data: any) => { setMessages((prevMessages) => { const newMessages = [...prevMessages, data]; return newMessages; }); }; const handleNotification = (data: any) => { // The socket sends a JSON:API document { data, included }; rehydrate expects // { jsonApi, included }. Map it (and guard against an unexpected shape so a // malformed payload never throws an uncaught error that breaks the socket). const resource = data?.data ?? data?.jsonApi; if (!resource?.type) { console.warn("[useSocket] ignoring notification with unexpected payload shape", data); return; } const notification = rehydrate(Modules.Notification, { jsonApi: resource, included: data?.included || [], }) as NotificationInterface; if (notification) { setSocketNotifications((prev) => { const newNotifications = [...prev, notification]; return newNotifications; }); } }; // Attach event listeners if (currentSocket) { currentSocket.on("connect", handleConnect); currentSocket.on("disconnect", handleDisconnect); currentSocket.on("message", handleMessage); currentSocket.on("notification", handleNotification); } return () => { shouldConnect.current = true; // In development, preserve socket in window for HMR but remove listeners if (currentSocket) { if (ENV.IS_DEVELOPMENT) { currentSocket.off("connect", handleConnect); currentSocket.off("disconnect", handleDisconnect); currentSocket.off("message", handleMessage); currentSocket.off("notification", handleNotification); } else { currentSocket.off("connect", handleConnect); currentSocket.off("disconnect", handleDisconnect); currentSocket.off("message", handleMessage); currentSocket.off("notification", handleNotification); currentSocket.disconnect(); if (typeof window !== "undefined") { delete (window as any)[globalSocketKey]; } } } }; }, [token]); const sendMessage = (event: string, data: any) => { if (socketRef.current && isConnected) { socketRef.current.emit(event, data); } }; const removeMessage = (index: number) => { setMessages((prevMessages) => { const newMessages = [...prevMessages]; newMessages.splice(index, 1); return newMessages; }); }; const removeSocketNotification = (index: number) => { setSocketNotifications((prevNotifications) => { const newNotifications = [...prevNotifications]; newNotifications.splice(index, 1); return newNotifications; }); }; const clearMessages = () => { setMessages([]); }; const clearSocketNotifications = () => { setSocketNotifications([]); }; return { socket, isConnected, messages, socketNotifications, sendMessage, removeMessage, removeSocketNotification, clearMessages, clearSocketNotifications, }; }