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
This commit is contained in:
chrisp 2026-05-25 04:34:47 +00:00
commit 874a2db853
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}`);