diff --git a/app/features/sendouq/actions/q.looking.server.ts b/app/features/sendouq/actions/q.looking.server.ts index ee08b0bf2..e84448b1c 100644 --- a/app/features/sendouq/actions/q.looking.server.ts +++ b/app/features/sendouq/actions/q.looking.server.ts @@ -8,8 +8,14 @@ import { matchMapList, } from "~/features/sendouq-match/core/match.server"; import * as QRepository from "~/features/sendouq/QRepository.server"; +import type { LookingGroupWithInviteCode } from "~/features/sendouq/q-types"; import invariant from "~/utils/invariant"; -import { errorToastIfFalsy, parseRequestPayload } from "~/utils/remix.server"; +import { logger } from "~/utils/logger"; +import { + errorToast, + errorToastIfFalsy, + parseRequestPayload, +} from "~/utils/remix.server"; import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql"; import { assertUnreachable } from "~/utils/types"; import { SENDOUQ_PAGE, sendouQMatchPage } from "~/utils/urls"; @@ -232,6 +238,21 @@ export const action: ActionFunction = async ({ request }) => { ignoreModePreferences: data._action === "MATCH_UP_RECHALLENGE", }, ); + + const memberInManyGroups = verifyNoMemberInTwoGroups( + [...ourGroup.members, ...theirGroup.members], + lookingGroups, + ); + if (memberInManyGroups) { + logger.error("User in two groups preventing match creation", { + userId: memberInManyGroups.id, + }); + + errorToast( + `${memberInManyGroups.username} is in two groups so match can't be started`, + ); + } + const createdMatch = createMatch({ alphaGroupId: ourGroup.id, bravoGroupId: theirGroup.id, @@ -367,3 +388,23 @@ export const action: ActionFunction = async ({ request }) => { return null; }; + +/** Sanity check that no member is in two groups due to a bug or race condition. + * + * @returns null if no member is in two groups, otherwise return the problematic member + */ +function verifyNoMemberInTwoGroups( + members: LookingGroupWithInviteCode["members"], + allGroups: LookingGroupWithInviteCode[], +) { + for (const member of members) { + if ( + allGroups.filter((group) => group.members.some((m) => m.id === member.id)) + .length > 1 + ) { + return member; + } + } + + return null; +} diff --git a/app/utils/remix.server.ts b/app/utils/remix.server.ts index 4b5d2335e..1b58de3e7 100644 --- a/app/utils/remix.server.ts +++ b/app/utils/remix.server.ts @@ -55,7 +55,7 @@ export function parseSearchParams({ } catch (e) { logger.error("Error parsing search params", e); - throw errorToast("Validation failed"); + throw errorToastRedirect("Validation failed"); } } @@ -93,7 +93,7 @@ export async function parseRequestPayload({ } catch (e) { logger.error("Error parsing request payload", e); - throw errorToast("Validation failed"); + throw errorToastRedirect("Validation failed"); } } @@ -117,7 +117,7 @@ export async function parseFormData({ } catch (e) { logger.error("Error parsing form data", e); - throw errorToast("Validation failed"); + throw errorToastRedirect("Validation failed"); } } @@ -183,7 +183,7 @@ function formDataToObject(formData: FormData) { // TODO: investigate better solution to toasts when middlewares land (current one has a problem of clearing search params) -export function errorToast(message: string) { +export function errorToastRedirect(message: string) { return redirect(`?__error=${message}`); } @@ -194,7 +194,12 @@ export function errorToastIfFalsy( ): asserts condition { if (condition) return; - throw errorToast(message); + throw errorToastRedirect(message); +} + +/** Throws a redirect triggering an error toast with given message. */ +export function errorToast(message: string) { + throw errorToastRedirect(message); } export function successToast(message: string) {