Replace LobbyRepository's three single-use edit writes with one transactional edit

This commit is contained in:
Kalle
2026-11-08 15:13:06 +02:00
parent 405b60ba89
commit b1698b552f
5 changed files with 125 additions and 102 deletions

View File

@@ -13,6 +13,7 @@ import * as LobbySet from "./core/LobbySet";
import "./core/LobbyProgression.server";
import * as LobbyMatchRepository from "./LobbyMatchRepository.server";
import * as LobbyRepository from "./LobbyRepository.server";
import { editArgs } from "./tests/fixtures";
const users = UserFactory.pool();
@@ -205,14 +206,12 @@ describe("LobbyRepository visibility", () => {
expect(Boolean(found)).toBe(expected);
});
test("updateVisibility: opening a lobby to everyone shows it to outsiders", async () => {
test("edit: opening a lobby to everyone shows it to outsiders", async () => {
const lobby = await associationLobby();
await LobbyRepository.updateVisibility({
id: lobby.id,
visibility: null,
ownerId: hostId(),
});
await LobbyRepository.edit(
editArgs({ id: lobby.id, ownerId: hostId(), visibility: null }),
);
const found = await withUserId(outsiderId(), () =>
LobbyRepository.findById(lobby.id),
@@ -220,15 +219,13 @@ describe("LobbyRepository visibility", () => {
expect(found?.isPrivate).toBe(false);
});
test("updateVisibility: making a lobby link-only keeps its share key", async () => {
test("edit: making a lobby link-only keeps its share key", async () => {
const lobby = await LobbyFactory.create({ hostUserId: hostId() });
const shareKey = await hostShareKeyOf(lobby.id);
await LobbyRepository.updateVisibility({
id: lobby.id,
visibility: "LINK_ONLY",
ownerId: hostId(),
});
await LobbyRepository.edit(
editArgs({ id: lobby.id, ownerId: hostId(), visibility: "LINK_ONLY" }),
);
expect(await LobbyRepository.isLinkOnlyById(lobby.id)).toBe(true);
expect(await hostShareKeyOf(lobby.id)).toBe(shareKey);

View File

@@ -752,8 +752,7 @@ export function insert({
.insertInto("Lobby")
.values({
...args,
modes: JSON.stringify(modes),
mapPool: mapPool ? JSON.stringify(mapPool) : null,
...serializeMaps({ modes, mapPool }),
isApplicationRequired: toDBBoolean(isApplicationRequired),
chatRoomId: chatRoom.id,
shareKey: shortNanoid(),
@@ -761,16 +760,14 @@ export function insert({
.returning(["id", "createdAt"])
.executeTakeFirstOrThrow();
if (visibility !== "LINK_ONLY") {
await trx
.insertInto("ContentVisibility")
.values(
Visibility.toRows(visibility, {
createdAt: lobby.createdAt,
ownerId: args.hostUserId,
}).map((row) => ({ ...row, lobbyId: lobby.id })),
)
.execute();
const rows = visibilityRows({
visibility,
lobbyId: lobby.id,
ownerId: args.hostUserId,
createdAt: lobby.createdAt,
});
if (rows.length > 0) {
await trx.insertInto("ContentVisibility").values(rows).execute();
}
await trx
@@ -984,73 +981,59 @@ export function kickParticipant(args: ParticipantRemoval) {
});
}
/** Updates the settings the host can change after creating the lobby. */
export async function updateSettings({
id,
isApplicationRequired,
...values
}: Pick<
type EditArgs = Pick<
Tables["Lobby"],
"id" | "name" | "description" | "label" | "bestOf" | "weaponRandomizer"
| "id"
| "name"
| "description"
| "label"
| "bestOf"
| "weaponRandomizer"
| "modes"
| "mapPool"
| "mapsTournamentId"
> & {
isApplicationRequired: boolean;
}) {
await db
.updateTable("Lobby")
.set({
...values,
isApplicationRequired: toDBBoolean(isApplicationRequired),
})
.where("Lobby.id", "=", id)
.execute();
}
/** Replaces where the lobby's sets get their maps from: the modes of the players' preferences or a map pool. */
export async function updateMaps({
id,
modes,
mapPool,
mapsTournamentId,
}: Pick<Tables["Lobby"], "id" | "modes" | "mapPool" | "mapsTournamentId">) {
await db
.updateTable("Lobby")
.set({
modes: JSON.stringify(modes),
mapPool: mapPool ? JSON.stringify(mapPool) : null,
mapsTournamentId,
})
.where("Lobby.id", "=", id)
.execute();
}
/** Replaces who sees the lobby, its new audience in effect right away. Its share key keeps working. */
export function updateVisibility({
id,
visibility,
ownerId,
}: {
id: number;
visibility: InsertArgs["visibility"];
/** The host, whose friends a friends only lobby is visible to */
ownerId: number;
}) {
};
/** Saves the settings the host can change after creating the lobby, the new audience in effect right away. Its share key keeps working. */
export function edit({
id,
ownerId,
visibility,
modes,
mapPool,
isApplicationRequired,
...values
}: EditArgs) {
return db.transaction().execute(async (trx) => {
await trx
.updateTable("Lobby")
.set({
...values,
...serializeMaps({ modes, mapPool }),
isApplicationRequired: toDBBoolean(isApplicationRequired),
})
.where("Lobby.id", "=", id)
.execute();
await trx
.deleteFrom("ContentVisibility")
.where("ContentVisibility.lobbyId", "=", id)
.execute();
if (visibility === "LINK_ONLY") return;
await trx
.insertInto("ContentVisibility")
.values(
Visibility.toRows(visibility, {
createdAt: databaseTimestampNow(),
ownerId,
}).map((row) => ({ ...row, lobbyId: id })),
)
.execute();
const rows = visibilityRows({
visibility,
lobbyId: id,
ownerId,
createdAt: databaseTimestampNow(),
});
if (rows.length > 0) {
await trx.insertInto("ContentVisibility").values(rows).execute();
}
});
}
@@ -1264,3 +1247,32 @@ async function deleteParticipantEndingSet(
return setEnding;
}
function serializeMaps({
modes,
mapPool,
}: Pick<Tables["Lobby"], "modes" | "mapPool">) {
return {
modes: JSON.stringify(modes),
mapPool: mapPool ? JSON.stringify(mapPool) : null,
};
}
function visibilityRows({
visibility,
lobbyId,
ownerId,
createdAt,
}: {
visibility: InsertArgs["visibility"];
lobbyId: number;
ownerId: number;
createdAt: number;
}) {
if (visibility === "LINK_ONLY") return [];
return Visibility.toRows(visibility, { createdAt, ownerId }).map((row) => ({
...row,
lobbyId,
}));
}

View File

@@ -312,7 +312,9 @@ export const action = defineAction(
});
if (issue) return { fieldErrors: { [issue.path[0]]: issue.message } };
await LobbyRepository.updateSettings({
const maps = await mapsFromForm(body);
await LobbyRepository.edit({
id,
name: body.name ?? `${lobby.hostUsername}'s lobby`,
description: body.description,
@@ -320,13 +322,7 @@ export const action = defineAction(
bestOf: Number(body.bestOf) as LobbyBestOf,
weaponRandomizer: body.weaponRandomizer,
isApplicationRequired: body.isApplicationRequired,
});
await LobbyRepository.updateMaps({
id,
...(await mapsFromForm(body)),
});
await LobbyRepository.updateVisibility({
id,
...maps,
visibility: visibilityFromForm(body),
ownerId: lobby.hostUserId,
});

View File

@@ -9,6 +9,7 @@ import { withUserId } from "~/utils/Test";
import * as LobbyMatchRepository from "../LobbyMatchRepository.server";
import * as LobbyRepository from "../LobbyRepository.server";
import type { LobbyWeaponRandomizer } from "../lobbies-constants";
import { editArgs } from "../tests/fixtures";
import * as LobbyProgression from "./LobbyProgression.server";
import * as LobbySet from "./LobbySet";
@@ -186,15 +187,9 @@ describe("LobbyProgression.advance", () => {
});
const set = await latestSet(lobbyId);
await LobbyRepository.updateSettings({
id: lobbyId,
name: "Lobby",
description: null,
label: "CASUAL",
bestOf: 3,
weaponRandomizer: "OFF",
isApplicationRequired: false,
});
await LobbyRepository.edit(
editArgs({ id: lobbyId, ownerId: hostId(), weaponRandomizer: "OFF" }),
);
await LobbyProgression.advance(lobbyId);
expect((await latestSet(lobbyId)).rolledWeapons).toEqual(set.rolledWeapons);
@@ -212,12 +207,9 @@ describe("LobbyProgression.advance", () => {
{ mode: "TC" as const, stageId: 3 as const },
];
await LobbyRepository.updateMaps({
id: lobbyId,
modes: ["TC"],
mapPool,
mapsTournamentId: null,
});
await LobbyRepository.edit(
editArgs({ id: lobbyId, ownerId: hostId(), modes: ["TC"], mapPool }),
);
await playOut(lobbyId);
const { games } = await latestSet(lobbyId);

View File

@@ -0,0 +1,26 @@
import type * as LobbyRepository from "../LobbyRepository.server";
type EditArgs = Parameters<typeof LobbyRepository.edit>[0];
/** `LobbyRepository.edit` args of a public lobby with `LobbyFactory`'s default settings, overridden by the given ones. */
export function editArgs({
id,
ownerId,
...overrides
}: Pick<EditArgs, "id" | "ownerId"> & Partial<EditArgs>): EditArgs {
return {
id,
ownerId,
name: "Lobby",
description: null,
label: "CASUAL",
bestOf: 3,
weaponRandomizer: "OFF",
isApplicationRequired: false,
modes: ["SZ", "TC", "RM", "CB"],
mapPool: null,
mapsTournamentId: null,
visibility: null,
...overrides,
};
}