brackt/server/timer.ts
Claude cb2fd1f031
Fix draft timer missing after rollback causing draft to freeze
The rollback endpoint was deleting all timers but only recreating one
for the team currently on the clock. After that team made their pick,
the next team had no timer row, causing the timer system to log
"No timer found" and skip indefinitely, freezing the draft.

Fix: recreate timers for ALL teams after a rollback, each starting at
draftInitialTime.

Also add a defensive fallback in the timer system: if a timer row is
missing for the current team during an active draft, create it at
draftInitialTime instead of skipping, so the draft can never get
permanently stuck due to a missing timer.

https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
2026-02-23 00:45:30 +00:00

244 lines
7 KiB
TypeScript

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<void> {
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}, creating with initial time`
);
const initialTime = season.draftInitialTime || 120;
await db
.insert(schema.draftTimers)
.values({
seasonId: season.id,
teamId: currentTeamId,
timeRemaining: initialTime,
});
// Emit timer update so clients are aware of the new timer
io.to(`draft-${season.id}`).emit("timer-update", {
seasonId: season.id,
teamId: currentTeamId,
timeRemaining: initialTime,
currentPickNumber,
});
// Continue processing with the newly created timer on the next tick
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<void> {
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);
}
}