From b2ff16426d6d5cb6b4ee2e22464e7205ddf9ea84 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 19 Jul 2025 13:54:29 +0300 Subject: [PATCH] Fix Swiss generate match ups internal server error by introducing neverthrow --- .../actions/to.$id.brackets.server.ts | 9 ++++----- app/features/tournament-bracket/core/Swiss.test.ts | 2 +- app/features/tournament-bracket/core/Swiss.ts | 10 ++++++---- app/utils/remix.server.ts | 14 ++++++++++++++ package-lock.json | 14 +++++++++++++- package.json | 1 + 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/app/features/tournament-bracket/actions/to.$id.brackets.server.ts b/app/features/tournament-bracket/actions/to.$id.brackets.server.ts index 6b92d2bb3..1617fd17c 100644 --- a/app/features/tournament-bracket/actions/to.$id.brackets.server.ts +++ b/app/features/tournament-bracket/actions/to.$id.brackets.server.ts @@ -8,6 +8,7 @@ import * as TournamentRepository from "~/features/tournament/TournamentRepositor import * as Progression from "~/features/tournament-bracket/core/Progression"; import invariant from "~/utils/invariant"; import { + errorToastIfErr, errorToastIfFalsy, parseParams, parseRequestPayload, @@ -180,17 +181,15 @@ export const action: ActionFunction = async ({ params, request }) => { const bracket = tournament.bracketByIdx(data.bracketIdx); errorToastIfFalsy(bracket, "Bracket not found"); - errorToastIfFalsy( - bracket.type === "swiss", - "Can't advance non-swiss bracket", - ); const matches = Swiss.generateMatchUps({ bracket, groupId: data.groupId, }); - await TournamentRepository.insertSwissMatches(matches); + errorToastIfErr(matches); + + await TournamentRepository.insertSwissMatches(matches.value); break; } diff --git a/app/features/tournament-bracket/core/Swiss.test.ts b/app/features/tournament-bracket/core/Swiss.test.ts index 5da8cb3c3..e7efb5e3c 100644 --- a/app/features/tournament-bracket/core/Swiss.test.ts +++ b/app/features/tournament-bracket/core/Swiss.test.ts @@ -113,7 +113,7 @@ describe("Swiss", () => { const matches = Swiss.generateMatchUps({ bracket, groupId: 4443, - }); + })._unsafeUnwrap(); it("finds new opponents for each team in the last round", () => { for (const match of matches) { diff --git a/app/features/tournament-bracket/core/Swiss.ts b/app/features/tournament-bracket/core/Swiss.ts index 98a0dc53a..785014464 100644 --- a/app/features/tournament-bracket/core/Swiss.ts +++ b/app/features/tournament-bracket/core/Swiss.ts @@ -1,6 +1,7 @@ // separate from brackets-manager as this wasn't part of the original brackets-manager library import blossom from "edmonds-blossom-fixed"; +import { err, ok } from "neverthrow"; import * as R from "remeda"; import type { TournamentRepositoryInsertableMatch } from "~/features/tournament/TournamentRepository.server"; import { TOURNAMENT } from "~/features/tournament/tournament-constants"; @@ -173,18 +174,19 @@ export function generateMatchUps({ }: { bracket: Bracket; groupId: number; -}): Array { +}) { // lets consider only this groups matches // in the case that there are more than one group const groupsMatches = bracket.data.match.filter( (m) => m.group_id === groupId, ); - invariant(groupsMatches.length > 0, "No matches found for group"); + if (groupsMatches.length === 0) return err("No matches found for group"); + if (bracket.type !== "swiss") return err("Bracket is not Swiss type"); // new matches can't be generated till old are over if (!everyMatchOver(groupsMatches)) { - throw new Error("Not all matches are over"); + return err("Not all matches are over"); } const groupsTeams = groupsMatches @@ -247,7 +249,7 @@ export function generateMatchUps({ }), ); - return result; + return ok(result); } interface SwissPairingTeam { diff --git a/app/utils/remix.server.ts b/app/utils/remix.server.ts index 49d4bb131..d2eb0d3ea 100644 --- a/app/utils/remix.server.ts +++ b/app/utils/remix.server.ts @@ -8,6 +8,7 @@ import { import type { Params, UIMatch } from "@remix-run/react"; import type { Namespace, TFunction } from "i18next"; import { nanoid } from "nanoid"; +import type { Ok, Result } from "neverthrow"; import type { z } from "zod/v4"; import type { navItems } from "~/components/layout/nav-items"; import { s3UploadHandler } from "~/features/img-upload"; @@ -208,6 +209,19 @@ export function errorToastIfFalsy( throw errorToastRedirect(message); } +/** + * To be used in loader or action function. Asserts that the provided `Result` value is an `Ok` variant of the `neverthrow` library. + * + * If the value is an `Err`, shows an error toast to the user with the error message. The function will stop execution by throwing a redirect meaning it is safe to operate on the value after this function call. + */ +export function errorToastIfErr( + value: Result, +): asserts value is Ok { + if (value.isErr()) { + throw errorToastRedirect(value.error); + } +} + /** Throws a redirect triggering an error toast with given message. */ export function errorToast(message: string) { throw errorToastRedirect(message); diff --git a/package-lock.json b/package-lock.json index 7dab1f330..6d21129e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "lru-cache": "^11.1.0", "markdown-to-jsx": "^7.7.6", "nanoid": "^5.1.5", + "neverthrow": "^8.2.0", "node-cron": "3.0.3", "nprogress": "^0.2.0", "openskill": "^4.1.0", @@ -5645,7 +5646,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13125,6 +13125,18 @@ "node": ">= 0.6" } }, + "node_modules/neverthrow": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-8.2.0.tgz", + "integrity": "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.24.0" + } + }, "node_modules/node-abi": { "version": "3.74.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", diff --git a/package.json b/package.json index eef3c83b1..096743474 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "lru-cache": "^11.1.0", "markdown-to-jsx": "^7.7.6", "nanoid": "^5.1.5", + "neverthrow": "^8.2.0", "node-cron": "3.0.3", "nprogress": "^0.2.0", "openskill": "^4.1.0",