Fix slow bracket save and stale projected points #54
3 changed files with 61 additions and 22 deletions
|
|
@ -26,7 +26,10 @@ import {
|
|||
getQPStandings,
|
||||
updateFinalRankings,
|
||||
} from "./qualifying-points";
|
||||
import { getParticipantEV } from "./participant-expected-value";
|
||||
import {
|
||||
getParticipantEV,
|
||||
type ParticipantEV,
|
||||
} from "./participant-expected-value";
|
||||
import { calculateEV } from "~/services/ev-calculator";
|
||||
|
||||
/**
|
||||
|
|
@ -165,7 +168,7 @@ function getRoundConfig(
|
|||
export async function processPlayoffEvent(
|
||||
eventId: string,
|
||||
providedDb?: ReturnType<typeof database>,
|
||||
options?: { skipRecalculate?: boolean }
|
||||
options?: { skipRecalculate?: boolean; skipProbabilities?: boolean }
|
||||
): Promise<void> {
|
||||
const db = providedDb || database();
|
||||
|
||||
|
|
@ -358,23 +361,25 @@ export async function processPlayoffEvent(
|
|||
}
|
||||
}
|
||||
|
||||
// Update probabilities first so the standings recalc reads fresh EVs.
|
||||
if (!options?.skipProbabilities) {
|
||||
try {
|
||||
await updateProbabilitiesAfterResult(event.sportsSeasonId, true);
|
||||
logger.log(
|
||||
`[ScoringCalculator] Auto-updated probabilities for sports season ${event.sportsSeasonId}`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[ScoringCalculator] Failed to auto-update probabilities for sports season ${event.sportsSeasonId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate standings for all affected leagues (skipped when caller handles it)
|
||||
if (!options?.skipRecalculate) {
|
||||
await recalculateAffectedLeagues(event.sportsSeasonId, db, { eventName: event.name, eventId });
|
||||
}
|
||||
|
||||
// Auto-trigger probability recalculation after result
|
||||
try {
|
||||
await updateProbabilitiesAfterResult(event.sportsSeasonId, true);
|
||||
logger.log(
|
||||
`[ScoringCalculator] Auto-updated probabilities for sports season ${event.sportsSeasonId}`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[ScoringCalculator] Failed to auto-update probabilities for sports season ${event.sportsSeasonId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -489,7 +494,8 @@ export async function processMatchResult(
|
|||
const sideEffectOptions = (eventName || eventId)
|
||||
? { eventName, eventId, matchIds: matchId ? [matchId] : undefined }
|
||||
: undefined;
|
||||
await recalculateAffectedLeagues(sportsSeasonId, db, sideEffectOptions);
|
||||
// Update probabilities first so the standings recalc reads fresh EVs and
|
||||
// projected points reflect the new result.
|
||||
try {
|
||||
await updateProbabilitiesAfterResult(sportsSeasonId, true);
|
||||
} catch (error) {
|
||||
|
|
@ -498,6 +504,7 @@ export async function processMatchResult(
|
|||
error
|
||||
);
|
||||
}
|
||||
await recalculateAffectedLeagues(sportsSeasonId, db, sideEffectOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1061,7 +1068,8 @@ export async function calculateTeamScore(
|
|||
export async function calculateTeamProjectedScore(
|
||||
teamId: string,
|
||||
seasonId: string,
|
||||
providedDb?: ReturnType<typeof database>
|
||||
providedDb?: ReturnType<typeof database>,
|
||||
evByParticipantId?: Map<string, ParticipantEV>
|
||||
): Promise<{
|
||||
actualPoints: number;
|
||||
projectedPoints: number;
|
||||
|
|
@ -1185,7 +1193,9 @@ export async function calculateTeamProjectedScore(
|
|||
let evSum = 0;
|
||||
if (unfinishedParticipants.length > 0) {
|
||||
for (const { participantId, sportsSeasonId, floorPoints } of unfinishedParticipants) {
|
||||
const ev = await getParticipantEV(participantId, sportsSeasonId);
|
||||
const ev = evByParticipantId
|
||||
? evByParticipantId.get(participantId) ?? null
|
||||
: await getParticipantEV(participantId, sportsSeasonId);
|
||||
|
||||
if (ev) {
|
||||
// EV is already calculated with default scoring (100/70/50/40/25/25/15/15)
|
||||
|
|
@ -1317,11 +1327,32 @@ export async function recalculateStandings(
|
|||
where: eq(schema.teams.seasonId, seasonId),
|
||||
});
|
||||
|
||||
// Pre-fetch every EV referenced by this season's draft picks in one query, then
|
||||
// pass the map into calculateTeamProjectedScore to avoid an N+1 per team.
|
||||
const evByParticipantId = new Map<string, ParticipantEV>();
|
||||
const allPicks = await db.query.draftPicks.findMany({
|
||||
where: eq(schema.draftPicks.seasonId, seasonId),
|
||||
columns: { participantId: true },
|
||||
with: { participant: { columns: { sportsSeasonId: true } } },
|
||||
});
|
||||
if (allPicks.length > 0) {
|
||||
const sportsSeasonIds = Array.from(
|
||||
new Set(allPicks.map((p) => p.participant.sportsSeasonId))
|
||||
);
|
||||
const evRows = await db
|
||||
.select()
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(inArray(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonIds));
|
||||
for (const ev of evRows) {
|
||||
evByParticipantId.set(ev.participantId, ev);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate scores for all teams
|
||||
const teamScores = await Promise.all(
|
||||
teams.map(async (team) => {
|
||||
const score = await calculateTeamScore(team.id, seasonId, db);
|
||||
const projected = await calculateTeamProjectedScore(team.id, seasonId, db);
|
||||
const projected = await calculateTeamProjectedScore(team.id, seasonId, db, evByParticipantId);
|
||||
return {
|
||||
teamId: team.id,
|
||||
totalPoints: score.totalPoints,
|
||||
|
|
|
|||
|
|
@ -238,7 +238,9 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
.set({ playoffRound: round, updatedAt: new Date() })
|
||||
.where(eq(schema.scoringEvents.id, eventId));
|
||||
|
||||
await processPlayoffEvent(eventId, db, { skipRecalculate: true });
|
||||
// skipProbabilities=true: callers (set-winner / set-round-winners) already ran
|
||||
// updateProbabilitiesAfterResult before reaching here, so re-running would be wasted.
|
||||
await processPlayoffEvent(eventId, db, { skipRecalculate: true, skipProbabilities: true });
|
||||
logger.log(`[AutoComplete] Round "${round}" auto-completed for event ${eventId}`);
|
||||
}
|
||||
|
||||
|
|
@ -444,12 +446,14 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
// previously completed matches in the event.
|
||||
if (successCount > 0) {
|
||||
const db = database();
|
||||
await recalculateAffectedLeagues(event.sportsSeasonId, db, { eventId: event.id, eventName: event.name ?? undefined, matchIds: processedMatchIds });
|
||||
// Update probabilities first so recalculateAffectedLeagues reads fresh EVs
|
||||
// when computing projected points.
|
||||
try {
|
||||
await updateProbabilitiesAfterResult(event.sportsSeasonId, true);
|
||||
} catch (error) {
|
||||
logger.error(`Error updating probabilities after batch round winners:`, error);
|
||||
}
|
||||
await recalculateAffectedLeagues(event.sportsSeasonId, db, { eventId: event.id, eventName: event.name ?? undefined, matchIds: processedMatchIds });
|
||||
await autoCompleteRoundIfDone(event.id, round, event.sportsSeasonId, db);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,9 @@ export async function updateProbabilitiesAfterResult(
|
|||
pointsFor8th: 15,
|
||||
};
|
||||
|
||||
// Sequential: upsertParticipantEV calls syncVorpForSeason internally, which
|
||||
// reads and rewrites every seasonParticipants row for this sportsSeasonId.
|
||||
// Running these in parallel would race on that shared state.
|
||||
for (const [participantId, finalPosition] of finishedMap.entries()) {
|
||||
try {
|
||||
const probs = createFinishedProbabilities(finalPosition);
|
||||
|
|
@ -185,7 +188,8 @@ export async function updateProbabilitiesAfterResult(
|
|||
// Recalculate ICM for unfinished participants
|
||||
const icmResults = calculateICMFromOdds(unfinishedOdds);
|
||||
|
||||
// Update each unfinished participant
|
||||
// Sequential for the same reason as the finished loop above:
|
||||
// upsertParticipantEV rewrites shared per-season state via syncVorpForSeason.
|
||||
for (const [participantId, icmResult] of icmResults.entries()) {
|
||||
try {
|
||||
const probs = [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue