import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "~/database/schema"; import { eq, and, asc } from "drizzle-orm"; import { getSocketIO } from "./socket"; import { executeAutoPick } from "~/models/draft-utils"; // 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 ${timer.timeRemaining} for team ${currentTeamId} in season ${season.id} (pick ${currentPickNumber})` ); // 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; console.log( `[Timer] Team ${currentTeamId} autodraft: ${shouldAutodraft ? 'ENABLED' : 'DISABLED'}, triggering forced auto-pick` ); await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? autodraftSettings : 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) ), }); const shouldAutodraft = autodraftSettings?.isEnabled ?? false; console.log( `[Timer] Autodraft ${shouldAutodraft ? 'enabled' : 'disabled'} for team ${currentTeamId}, triggering auto-pick` ); await triggerAutoPick(season.id, currentTeamId, currentPickNumber, autodraftSettings || null); } } } /** * Trigger an automatic pick when timer expires * Uses the unified executeAutoPick function */ async function triggerAutoPick( seasonId: string, teamId: string, pickNumber: number, autodraftSettings: any | null ): Promise { try { console.log( `[Timer] Triggering auto-pick for team ${teamId} in season ${seasonId}, pick ${pickNumber}` ); // Execute the autopick using the unified function const result = await executeAutoPick({ seasonId, teamId, pickNumber, triggeredBy: "timer", autodraftSettings, db, }); if (!result.success) { console.error(`[Timer] Auto-pick failed: ${result.error}`); return; } console.log(`[Timer] Auto-pick complete and broadcasted`); } catch (error) { console.error("[Timer] Error in triggerAutoPick:", error); } }