2025-10-18 22:16:04 -07:00
|
|
|
import { Server as SocketIOServer, Socket } from "socket.io";
|
|
|
|
|
import type { Server as HTTPServer } from "http";
|
2025-10-16 18:15:04 -07:00
|
|
|
|
2025-10-18 22:16:04 -07:00
|
|
|
// 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;
|
2025-10-21 23:22:17 -07:00
|
|
|
"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;
|
2025-10-24 21:31:57 -07:00
|
|
|
"connected-teams-list": (data: { teamIds: string[] }) => void;
|
2025-10-21 23:22:17 -07:00
|
|
|
"draft-paused": () => void;
|
|
|
|
|
"draft-resumed": () => void;
|
2025-10-24 21:46:55 -07:00
|
|
|
"participant-removed-from-queues": (data: { participantId: string }) => void;
|
2025-10-18 22:16:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ClientToServerEvents {
|
2025-10-21 23:22:17 -07:00
|
|
|
"join-draft": (seasonId: string, teamId?: string) => void;
|
2025-10-18 22:16:04 -07:00
|
|
|
"leave-draft": (seasonId: string) => void;
|
|
|
|
|
"test-event": (data: any) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Global type augmentation
|
|
|
|
|
declare global {
|
|
|
|
|
var __socketIO: SocketIOServer | undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let io: SocketIOServer<ClientToServerEvents, ServerToClientEvents> | null = null;
|
2025-10-16 18:15:04 -07:00
|
|
|
|
2025-10-24 21:31:57 -07:00
|
|
|
// Track connected teams per season
|
|
|
|
|
const connectedTeams = new Map<string, Set<string>>(); // seasonId -> Set<teamId>
|
|
|
|
|
|
2025-10-16 18:15:04 -07:00
|
|
|
/**
|
|
|
|
|
* Initialize Socket.IO server
|
|
|
|
|
*/
|
2025-10-18 22:16:04 -07:00
|
|
|
export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
2025-10-16 18:15:04 -07:00
|
|
|
if (io) {
|
|
|
|
|
console.log("Socket.IO already initialized");
|
|
|
|
|
return io;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-18 22:16:04 -07:00
|
|
|
// Create typed Socket.IO server
|
|
|
|
|
io = new SocketIOServer<ClientToServerEvents, ServerToClientEvents>(httpServer, {
|
2025-10-16 18:15:04 -07:00
|
|
|
cors: process.env.NODE_ENV === "production" && process.env.APP_URL
|
|
|
|
|
? {
|
|
|
|
|
origin: process.env.APP_URL,
|
|
|
|
|
credentials: true,
|
|
|
|
|
}
|
2025-10-18 22:16:04 -07:00
|
|
|
: undefined,
|
2025-10-16 18:15:04 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Connection handling
|
2025-10-18 22:16:04 -07:00
|
|
|
io.on("connection", (socket: Socket<ClientToServerEvents, ServerToClientEvents>) => {
|
2025-10-16 18:15:04 -07:00
|
|
|
console.log("Client connected:", socket.id);
|
|
|
|
|
|
2025-10-21 23:22:17 -07:00
|
|
|
// Store team ID for this socket
|
|
|
|
|
let currentTeamId: string | undefined;
|
|
|
|
|
let currentSeasonId: string | undefined;
|
|
|
|
|
|
|
|
|
|
socket.on("join-draft", (seasonId: string, teamId?: string) => {
|
2025-10-16 18:15:04 -07:00
|
|
|
if (!seasonId) {
|
|
|
|
|
console.error("No seasonId provided for join-draft");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
socket.join(`draft-${seasonId}`);
|
2025-10-21 23:22:17 -07:00
|
|
|
currentSeasonId = seasonId;
|
2025-10-16 18:15:04 -07:00
|
|
|
console.log(`Socket ${socket.id} joined draft-${seasonId}`);
|
2025-10-21 23:22:17 -07:00
|
|
|
|
2025-10-24 21:31:57 -07:00
|
|
|
// If teamId provided, track connection and emit events
|
2025-10-21 23:22:17 -07:00
|
|
|
if (teamId) {
|
|
|
|
|
currentTeamId = teamId;
|
|
|
|
|
socket.join(`team-${teamId}`);
|
2025-10-24 21:31:57 -07:00
|
|
|
|
|
|
|
|
// 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
|
2025-10-21 23:22:17 -07:00
|
|
|
io!.to(`draft-${seasonId}`).emit("team-connected", { teamId });
|
2025-10-24 21:31:57 -07:00
|
|
|
console.log(`Team ${teamId} connected to draft-${seasonId}. Total connected: ${seasonConnectedTeams.size}`);
|
2025-10-21 23:22:17 -07:00
|
|
|
}
|
2025-10-16 18:15:04 -07:00
|
|
|
});
|
|
|
|
|
|
2025-10-18 22:16:04 -07:00
|
|
|
socket.on("leave-draft", (seasonId: string) => {
|
2025-10-16 18:15:04 -07:00
|
|
|
if (!seasonId) return;
|
|
|
|
|
socket.leave(`draft-${seasonId}`);
|
|
|
|
|
console.log(`Socket ${socket.id} left draft-${seasonId}`);
|
2025-10-21 23:22:17 -07:00
|
|
|
|
|
|
|
|
// Emit disconnection event if team was tracked
|
|
|
|
|
if (currentTeamId) {
|
|
|
|
|
socket.leave(`team-${currentTeamId}`);
|
2025-10-24 21:31:57 -07:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 23:22:17 -07:00
|
|
|
io!.to(`draft-${seasonId}`).emit("team-disconnected", { teamId: currentTeamId });
|
|
|
|
|
console.log(`Team ${currentTeamId} disconnected from draft-${seasonId}`);
|
|
|
|
|
}
|
2025-10-16 18:15:04 -07:00
|
|
|
});
|
|
|
|
|
|
2025-10-18 22:16:04 -07:00
|
|
|
socket.on("test-event", (data: any) => {
|
2025-10-17 12:15:07 -07:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-16 18:15:04 -07:00
|
|
|
socket.on("disconnect", () => {
|
|
|
|
|
console.log("Client disconnected:", socket.id);
|
2025-10-21 23:22:17 -07:00
|
|
|
|
|
|
|
|
// Emit disconnection event if team was tracked
|
|
|
|
|
if (currentTeamId && currentSeasonId) {
|
2025-10-24 21:31:57 -07:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 23:22:17 -07:00
|
|
|
io!.to(`draft-${currentSeasonId}`).emit("team-disconnected", { teamId: currentTeamId });
|
|
|
|
|
console.log(`Team ${currentTeamId} disconnected from draft-${currentSeasonId}`);
|
|
|
|
|
}
|
2025-10-16 18:15:04 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-18 22:16:04 -07:00
|
|
|
// Store globally for route handlers
|
2025-10-16 18:15:04 -07:00
|
|
|
global.__socketIO = io;
|
|
|
|
|
|
|
|
|
|
console.log("Socket.IO initialized");
|
2025-11-14 09:15:58 -08:00
|
|
|
|
2025-10-18 23:13:04 -07:00
|
|
|
// 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);
|
|
|
|
|
});
|
2025-11-14 09:15:58 -08:00
|
|
|
|
|
|
|
|
// Start the daily snapshot system
|
|
|
|
|
import("./snapshots").then(({ startSnapshotSystem }) => {
|
|
|
|
|
startSnapshotSystem();
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
console.error("Failed to start snapshot system:", error);
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-16 18:15:04 -07:00
|
|
|
return io;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the Socket.IO server instance
|
|
|
|
|
*/
|
2025-10-18 22:16:04 -07:00
|
|
|
export function getSocketIO(): SocketIOServer {
|
2025-10-16 18:15:04 -07:00
|
|
|
const instance = io || global.__socketIO;
|
|
|
|
|
if (!instance) {
|
|
|
|
|
throw new Error("Socket.IO not initialized. Call initializeSocketIO first.");
|
|
|
|
|
}
|
|
|
|
|
return instance;
|
|
|
|
|
}
|