import { Server as SocketIOServer, Socket } from "socket.io"; import type { Server as HTTPServer } from "http"; // Socket event types interface ServerToClientEvents { "test-message": (data: { originalMessage: any; serverResponse: string; serverTimestamp: string; socketId: string; }) => void; "pick-made": (data: any) => void; "draft-started": (data: { seasonId: string; currentPickNumber: number }) => void; "draft-completed": () => void; "timer-update": (data: { seasonId: string; teamId: string; timeRemaining: number; currentPickNumber: number; }) => void; "autodraft-updated": (data: { teamId: string; isEnabled: boolean; mode: "next_pick" | "while_on"; }) => void; "team-connected": (data: { teamId: string }) => void; "team-disconnected": (data: { teamId: string }) => void; "connected-teams-list": (data: { teamIds: string[] }) => void; "draft-paused": (data: { seasonId: string; paused: boolean }) => void; "draft-resumed": (data: { seasonId: string; paused: boolean }) => void; "participant-removed-from-queues": (data: { participantId: string }) => void; } interface ClientToServerEvents { "join-draft": (seasonId: string, teamId?: string) => void; "leave-draft": (seasonId: string) => void; "test-event": (data: any) => void; } // Global type augmentation declare global { var __socketIO: SocketIOServer | undefined; } let io: SocketIOServer | null = null; // Track connected teams per season const connectedTeams = new Map>(); // seasonId -> Set /** * Initialize Socket.IO server */ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { if (io) { console.log("Socket.IO already initialized"); return io; } // Create typed Socket.IO server io = new SocketIOServer(httpServer, { cors: process.env.NODE_ENV === "production" && process.env.APP_URL ? { origin: process.env.APP_URL, credentials: true, } : undefined, }); // Connection handling io.on("connection", (socket: Socket) => { console.log("Client connected:", socket.id); // Store team ID for this socket let currentTeamId: string | undefined; let currentSeasonId: string | undefined; socket.on("join-draft", (seasonId: string, teamId?: string) => { if (!seasonId) { console.error("No seasonId provided for join-draft"); return; } socket.join(`draft-${seasonId}`); currentSeasonId = seasonId; console.log(`Socket ${socket.id} joined draft-${seasonId}`); // If teamId provided, track connection and emit events if (teamId) { currentTeamId = teamId; socket.join(`team-${teamId}`); // Initialize the season's connected teams set if it doesn't exist if (!connectedTeams.has(seasonId)) { connectedTeams.set(seasonId, new Set()); } // Get current connected teams for this season const seasonConnectedTeams = connectedTeams.get(seasonId)!; // Send the list of already-connected teams to this socket socket.emit("connected-teams-list", { teamIds: Array.from(seasonConnectedTeams) }); // Add this team to the connected teams set seasonConnectedTeams.add(teamId); // Broadcast to everyone else that this team connected io!.to(`draft-${seasonId}`).emit("team-connected", { teamId }); console.log(`Team ${teamId} connected to draft-${seasonId}. Total connected: ${seasonConnectedTeams.size}`); } }); socket.on("leave-draft", (seasonId: string) => { if (!seasonId) return; socket.leave(`draft-${seasonId}`); console.log(`Socket ${socket.id} left draft-${seasonId}`); // Emit disconnection event if team was tracked if (currentTeamId) { socket.leave(`team-${currentTeamId}`); // Remove team from connected teams tracking const seasonConnectedTeams = connectedTeams.get(seasonId); if (seasonConnectedTeams) { seasonConnectedTeams.delete(currentTeamId); console.log(`Team ${currentTeamId} removed from tracking. Remaining: ${seasonConnectedTeams.size}`); // Clean up empty sets if (seasonConnectedTeams.size === 0) { connectedTeams.delete(seasonId); } } io!.to(`draft-${seasonId}`).emit("team-disconnected", { teamId: currentTeamId }); console.log(`Team ${currentTeamId} disconnected from draft-${seasonId}`); } }); socket.on("test-event", (data: any) => { console.log("📨 Received test-event from client:", socket.id, data); socket.emit("test-message", { originalMessage: data, serverResponse: "Hello from server!", serverTimestamp: new Date().toISOString(), socketId: socket.id, }); console.log("✅ Sent test-message response to client:", socket.id); }); socket.on("disconnect", () => { console.log("Client disconnected:", socket.id); // Emit disconnection event if team was tracked if (currentTeamId && currentSeasonId) { // Remove team from connected teams tracking const seasonConnectedTeams = connectedTeams.get(currentSeasonId); if (seasonConnectedTeams) { seasonConnectedTeams.delete(currentTeamId); console.log(`Team ${currentTeamId} removed from tracking on disconnect. Remaining: ${seasonConnectedTeams.size}`); // Clean up empty sets if (seasonConnectedTeams.size === 0) { connectedTeams.delete(currentSeasonId); } } io!.to(`draft-${currentSeasonId}`).emit("team-disconnected", { teamId: currentTeamId }); console.log(`Team ${currentTeamId} disconnected from draft-${currentSeasonId}`); } }); }); // Store globally for route handlers global.__socketIO = io; console.log("Socket.IO initialized"); // Start the draft timer system (async import but don't await) import("./timer").then(({ startDraftTimerSystem }) => { startDraftTimerSystem(); }).catch((error) => { console.error("Failed to start timer system:", error); }); // Start the daily snapshot system import("./snapshots").then(({ startSnapshotSystem }) => { startSnapshotSystem(); }).catch((error) => { console.error("Failed to start snapshot system:", error); }); return io; } /** * Get the Socket.IO server instance */ export function getSocketIO(): SocketIOServer { const instance = io || global.__socketIO; if (!instance) { throw new Error("Socket.IO not initialized. Call initializeSocketIO first."); } return instance; }