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
244 lines
7 KiB
TypeScript
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);
|
|
}
|
|
}
|