import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "../database/schema"; import { eq, and, desc, asc, inArray, notInArray } from "drizzle-orm"; import { getSocketIO } from "./socket"; // Create a dedicated database connection for the timer const connectionString = process.env.DATABASE_URL; if (!connectionString) { throw new Error("DATABASE_URL is required for timer system"); } const client = postgres(connectionString); const db = drizzle(client, { schema }); let timerInterval: NodeJS.Timeout | null = null; /** * Start the draft timer system * Runs every second to update all active draft timers */ export function startDraftTimerSystem(): void { if (timerInterval) { console.log("[Timer] Timer system already running"); return; } timerInterval = setInterval(async () => { try { await updateDraftTimers(); } catch (error) { console.error("[Timer] Error updating draft timers:", error); } }, 1000); console.log("[Timer] Draft timer system started"); } /** * Stop the draft timer system */ export function stopDraftTimerSystem(): void { if (timerInterval) { clearInterval(timerInterval); timerInterval = null; console.log("[Timer] Draft timer system stopped"); } } /** * Update all active draft timers * Called every second by the timer interval */ async function updateDraftTimers(): Promise { const io = getSocketIO(); // Get all active drafts const activeDrafts = await db.query.seasons.findMany({ where: eq(schema.seasons.status, "draft"), }); if (activeDrafts.length === 0) { return; // No active drafts } console.log(`[Timer] Updating timers for ${activeDrafts.length} active draft(s)`); for (const season of activeDrafts) { // Skip if draft is paused if (season.draftPaused) { continue; } const currentPickNumber = season.currentPickNumber || 1; // Get draft slots to determine whose turn it is const draftSlots = await db.query.draftSlots.findMany({ where: eq(schema.draftSlots.seasonId, season.id), orderBy: asc(schema.draftSlots.draftOrder), }); const totalTeams = draftSlots.length; if (totalTeams === 0) continue; // Calculate current round and pick const currentRound = Math.ceil(currentPickNumber / totalTeams); const isEvenRound = currentRound % 2 === 0; let pickInRound = ((currentPickNumber - 1) % totalTeams) + 1; // Snake draft: reverse order on even rounds if (isEvenRound) { pickInRound = totalTeams - pickInRound + 1; } const currentDraftSlot = draftSlots.find( (slot: any) => slot.draftOrder === pickInRound ); if (!currentDraftSlot) { continue; } const currentTeamId = currentDraftSlot.teamId; // Get current team's timer const timer = await db.query.draftTimers.findFirst({ where: and( eq(schema.draftTimers.seasonId, season.id), eq(schema.draftTimers.teamId, currentTeamId) ), }); if (!timer) { console.warn(`[Timer] No timer found for team ${currentTeamId} in season ${season.id}`); continue; } // If timer is at 0 or below, check autodraft settings and trigger auto-pick if (timer.timeRemaining <= 0) { console.log( `[Timer] Timer at 0 for team ${currentTeamId} in season ${season.id}, checking autodraft settings` ); // Check if team has autodraft enabled const autodraftSettings = await db.query.autodraftSettings.findFirst({ where: and( eq(schema.autodraftSettings.seasonId, season.id), eq(schema.autodraftSettings.teamId, currentTeamId) ), }); const shouldAutodraft = autodraftSettings?.isEnabled ?? false; if (shouldAutodraft) { console.log(`[Timer] Autodraft enabled for team ${currentTeamId}, triggering auto-pick`); await triggerAutoPick(season.id, currentTeamId, currentPickNumber, autodraftSettings); } else { console.log(`[Timer] Autodraft disabled for team ${currentTeamId}, triggering regular auto-pick`); await triggerAutoPick(season.id, currentTeamId, currentPickNumber, null); } continue; // Skip to next draft after triggering pick } // Decrement timer const newTimeRemaining = timer.timeRemaining - 1; console.log( `[Timer] Season ${season.id}: Team ${currentTeamId} - ${newTimeRemaining}s remaining (pick ${currentPickNumber})` ); // Update timer in database await db .update(schema.draftTimers) .set({ timeRemaining: newTimeRemaining, updatedAt: new Date(), }) .where(eq(schema.draftTimers.id, timer.id)); // Emit timer update to all clients in the draft room io.to(`draft-${season.id}`).emit("timer-update", { seasonId: season.id, teamId: currentTeamId, timeRemaining: newTimeRemaining, currentPickNumber, }); // If timer just hit 0, check autodraft settings and trigger auto-pick if (newTimeRemaining === 0) { console.log( `[Timer] Timer expired for team ${currentTeamId} in season ${season.id}, checking autodraft settings` ); // Check if team has autodraft enabled const autodraftSettings = await db.query.autodraftSettings.findFirst({ where: and( eq(schema.autodraftSettings.seasonId, season.id), eq(schema.autodraftSettings.teamId, currentTeamId) ), }); await triggerAutoPick(season.id, currentTeamId, currentPickNumber, autodraftSettings || null); } } } /** * Trigger an automatic pick when timer expires */ async function triggerAutoPick( seasonId: string, teamId: string, pickNumber: number, autodraftSettings: any | null ): Promise { try { const io = getSocketIO(); // Get season details const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), }); if (!season) { console.error(`[Timer] Season ${seasonId} not found for auto-pick`); return; } // Check if pick already made (race condition protection) const existingPick = await db.query.draftPicks.findFirst({ where: and( eq(schema.draftPicks.seasonId, seasonId), eq(schema.draftPicks.pickNumber, pickNumber) ), }); if (existingPick) { console.log(`[Timer] Pick ${pickNumber} already made, skipping auto-pick`); return; } // Get team's queue const queueItems = await db.query.draftQueue.findMany({ where: and( eq(schema.draftQueue.seasonId, seasonId), eq(schema.draftQueue.teamId, teamId) ), orderBy: asc(schema.draftQueue.queuePosition), }); let participantId: string | null = null; // Try to pick from queue first if (queueItems.length > 0) { // Get already drafted participant IDs const draftedPicks = await db.query.draftPicks.findMany({ where: eq(schema.draftPicks.seasonId, seasonId), }); const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId); // Find first queued participant that hasn't been drafted const availableQueueItem = queueItems.find( (item: any) => !draftedParticipantIds.includes(item.participantId) ); if (availableQueueItem) { participantId = availableQueueItem.participantId; console.log(`[Timer] Auto-picking from queue: ${participantId}`); } } // If no valid queue item, pick highest EV available participant if (!participantId) { // Get sports season IDs for this season const seasonSports = await db.query.seasonTemplateSports.findMany({ where: eq(schema.seasonTemplateSports.templateId, season.templateId!), }); const sportsSeasonIds = seasonSports.map((ss: any) => ss.sportsSeasonId); if (sportsSeasonIds.length === 0) { console.error(`[Timer] No sports seasons found for season ${seasonId}`); return; } // Get already drafted participant IDs const draftedPicks = await db.query.draftPicks.findMany({ where: eq(schema.draftPicks.seasonId, seasonId), }); const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId); // Query available participants from the sports seasons const availableParticipants = await db.query.participants.findMany({ where: and( sportsSeasonIds.length === 1 ? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]) : inArray(schema.participants.sportsSeasonId, sportsSeasonIds), draftedParticipantIds.length > 0 ? notInArray(schema.participants.id, draftedParticipantIds) : undefined ), orderBy: desc(schema.participants.expectedValue), limit: 1, }); if (availableParticipants.length === 0) { console.error(`[Timer] No available participants for auto-pick in season ${seasonId}`); return; } participantId = availableParticipants[0].id; console.log(`[Timer] Auto-picking highest EV: ${participantId}`); } // Calculate round and pick in round const draftSlots = await db.query.draftSlots.findMany({ where: eq(schema.draftSlots.seasonId, seasonId), orderBy: asc(schema.draftSlots.draftOrder), }); const totalTeams = draftSlots.length; const round = Math.ceil(pickNumber / totalTeams); const pickInRound = ((pickNumber - 1) % totalTeams) + 1; // Create the draft pick const [createdPick] = await db.insert(schema.draftPicks).values({ seasonId, teamId, participantId, pickNumber, round, pickInRound, pickedByUserId: "", // Auto-pick has no user pickedByType: "auto", timeUsed: season.draftInitialTime || 120, // Used all time createdAt: new Date(), }).returning(); console.log(`[Timer] Auto-pick created: Pick ${pickNumber} - Participant ${participantId}`); // Remove from ALL team queues in this season (participant is now drafted) await db .delete(schema.draftQueue) .where( and( eq(schema.draftQueue.seasonId, seasonId), eq(schema.draftQueue.participantId, participantId) ) ); // Check if draft is complete const totalPicks = totalTeams * (season.draftRounds || 1); const allPicks = await db.query.draftPicks.findMany({ where: eq(schema.draftPicks.seasonId, seasonId), }); const isDraftComplete = allPicks.length >= totalPicks; const nextPickNumber = isDraftComplete ? pickNumber : pickNumber + 1; // If autodraft was enabled with "next_pick" mode, disable it now if (autodraftSettings?.isEnabled && autodraftSettings.mode === "next_pick") { console.log(`[Timer] Disabling autodraft for team ${teamId} after next_pick`); await db .update(schema.autodraftSettings) .set({ isEnabled: false, updatedAt: new Date(), }) .where(eq(schema.autodraftSettings.id, autodraftSettings.id)); // Emit socket event to notify clients io.to(`draft-${seasonId}`).emit("autodraft-updated", { teamId, isEnabled: false, mode: autodraftSettings.mode, }); } // Add increment to the team that just picked const pickingTeamTimer = await db.query.draftTimers.findFirst({ where: and( eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, teamId) ), }); if (pickingTeamTimer) { const incrementTime = season.draftIncrementTime || 30; const newTimeRemaining = pickingTeamTimer.timeRemaining + incrementTime; await db .update(schema.draftTimers) .set({ timeRemaining: newTimeRemaining, updatedAt: new Date(), }) .where(eq(schema.draftTimers.id, pickingTeamTimer.id)); // Emit timer update to all clients io.to(`draft-${seasonId}`).emit("timer-update", { seasonId, teamId, timeRemaining: newTimeRemaining, currentPickNumber: pickNumber, }); } if (isDraftComplete) { // Draft complete await db .update(schema.seasons) .set({ status: "active" }) .where(eq(schema.seasons.id, seasonId)); io.to(`draft-${seasonId}`).emit("draft-completed"); console.log(`[Timer] Draft completed for season ${seasonId}`); } else { // Move to next pick await db .update(schema.seasons) .set({ currentPickNumber: nextPickNumber }) .where(eq(schema.seasons.id, seasonId)); } // Fetch the complete pick with all relations (same as API routes do) const completePick = await db .select({ id: schema.draftPicks.id, seasonId: schema.draftPicks.seasonId, teamId: schema.draftPicks.teamId, participantId: schema.draftPicks.participantId, pickNumber: schema.draftPicks.pickNumber, round: schema.draftPicks.round, pickInRound: schema.draftPicks.pickInRound, pickedByUserId: schema.draftPicks.pickedByUserId, pickedByType: schema.draftPicks.pickedByType, timeUsed: schema.draftPicks.timeUsed, createdAt: schema.draftPicks.createdAt, team: schema.teams, participant: schema.participants, sport: schema.sports, }) .from(schema.draftPicks) .innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id)) .innerJoin(schema.participants, eq(schema.draftPicks.participantId, schema.participants.id)) .innerJoin(schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)) .innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id)) .where(eq(schema.draftPicks.id, createdPick.id)) .limit(1); if (completePick.length === 0) { console.error(`[Timer] Could not fetch complete pick data`); return; } // Emit pick-made event with the same structure as API routes io.to(`draft-${seasonId}`).emit("pick-made", { pick: { ...completePick[0], participant: { ...completePick[0].participant, sport: completePick[0].sport, }, }, nextPickNumber, isDraftComplete, }); console.log(`[Timer] Auto-pick complete and broadcasted`); } catch (error) { console.error("[Timer] Error in triggerAutoPick:", error); } }