brackt/server/timer.ts

240 lines
7.1 KiB
TypeScript
Raw Normal View History

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
}
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(
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
(slot) => 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) {
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
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(
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
`[Timer] ⚠️ Timer expired 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;
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
const success = await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? autodraftSettings : null);
if (!success) {
console.error(`[Timer] Pausing draft ${season.id} — auto-pick failed for team ${currentTeamId} pick ${currentPickNumber}`);
await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, season.id));
io.to(`draft-${season.id}`).emit("draft-paused", { seasonId: season.id, paused: true });
}
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
continue;
}
// Decrement timer
const newTimeRemaining = timer.timeRemaining - 1;
// 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,
});
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
// If timer just hit 0, trigger auto-pick
if (newTimeRemaining === 0) {
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;
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
const success = await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? autodraftSettings : null);
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
if (!success) {
console.error(`[Timer] Pausing draft ${season.id} — auto-pick failed for team ${currentTeamId} pick ${currentPickNumber}`);
await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, season.id));
io.to(`draft-${season.id}`).emit("draft-paused", { seasonId: season.id, paused: true });
}
}
}
}
/**
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
* Trigger an automatic pick when timer expires.
* Returns true if the pick succeeded (or was already made by another path),
* false if a real failure occurred that requires commissioner intervention.
*/
async function triggerAutoPick(
seasonId: string,
teamId: string,
pickNumber: number,
autodraftSettings: any | null
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
): Promise<boolean> {
try {
const result = await executeAutoPick({
seasonId,
teamId,
pickNumber,
triggeredBy: "timer",
autodraftSettings,
db,
});
if (!result.success) {
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
// A race condition where the pick was already made is not a real failure
if (result.error === "Pick already made") {
return true;
}
console.error(`[Timer] Auto-pick failed: ${result.error}`);
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
return false;
}
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
return true;
} catch (error) {
console.error("[Timer] Error in triggerAutoPick:", error);
Refactor autodraft chain logic and improve timer handling (#25) * 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 * Fix rollback to not touch timers at all Timers are each team's time bank and should not be affected by rolling back a pick. The previous code (both original and the first fix) was deleting all timer rows during rollback, which was the actual root cause of the missing timer bug. Rollback now only does what it should: delete picks from the rollback point onwards and reset currentPickNumber. The timer system will naturally resume for the correct team on its next tick. Also removes the incorrect timer-update socket emit that was sending initialTime instead of the team's actual remaining bank. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV * Fix all code review issues across timer, rollback, start, and draft-utils server/timer.ts: - Remove noisy per-tick console.log that fired every second per active draft - Remove slot:any type cast (Drizzle query results are already typed) - Fix inconsistent autodraftSettings argument in the === 0 trigger path to match the <= 0 path (pass null when autodraft is disabled) - Fix infinite auto-pick loop: triggerAutoPick now returns boolean; on failure the draft is paused and a draft-paused socket event is emitted so clients and commissioners are notified. "Pick already made" (race condition) is treated as success, not failure. draft.start.ts: - Validate that draft slots exist before updating season status to draft. Previously a missing-slots error left the season stuck in draft status with no timer rows. draft.rollback.ts: - Guard against empty draftSlots before performing snake draft math, which would produce Infinity/NaN with zero teams. draft-utils.ts: - Convert checkAndTriggerNextAutodraft from recursive to iterative. The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft → executeAutoPick) is now a while loop, preventing unbounded call stack growth in all-autodraft leagues. Add chainEnabled param to executeAutoPick so chain calls skip spawning a second chain. - Restructure getTopAvailableParticipant so the single-sport query is only built when actually needed (not thrown away in the multi-sport branch). - Update misleading timeUsed comment to accurately describe that the stored value is the team's bank balance at pick time, not time spent. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
return false;
}
}