Fix three code-review findings from Discord notification refactor
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m38s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m26s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 51s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped

- Add reference-equality cleanup guard to enqueuePickNotification so
  leagueChains entries are deleted once a league's chain settles (matching
  the old queue's finally-block cleanup), while correctly skipping deletion
  if a new pick was enqueued before the current one resolved
- Use getTeamForPick from ~/lib/draft-order in draft.make-pick.ts and
  draft.force-manual-pick.ts instead of the inline calculatePickInfo +
  draftSlots.find pattern (draft-utils.ts has a same-named local function
  with a different signature so it keeps the two-liner)
- Replace pickedTeam?.name ?? teamId UUID fallback in force-manual-pick
  with expectedDraftSlot.team.name, which is already validated non-null
  by the route's own guard; also removes two duplicate draftSlots.find
  calls that re-searched for a slot already in hand

https://claude.ai/code/session_01GCkguG2muQwnh3WTvENrwJ
This commit is contained in:
Claude 2026-05-29 16:15:31 +00:00
parent ad10bd3f7d
commit 5506af65fd
No known key found for this signature in database
3 changed files with 12 additions and 10 deletions

View file

@ -8,6 +8,7 @@ import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/d
import { getParticipantsForSeasonWithSports } from "~/models/season-participant"; import { getParticipantsForSeasonWithSports } from "~/models/season-participant";
import { getSeasonSportsSimple } from "~/models/season-sport"; import { getSeasonSportsSimple } from "~/models/season-sport";
import { calculatePickInfo, checkAndTriggerNextAutodraft } from "~/models/draft-utils"; import { calculatePickInfo, checkAndTriggerNextAutodraft } from "~/models/draft-utils";
import { getTeamForPick } from "~/lib/draft-order";
import { enqueuePickNotification } from "~/services/discord"; import { enqueuePickNotification } from "~/services/discord";
import { notifyPickMadeOnDiscord } from "~/services/draft-discord.server"; import { notifyPickMadeOnDiscord } from "~/services/draft-discord.server";
import { logCommissionerAction } from "~/models/audit-log"; import { logCommissionerAction } from "~/models/audit-log";
@ -240,7 +241,7 @@ export async function action(args: ActionFunctionArgs) {
// Emit socket event // Emit socket event
try { try {
const io = getSocketIO(); const io = getSocketIO();
const team = draftSlots.find((slot) => slot.team.id === teamId)?.team; const team = expectedDraftSlot.team;
io.to(`draft-${seasonId}`).emit("pick-made", { io.to(`draft-${seasonId}`).emit("pick-made", {
pick: { pick: {
@ -264,16 +265,13 @@ export async function action(args: ActionFunctionArgs) {
logger.error("Socket.IO error:", error); logger.error("Socket.IO error:", error);
} }
const pickedTeam = draftSlots.find((slot) => slot.team.id === teamId)?.team;
// Announce pick on Discord // Announce pick on Discord
const { pickInRound: nextPickInRound } = calculatePickInfo(nextPickNumber, totalTeams); const nextSlot = !isDraftComplete ? getTeamForPick(nextPickNumber, draftSlots) : undefined;
const nextSlot = !isDraftComplete ? draftSlots.find((s) => s.draftOrder === nextPickInRound) : undefined;
enqueuePickNotification(season.leagueId, () => enqueuePickNotification(season.leagueId, () =>
notifyPickMadeOnDiscord({ notifyPickMadeOnDiscord({
seasonId, seasonId,
leagueId: season.leagueId, leagueId: season.leagueId,
pickedTeamName: pickedTeam?.name ?? teamId, pickedTeamName: expectedDraftSlot.team.name,
participantName: participant.name, participantName: participant.name,
sportName: participant.sportsSeason.sport.name, sportName: participant.sportsSeason.sport.name,
pickNumber, pickNumber,
@ -295,7 +293,7 @@ export async function action(args: ActionFunctionArgs) {
details: { details: {
pickNumber, pickNumber,
teamId, teamId,
teamName: pickedTeam?.name ?? teamId, teamName: expectedDraftSlot.team.name,
participantId, participantId,
participantName: participant.name, participantName: participant.name,
}, },

View file

@ -1,4 +1,5 @@
import { auth } from "~/lib/auth.server"; import { auth } from "~/lib/auth.server";
import { getTeamForPick } from "~/lib/draft-order";
import { database } from "~/database/context"; import { database } from "~/database/context";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
import { eq, and, sql } from "drizzle-orm"; import { eq, and, sql } from "drizzle-orm";
@ -306,8 +307,7 @@ export async function action(args: ActionFunctionArgs) {
// Announce before triggering the chain so Discord messages arrive in pick-number // 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. // order. If the chain ran first, chained picks would announce before this one.
const { pickInRound: nextPickInRound } = calculatePickInfo(nextPickNumber, totalTeams); const nextSlot = !isDraftComplete ? getTeamForPick(nextPickNumber, draftSlots) : undefined;
const nextSlot = !isDraftComplete ? draftSlots.find((s) => s.draftOrder === nextPickInRound) : undefined;
enqueuePickNotification(season.leagueId, () => enqueuePickNotification(season.leagueId, () =>
notifyPickMadeOnDiscord({ notifyPickMadeOnDiscord({
seasonId, seasonId,

View file

@ -20,7 +20,11 @@ const leagueChains = new Map<string, Promise<void>>();
export function enqueuePickNotification(leagueId: string, fn: () => Promise<void>): void { export function enqueuePickNotification(leagueId: string, fn: () => Promise<void>): void {
const prev = leagueChains.get(leagueId) ?? Promise.resolve(); const prev = leagueChains.get(leagueId) ?? Promise.resolve();
leagueChains.set(leagueId, prev.then(() => fn().catch(() => {}))); const next = prev.then(() => fn().catch(() => {}));
leagueChains.set(leagueId, next);
// Clean up the map entry once the chain settles, but only if no newer pick
// was enqueued after this one (reference equality guards against that race).
next.then(() => { if (leagueChains.get(leagueId) === next) leagueChains.delete(leagueId); });
} }
export async function sendDiscordWebhook( export async function sendDiscordWebhook(