import { ref, onUnmounted } from "vue"; import { io, Socket } from "socket.io-client"; // Singleton socket instance let socketInstance: Socket | null = null; // Singleton reactive refs (shared across all useSocket() calls) const connected = ref(false); const error = ref(null); export function useSocket() { // Get or create socket instance const getSocket = (): Socket => { if (!socketInstance) { // Always use current origin - Vite dev server proxies /socket.io to backend // This ensures socket connections work through tunnels and proxies const serverUrl = window.location.origin; console.log("[Socket.io] Connecting to:", serverUrl); socketInstance = io(serverUrl, { autoConnect: true, reconnection: true, reconnectionDelay: 1000, reconnectionAttempts: 5, // Use polling first for better tunnel/proxy compatibility, then upgrade to websocket transports: ["polling", "websocket"] }); // Connection event handlers socketInstance.on("connect", () => { connected.value = true; error.value = null; console.log("[Socket.io] Connected:", socketInstance?.id); }); socketInstance.on("disconnect", (reason) => { connected.value = false; console.log("[Socket.io] Disconnected:", reason); }); socketInstance.on("connect_error", (err) => { error.value = err.message; console.error("[Socket.io] Connection error:", err.message); }); } else { // Socket already exists, sync the connected state connected.value = socketInstance.connected; } return socketInstance; }; // Send echo test message const sendEcho = (message: string): Promise => { return new Promise((resolve, reject) => { const socket = getSocket(); if (!socket.connected) { reject(new Error("Socket not connected")); return; } let settled = false; // Timeout after 5 seconds const timeoutId = window.setTimeout(() => { if (settled) return; settled = true; reject(new Error("Echo request timeout")); }, 5000); // Send echo request and wait for response socket.emit( "echo", { message, timestamp: new Date().toISOString() }, (response: any) => { if (settled) return; settled = true; clearTimeout(timeoutId); if (response.error) { reject(new Error(response.error)); } else { resolve(response.message); } } ); }); }; // Join a room const joinRoom = ( data: { roomId?: string; nickname: string }, callback: (response: any) => void ): void => { const socket = getSocket(); console.log("[useSocket] joinRoom called:", { roomId: data.roomId, nickname: data.nickname, socketConnected: socket.connected, socketId: socket.id }); socket.emit("joinRoom", data, callback); console.log("[useSocket] joinRoom event emitted"); }; // Leave current room const leaveRoom = (): void => { const socket = getSocket(); socket.emit("leaveRoom"); }; // Change player nickname const changeNickname = (nickname: string, callback?: (response: any) => void): void => { const socket = getSocket(); socket.emit("changeNickname", { nickname }, callback); }; // Game actions // Make a move (place stone) const makeMove = (x: number, y: number, z: number): void => { const socket = getSocket(); socket.emit("makeMove", { x, y, z }); }; // Pass turn const pass = (): void => { const socket = getSocket(); socket.emit("pass"); }; // Resign game const resign = (): void => { const socket = getSocket(); socket.emit("resign"); }; // Undo move const undoMove = (callback?: (response: any) => void): void => { const socket = getSocket(); socket.emit("undoMove", callback); }; // Redo move const redoMove = (callback?: (response: any) => void): void => { const socket = getSocket(); socket.emit("redoMove", callback); }; // Reset game const resetGame = ( options?: { boardShape?: { x: number; y: number; z: number }; playerColors?: { [playerId: string]: "black" | "white" } }, callback?: (response: any) => void ): void => { const socket = getSocket(); socket.emit("resetGame", options, callback); }; // Rename room const renameRoom = (name: string, callback?: (response: any) => void): void => { const socket = getSocket(); socket.emit("renameRoom", { name }, callback); }; // Room list management // List available rooms const listRooms = (callback: (response: any) => void): void => { const socket = getSocket(); socket.emit("listRooms", callback); }; // Room list event listeners const onRoomCreated = (handler: (data: any) => void): void => { const socket = getSocket(); socket.on("roomCreated", handler); }; const onRoomUpdated = (handler: (data: any) => void): void => { const socket = getSocket(); socket.on("roomUpdated", handler); }; const onRoomDeleted = (handler: (data: { roomId: string }) => void): void => { const socket = getSocket(); socket.on("roomDeleted", handler); }; const onRoomRenamed = (handler: (data: { roomId: string; name: string }) => void): void => { const socket = getSocket(); socket.on("roomRenamed", handler); }; // Room list event cleanup // Note: If handler is provided, only that specific handler is removed. // If handler is undefined, all handlers for that event are removed. const offRoomCreated = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("roomCreated", handler); } else { socket.off("roomCreated"); } }; const offRoomUpdated = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("roomUpdated", handler); } else { socket.off("roomUpdated"); } }; const offRoomDeleted = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("roomDeleted", handler); } else { socket.off("roomDeleted"); } }; const offRoomRenamed = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("roomRenamed", handler); } else { socket.off("roomRenamed"); } }; // Event listeners const onPlayerJoined = (handler: (data: { playerId: string; nickname: string }) => void): void => { const socket = getSocket(); socket.on("playerJoined", handler); }; const onPlayerLeft = (handler: (data: { playerId: string }) => void): void => { const socket = getSocket(); socket.on("playerLeft", handler); }; const onNicknameChanged = ( handler: (data: { playerId: string; nickname: string; oldNickname: string }) => void ): void => { const socket = getSocket(); socket.on("nicknameChanged", handler); }; const onRoomJoined = (handler: (data: any) => void): void => { const socket = getSocket(); socket.on("roomJoined", handler); }; const onGameUpdate = (handler: (data: any) => void): void => { const socket = getSocket(); socket.on("gameUpdate", handler); }; const onGameEnded = (handler: (data: any) => void): void => { const socket = getSocket(); socket.on("gameEnded", handler); }; const onGameReset = (handler: (data: any) => void): void => { const socket = getSocket(); socket.on("gameReset", handler); }; const onPlayerDisconnected = (handler: (data: { playerId: string }) => void): void => { const socket = getSocket(); socket.on("playerDisconnected", handler); }; const onError = (handler: (data: { message: string }) => void): void => { const socket = getSocket(); socket.on("error", handler); }; // Remove event listeners // Note: If handler is provided, only that specific handler is removed. // If handler is undefined, all handlers for that event are removed. const offPlayerJoined = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("playerJoined", handler); } else { socket.off("playerJoined"); } }; const offPlayerLeft = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("playerLeft", handler); } else { socket.off("playerLeft"); } }; const offNicknameChanged = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("nicknameChanged", handler); } else { socket.off("nicknameChanged"); } }; const offRoomJoined = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("roomJoined", handler); } else { socket.off("roomJoined"); } }; const offGameUpdate = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("gameUpdate", handler); } else { socket.off("gameUpdate"); } }; const offGameEnded = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("gameEnded", handler); } else { socket.off("gameEnded"); } }; const offGameReset = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("gameReset", handler); } else { socket.off("gameReset"); } }; const offPlayerDisconnected = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("playerDisconnected", handler); } else { socket.off("playerDisconnected"); } }; const offError = (handler?: any): void => { const socket = getSocket(); if (handler) { socket.off("error", handler); } else { socket.off("error"); } }; // Clean up on unmount onUnmounted(() => { // Note: We don't disconnect the singleton instance // It will be reused by other components }); return { socket: getSocket(), connected, error, sendEcho, // Room management joinRoom, leaveRoom, changeNickname, listRooms, // Game actions makeMove, pass, resign, undoMove, redoMove, resetGame, renameRoom, // Event listeners onPlayerJoined, onPlayerLeft, onNicknameChanged, onRoomJoined, onGameUpdate, onGameEnded, onGameReset, onPlayerDisconnected, onError, onRoomCreated, onRoomUpdated, onRoomDeleted, onRoomRenamed, // Event cleanup offPlayerJoined, offPlayerLeft, offNicknameChanged, offRoomJoined, offGameUpdate, offGameEnded, offGameReset, offPlayerDisconnected, offError, offRoomCreated, offRoomUpdated, offRoomDeleted, offRoomRenamed }; } // Export function to manually disconnect (for cleanup) export function disconnectSocket() { if (socketInstance) { // Remove all listeners before disconnecting to prevent memory leaks socketInstance.removeAllListeners(); socketInstance.disconnect(); socketInstance = null; // Reset reactive state connected.value = false; error.value = null; } }