Skip to content

Commit

Permalink
Safe Loaders (#837)
Browse files Browse the repository at this point in the history
:shipit:
  • Loading branch information
lowtorola authored Oct 19, 2024
1 parent ba855bb commit d2b8322
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 246 deletions.
71 changes: 16 additions & 55 deletions frontend2/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
RouterProvider,
createBrowserRouter,
Navigate,
type LoaderFunction,
} from "react-router-dom";
import { DEFAULT_EPISODE } from "./utils/constants";
import EpisodeNotFound from "./views/EpisodeNotFound";
Expand All @@ -38,23 +37,20 @@ import CommonIssues from "./views/CommonIssues";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { toast, Toaster } from "react-hot-toast";
import { ResponseError } from "./api/_autogen/runtime";
import { loginCheck } from "./api/auth/authApi";
import { submissionsLoader } from "./api/loaders/submissionsLoader";
import { myTeamLoader } from "./api/loaders/myTeamLoader";
import { scrimmagingLoader } from "./api/loaders/scrimmagingLoader";
import { rankingsLoader } from "./api/loaders/rankingsLoader";
import { teamProfileLoader } from "./api/loaders/teamProfileLoader";
import { episodeInfoFactory } from "./api/episode/episodeFactories";
import { buildKey } from "./api/helpers";
import { queueLoader } from "./api/loaders/queueLoader";
import { tournamentsLoader } from "./api/loaders/tournamentsLoader";
import { tournamentLoader } from "./api/loaders/tournamentLoader";
import { homeLoader } from "./api/loaders/homeLoader";
import ErrorBoundary from "./views/ErrorBoundary";
import { myTeamFactory, searchTeamsFactory } from "api/team/teamFactories";
import PageNotFound from "views/PageNotFound";
import TeamProfile from "views/TeamProfile";
import { passwordForgotLoader } from "api/loaders/passwordForgotLoader";
import { homeIfLoggedIn } from "api/loaders/homeIfLoggedIn";
import { episodeLoader } from "api/loaders/episodeLoader";

