From 77f520823c93b916148b4e5770695cb784a248bc Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sun, 24 May 2026 21:29:02 -0700 Subject: [PATCH] Fix Discord pick announcements arriving out of order and add 429 retry - Move notifyPickMadeOnDiscord() before checkAndTriggerNextAutodraft() in both executeAutoPick() and the manual pick action. Previously the triggering pick's announcement fired after all chained autodraft picks had already announced, causing Discord to show e.g. pick #106 before #105 before #104. Now each pick announces itself before the next pick in the chain is triggered. - Add a single retry with Retry-After delay in sendDiscordWebhook() on HTTP 429. A burst of consecutive autodraft picks could hit Discord's per-webhook rate limit and silently drop announcements. Use || 1 (not ?? 1) to handle non-numeric/NaN headers, and cap at 10 s so that awaited callers such as the draft-order settings actions are never hung for a full global rate-limit window. Co-Authored-By: Claude Sonnet 4.6 --- app/models/draft-utils.ts | 36 ++++++++++++++++--------------- app/routes/api/draft.make-pick.ts | 34 +++++++++++++++-------------- app/services/discord.ts | 18 ++++++++++++++++ 3 files changed, 55 insertions(+), 33 deletions(-) diff --git a/app/models/draft-utils.ts b/app/models/draft-utils.ts index d50a537..9707204 100644 --- a/app/models/draft-utils.ts +++ b/app/models/draft-utils.ts @@ -827,23 +827,8 @@ export async function executeAutoPick(params: { logger.error("[AutoPick] Error updating Brackt EVs after pick:", error); } - // Check if next team has autodraft enabled and trigger immediately - // Only run the chain from the top-level call to prevent recursion - if (!isDraftComplete && params.chainEnabled !== false) { - await checkAndTriggerNextAutodraft({ - seasonId, - nextPickNumber, - totalTeams, - draftSlots, - db, - }); - - // Fire-and-forget: sendOnTheClockEmail fetches the current pick number from - // the DB itself, so it always sees the post-chain state without blocking here. - sendOnTheClockEmail({ seasonId, totalTeams, draftSlots, db }) - .catch((err) => logger.error("[AutoPick] On-the-clock email failed:", err)); - } - + // Announce before triggering the chain so Discord messages arrive in pick-number + // order. If the chain ran first, chained picks would announce before this one. const pickedSlot = draftSlots.find((s) => s.teamId === teamId); if (!pickedSlot) { logger.warn(`[AutoPick] Skipping Discord pick announcement: no draft slot found for team ${teamId}`); @@ -865,6 +850,23 @@ export async function executeAutoPick(params: { }).catch((err) => logger.error("[AutoPick] Discord pick announcement failed:", err)); } + // Check if next team has autodraft enabled and trigger immediately + // Only run the chain from the top-level call to prevent recursion + if (!isDraftComplete && params.chainEnabled !== false) { + await checkAndTriggerNextAutodraft({ + seasonId, + nextPickNumber, + totalTeams, + draftSlots, + db, + }); + + // Fire-and-forget: sendOnTheClockEmail fetches the current pick number from + // the DB itself, so it always sees the post-chain state without blocking here. + sendOnTheClockEmail({ seasonId, totalTeams, draftSlots, db }) + .catch((err) => logger.error("[AutoPick] On-the-clock email failed:", err)); + } + return { success: true, pick: draftPick, diff --git a/app/routes/api/draft.make-pick.ts b/app/routes/api/draft.make-pick.ts index ed89589..813ce90 100644 --- a/app/routes/api/draft.make-pick.ts +++ b/app/routes/api/draft.make-pick.ts @@ -303,6 +303,24 @@ export async function action(args: ActionFunctionArgs) { logger.error("Socket.IO error:", error); } + // Announce before triggering the chain so Discord messages arrive in pick-number + // order. If the chain ran first, chained picks would announce before this one. + notifyPickMadeOnDiscord({ + seasonId, + leagueId: season.leagueId, + pickedTeamName: currentDraftSlot.team.name, + participantName: participant.name, + sportName: participant.sportsSeason.sport.name, + pickNumber: currentPickNumber, + nextPickNumber, + round: currentRound, + rawPickInRound, + isDraftComplete, + totalTeams, + draftSlots: draftSlots.map((s) => ({ teamId: s.teamId, draftOrder: s.draftOrder })), + db, + }).catch((err) => logger.error("Discord pick announcement failed:", err)); + // Check if next team has autodraft enabled and trigger immediately if (!isDraftComplete) { try { @@ -331,22 +349,6 @@ export async function action(args: ActionFunctionArgs) { .catch((err) => logger.error("On-the-clock email failed:", err)); } - notifyPickMadeOnDiscord({ - seasonId, - leagueId: season.leagueId, - pickedTeamName: currentDraftSlot.team.name, - participantName: participant.name, - sportName: participant.sportsSeason.sport.name, - pickNumber: currentPickNumber, - nextPickNumber, - round: currentRound, - rawPickInRound, - isDraftComplete, - totalTeams, - draftSlots: draftSlots.map((s) => ({ teamId: s.teamId, draftOrder: s.draftOrder })), - db, - }).catch((err) => logger.error("Discord pick announcement failed:", err)); - return Response.json({ success: true, pick: draftPick, diff --git a/app/services/discord.ts b/app/services/discord.ts index 25d9188..2ad9c28 100644 --- a/app/services/discord.ts +++ b/app/services/discord.ts @@ -24,6 +24,24 @@ export async function sendDiscordWebhook( body: JSON.stringify(payload), }); + if (response.status === 429) { + // Cap at 10 s so awaited callers (e.g. settings actions) are never hung for + // a full Discord global-rate-limit window. Use || 1 instead of ?? 1 so that + // NaN (non-numeric header) and 0 both fall back to a 1-second default. + const retryAfterSeconds = Math.min(Number(response.headers.get("Retry-After")) || 1, 10); + await new Promise((r) => setTimeout(r, retryAfterSeconds * 1000)); + const retry = await fetch(webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!retry.ok) { + const text = await retry.text().catch(() => ""); + throw new Error(`Discord webhook failed after retry: ${retry.status} ${text}`); + } + return; + } + if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error(`Discord webhook failed: ${response.status} ${text}`); -- 2.45.3