k-l-lambda's picture
feat: room rename and room switching with confirmation
6f4808d
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;
}
}