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}`);