import type { Transaction } from "kysely"; import { db } from "~/db/sql"; import type { DB, TablesInsertable } from "~/db/tables"; import { actorId } from "~/features/auth/core/user.server"; import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; import { databaseTimestampNow } from "~/utils/dates"; import * as Scrim from "./core/Scrim"; import * as ScrimMapByMap from "./core/ScrimMapByMap"; import * as ScrimMapListRepository from "./ScrimMapListRepository.server"; interface ReportMapArgs { scrimPostId: number; mapId: number; winnerSide: NonNullable; } /** * Marks an existing map as reported with the given winner side, and * (atomically) generates and inserts the next map for the scrim if no * unreported map is currently waiting. */ export async function reportMapAndGenerateNext( args: ReportMapArgs, ): Promise { await db.transaction().execute(async (trx) => { await trx .updateTable("ScrimMap") .set({ winnerSide: args.winnerSide, reportedAt: databaseTimestampNow(), reportedByUserId: actorId(), }) .where("id", "=", args.mapId) .where("reportedAt", "is", null) .execute(); await tryGenerateAndInsertNextMapInTrx(trx, args.scrimPostId); }); } /** * Reverses the most recent report: deletes the currently unreported map (the * auto-generated next slot, if any) and clears the winner/reportedAt fields on * the most recently reported map so it can be played again. */ export async function undoMostRecentMap(scrimPostId: number): Promise { await db.transaction().execute(async (trx) => { await trx .deleteFrom("ScrimMap") .where("scrimPostId", "=", scrimPostId) .where("reportedAt", "is", null) .execute(); const latestReported = await trx .selectFrom("ScrimMap") .select("id") .where("scrimPostId", "=", scrimPostId) .where("reportedAt", "is not", null) .orderBy("index", "desc") .limit(1) .executeTakeFirst(); if (!latestReported) return; await trx .updateTable("ScrimMap") .set({ reportedAt: null, winnerSide: null, reportedByUserId: null, }) .where("id", "=", latestReported.id) .execute(); }); } interface ReplaceCurrentMapArgs { scrimPostId: number; mode: ModeShort; stageId: StageId; } /** * Replaces the currently unreported map for the scrim with the given * mode/stage. Used by both the "replay previous map" and "pick a map" actions. * The current map's index is preserved. */ export async function replaceCurrentMap( args: ReplaceCurrentMapArgs, ): Promise { await db .updateTable("ScrimMap") .set({ mode: args.mode, stageId: args.stageId, }) .where("scrimPostId", "=", args.scrimPostId) .where("reportedAt", "is", null) .execute(); } /** Returns the scrim's maps ordered by index ascending. */ export function findMapsByScrimPostId(scrimPostId: number) { return db .selectFrom("ScrimMap") .select(["id", "index", "mode", "stageId", "winnerSide", "reportedAt"]) .where("scrimPostId", "=", scrimPostId) .orderBy("index", "asc") .execute(); } /** * If a pool can be derived from the submitted map lists and no unreported map * is currently waiting, generates and inserts the next map. Runs entirely * within the caller's transaction so the read of the existing maps and the * insert see a consistent snapshot and no two concurrent report/submit actions * can insert a "next" map at the same index. */ export async function tryGenerateAndInsertNextMapInTrx( trx: Transaction, scrimPostId: number, ): Promise { const mapLists = await ScrimMapListRepository.findMapListsByScrimPostId( scrimPostId, trx, ); if (mapLists.length === 0) return; const pool = ScrimMapByMap.unionPool(mapLists); if (pool.isEmpty()) return; const maps = await trx .selectFrom("ScrimMap") .select(["index", "mode", "stageId", "reportedAt"]) .where("scrimPostId", "=", scrimPostId) .execute(); if (maps.some((m) => m.reportedAt === null)) return; const next = ScrimMapByMap.generateNextMap({ pool, history: maps.map((m) => ({ mode: m.mode, stageId: m.stageId })), }); await trx .insertInto("ScrimMap") .values({ scrimPostId, index: Scrim.nextMapIndex(maps), mode: next.mode, stageId: next.stageId, }) .execute(); }