Compare commits

...

2 commits

Author SHA1 Message Date
874a2db853 Merge pull request 'Fix Discord pick announcements arriving out of order and add 429 retry' (#51) from fix/discord-pick-announcement-ordering into main
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m27s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m18s
🚀 Deploy / 🔍 Lint (push) Successful in 47s
🚀 Deploy / 🐳 Build (push) Successful in 14m54s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
Reviewed-on: #51
2026-05-25 04:34:47 +00:00
Chris Parsons
77f520823c Fix Discord pick announcements arriving out of order and add 429 retry
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m30s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m16s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- 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 <noreply@anthropic.com>
2026-05-24 21:29:02 -07:00
3 changed files with 55 additions and 33 deletions

View file

@ -827,23 +827,8 @@ export async function executeAutoPick(params: {
logger.error("[AutoPick] Error updating Brackt EVs after pick:", error); logger.error("[AutoPick] Error updating Brackt EVs after pick:", error);
} }
// Check if next team has autodraft enabled and trigger immediately // Announce before triggering the chain so Discord messages arrive in pick-number
// Only run the chain from the top-level call to prevent recursion // order. If the chain ran first, chained picks would announce before this one.
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));
}
const pickedSlot = draftSlots.find((s) => s.teamId === teamId); const pickedSlot = draftSlots.find((s) => s.teamId === teamId);
if (!pickedSlot) { if (!pickedSlot) {
logger.warn(`[AutoPick] Skipping Discord pick announcement: no draft slot found for team ${teamId}`); 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)); }).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 { return {
success: true, success: true,
pick: draftPick, pick: draftPick,

View file

@ -303,6 +303,24 @@ export async function action(args: ActionFunctionArgs) {
logger.error("Socket.IO error:", error); 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 // Check if next team has autodraft enabled and trigger immediately
if (!isDraftComplete) { if (!isDraftComplete) {
try { try {
@ -331,22 +349,6 @@ export async function action(args: ActionFunctionArgs) {
.catch((err) => logger.error("On-the-clock email failed:", err)); .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({ return Response.json({
success: true, success: true,
pick: draftPick, pick: draftPick,

View file

@ -24,6 +24,24 @@ export async function sendDiscordWebhook(
body: JSON.stringify(payload), 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) { if (!response.ok) {
const text = await response.text().catch(() => ""); const text = await response.text().catch(() => "");
throw new Error(`Discord webhook failed: ${response.status} ${text}`); throw new Error(`Discord webhook failed: ${response.status} ${text}`);