diff --git a/src/Projects.tsx b/src/Projects.tsx index 9d6fdc6..d278687 100644 --- a/src/Projects.tsx +++ b/src/Projects.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { ProjectData, SERVER } from "./api"; +import { ProjectData, ProjectsRequestTimeoutError, SERVER } from "./api"; export interface NameById { [key: number]: string; @@ -22,7 +22,11 @@ export default function Projects({ selectedUser, nameById }: ProjectsProps) { setProjects(projects => [...(projects ?? []), ...page.projects]); } setHasMoreResults(page.hasMoreResults); - }).catch(() => { + }).catch((error) => { + if (error instanceof ProjectsRequestTimeoutError) { + alert(error.message); + return; + } alert("Something went wrong..."); }); }, [selectedUser]); @@ -47,4 +51,4 @@ export default function Projects({ selectedUser, nameById }: ProjectsProps) { ); -} \ No newline at end of file +} diff --git a/src/api/apiImpl.ts b/src/api/apiImpl.ts index 41a1a55..6e172df 100644 --- a/src/api/apiImpl.ts +++ b/src/api/apiImpl.ts @@ -5,6 +5,27 @@ export interface ProjectsResponse { hasMoreResults: boolean; } +const PROJECTS_REQUEST_TIMEOUT_MS = 1000; + +export const PROJECTS_REQUEST_TIMEOUT_MESSAGE = + "Request timed out. Possible infinite loop in api/pagination.py."; + +export class ProjectsRequestTimeoutError extends Error { + constructor(message: string = PROJECTS_REQUEST_TIMEOUT_MESSAGE) { + super(message); + this.name = "ProjectsRequestTimeoutError"; + } +} + +function isAbortError(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "name" in error && + error.name === "AbortError" + ); +} + class DefaultServer { async getUsers(): Promise { const response = await fetch('http://127.0.0.1:5000/api/users'); @@ -28,8 +49,20 @@ class DefaultServer { url.searchParams.append('pageSize', options.pageSize.toString()); } - const response = await fetch(url); - return response.json(); + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), PROJECTS_REQUEST_TIMEOUT_MS); + + try { + const response = await fetch(url, { signal: controller.signal }); + return response.json(); + } catch (error) { + if (isAbortError(error)) { + throw new ProjectsRequestTimeoutError(); + } + throw error; + } finally { + window.clearTimeout(timeoutId); + } } } diff --git a/src/index.tsx b/src/index.tsx index 06bf5aa..7fc7a2b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,7 @@ import { render } from "react-dom"; import App from "./App"; import { PROJECTS, USERS } from "./api/data"; -import { ProjectData, ProjectsResponse, SERVER, UserData } from "./api"; +import { ProjectData, ProjectsRequestTimeoutError, ProjectsResponse, SERVER, UserData } from "./api"; const rootElement = document.getElementById("root"); render(, rootElement); @@ -16,18 +16,26 @@ render(, rootElement); let hasMoreResults = true; let lastProject: ProjectData | undefined = undefined; - while (hasMoreResults) { - const page: ProjectsResponse = await SERVER.getProjects({ pageSize, startAfter: lastProject, userId: user?.id?.toString() }); - if (page.hasMoreResults && page.projects.length < pageSize) { - console.log( - `❌ ${userString} // Improperly sized page - hasMoreResults: true but results.length < pageSize` - ); - console.groupEnd(); + try { + while (hasMoreResults) { + const page: ProjectsResponse = await SERVER.getProjects({ pageSize, startAfter: lastProject, userId: user?.id?.toString() }); + if (page.hasMoreResults && page.projects.length < pageSize) { + console.log( + `❌ ${userString} // Improperly sized page - hasMoreResults: true but results.length < pageSize` + ); + console.groupEnd(); + return; + } + totalCountFromApi += page.projects.length; + hasMoreResults = page.hasMoreResults; + lastProject = page.projects[page.projects.length - 1]; + } + } catch (error) { + if (error instanceof ProjectsRequestTimeoutError) { + console.log(`❌ ${userString} // ${error.message}`); return; } - totalCountFromApi += page.projects.length; - hasMoreResults = page.hasMoreResults; - lastProject = page.projects[page.projects.length - 1]; + throw error; } const filterCallback = user ? (project: ProjectData) => project.creatorId === user.id : undefined; @@ -39,5 +47,7 @@ render(, rootElement); console.log(`✅ ${userString} // Counts match: ${totalCountFromApi}`); } } - Promise.all([...USERS.map(testUser), testUser()]); -} \ No newline at end of file + Promise.all([...USERS.map(testUser), testUser()]).catch((error) => { + console.error(error); + }); +}