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
This commit is contained in:
parent
b251304b4d
commit
6e0db1304d
4 changed files with 120 additions and 119 deletions
|
|
@ -19,15 +19,18 @@ export async function checkAndTriggerNextAutodraft(params: {
|
|||
draftSlots: any[];
|
||||
db?: ReturnType<typeof database>;
|
||||
}): Promise<void> {
|
||||
const { seasonId, nextPickNumber, totalTeams, draftSlots, db: providedDb } = params;
|
||||
const { seasonId, totalTeams, draftSlots, db: providedDb } = params;
|
||||
const db = providedDb || database();
|
||||
|
||||
// Calculate which team is next using snake draft logic
|
||||
const nextRound = Math.ceil(nextPickNumber / totalTeams);
|
||||
const isNextRoundEven = nextRound % 2 === 0;
|
||||
let nextPickInRound = ((nextPickNumber - 1) % totalTeams) + 1;
|
||||
let currentPickNumber = params.nextPickNumber;
|
||||
|
||||
// Iteratively execute autodraft picks for consecutive teams with autodraft enabled
|
||||
while (true) {
|
||||
// Calculate which team is next using snake draft logic
|
||||
const nextRound = Math.ceil(currentPickNumber / totalTeams);
|
||||
const isNextRoundEven = nextRound % 2 === 0;
|
||||
let nextPickInRound = ((currentPickNumber - 1) % totalTeams) + 1;
|
||||
|
||||
// Apply snake draft reversal for even rounds
|
||||
if (isNextRoundEven) {
|
||||
nextPickInRound = totalTeams - nextPickInRound + 1;
|
||||
}
|
||||
|
|
@ -37,7 +40,6 @@ export async function checkAndTriggerNextAutodraft(params: {
|
|||
|
||||
const nextTeamId = nextDraftSlot.teamId;
|
||||
|
||||
// Check if next team has autodraft enabled
|
||||
const autodraftSettings = await db.query.autodraftSettings.findFirst({
|
||||
where: and(
|
||||
eq(schema.autodraftSettings.seasonId, seasonId),
|
||||
|
|
@ -45,24 +47,25 @@ export async function checkAndTriggerNextAutodraft(params: {
|
|||
),
|
||||
});
|
||||
|
||||
if (autodraftSettings?.isEnabled) {
|
||||
if (!autodraftSettings?.isEnabled) return;
|
||||
|
||||
console.log(
|
||||
`[AutodraftChain] Team ${nextTeamId} has autodraft enabled, triggering immediate pick for pick ${nextPickNumber}`
|
||||
`[AutodraftChain] Team ${nextTeamId} has autodraft enabled, triggering immediate pick for pick ${currentPickNumber}`
|
||||
);
|
||||
|
||||
// Immediately execute autopick for this team
|
||||
await executeAutoPick({
|
||||
const result = await executeAutoPick({
|
||||
seasonId,
|
||||
teamId: nextTeamId,
|
||||
pickNumber: nextPickNumber,
|
||||
triggeredBy: "timer", // Use "timer" to indicate automatic (not commissioner-forced)
|
||||
pickNumber: currentPickNumber,
|
||||
triggeredBy: "timer",
|
||||
autodraftSettings,
|
||||
db,
|
||||
chainEnabled: false,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
`[AutodraftChain] Team ${nextTeamId} does not have autodraft enabled, waiting for manual pick`
|
||||
);
|
||||
|
||||
if (!result.success || result.isDraftComplete || !result.nextPickNumber) return;
|
||||
|
||||
currentPickNumber = result.nextPickNumber;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,31 +249,8 @@ export async function getTopAvailableParticipant(
|
|||
return null;
|
||||
}
|
||||
|
||||
// Get top available participant by EV
|
||||
let query = db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]))
|
||||
.orderBy(desc(schema.participants.expectedValue), schema.participants.name);
|
||||
|
||||
// Filter out drafted participants if any exist
|
||||
if (draftedIds.length > 0) {
|
||||
query = db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]),
|
||||
notInArray(schema.participants.id, draftedIds)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(schema.participants.expectedValue), schema.participants.name);
|
||||
}
|
||||
|
||||
// Handle multiple sports seasons
|
||||
// Handle multiple sports seasons: query each and sort in memory
|
||||
if (sportsSeasonIds.length > 1) {
|
||||
// For simplicity, we'll query all and sort in memory
|
||||
// In production, might want to optimize this
|
||||
const allParticipants = [];
|
||||
|
||||
for (const sportsSeasonId of sportsSeasonIds) {
|
||||
|
|
@ -308,6 +288,26 @@ export async function getTopAvailableParticipant(
|
|||
return allParticipants[0]?.id || null;
|
||||
}
|
||||
|
||||
// Single sport season
|
||||
let query = db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]))
|
||||
.orderBy(desc(schema.participants.expectedValue), schema.participants.name);
|
||||
|
||||
if (draftedIds.length > 0) {
|
||||
query = db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]),
|
||||
notInArray(schema.participants.id, draftedIds)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(schema.participants.expectedValue), schema.participants.name);
|
||||
}
|
||||
|
||||
const [topParticipant] = await query;
|
||||
return topParticipant?.id || null;
|
||||
}
|
||||
|
|
@ -375,6 +375,7 @@ export async function executeAutoPick(params: {
|
|||
commissionerUserId?: string;
|
||||
autodraftSettings?: any;
|
||||
db?: ReturnType<typeof database>;
|
||||
chainEnabled?: boolean; // Set to false when called from within the autodraft chain to prevent recursion
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
|
|
@ -518,7 +519,7 @@ export async function executeAutoPick(params: {
|
|||
pickInRound,
|
||||
pickedByUserId,
|
||||
pickedByType: "auto",
|
||||
// timeRemaining at pick moment: 0 when timer expired, full bank for immediate autodraft
|
||||
// Records the team's bank balance at the moment the pick was made (seconds remaining)
|
||||
timeUsed: currentTimer ? currentTimer.timeRemaining : undefined,
|
||||
})
|
||||
.returning();
|
||||
|
|
@ -634,7 +635,8 @@ export async function executeAutoPick(params: {
|
|||
}
|
||||
|
||||
// Check if next team has autodraft enabled and trigger immediately
|
||||
if (!isDraftComplete) {
|
||||
// Only run the chain from the top-level call to prevent recursion
|
||||
if (!isDraftComplete && params.chainEnabled !== false) {
|
||||
await checkAndTriggerNextAutodraft({
|
||||
seasonId,
|
||||
nextPickNumber,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ export async function action(args: any) {
|
|||
|
||||
const totalTeams = draftSlots.length;
|
||||
|
||||
if (totalTeams === 0) {
|
||||
return Response.json({ error: "No draft slots found for this season" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Calculate which team is on the clock at the rollback pick (snake draft)
|
||||
const round = Math.ceil(pickNumber / totalTeams);
|
||||
const isEvenRound = round % 2 === 0;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,15 @@ export async function action(args: any) {
|
|||
return Response.json({ error: "Draft already started or completed" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate draft slots exist before modifying any state
|
||||
const draftSlots = await db.query.draftSlots.findMany({
|
||||
where: eq(schema.draftSlots.seasonId, seasonId),
|
||||
});
|
||||
|
||||
if (draftSlots.length === 0) {
|
||||
return Response.json({ error: "No draft slots found for this season" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Update season status to draft
|
||||
await db
|
||||
.update(schema.seasons)
|
||||
|
|
@ -57,15 +66,6 @@ export async function action(args: any) {
|
|||
})
|
||||
.where(eq(schema.seasons.id, seasonId));
|
||||
|
||||
// Initialize timers for all teams
|
||||
const draftSlots = await db.query.draftSlots.findMany({
|
||||
where: eq(schema.draftSlots.seasonId, seasonId),
|
||||
});
|
||||
|
||||
if (draftSlots.length === 0) {
|
||||
return Response.json({ error: "No draft slots found for this season" }, { status: 400 });
|
||||
}
|
||||
|
||||
const initialTime = season.draftInitialTime || 120;
|
||||
|
||||
// Delete existing timers for this season
|
||||
|
|
|
|||
|
|
@ -63,8 +63,6 @@ async function updateDraftTimers(): Promise<void> {
|
|||
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) {
|
||||
|
|
@ -93,7 +91,7 @@ async function updateDraftTimers(): Promise<void> {
|
|||
}
|
||||
|
||||
const currentDraftSlot = draftSlots.find(
|
||||
(slot: any) => slot.draftOrder === pickInRound
|
||||
(slot) => slot.draftOrder === pickInRound
|
||||
);
|
||||
|
||||
if (!currentDraftSlot) {
|
||||
|
|
@ -138,7 +136,7 @@ async function updateDraftTimers(): Promise<void> {
|
|||
// 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})`
|
||||
`[Timer] ⚠️ Timer expired for team ${currentTeamId} in season ${season.id} (pick ${currentPickNumber})`
|
||||
);
|
||||
|
||||
// Check if team has autodraft enabled
|
||||
|
|
@ -150,22 +148,21 @@ async function updateDraftTimers(): Promise<void> {
|
|||
});
|
||||
|
||||
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);
|
||||
const success = await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? autodraftSettings : null);
|
||||
|
||||
continue; // Skip to next draft after triggering pick
|
||||
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 });
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
@ -183,13 +180,8 @@ async function updateDraftTimers(): Promise<void> {
|
|||
currentPickNumber,
|
||||
});
|
||||
|
||||
// If timer just hit 0, check autodraft settings and trigger auto-pick
|
||||
// If timer just hit 0, 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),
|
||||
|
|
@ -198,31 +190,29 @@ async function updateDraftTimers(): Promise<void> {
|
|||
});
|
||||
|
||||
const shouldAutodraft = autodraftSettings?.isEnabled ?? false;
|
||||
console.log(
|
||||
`[Timer] Autodraft ${shouldAutodraft ? 'enabled' : 'disabled'} for team ${currentTeamId}, triggering auto-pick`
|
||||
);
|
||||
const success = await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? autodraftSettings : null);
|
||||
|
||||
await triggerAutoPick(season.id, currentTeamId, currentPickNumber, 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an automatic pick when timer expires
|
||||
* Uses the unified executeAutoPick function
|
||||
* 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
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
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,
|
||||
|
|
@ -233,12 +223,17 @@ async function triggerAutoPick(
|
|||
});
|
||||
|
||||
if (!result.success) {
|
||||
// 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}`);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[Timer] Auto-pick complete and broadcasted`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[Timer] Error in triggerAutoPick:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue