Add draft room closure after completion (#359)

* Add draft room closure: redirect to draft board 5 min after completion

* Fix lint: use nowForPauseCheck instead of Date.now() in room closure countdown

Fixes #253
This commit is contained in:
Chris Parsons 2026-04-30 10:14:14 -07:00 committed by GitHub
parent ef0ddeff39
commit 437ac2ce24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 5348 additions and 16 deletions

View file

@ -135,6 +135,7 @@ export function useDraftRoomState({
const [isOvernightPause, setIsOvernightPause] = useState(false); const [isOvernightPause, setIsOvernightPause] = useState(false);
const [overnightResumesAt, setOvernightResumesAt] = useState<Date | null>(null); const [overnightResumesAt, setOvernightResumesAt] = useState<Date | null>(null);
const [roomClosed, setRoomClosed] = useState(false);
return { return {
picks, setPicks, picks, setPicks,
@ -175,5 +176,6 @@ export function useDraftRoomState({
isAdjustingTimeBank, setIsAdjustingTimeBank, isAdjustingTimeBank, setIsAdjustingTimeBank,
isOvernightPause, setIsOvernightPause, isOvernightPause, setIsOvernightPause,
overnightResumesAt, setOvernightResumesAt, overnightResumesAt, setOvernightResumesAt,
roomClosed, setRoomClosed,
}; };
} }

View file

@ -39,6 +39,7 @@ interface UseDraftSocketEventsParams {
setIsOvernightPause: (value: boolean) => void; setIsOvernightPause: (value: boolean) => void;
setOvernightResumesAt: (value: Date | null) => void; setOvernightResumesAt: (value: Date | null) => void;
setWatchedParticipantIds: (value: Set<string>) => void; setWatchedParticipantIds: (value: Set<string>) => void;
setRoomClosed: (value: boolean) => void;
} }
export function useDraftSocketEvents({ export function useDraftSocketEvents({
@ -66,6 +67,7 @@ export function useDraftSocketEvents({
setIsOvernightPause, setIsOvernightPause,
setOvernightResumesAt, setOvernightResumesAt,
setWatchedParticipantIds, setWatchedParticipantIds,
setRoomClosed,
}: UseDraftSocketEventsParams) { }: UseDraftSocketEventsParams) {
useEffect(() => { useEffect(() => {
type PickMadePayload = { type PickMadePayload = {
@ -229,6 +231,10 @@ export function useDraftSocketEvents({
setWatchedParticipantIds(new Set(data.participantIds)); setWatchedParticipantIds(new Set(data.participantIds));
}; };
const handleDraftRoomClosed = () => {
setRoomClosed(true);
};
on("pick-made", handlePickMade as (data: unknown) => void); on("pick-made", handlePickMade as (data: unknown) => void);
on("timer-update", handleTimerUpdate as (data: unknown) => void); on("timer-update", handleTimerUpdate as (data: unknown) => void);
on("draft-paused", handleDraftPaused as (data: unknown) => void); on("draft-paused", handleDraftPaused as (data: unknown) => void);
@ -245,6 +251,7 @@ export function useDraftSocketEvents({
on("draft-rolled-back", handleDraftRolledBack as (data: unknown) => void); on("draft-rolled-back", handleDraftRolledBack as (data: unknown) => void);
on("draft-state-sync", handleDraftStateSync as (data: unknown) => void); on("draft-state-sync", handleDraftStateSync as (data: unknown) => void);
on("watchlist-updated", handleWatchlistUpdated as (data: unknown) => void); on("watchlist-updated", handleWatchlistUpdated as (data: unknown) => void);
on("draft-room-closed", handleDraftRoomClosed as (data: unknown) => void);
return () => { return () => {
off("pick-made", handlePickMade as (data: unknown) => void); off("pick-made", handlePickMade as (data: unknown) => void);
@ -263,6 +270,7 @@ export function useDraftSocketEvents({
off("draft-rolled-back", handleDraftRolledBack as (data: unknown) => void); off("draft-rolled-back", handleDraftRolledBack as (data: unknown) => void);
off("draft-state-sync", handleDraftStateSync as (data: unknown) => void); off("draft-state-sync", handleDraftStateSync as (data: unknown) => void);
off("watchlist-updated", handleWatchlistUpdated as (data: unknown) => void); off("watchlist-updated", handleWatchlistUpdated as (data: unknown) => void);
off("draft-room-closed", handleDraftRoomClosed as (data: unknown) => void);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [on, off, socketVersion]); }, [on, off, socketVersion]);

View file

@ -12,6 +12,7 @@ import { executeAutoPick } from "../draft-utils";
vi.mock("~/server/socket", () => ({ vi.mock("~/server/socket", () => ({
getSocketIO: vi.fn(), getSocketIO: vi.fn(),
scheduleDraftRoomClosure: vi.fn(),
})); }));
vi.mock("~/models/draft-pick", () => ({ vi.mock("~/models/draft-pick", () => ({

View file

@ -8,7 +8,7 @@ import { isParticipantDrafted, getDraftPicksWithSports, getTeamDraftPicksWithSpo
import { getParticipantsForSeasonWithSports } from "./participant"; import { getParticipantsForSeasonWithSports } from "./participant";
import { getSeasonSportsSimple } from "./season-sport"; import { getSeasonSportsSimple } from "./season-sport";
import { calculateDraftEligibility } from "~/lib/draft-eligibility"; import { calculateDraftEligibility } from "~/lib/draft-eligibility";
import { getSocketIO } from "../../server/socket"; import { getSocketIO, scheduleDraftRoomClosure } from "../../server/socket";
/** /**
* Check if the next team has autodraft enabled and immediately execute their pick * Check if the next team has autodraft enabled and immediately execute their pick
@ -719,6 +719,7 @@ export async function executeAutoPick(params: {
.set({ .set({
currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber, currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
status: isDraftComplete ? "active" : season.status, status: isDraftComplete ? "active" : season.status,
draftCompletedAt: isDraftComplete ? new Date() : undefined,
}) })
.where(eq(schema.seasons.id, seasonId)); .where(eq(schema.seasons.id, seasonId));
@ -804,6 +805,7 @@ export async function executeAutoPick(params: {
// Emit draft-completed event if applicable // Emit draft-completed event if applicable
if (isDraftComplete) { if (isDraftComplete) {
io.to(`draft-${seasonId}`).emit("draft-completed"); io.to(`draft-${seasonId}`).emit("draft-completed");
scheduleDraftRoomClosure(seasonId);
} }
} catch (error) { } catch (error) {
logger.error("[AutoPick] Socket.IO events error:", error); logger.error("[AutoPick] Socket.IO events error:", error);

View file

@ -7,6 +7,7 @@ const ctx = {} as unknown as RouterContextProvider;
vi.mock("~/database/context"); vi.mock("~/database/context");
vi.mock("~/server/socket", () => ({ vi.mock("~/server/socket", () => ({
getSocketIO: vi.fn(), getSocketIO: vi.fn(),
scheduleDraftRoomClosure: vi.fn(),
})); }));
vi.mock("~/lib/auth.server", () => ({ vi.mock("~/lib/auth.server", () => ({
auth: { api: { getSession: vi.fn() } }, auth: { api: { getSession: vi.fn() } },

View file

@ -13,6 +13,7 @@ const ctx = {} as unknown as RouterContextProvider;
vi.mock("~/database/context"); vi.mock("~/database/context");
vi.mock("~/server/socket", () => ({ vi.mock("~/server/socket", () => ({
getSocketIO: vi.fn(), getSocketIO: vi.fn(),
scheduleDraftRoomClosure: vi.fn(),
})); }));
vi.mock("~/lib/auth.server", () => ({ vi.mock("~/lib/auth.server", () => ({
auth: { api: { getSession: vi.fn() } }, auth: { api: { getSession: vi.fn() } },

View file

@ -13,6 +13,7 @@ const ctx = {} as unknown as RouterContextProvider;
vi.mock("~/database/context"); vi.mock("~/database/context");
vi.mock("~/server/socket", () => ({ vi.mock("~/server/socket", () => ({
getSocketIO: vi.fn(), getSocketIO: vi.fn(),
scheduleDraftRoomClosure: vi.fn(),
})); }));
vi.mock("~/lib/auth.server", () => ({ vi.mock("~/lib/auth.server", () => ({
auth: { api: { getSession: vi.fn() } }, auth: { api: { getSession: vi.fn() } },

View file

@ -7,6 +7,7 @@ const ctx = {} as unknown as RouterContextProvider;
vi.mock("~/database/context"); vi.mock("~/database/context");
vi.mock("~/server/socket", () => ({ vi.mock("~/server/socket", () => ({
getSocketIO: vi.fn(), getSocketIO: vi.fn(),
scheduleDraftRoomClosure: vi.fn(),
})); }));
vi.mock("~/lib/auth.server", () => ({ vi.mock("~/lib/auth.server", () => ({
auth: { api: { getSession: vi.fn() } }, auth: { api: { getSession: vi.fn() } },

View file

@ -9,7 +9,7 @@ import { getParticipantsForSeasonWithSports } from "~/models/participant";
import { getSeasonSportsSimple } from "~/models/season-sport"; import { getSeasonSportsSimple } from "~/models/season-sport";
import { calculatePickInfo, checkAndTriggerNextAutodraft } from "~/models/draft-utils"; import { calculatePickInfo, checkAndTriggerNextAutodraft } from "~/models/draft-utils";
import { logCommissionerAction } from "~/models/audit-log"; import { logCommissionerAction } from "~/models/audit-log";
import { getSocketIO } from "../../../server/socket"; import { getSocketIO, scheduleDraftRoomClosure } from "../../../server/socket";
import { logger } from "~/lib/logger"; import { logger } from "~/lib/logger";
import type { ActionFunctionArgs } from "react-router"; import type { ActionFunctionArgs } from "react-router";
@ -211,6 +211,7 @@ export async function action(args: ActionFunctionArgs) {
.set({ .set({
currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber, currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
status: isDraftComplete ? "active" : season.status, status: isDraftComplete ? "active" : season.status,
draftCompletedAt: isDraftComplete ? new Date() : undefined,
}) })
.where(eq(schema.seasons.id, seasonId)); .where(eq(schema.seasons.id, seasonId));
@ -254,6 +255,7 @@ export async function action(args: ActionFunctionArgs) {
if (isDraftComplete) { if (isDraftComplete) {
io.to(`draft-${seasonId}`).emit("draft-completed"); io.to(`draft-${seasonId}`).emit("draft-completed");
scheduleDraftRoomClosure(seasonId);
} }
} catch (error) { } catch (error) {
logger.error("Socket.IO error:", error); logger.error("Socket.IO error:", error);

View file

@ -8,7 +8,7 @@ import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/d
import { getParticipantsForSeasonWithSports } from "~/models/participant"; import { getParticipantsForSeasonWithSports } from "~/models/participant";
import { getSeasonSportsSimple } from "~/models/season-sport"; import { getSeasonSportsSimple } from "~/models/season-sport";
import { calculatePickInfo, checkAndTriggerNextAutodraft, pruneIneligibleQueueItems } from "~/models/draft-utils"; import { calculatePickInfo, checkAndTriggerNextAutodraft, pruneIneligibleQueueItems } from "~/models/draft-utils";
import { getSocketIO } from "../../../server/socket"; import { getSocketIO, scheduleDraftRoomClosure } from "../../../server/socket";
import { logger } from "~/lib/logger"; import { logger } from "~/lib/logger";
import type { ActionFunctionArgs } from "react-router"; import type { ActionFunctionArgs } from "react-router";
@ -272,6 +272,7 @@ export async function action(args: ActionFunctionArgs) {
.set({ .set({
currentPickNumber: isDraftComplete ? currentPickNumber : nextPickNumber, currentPickNumber: isDraftComplete ? currentPickNumber : nextPickNumber,
status: isDraftComplete ? "active" : season.status, status: isDraftComplete ? "active" : season.status,
draftCompletedAt: isDraftComplete ? new Date() : undefined,
}) })
.where(eq(schema.seasons.id, seasonId)); .where(eq(schema.seasons.id, seasonId));
@ -293,6 +294,7 @@ export async function action(args: ActionFunctionArgs) {
if (isDraftComplete) { if (isDraftComplete) {
getSocketIO().to(`draft-${seasonId}`).emit("draft-completed"); getSocketIO().to(`draft-${seasonId}`).emit("draft-completed");
scheduleDraftRoomClosure(seasonId);
} }
} catch (error) { } catch (error) {
logger.error("Socket.IO error:", error); logger.error("Socket.IO error:", error);

View file

@ -1,4 +1,4 @@
import { useLoaderData, Link, useRevalidator } from "react-router"; import { useLoaderData, Link, useRevalidator, redirect, useNavigate } from "react-router";
import { useDraftSocket } from "~/hooks/useDraftSocket"; import { useDraftSocket } from "~/hooks/useDraftSocket";
import { logger } from "~/lib/logger"; import { logger } from "~/lib/logger";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@ -74,6 +74,13 @@ export async function loader(args: Route.LoaderArgs) {
throw new Response("Season not found", { status: 404 }); throw new Response("Season not found", { status: 404 });
} }
if (season.draftCompletedAt) {
const elapsed = Date.now() - season.draftCompletedAt.getTime();
if (elapsed >= 5 * 60 * 1000) {
return redirect(`/leagues/${leagueId}/draft-board/${seasonId}`);
}
}
const userTeam = userId const userTeam = userId
? season.teams.find((team) => team.ownerId === userId) ? season.teams.find((team) => team.ownerId === userId)
: undefined; : undefined;
@ -184,6 +191,7 @@ export default function DraftRoom() {
ownerMap, ownerMap,
teamTimezoneMap, teamTimezoneMap,
} = useLoaderData<typeof loader>(); } = useLoaderData<typeof loader>();
const navigate = useNavigate();
const { revalidate, state: revalidatorState } = useRevalidator(); const { revalidate, state: revalidatorState } = useRevalidator();
const { isConnected, connectionError, isReconnecting, reconnectCount, socketVersion, on, off } = useDraftSocket(season.id, userTeam?.id); const { isConnected, connectionError, isReconnecting, reconnectCount, socketVersion, on, off } = useDraftSocket(season.id, userTeam?.id);
const { const {
@ -245,6 +253,7 @@ export default function DraftRoom() {
isAdjustingTimeBank, setIsAdjustingTimeBank, isAdjustingTimeBank, setIsAdjustingTimeBank,
isOvernightPause, setIsOvernightPause, isOvernightPause, setIsOvernightPause,
overnightResumesAt, setOvernightResumesAt, overnightResumesAt, setOvernightResumesAt,
roomClosed, setRoomClosed,
} = useDraftRoomState({ } = useDraftRoomState({
draftPicks, draftPicks,
season, season,
@ -264,6 +273,27 @@ export default function DraftRoom() {
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
const draftBoardUrl = `/leagues/${season.leagueId}/draft-board/${season.id}`;
const roomClosureCountdown = useMemo(() => {
if (!isDraftComplete) return null;
const completedAt = season.draftCompletedAt;
if (!completedAt) return 300;
const elapsed = (nowForPauseCheck.getTime() - completedAt.getTime()) / 1000;
const remaining = Math.max(0, 300 - elapsed);
return Math.ceil(remaining);
}, [isDraftComplete, season.draftCompletedAt, nowForPauseCheck]);
useEffect(() => {
if (!isDraftComplete) return;
if (roomClosed) {
navigate(draftBoardUrl);
return;
}
if (roomClosureCountdown !== null && roomClosureCountdown <= 0) {
navigate(draftBoardUrl);
}
}, [isDraftComplete, roomClosed, roomClosureCountdown, navigate, draftBoardUrl]);
// Compute which teams are currently in their overnight pause window. // Compute which teams are currently in their overnight pause window.
const pausedTeamIds = useMemo(() => { const pausedTeamIds = useMemo(() => {
const { overnightPauseMode: mode, overnightPauseStart: start, overnightPauseEnd: end, overnightPauseTimezone: fallbackTz } = season; const { overnightPauseMode: mode, overnightPauseStart: start, overnightPauseEnd: end, overnightPauseTimezone: fallbackTz } = season;
@ -432,6 +462,7 @@ export default function DraftRoom() {
setIsOvernightPause, setIsOvernightPause,
setOvernightResumesAt, setOvernightResumesAt,
setWatchedParticipantIds, setWatchedParticipantIds,
setRoomClosed,
}); });
// Persist sidebar collapsed state // Persist sidebar collapsed state
@ -1180,18 +1211,18 @@ export default function DraftRoom() {
<div className="h-dvh bg-background flex flex-col overflow-hidden"> <div className="h-dvh bg-background flex flex-col overflow-hidden">
{/* Draft Completion Banner */} {/* Draft Completion Banner */}
{isDraftComplete && ( {isDraftComplete && (
<div className="bg-emerald-500/20 border-b border-emerald-500/30 text-emerald-400 px-4 py-3 text-center font-semibold flex-shrink-0"> <div className="bg-emerald-500/20 border-b border-emerald-500/30 text-emerald-400 px-4 py-3 text-center font-semibold flex-shrink-0 flex items-center justify-center gap-2 flex-wrap">
🎉 Draft Complete! The season is now active. <span>Draft Complete! The season is now active.</span>
{season.league.isPublicDraftBoard && ( <a
<> href={draftBoardUrl}
{" "} className="inline-flex items-center gap-1 bg-emerald-500/30 hover:bg-emerald-500/40 text-emerald-300 px-3 py-1 rounded-md text-sm transition-colors"
<a >
href={`/leagues/${season.leagueId}/draft-board/${season.id}`} View Draft Board
className="underline hover:text-emerald-300" </a>
> {roomClosureCountdown !== null && roomClosureCountdown > 0 && (
View Draft Board <span className="text-sm text-emerald-500">
</a> Room closes in {Math.floor(roomClosureCountdown / 60)}:{String(roomClosureCountdown % 60).padStart(2, "0")}
</> </span>
)} )}
</div> </div>
)} )}

View file

@ -8,6 +8,7 @@ const ctx = {} as unknown as RouterContextProvider;
vi.mock("~/database/context"); vi.mock("~/database/context");
vi.mock("~/server/socket", () => ({ vi.mock("~/server/socket", () => ({
getSocketIO: vi.fn(), getSocketIO: vi.fn(),
scheduleDraftRoomClosure: vi.fn(),
})); }));
vi.mock("~/lib/auth.server", () => ({ vi.mock("~/lib/auth.server", () => ({
auth: { api: { getSession: vi.fn() } }, auth: { api: { getSession: vi.fn() } },

View file

@ -25,6 +25,7 @@ const createMockSeason = (overrides: {
draftTimerMode: 'chess_clock' as const, draftTimerMode: 'chess_clock' as const,
currentPickNumber: null, currentPickNumber: null,
draftStartedAt: null, draftStartedAt: null,
draftCompletedAt: null,
draftPaused: false, draftPaused: false,
overnightPauseMode: 'none' as const, overnightPauseMode: 'none' as const,
overnightPauseStart: null, overnightPauseStart: null,

View file

@ -55,6 +55,7 @@ const mockSeason = {
draftTimerMode: 'chess_clock' as const, draftTimerMode: 'chess_clock' as const,
currentPickNumber: null, currentPickNumber: null,
draftStartedAt: null, draftStartedAt: null,
draftCompletedAt: null,
draftPaused: false, draftPaused: false,
overnightPauseMode: 'none' as const, overnightPauseMode: 'none' as const,
overnightPauseStart: null, overnightPauseStart: null,

View file

@ -194,6 +194,7 @@ export const seasons = pgTable("seasons", {
draftTimerMode: draftTimerModeEnum("draft_timer_mode").notNull().default("chess_clock"), draftTimerMode: draftTimerModeEnum("draft_timer_mode").notNull().default("chess_clock"),
currentPickNumber: integer("current_pick_number").default(1), currentPickNumber: integer("current_pick_number").default(1),
draftStartedAt: timestamp("draft_started_at"), draftStartedAt: timestamp("draft_started_at"),
draftCompletedAt: timestamp("draft_completed_at"),
draftPaused: boolean("draft_paused").notNull().default(false), draftPaused: boolean("draft_paused").notNull().default(false),
overnightPauseMode: overnightPauseModeEnum("overnight_pause_mode").notNull().default("none"), overnightPauseMode: overnightPauseModeEnum("overnight_pause_mode").notNull().default("none"),
overnightPauseStart: varchar("overnight_pause_start", { length: 5 }), // "23:00" overnightPauseStart: varchar("overnight_pause_start", { length: 5 }), // "23:00"

View file

@ -0,0 +1 @@
ALTER TABLE "seasons" ADD COLUMN "draft_completed_at" timestamp;

File diff suppressed because it is too large Load diff

View file

@ -603,6 +603,13 @@
"when": 1777483407743, "when": 1777483407743,
"tag": "0085_young_cobalt_man", "tag": "0085_young_cobalt_man",
"breakpoints": true "breakpoints": true
},
{
"idx": 86,
"version": "7",
"when": 1777505810235,
"tag": "0086_marvelous_infant_terrible",
"breakpoints": true
} }
] ]
} }

View file

@ -17,6 +17,7 @@ interface ServerToClientEvents {
"pick-made": (data: unknown) => void; "pick-made": (data: unknown) => void;
"draft-started": (data: { seasonId: string; currentPickNumber: number }) => void; "draft-started": (data: { seasonId: string; currentPickNumber: number }) => void;
"draft-completed": () => void; "draft-completed": () => void;
"draft-room-closed": () => void;
"timer-update": (data: { "timer-update": (data: {
seasonId: string; seasonId: string;
teamId: string; teamId: string;
@ -80,11 +81,28 @@ declare global {
let io: SocketIOServer<ClientToServerEvents, ServerToClientEvents> | null = null; let io: SocketIOServer<ClientToServerEvents, ServerToClientEvents> | null = null;
function scheduleDraftRoomClosure(seasonId: string) {
if (draftRoomClosureTimers.has(seasonId)) return;
const timeout = setTimeout(() => {
logger.log(`[Socket] Draft room closure: emitting draft-room-closed for ${seasonId}`);
io?.to(`draft-${seasonId}`).emit("draft-room-closed");
draftRoomClosureTimers.delete(seasonId);
}, ROOM_CLOSURE_DELAY_MS);
draftRoomClosureTimers.set(seasonId, timeout);
}
export { scheduleDraftRoomClosure };
// Track connected teams per season (in-memory, single-instance only). // Track connected teams per season (in-memory, single-instance only).
// If the server restarts or runs as multiple instances this map resets. // If the server restarts or runs as multiple instances this map resets.
// For multi-instance deployments, replace with a Redis-backed Socket.IO adapter. // For multi-instance deployments, replace with a Redis-backed Socket.IO adapter.
const connectedTeams = new Map<string, Set<string>>(); // seasonId -> Set<teamId> const connectedTeams = new Map<string, Set<string>>(); // seasonId -> Set<teamId>
const ROOM_CLOSURE_DELAY_MS = 5 * 60 * 1000; // 5 minutes
const draftRoomClosureTimers = new Map<string, NodeJS.Timeout>(); // seasonId -> timeout
/** /**
* Initialize Socket.IO server * Initialize Socket.IO server
*/ */
@ -124,6 +142,35 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
logger.error("No seasonId provided for join-draft"); logger.error("No seasonId provided for join-draft");
return; return;
} }
try {
const seasonData = await db.query.seasons.findFirst({
where: eq(schema.seasons.id, seasonId),
columns: { id: true, status: true, draftCompletedAt: true },
});
if (seasonData?.draftCompletedAt) {
const elapsed = Date.now() - seasonData.draftCompletedAt.getTime();
if (elapsed >= ROOM_CLOSURE_DELAY_MS) {
socket.emit("draft-room-closed");
logger.log(`[Socket] join-draft rejected for ${seasonId}: room closed (completed ${Math.round(elapsed / 60000)}m ago)`);
return;
}
if (!draftRoomClosureTimers.has(seasonId)) {
const remaining = ROOM_CLOSURE_DELAY_MS - elapsed;
const timeout = setTimeout(() => {
logger.log(`[Socket] Draft room closure: emitting draft-room-closed for ${seasonId}`);
io?.to(`draft-${seasonId}`).emit("draft-room-closed");
draftRoomClosureTimers.delete(seasonId);
}, remaining);
draftRoomClosureTimers.set(seasonId, timeout);
}
}
} catch (err) {
logger.error("[Socket] join-draft room-closure check failed:", err);
}
socket.join(`draft-${seasonId}`); socket.join(`draft-${seasonId}`);
currentSeasonId = seasonId; currentSeasonId = seasonId;
logger.log(`Socket ${socket.id} joined draft-${seasonId}`); logger.log(`Socket ${socket.id} joined draft-${seasonId}`);