const queryClient = new QueryClient({
queryCache: new QueryCache({
Expand All @@ -75,9 +71,6 @@ queryClient.setQueryDefaults(["episode"], {
queryClient.setQueryDefaults(["team"], { retry: false });
queryClient.setQueryDefaults(["user"], { retry: false });

// Run a check to see if the user has an invalid token
const loggedIn = await loginCheck(queryClient);

const App: React.FC = () => {
return (
<QueryClientProvider client={queryClient}>
Expand All @@ -92,58 +85,26 @@ const App: React.FC = () => {
);
};

const episodeLoader: LoaderFunction = async ({ params }) => {
// check if the episodeId is a valid one.
// if the episode is not found, throw an error.
const id = params.episodeId ?? "";
if (id === "") {
throw new ResponseError(
new Response("Episode not found.", { status: 404 }),
);
}

// Await the episode info so we can be sure that it exists.
const episodeInfo = await queryClient.ensureQueryData({
queryKey: buildKey(episodeInfoFactory.queryKey, { id }),
queryFn: async () => await episodeInfoFactory.queryFn({ id }),
staleTime: Infinity,
});

// Prefetch the top 10 ranked teams' rating histories.
void queryClient.ensureQueryData({
queryKey: buildKey(searchTeamsFactory.queryKey, {
episodeId: id,
page: 1,
}),
queryFn: async () =>
await searchTeamsFactory.queryFn(
{ episodeId: id, page: 1 },
queryClient,
false, // We don't want to prefetch teams 11-20
),
});

// Prefetch the user's team.
if (loggedIn) {
void queryClient.ensureQueryData({
queryKey: buildKey(myTeamFactory.queryKey, { episodeId: id }),
queryFn: async () => await myTeamFactory.queryFn({ episodeId: id }),
});
}

return episodeInfo;
};

const router = createBrowserRouter([
// Pages that should render without a sidebar/navbar
{ path: "/login", element: <Login />, errorElement: <ErrorBoundary /> },
{
path: "/login",
element: <Login />,
errorElement: <ErrorBoundary />,
loader: homeIfLoggedIn(queryClient),
},
{ path: "/logout", element: <Logout />, errorElement: <ErrorBoundary /> },
{ path: "/register", element: <Register />, errorElement: <ErrorBoundary /> },
{
path: "/register",
element: <Register />,
errorElement: <ErrorBoundary />,
loader: homeIfLoggedIn(queryClient),
},
{
path: "/password_forgot",
element: <PasswordForgot />,
errorElement: <ErrorBoundary />,
loader: passwordForgotLoader(queryClient),
loader: homeIfLoggedIn(queryClient),
},
{
path: "/password_change",
Expand All @@ -166,7 +127,7 @@ const router = createBrowserRouter([
element: <EpisodeLayout />,
errorElement: <EpisodeNotFound />,
path: "/:episodeId",
loader: episodeLoader,
loader: episodeLoader(queryClient),
children: [
{
// Pages that should only be visible when logged in
Expand Down
35 changes: 35 additions & 0 deletions frontend2/src/api/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Cookies from "js-cookie";
import { Configuration, type ResponseError } from "./_autogen";
import type {
PaginatedQueryFactory,
PaginatedQueryFuncBuilder,
PaginatedRequestMinimal,
PaginatedResultMinimal,
QueryFactory,
QueryKeyBuilder,
} from "./apiTypes";
import type { QueryClient, QueryKey } from "@tanstack/react-query";
Expand Down Expand Up @@ -84,3 +86,36 @@ export const prefetchNextPage = async <
}
}
};

const safeEnsureQueryDataHelper = async <T, K>(
request: T,
factory: QueryFactory<T, K> | PaginatedQueryFactory<T, K>,
queryClient: QueryClient,
): Promise<void> => {
try {
await queryClient.ensureQueryData({
queryKey: buildKey(factory.queryKey, request),
// TypeScript allows passing in extra params so this works for regular & paginated
queryFn: async () => await factory.queryFn(request, queryClient, true),
});
} catch (err) {
// This error will have toasted from the QueryClient if necessary
}
};

/**
* Given a request and query identification, safely ensure that the query data exists
* or is fetched with no risk of throwing a Runtime Error. If the request has a server
* error, it will be displayed as a toast.
* @param request the parameters of the request
* @param queryKey
* @param queryFn
* @param queryClient
*/
export const safeEnsureQueryData = <T, K>(
request: T,
factory: QueryFactory<T, K> | PaginatedQueryFactory<T, K>,
queryClient: QueryClient,
): void => {
void safeEnsureQueryDataHelper(request, factory, queryClient);
};
47 changes: 47 additions & 0 deletions frontend2/src/api/loaders/episodeLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { type QueryClient } from "@tanstack/react-query";
import { ResponseError } from "api/_autogen";
import { loginCheck } from "api/auth/authApi";
import { episodeInfoFactory } from "api/episode/episodeFactories";
import { buildKey, safeEnsureQueryData } from "api/helpers";
import { searchTeamsFactory, myTeamFactory } from "api/team/teamFactories";
import { type LoaderFunction } from "react-router-dom";

export const episodeLoader =
(queryClient: QueryClient): LoaderFunction =>
async ({ params }) => {
// check if the episodeId is a valid one.
// if the episode is not found, throw an error.
const id = params.episodeId ?? "";
if (id === "") {
throw new ResponseError(
new Response("Episode not found.", { status: 404 }),
);
}

// Run a check to see if the user has an invalid token
const loggedIn = await loginCheck(queryClient);

// Await the episode info so we can be sure that it exists
const episodeInfo = await queryClient.ensureQueryData({
queryKey: buildKey(episodeInfoFactory.queryKey, { id }),
queryFn: async () => await episodeInfoFactory.queryFn({ id }),
staleTime: Infinity,
});

// Prefetch the top 10 ranked teams' rating histories
safeEnsureQueryData(
{
episodeId: id,
page: 1,
},
searchTeamsFactory,
queryClient,
);

// Prefetch the user's team
if (loggedIn) {
safeEnsureQueryData({ episodeId: id }, myTeamFactory, queryClient);
}

return episodeInfo;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { loginCheck } from "api/auth/authApi";
import { type LoaderFunction } from "react-router-dom";
import { DEFAULT_EPISODE } from "utils/constants";

export const passwordForgotLoader =
export const homeIfLoggedIn =
(queryClient: QueryClient): LoaderFunction =>
async () => {
// Check if user is logged in
Expand Down
33 changes: 10 additions & 23 deletions frontend2/src/api/loaders/homeLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { QueryClient } from "@tanstack/react-query";
import type { LoaderFunction } from "react-router-dom";
import { buildKey } from "../helpers";
import { safeEnsureQueryData } from "../helpers";
import {
episodeInfoFactory,
nextTournamentFactory,
Expand All @@ -18,37 +18,24 @@ export const homeLoader =
if (episodeId === undefined) return null;

// Episode Info
void queryClient.ensureQueryData({
queryKey: buildKey(episodeInfoFactory.queryKey, { id: episodeId }),
queryFn: async () => await episodeInfoFactory.queryFn({ id: episodeId }),
});
safeEnsureQueryData({ id: episodeId }, episodeInfoFactory, queryClient);

// Next Tournament
void queryClient.ensureQueryData({
queryKey: buildKey(nextTournamentFactory.queryKey, { episodeId }),
queryFn: async () => await nextTournamentFactory.queryFn({ episodeId }),
});
safeEnsureQueryData({ episodeId }, nextTournamentFactory, queryClient);

// User Team Rating History
void queryClient.ensureQueryData({
queryKey: buildKey(ratingHistoryMeFactory.queryKey, { episodeId }),
queryFn: async () => await ratingHistoryMeFactory.queryFn({ episodeId }),
});
safeEnsureQueryData({ episodeId }, ratingHistoryMeFactory, queryClient);

// User Team Scrimmage Record
void queryClient.ensureQueryData({
queryKey: buildKey(scrimmagingRecordFactory.queryKey, {
safeEnsureQueryData(
{
episodeId,
scrimmageType:
CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum.All,
}),
queryFn: async () =>
await scrimmagingRecordFactory.queryFn({
episodeId,
scrimmageType:
CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum.All,
}),
});
},
scrimmagingRecordFactory,
queryClient,
);

return null;
};
39 changes: 14 additions & 25 deletions frontend2/src/api/loaders/myTeamLoader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { QueryClient } from "@tanstack/react-query";
import type { LoaderFunction } from "react-router-dom";
import { myTeamFactory } from "../team/teamFactories";
import { buildKey } from "../helpers";
import { safeEnsureQueryData } from "../helpers";
import { scrimmagingRecordFactory } from "api/compete/competeFactories";
import { CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum } from "api/_autogen";

Expand All @@ -13,38 +13,27 @@ export const myTeamLoader =
if (episodeId === undefined) return null;

// My team info
void queryClient.ensureQueryData({
queryKey: buildKey(myTeamFactory.queryKey, { episodeId }),
queryFn: async () => await myTeamFactory.queryFn({ episodeId }),
});
safeEnsureQueryData({ episodeId }, myTeamFactory, queryClient);

// Ranked and Unranked Scrimmage Record
void queryClient.ensureQueryData({
queryKey: buildKey(scrimmagingRecordFactory.queryKey, {
safeEnsureQueryData(
{
episodeId,
scrimmageType:
CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum.Ranked,
}),
queryFn: async () =>
await scrimmagingRecordFactory.queryFn({
episodeId,
scrimmageType:
CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum.Ranked,
}),
});
void queryClient.ensureQueryData({
queryKey: buildKey(scrimmagingRecordFactory.queryKey, {
},
scrimmagingRecordFactory,
queryClient,
);
safeEnsureQueryData(
{
episodeId,
scrimmageType:
CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum.Unranked,
}),
queryFn: async () =>
await scrimmagingRecordFactory.queryFn({
episodeId,
scrimmageType:
CompeteMatchScrimmagingRecordRetrieveScrimmageTypeEnum.Unranked,
}),
});
},
scrimmagingRecordFactory,
queryClient,
);

return null;
};
14 changes: 3 additions & 11 deletions frontend2/src/api/loaders/queueLoader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { QueryClient } from "@tanstack/react-query";
import type { LoaderFunction } from "react-router-dom";
import { matchListFactory } from "../compete/competeFactories";
import { buildKey } from "../helpers";
import { safeEnsureQueryData } from "../helpers";
import { searchTeamsFactory } from "../team/teamFactories";

export const queueLoader =
Expand All @@ -11,18 +11,10 @@ export const queueLoader =
if (episodeId === undefined) return null;

// All matches
void queryClient.ensureQueryData({
queryKey: buildKey(matchListFactory.queryKey, { episodeId }),
queryFn: async () =>
await matchListFactory.queryFn({ episodeId }, queryClient, true),
});
safeEnsureQueryData({ episodeId }, matchListFactory, queryClient);

// All teams
void queryClient.ensureQueryData({
queryKey: buildKey(searchTeamsFactory.queryKey, { episodeId }),
queryFn: async () =>
await searchTeamsFactory.queryFn({ episodeId }, queryClient, true),
});
safeEnsureQueryData({ episodeId }, searchTeamsFactory, queryClient);

return null;
};
Loading

0 comments on commit d2b8322

Please sign in to comment.