**Problem:** - player-ownership.ts imported drizzle-orm and @/db at top level - When RoomMemoryPairsProvider imported client-safe utilities, Webpack bundled ALL imports including database code - This caused hydration error: "The 'original' argument must be of type Function" - Node.js util.promisify was being called in browser context **Solution:** 1. Created player-ownership.client.ts with ONLY client-safe utilities - No database imports - Safe to import from 'use client' components - Contains: buildPlayerOwnershipFromRoomData(), buildPlayerMetadata(), helper functions 2. Updated player-ownership.ts to re-export client utilities and add server-only functions - Re-exports everything from .client.ts - Adds buildPlayerOwnershipMap() (async, database-backed) - Safe to import from server components/API routes 3. Updated RoomMemoryPairsProvider to import from .client.ts **Result:** - No more hydration errors on /arcade/room - Client bundle doesn't include database code - Server code can still use both client and server utilities 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
344 lines
13 KiB
JavaScript
344 lines
13 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.getSocketIO = getSocketIO;
|
|
exports.initializeSocketServer = initializeSocketServer;
|
|
const socket_io_1 = require("socket.io");
|
|
const session_manager_1 = require("./src/lib/arcade/session-manager");
|
|
const room_manager_1 = require("./src/lib/arcade/room-manager");
|
|
const room_membership_1 = require("./src/lib/arcade/room-membership");
|
|
const player_manager_1 = require("./src/lib/arcade/player-manager");
|
|
const MatchingGameValidator_1 = require("./src/lib/arcade/validation/MatchingGameValidator");
|
|
/**
|
|
* Get the socket.io server instance
|
|
* Returns null if not initialized
|
|
*/
|
|
function getSocketIO() {
|
|
return globalThis.__socketIO || null;
|
|
}
|
|
function initializeSocketServer(httpServer) {
|
|
const io = new socket_io_1.Server(httpServer, {
|
|
path: "/api/socket",
|
|
cors: {
|
|
origin: process.env.NEXT_PUBLIC_URL || "http://localhost:3000",
|
|
credentials: true,
|
|
},
|
|
});
|
|
io.on("connection", (socket) => {
|
|
console.log("🔌 Client connected:", socket.id);
|
|
let currentUserId = null;
|
|
// Join arcade session room
|
|
socket.on("join-arcade-session", async ({ userId, roomId }) => {
|
|
currentUserId = userId;
|
|
socket.join(`arcade:${userId}`);
|
|
console.log(`👤 User ${userId} joined arcade room`);
|
|
// If this session is part of a room, also join the game room for multi-user sync
|
|
if (roomId) {
|
|
socket.join(`game:${roomId}`);
|
|
console.log(`🎮 User ${userId} joined game room ${roomId}`);
|
|
}
|
|
// Send current session state if exists
|
|
// For room-based games, look up shared room session
|
|
try {
|
|
const session = roomId
|
|
? await (0, session_manager_1.getArcadeSessionByRoom)(roomId)
|
|
: await (0, session_manager_1.getArcadeSession)(userId);
|
|
if (session) {
|
|
console.log("[join-arcade-session] Found session:", {
|
|
userId,
|
|
roomId,
|
|
version: session.version,
|
|
sessionUserId: session.userId,
|
|
});
|
|
socket.emit("session-state", {
|
|
gameState: session.gameState,
|
|
currentGame: session.currentGame,
|
|
gameUrl: session.gameUrl,
|
|
activePlayers: session.activePlayers,
|
|
version: session.version,
|
|
});
|
|
} else {
|
|
console.log("[join-arcade-session] No active session found for:", {
|
|
userId,
|
|
roomId,
|
|
});
|
|
socket.emit("no-active-session");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching session:", error);
|
|
socket.emit("session-error", { error: "Failed to fetch session" });
|
|
}
|
|
});
|
|
// Handle game moves
|
|
socket.on("game-move", async (data) => {
|
|
console.log("🎮 Game move received:", {
|
|
userId: data.userId,
|
|
moveType: data.move.type,
|
|
playerId: data.move.playerId,
|
|
timestamp: data.move.timestamp,
|
|
roomId: data.roomId,
|
|
fullMove: JSON.stringify(data.move, null, 2),
|
|
});
|
|
try {
|
|
// Special handling for START_GAME - create session if it doesn't exist
|
|
if (data.move.type === "START_GAME") {
|
|
// For room-based games, check if room session exists
|
|
const existingSession = data.roomId
|
|
? await (0, session_manager_1.getArcadeSessionByRoom)(data.roomId)
|
|
: await (0, session_manager_1.getArcadeSession)(data.userId);
|
|
if (!existingSession) {
|
|
console.log("🎯 Creating new session for START_GAME");
|
|
// activePlayers must be provided in the START_GAME move data
|
|
const activePlayers = data.move.data?.activePlayers;
|
|
if (!activePlayers || activePlayers.length === 0) {
|
|
console.error("❌ START_GAME move missing activePlayers");
|
|
socket.emit("move-rejected", {
|
|
error: "START_GAME requires at least one active player",
|
|
move: data.move,
|
|
});
|
|
return;
|
|
}
|
|
// Get initial state from validator
|
|
const initialState =
|
|
MatchingGameValidator_1.matchingGameValidator.getInitialState({
|
|
difficulty: 6,
|
|
gameType: "abacus-numeral",
|
|
turnTimer: 30,
|
|
});
|
|
// Check if user is already in a room for this game
|
|
const userRoomIds = await (0, room_membership_1.getUserRooms)(
|
|
data.userId,
|
|
);
|
|
let room = null;
|
|
// Look for an existing active room for this game
|
|
for (const roomId of userRoomIds) {
|
|
const existingRoom = await (0, room_manager_1.getRoomById)(
|
|
roomId,
|
|
);
|
|
if (
|
|
existingRoom &&
|
|
existingRoom.gameName === "matching" &&
|
|
existingRoom.status !== "finished"
|
|
) {
|
|
room = existingRoom;
|
|
console.log("🏠 Using existing room:", room.code);
|
|
break;
|
|
}
|
|
}
|
|
// If no suitable room exists, create a new one
|
|
if (!room) {
|
|
room = await (0, room_manager_1.createRoom)({
|
|
name: "Auto-generated Room",
|
|
createdBy: data.userId,
|
|
creatorName: "Player",
|
|
gameName: "matching",
|
|
gameConfig: {
|
|
difficulty: 6,
|
|
gameType: "abacus-numeral",
|
|
turnTimer: 30,
|
|
},
|
|
ttlMinutes: 60,
|
|
});
|
|
console.log("🏠 Created new room:", room.code);
|
|
}
|
|
// Now create the session linked to the room
|
|
await (0, session_manager_1.createArcadeSession)({
|
|
userId: data.userId,
|
|
gameName: "matching",
|
|
gameUrl: "/arcade/room", // Room-based sessions use /arcade/room
|
|
initialState,
|
|
activePlayers,
|
|
roomId: room.id,
|
|
});
|
|
console.log(
|
|
"✅ Session created successfully with room association",
|
|
);
|
|
// Notify all connected clients about the new session
|
|
const newSession = await (0, session_manager_1.getArcadeSession)(
|
|
data.userId,
|
|
);
|
|
if (newSession) {
|
|
io.to(`arcade:${data.userId}`).emit("session-state", {
|
|
gameState: newSession.gameState,
|
|
currentGame: newSession.currentGame,
|
|
gameUrl: newSession.gameUrl,
|
|
activePlayers: newSession.activePlayers,
|
|
version: newSession.version,
|
|
});
|
|
console.log(
|
|
"📢 Emitted session-state to notify clients of new session",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// Apply game move - use roomId for room-based games to access shared session
|
|
const result = await (0, session_manager_1.applyGameMove)(
|
|
data.userId,
|
|
data.move,
|
|
data.roomId,
|
|
);
|
|
if (result.success && result.session) {
|
|
const moveAcceptedData = {
|
|
gameState: result.session.gameState,
|
|
version: result.session.version,
|
|
move: data.move,
|
|
};
|
|
// Broadcast the updated state to all devices for this user
|
|
io.to(`arcade:${data.userId}`).emit(
|
|
"move-accepted",
|
|
moveAcceptedData,
|
|
);
|
|
// If this is a room-based session, ALSO broadcast to all users in the room
|
|
if (result.session.roomId) {
|
|
io.to(`game:${result.session.roomId}`).emit(
|
|
"move-accepted",
|
|
moveAcceptedData,
|
|
);
|
|
console.log(
|
|
`📢 Broadcasted move to game room ${result.session.roomId}`,
|
|
);
|
|
}
|
|
// Update activity timestamp
|
|
await (0, session_manager_1.updateSessionActivity)(data.userId);
|
|
} else {
|
|
// Send rejection only to the requesting socket
|
|
socket.emit("move-rejected", {
|
|
error: result.error,
|
|
move: data.move,
|
|
versionConflict: result.versionConflict,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Error processing move:", error);
|
|
socket.emit("move-rejected", {
|
|
error: "Server error processing move",
|
|
move: data.move,
|
|
});
|
|
}
|
|
});
|
|
// Handle session exit
|
|
socket.on("exit-arcade-session", async ({ userId }) => {
|
|
console.log("🚪 User exiting arcade session:", userId);
|
|
try {
|
|
await (0, session_manager_1.deleteArcadeSession)(userId);
|
|
io.to(`arcade:${userId}`).emit("session-ended");
|
|
} catch (error) {
|
|
console.error("Error ending session:", error);
|
|
socket.emit("session-error", { error: "Failed to end session" });
|
|
}
|
|
});
|
|
// Keep-alive ping
|
|
socket.on("ping-session", async ({ userId }) => {
|
|
try {
|
|
await (0, session_manager_1.updateSessionActivity)(userId);
|
|
socket.emit("pong-session");
|
|
} catch (error) {
|
|
console.error("Error updating activity:", error);
|
|
}
|
|
});
|
|
// Room: Join
|
|
socket.on("join-room", async ({ roomId, userId }) => {
|
|
console.log(`🏠 User ${userId} joining room ${roomId}`);
|
|
try {
|
|
// Join the socket room
|
|
socket.join(`room:${roomId}`);
|
|
// Mark member as online
|
|
await (0, room_membership_1.setMemberOnline)(roomId, userId, true);
|
|
// Get room data
|
|
const members = await (0, room_membership_1.getRoomMembers)(roomId);
|
|
const memberPlayers = await (0, player_manager_1.getRoomActivePlayers)(
|
|
roomId,
|
|
);
|
|
// Convert memberPlayers Map to object for JSON serialization
|
|
const memberPlayersObj = {};
|
|
for (const [uid, players] of memberPlayers.entries()) {
|
|
memberPlayersObj[uid] = players;
|
|
}
|
|
// Send current room state to the joining user
|
|
socket.emit("room-joined", {
|
|
roomId,
|
|
members,
|
|
memberPlayers: memberPlayersObj,
|
|
});
|
|
// Notify all other members in the room
|
|
socket.to(`room:${roomId}`).emit("member-joined", {
|
|
roomId,
|
|
userId,
|
|
members,
|
|
memberPlayers: memberPlayersObj,
|
|
});
|
|
console.log(`✅ User ${userId} joined room ${roomId}`);
|
|
} catch (error) {
|
|
console.error("Error joining room:", error);
|
|
socket.emit("room-error", { error: "Failed to join room" });
|
|
}
|
|
});
|
|
// Room: Leave
|
|
socket.on("leave-room", async ({ roomId, userId }) => {
|
|
console.log(`🚪 User ${userId} leaving room ${roomId}`);
|
|
try {
|
|
// Leave the socket room
|
|
socket.leave(`room:${roomId}`);
|
|
// Mark member as offline
|
|
await (0, room_membership_1.setMemberOnline)(roomId, userId, false);
|
|
// Get updated members
|
|
const members = await (0, room_membership_1.getRoomMembers)(roomId);
|
|
const memberPlayers = await (0, player_manager_1.getRoomActivePlayers)(
|
|
roomId,
|
|
);
|
|
// Convert memberPlayers Map to object
|
|
const memberPlayersObj = {};
|
|
for (const [uid, players] of memberPlayers.entries()) {
|
|
memberPlayersObj[uid] = players;
|
|
}
|
|
// Notify remaining members
|
|
io.to(`room:${roomId}`).emit("member-left", {
|
|
roomId,
|
|
userId,
|
|
members,
|
|
memberPlayers: memberPlayersObj,
|
|
});
|
|
console.log(`✅ User ${userId} left room ${roomId}`);
|
|
} catch (error) {
|
|
console.error("Error leaving room:", error);
|
|
}
|
|
});
|
|
// Room: Players updated
|
|
socket.on("players-updated", async ({ roomId, userId }) => {
|
|
console.log(`🎯 Players updated for user ${userId} in room ${roomId}`);
|
|
try {
|
|
// Get updated player data
|
|
const memberPlayers = await (0, player_manager_1.getRoomActivePlayers)(
|
|
roomId,
|
|
);
|
|
// Convert memberPlayers Map to object
|
|
const memberPlayersObj = {};
|
|
for (const [uid, players] of memberPlayers.entries()) {
|
|
memberPlayersObj[uid] = players;
|
|
}
|
|
// Broadcast to all members in the room (including sender)
|
|
io.to(`room:${roomId}`).emit("room-players-updated", {
|
|
roomId,
|
|
memberPlayers: memberPlayersObj,
|
|
});
|
|
console.log(`✅ Broadcasted player updates for room ${roomId}`);
|
|
} catch (error) {
|
|
console.error("Error updating room players:", error);
|
|
socket.emit("room-error", { error: "Failed to update players" });
|
|
}
|
|
});
|
|
socket.on("disconnect", () => {
|
|
console.log("🔌 Client disconnected:", socket.id);
|
|
if (currentUserId) {
|
|
// Don't delete session on disconnect - it persists across devices
|
|
console.log(
|
|
`👤 User ${currentUserId} disconnected but session persists`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
// Store in globalThis to make accessible across module boundaries
|
|
globalThis.__socketIO = io;
|
|
console.log("✅ Socket.IO initialized on /api/socket");
|
|
return io;
|
|
}
|