2025-10-18 23:13:04 -07:00
|
|
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
|
|
|
import postgres from "postgres";
|
2025-10-24 21:20:19 -07:00
|
|
|
import * as schema from "~/database/schema";
|
2025-10-25 10:14:36 -07:00
|
|
|
import { eq, and, asc } from "drizzle-orm";
|
2025-10-18 23:13:04 -07:00
|
|
|
import { getSocketIO } from "./socket";
|
2025-10-25 10:14:36 -07:00
|
|
|
import { executeAutoPick } from "~/models/draft-utils";
|
2025-10-18 23:13:04 -07:00
|
|
|
|
|
|
|
|
// 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}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 23:22:17 -07:00
|
|
|
// If timer is at 0 or below, check autodraft settings and trigger auto-pick
|
2025-10-18 23:13:04 -07:00
|
|
|
if (timer.timeRemaining <= 0) {
|
|
|
|
|
console.log(
|
2025-10-26 20:35:55 -07:00
|
|
|
`[Timer] ⚠️ Timer at ${timer.timeRemaining} for team ${currentTeamId} in season ${season.id} (pick ${currentPickNumber})`
|
2025-10-18 23:13:04 -07:00
|
|
|
);
|
2025-10-26 20:35:55 -07:00
|
|
|
|
2025-10-21 23:22:17 -07:00
|
|
|
// 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;
|
2025-10-26 20:35:55 -07:00
|
|
|
console.log(
|
|
|
|
|
`[Timer] Team ${currentTeamId} autodraft: ${shouldAutodraft ? 'ENABLED' : 'DISABLED'}, triggering forced auto-pick`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? autodraftSettings : null);
|
|
|
|
|
|
2025-10-18 23:13:04 -07:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-21 23:22:17 -07:00
|
|
|
// If timer just hit 0, check autodraft settings and trigger auto-pick
|
2025-10-18 23:13:04 -07:00
|
|
|
if (newTimeRemaining === 0) {
|
|
|
|
|
console.log(
|
2025-10-21 23:22:17 -07:00
|
|
|
`[Timer] Timer expired for team ${currentTeamId} in season ${season.id}, checking autodraft settings`
|
2025-10-18 23:13:04 -07:00
|
|
|
);
|
2025-10-26 20:35:55 -07:00
|
|
|
|
2025-10-21 23:22:17 -07:00
|
|
|
// 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)
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-26 20:35:55 -07:00
|
|
|
const shouldAutodraft = autodraftSettings?.isEnabled ?? false;
|
|
|
|
|
console.log(
|
|
|
|
|
`[Timer] Autodraft ${shouldAutodraft ? 'enabled' : 'disabled'} for team ${currentTeamId}, triggering auto-pick`
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-21 23:22:17 -07:00
|
|
|
await triggerAutoPick(season.id, currentTeamId, currentPickNumber, autodraftSettings || null);
|
2025-10-18 23:13:04 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Trigger an automatic pick when timer expires
|
2025-10-25 10:14:36 -07:00
|
|
|
* Uses the unified executeAutoPick function
|
2025-10-18 23:13:04 -07:00
|
|
|
*/
|
|
|
|
|
async function triggerAutoPick(
|
|
|
|
|
seasonId: string,
|
|
|
|
|
teamId: string,
|
2025-10-21 23:22:17 -07:00
|
|
|
pickNumber: number,
|
|
|
|
|
autodraftSettings: any | null
|
2025-10-18 23:13:04 -07:00
|
|
|
): Promise<void> {
|
|
|
|
|
try {
|
2025-10-25 10:14:36 -07:00
|
|
|
console.log(
|
|
|
|
|
`[Timer] Triggering auto-pick for team ${teamId} in season ${seasonId}, pick ${pickNumber}`
|
|
|
|
|
);
|
2025-10-18 23:13:04 -07:00
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
// Execute the autopick using the unified function
|
|
|
|
|
const result = await executeAutoPick({
|
2025-10-18 23:13:04 -07:00
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
pickNumber,
|
2025-10-25 10:14:36 -07:00
|
|
|
triggeredBy: "timer",
|
|
|
|
|
autodraftSettings,
|
|
|
|
|
db,
|
2025-10-18 23:13:04 -07:00
|
|
|
});
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
if (!result.success) {
|
|
|
|
|
console.error(`[Timer] Auto-pick failed: ${result.error}`);
|
2025-10-18 23:13:04 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[Timer] Auto-pick complete and broadcasted`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[Timer] Error in triggerAutoPick:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|