src/components/CompareResults/loader.ts (234 lines of code) (raw):

import { repoMap, frameworks } from '../../common/constants'; import { compareView } from '../../common/constants'; import { fetchCompareResults, fetchFakeCompareResults, memoizedFetchRevisionForRepository, } from '../../logic/treeherder'; import { Changeset, CompareResultsItem, Repository } from '../../types/state'; import { FakeCommitHash, Framework } from '../../types/types'; // This function checks and sanitizes the input values, then returns values that // we can then use in the rest of the application. export function checkValues({ baseRev, baseRepo, newRevs, newRepos, framework, }: { baseRev: string | null; baseRepo: Repository['name'] | null; newRevs: string[]; newRepos: Repository['name'][]; framework: string | number | null; }): { baseRev: string; baseRepo: Repository['name']; newRevs: string[]; newRepos: Repository['name'][]; frameworkId: Framework['id']; frameworkName: Framework['name']; } { if (baseRev === null) { throw new Error('The parameter baseRev is missing.'); } if (baseRepo === null) { throw new Error('The parameter baseRepo is missing.'); } const validRepoValues = Object.values(repoMap); if (!validRepoValues.includes(baseRepo)) { throw new Error( `The parameter baseRepo "${baseRepo}" should be one of ${validRepoValues.join( ', ', )}.`, ); } if (framework === null) { framework = 1; // default to talos so that manually typing the URL is easier } const frameworkId = +framework as Framework['id']; if (Number.isNaN(frameworkId)) { throw new Error( `The parameter framework should be a number, but it is "${framework}".`, ); } const frameworkName = frameworks.find( (entry) => entry.id === frameworkId, )?.name; if (!frameworkName) { throw new Error( `The parameter framework isn't a valid value: "${framework}".`, ); } if (!newRevs.length) { return { baseRev, baseRepo, newRevs: [baseRev], newRepos: [baseRepo], frameworkId, frameworkName, }; } if (newRevs.length !== newRepos.length) { throw new Error( 'There should be as many "newRepo" parameters as there are "newRev" parameters.', ); } if (!newRepos.every((newRepo) => validRepoValues.includes(newRepo))) { throw new Error( `Every parameter newRepo "${newRepos.join( '", "', )}" should be one of ${validRepoValues.join(', ')}.`, ); } return { baseRev, baseRepo, newRevs, newRepos, frameworkId, frameworkName, }; } // This is essentially a glue to call the related function from // /logic/treeherder.ts for all the revs we need results for. async function fetchCompareResultsOnTreeherder({ baseRev, baseRepo, newRevs, newRepos, framework, }: { baseRev: string; baseRepo: Repository['name']; newRevs: string[]; newRepos: Repository['name'][]; framework: Framework['id']; }) { const promises = newRevs.map((newRev, i) => fetchCompareResults({ baseRev, baseRepo, newRev, newRepo: newRepos[i], framework, }), ); return Promise.all(promises); } const fakeCommitHashes: FakeCommitHash[] = [ 'bb6a5e451dace3b9c7be42d24c9272738d73e6db', '9d50665254899d8431813bdc04178e6006ce6d59', 'a998c42399a8fcea623690bf65bef49de20535b4', ]; // This is essentially a glue to call the related function from // /logic/treeherder.ts for all the revs we need results for. async function fetchAllFakeCompareResults() { const promises = fakeCommitHashes.map((hash) => fetchFakeCompareResults(hash), ); return Promise.all(promises); } // This counter is incremented for each call of the loader. This allows the // components to know when a new load happened and use it in keys. // Essentially a workaround to https://github.com/remix-run/react-router/issues/11864 let generationCounter = 0; // This function is responsible for fetching the data from the URL. It's called // by React Router DOM when the compare-results path is requested. // It uses the URL parameters as inputs, and returns all the fetched data to the // React components through React Router's useLoaderData hook. export async function loader({ request }: { request: Request }) { const url = new URL(request.url); const useFakeData = url.searchParams.has('fakedata'); if (useFakeData) { const results = await fetchAllFakeCompareResults(); // They're all based on the same rev const baseRev = results[0][0].base_rev; // And the same repository const baseRepo = results[0][0].base_repository_name; const newRevs = fakeCommitHashes; const newRepos = results.map((result) => result[0].new_repository_name); const frameworkId = results[0][0].framework_id; const frameworkName = frameworks.find((entry) => entry.id === frameworkId)?.name ?? ''; return { results, baseRev, baseRepo, newRevs, newRepos, frameworkId, frameworkName, view: compareView, generation: generationCounter++, }; } const baseRevFromUrl = url.searchParams.get('baseRev'); const baseRepoFromUrl = url.searchParams.get('baseRepo') as | Repository['name'] | null; const newRevsFromUrl = url.searchParams.getAll('newRev'); const newReposFromUrl = url.searchParams.getAll( 'newRepo', ) as Repository['name'][]; const frameworkFromUrl = url.searchParams.get('framework'); const { baseRev, baseRepo, newRevs, newRepos, frameworkId, frameworkName } = checkValues({ baseRev: baseRevFromUrl, baseRepo: baseRepoFromUrl, newRevs: newRevsFromUrl, newRepos: newReposFromUrl, framework: frameworkFromUrl, }); return await getComparisonInformation( baseRev, baseRepo, newRevs, newRepos, frameworkId, frameworkName, ); } export async function getComparisonInformation( baseRev: string, baseRepo: Repository['name'], newRevs: string[], newRepos: Repository['name'][], frameworkId: Framework['id'], frameworkName: Framework['name'], ) { const resultsPromise = fetchCompareResultsOnTreeherder({ baseRev, baseRepo, newRevs, newRepos, framework: frameworkId, }); // TODO what happens if there's no result? const baseRevInfoPromise = memoizedFetchRevisionForRepository({ repository: baseRepo, hash: baseRev, }); const newRevsInfoPromises = newRevs.map((newRev, i) => memoizedFetchRevisionForRepository({ repository: newRepos[i], hash: newRev, }), ); const [baseRevInfo, ...newRevsInfo] = await Promise.all([ baseRevInfoPromise, ...newRevsInfoPromises, ]); return { results: resultsPromise, baseRev, baseRevInfo, baseRepo, newRevs, newRevsInfo, newRepos, frameworkId, frameworkName, view: compareView, generation: generationCounter++, }; } type DeferredLoaderData = { results: Promise<CompareResultsItem[][]>; baseRev: string; baseRevInfo: Changeset; baseRepo: Repository['name']; newRevs: string[]; newRevsInfo: Changeset[]; newRepos: Repository['name'][]; frameworkId: Framework['id']; frameworkName: Framework['name']; view: typeof compareView; generation: number; }; // Be explicit with the returned type to control it better than if we were // inferring it. export type LoaderReturnValue = DeferredLoaderData;