Spaces:
Running
Running
| 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<string | null>(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<string> => { | |
| 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; | |
| } | |
| } | |