From 8c03043f0804873c99ec034c1adb18d25e612b21 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:25:14 +0300 Subject: [PATCH] Fix server restarting mid-set changing map list --- app/db/seed/factories/TournamentFactory.ts | 2 +- .../api-public/routes/tournament-match.$id.ts | 1 + .../tournament-match/core/mapList.server.ts | 1 + .../loaders/to.$id.matches.$mid.server.ts | 1 + .../TournamentTeamRepository.server.test.ts | 66 +++++++++++++++++++ .../TournamentTeamRepository.server.ts | 16 ++++- 6 files changed, 83 insertions(+), 4 deletions(-) diff --git a/app/db/seed/factories/TournamentFactory.ts b/app/db/seed/factories/TournamentFactory.ts index 866e0c242..75fac5c79 100644 --- a/app/db/seed/factories/TournamentFactory.ts +++ b/app/db/seed/factories/TournamentFactory.ts @@ -44,7 +44,7 @@ const ROUND_MAPS = { } satisfies RoundMaps; /** The maps every round of a factory-started bracket is played on. */ -type RoundMaps = Omit; +export type RoundMaps = Omit; /** The wrapping calendar event is not the caller's to choose, so it is not an argument. */ type InsertArgs = Omit< diff --git a/app/features/api-public/routes/tournament-match.$id.ts b/app/features/api-public/routes/tournament-match.$id.ts index c7a4aceaa..e531d312f 100644 --- a/app/features/api-public/routes/tournament-match.$id.ts +++ b/app/features/api-public/routes/tournament-match.$id.ts @@ -123,6 +123,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { match.mapPickingStyle !== "TO" ? await TournamentTeamRepository.findRecentlyPlayedMapsByIds({ teamIds: [opponentOne.id, opponentTwo.id], + excludeMatchId: id, }).catch((error) => { logger.error("Failed to fetch recently played maps", error); return []; diff --git a/app/features/tournament-match/core/mapList.server.ts b/app/features/tournament-match/core/mapList.server.ts index 0bb782fe1..35645faa7 100644 --- a/app/features/tournament-match/core/mapList.server.ts +++ b/app/features/tournament-match/core/mapList.server.ts @@ -117,6 +117,7 @@ export async function resolveMatchMapList({ match.mapPickingStyle !== "TO" ? await TournamentTeamRepository.findRecentlyPlayedMapsByIds({ teamIds: teams, + excludeMatchId: match.id, }).catch((error) => { logger.error("Failed to fetch recently played maps", error); return []; diff --git a/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts b/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts index 208ba476e..368f62965 100644 --- a/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts +++ b/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts @@ -143,6 +143,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { match.mapPickingStyle !== "TO" ? await TournamentTeamRepository.findRecentlyPlayedMapsByIds({ teamIds: [match.opponentOne.id, match.opponentTwo.id], + excludeMatchId: matchId, }).catch((error) => { logger.error("Failed to fetch recently played maps", error); return []; diff --git a/app/features/tournament/TournamentTeamRepository.server.test.ts b/app/features/tournament/TournamentTeamRepository.server.test.ts index 697d91629..671b9087e 100644 --- a/app/features/tournament/TournamentTeamRepository.server.test.ts +++ b/app/features/tournament/TournamentTeamRepository.server.test.ts @@ -3,9 +3,40 @@ import * as TournamentFactory from "~/db/seed/factories/TournamentFactory"; import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { db } from "~/db/sql"; +import type { TournamentSettings } from "~/db/tables-json"; import { withUserId } from "~/utils/Test"; import * as TournamentTeamRepository from "./TournamentTeamRepository.server"; +const TEAM_COUNT = 4; + +/** Pools of two teams each, followed by a final between the two pool winners. */ +const POOLS_TO_FINAL: TournamentSettings["bracketProgression"] = [ + { + name: "Pools", + type: "round_robin", + requiresCheckIn: false, + settings: { teamsPerGroup: 2 }, + }, + { + name: "Final", + type: "single_elimination", + requiresCheckIn: false, + settings: { thirdPlaceMatch: false }, + sources: [{ bracketIdx: 0, placements: [1] }], + }, +]; + +const POOL_MAPS: TournamentFactory.RoundMaps = { + count: 1, + type: "BEST_OF", + list: [{ mode: "SZ", stageId: 1 }], +}; +const FINAL_MAPS: TournamentFactory.RoundMaps = { + count: 1, + type: "BEST_OF", + list: [{ mode: "TC", stageId: 2 }], +}; + let organizer: { id: number }; let owner: { id: number }; let member: { id: number }; @@ -285,4 +316,39 @@ describe("TournamentTeamRepository", () => { ).toBe(0); }); }); + + describe("findRecentlyPlayedMapsByIds", () => { + test("leaves out the games of the match the maps are resolved for", async () => { + // the map list of an in-progress set is regenerated whenever its cache entry + // is lost, so counting the set's own games as recently played would change + // the maps the teams have left to play under them + const players = await UserFactory.createMany(TEAM_COUNT); + const tournament = await TournamentFactory.createPlayed( + { + authorId: organizer.id, + bracketProgression: POOLS_TO_FINAL, + minMembersPerTeam: 1, + }, + { + teamRosters: players.map((player) => [player.id]), + playedOut: 0, + maps: POOL_MAPS, + }, + ); + const [final] = await TournamentFactory.playOut(tournament.id, 1, { + maps: FINAL_MAPS, + }); + + const recentMaps = + await TournamentTeamRepository.findRecentlyPlayedMapsByIds({ + teamIds: [final.winnerTeamId, final.loserTeamId], + excludeMatchId: final.id, + }); + + expect(recentMaps).toEqual([ + { mode: "SZ", stageId: 1 }, + { mode: "SZ", stageId: 1 }, + ]); + }); + }); }); diff --git a/app/features/tournament/TournamentTeamRepository.server.ts b/app/features/tournament/TournamentTeamRepository.server.ts index 59953bf84..61a64573c 100644 --- a/app/features/tournament/TournamentTeamRepository.server.ts +++ b/app/features/tournament/TournamentTeamRepository.server.ts @@ -999,7 +999,11 @@ async function replaceCounterpickMaps( .execute(); } -async function findTeamRecentMaps(teamId: number, limit: number) { +async function findTeamRecentMaps( + teamId: number, + excludeMatchId: number, + limit: number, +) { return db .selectFrom("TournamentMatchGameResult") .innerJoin( @@ -1012,6 +1016,7 @@ async function findTeamRecentMaps(teamId: number, limit: number) { "TournamentMatchGameResult.stageId", ]) .where("TournamentMatchGameResultParticipant.tournamentTeamId", "=", teamId) + .where("TournamentMatchGameResult.matchId", "!=", excludeMatchId) .orderBy("TournamentMatchGameResult.createdAt", "desc") .limit(limit) .execute(); @@ -1058,10 +1063,15 @@ export async function findMapPoolsByTeamIds(tournamentTeamIds: number[]) { export async function findRecentlyPlayedMapsByIds({ teamIds, + excludeMatchId, limit = 5, }: { /** Team IDs to retrieve recent maps for */ teamIds: [number, number]; + /** Match whose own games are left out, being the match the maps are resolved for. + * Without it a set's map list changes under the teams mid-set, as the games they + * already played would count as recently played when it is regenerated. */ + excludeMatchId: number; /** Limit of recent maps to retrieve per team * * @default 5 @@ -1069,8 +1079,8 @@ export async function findRecentlyPlayedMapsByIds({ limit?: number; }): Promise> { const [teamOneMaps, teamTwoMaps] = await Promise.all([ - findTeamRecentMaps(teamIds[0], limit), - findTeamRecentMaps(teamIds[1], limit), + findTeamRecentMaps(teamIds[0], excludeMatchId, limit), + findTeamRecentMaps(teamIds[1], excludeMatchId, limit), ]); return flatZip(teamOneMaps, teamTwoMaps);