import { db } from "@lila/db"; import { lobbies, lobby_players } from "@lila/db/schema"; import { eq, and, sql } from "drizzle-orm"; import type { LobbyStatus } from "@lila/shared"; export type Lobby = typeof lobbies.$inferSelect; export type LobbyPlayer = typeof lobby_players.$inferSelect; export type LobbyWithPlayers = Lobby & { players: (LobbyPlayer & { user: { id: string; name: string } })[]; }; export const createLobby = async ( code: string, hostUserId: string, ): Promise => { const [newLobby] = await db .insert(lobbies) .values({ code, hostUserId, status: "waiting" }) .returning(); if (!newLobby) { throw new Error("Failed to create lobby"); } return newLobby; }; export const getLobbyByCodeWithPlayers = async ( code: string, ): Promise => { return db.query.lobbies.findFirst({ where: eq(lobbies.code, code), with: { players: { with: { user: { columns: { id: true, name: true } } } }, }, }); }; export const getLobbyByIdWithPlayers = async ( lobbyId: string, ): Promise => { return db.query.lobbies.findFirst({ where: eq(lobbies.id, lobbyId), with: { players: { with: { user: { columns: { id: true, name: true } } } }, }, }); }; export const updateLobbyStatus = async ( lobbyId: string, status: LobbyStatus, ): Promise => { await db.update(lobbies).set({ status }).where(eq(lobbies.id, lobbyId)); }; export const deleteLobby = async (lobbyId: string): Promise => { await db.delete(lobbies).where(eq(lobbies.id, lobbyId)); }; /** * Atomically inserts a player into a lobby. Returns the new player row, * or undefined if the insert was skipped because: * - the lobby is at capacity, or * - the lobby is not in 'waiting' status, or * - the user is already in the lobby (PK conflict). * * Callers are expected to pre-check these conditions against a hydrated * lobby state to produce specific error messages; the undefined return * is a safety net for concurrent races. */ export const addPlayer = async ( lobbyId: string, userId: string, maxPlayers: number, ): Promise => { const result = await db.execute(sql` INSERT INTO lobby_players (lobby_id, user_id) SELECT ${lobbyId}::uuid, ${userId} WHERE ( SELECT COUNT(*) FROM lobby_players WHERE lobby_id = ${lobbyId}::uuid ) < ${maxPlayers} AND EXISTS ( SELECT 1 FROM lobbies WHERE id = ${lobbyId}::uuid AND status = 'waiting' ) ON CONFLICT (lobby_id, user_id) DO NOTHING `); if (!result.rowCount) return undefined; const [player] = await db .select() .from(lobby_players) .where( and(eq(lobby_players.lobbyId, lobbyId), eq(lobby_players.userId, userId)), ); return player; }; export const removePlayer = async ( lobbyId: string, userId: string, ): Promise => { await db .delete(lobby_players) .where( and(eq(lobby_players.lobbyId, lobbyId), eq(lobby_players.userId, userId)), ); }; export const finishGame = async ( lobbyId: string, scoresByUser: Map, ): Promise => { await db.transaction(async (tx) => { for (const [userId, score] of scoresByUser) { await tx .update(lobby_players) .set({ score }) .where( and( eq(lobby_players.lobbyId, lobbyId), eq(lobby_players.userId, userId), ), ); } await tx .update(lobbies) .set({ status: "finished" }) .where(eq(lobbies.id, lobbyId)); }); };