diff --git a/src/client.ts b/src/client.ts index f4b41fcb..59fdd09a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -30,6 +30,15 @@ const MAX_PAGE_SIZE = 100; */ export const REPOSITORY_CACHE_TTL = 5000; +/** + * The number of milliseconds in which a multi-page `getAll` (e.g. `getAll`, + * `getAllByType`, `getAllByTag`) will wait between individual page requests. + * + * This is done to ensure API performance is sustainable and reduces the chance + * of a failed API request due to overloading. + */ +export const GET_ALL_QUERY_DELAY = 500; + /** * Modes for client ref management. */ @@ -463,6 +472,10 @@ export class Client { } /** + * **IMPORTANT**: Avoid using `dangerouslyGetAll` as it may be slower and + * require more resources than other methods. Prefer using other methods that + * filter by predicates such as `getAllByType`. + * * Queries content from the Prismic repository and returns all matching * content. If no predicates are provided, all documents will be fetched. * @@ -471,14 +484,15 @@ export class Client { * @example * * ```ts - * const response = await client.getAll(); + * const response = await client.dangerouslyGetAll(); * ``` * - * @typeParam TDocument - Type of Prismic documents returned. @param params - - * Parameters to filter, sort, and paginate results. @returns A list of - * documents matching the query. + * @typeParam TDocument - Type of Prismic documents returned. + * @param params - Parameters to filter, sort, and paginate results. + * + * @returns A list of documents matching the query. */ - async getAll( + async dangerouslyGetAll( params: Partial> & GetAllParams = {}, ): Promise { const { limit = Infinity, ...actualParams } = params; @@ -487,15 +501,21 @@ export class Client { pageSize: actualParams.pageSize || MAX_PAGE_SIZE, }; - const result = await this.get(resolvedParams); + const documents: TDocument[] = []; + let latestResult: prismicT.Query | undefined; - let page = result.page; - let documents = result.results; + while ( + (!latestResult || latestResult.next_page) && + documents.length < limit + ) { + const page = latestResult ? latestResult.page + 1 : undefined; - while (page < result.total_pages && documents.length < limit) { - page += 1; - const result = await this.get({ ...resolvedParams, page }); - documents = [...documents, ...result.results]; + latestResult = await this.get({ ...resolvedParams, page }); + documents.push(...latestResult.results); + + if (latestResult.next_page) { + await new Promise((res) => setTimeout(res, GET_ALL_QUERY_DELAY)); + } } return documents.slice(0, limit); @@ -590,7 +610,7 @@ export class Client { ids: string[], params?: Partial, ): Promise { - return await this.getAll( + return await this.dangerouslyGetAll( appendPredicates(params, predicate.in("document.id", ids)), ); } @@ -696,7 +716,7 @@ export class Client { uids: string[], params?: Partial, ): Promise { - return await this.getAll( + return await this.dangerouslyGetAll( appendPredicates(params, [ typePredicate(documentType), predicate.in(`my.${documentType}.uid`, uids), @@ -781,7 +801,7 @@ export class Client { documentType: string, params?: Partial>, ): Promise { - return await this.getAll( + return await this.dangerouslyGetAll( appendPredicates(params, typePredicate(documentType)), ); } @@ -833,7 +853,7 @@ export class Client { tag: string, params?: Partial>, ): Promise { - return await this.getAll( + return await this.dangerouslyGetAll( appendPredicates(params, everyTagPredicate(tag)), ); } @@ -885,7 +905,7 @@ export class Client { tags: string[], params?: Partial>, ): Promise { - return await this.getAll( + return await this.dangerouslyGetAll( appendPredicates(params, everyTagPredicate(tags)), ); } @@ -937,7 +957,7 @@ export class Client { tags: string[], params?: Partial>, ): Promise { - return await this.getAll( + return await this.dangerouslyGetAll( appendPredicates(params, someTagsPredicate(tags)), ); } diff --git a/test/__testutils__/createMockQueryHandler.ts b/test/__testutils__/createMockQueryHandler.ts index a4c2210c..f3871769 100644 --- a/test/__testutils__/createMockQueryHandler.ts +++ b/test/__testutils__/createMockQueryHandler.ts @@ -21,6 +21,7 @@ export const createMockQueryHandler = < string, string | number | (string | number)[] | null | undefined >, + duration = 0, debug = true, ): msw.RestHandler => { const repositoryName = crypto.createHash("md5").update(t.title).digest("hex"); @@ -54,7 +55,10 @@ export const createMockQueryHandler = < ); } - if (!("page" in requiredSearchParams) && page > 1) { + if ( + !("page" in requiredSearchParams) && + req.url.searchParams.has("page") + ) { requiredSearchParamsInstance.append("page", page.toString()); } @@ -82,9 +86,9 @@ export const createMockQueryHandler = < if (requestMatches) { const response = pagedResponses[page - 1]; - return res(ctx.json(response)); + return res(ctx.delay(duration), ctx.json(response)); } - return res(ctx.status(404)); + return res(ctx.delay(duration), ctx.status(404)); }); }; diff --git a/test/__testutils__/createQueryResponse.ts b/test/__testutils__/createQueryResponse.ts index f98f8318..203cd127 100644 --- a/test/__testutils__/createQueryResponse.ts +++ b/test/__testutils__/createQueryResponse.ts @@ -8,14 +8,19 @@ export const createQueryResponse = < >( docs: SetRequired[] = [createDocument(), createDocument()], overrides?: Partial>, -): prismicT.Query> => ({ - page: 1, - results_per_page: docs.length, - results_size: docs.length, - total_results_size: docs.length, - total_pages: 1, - next_page: "", - prev_page: "", - results: docs, - ...overrides, -}); +): prismicT.Query> => { + const page = overrides?.page ?? 1; + const totalPages = overrides?.total_pages ?? 1; + + return { + page, + results_per_page: docs.length, + results_size: docs.length, + total_results_size: docs.length, + total_pages: totalPages, + next_page: page < totalPages ? "next_page_url" : null, + prev_page: page > 0 ? "prev_page_url" : null, + results: docs, + ...overrides, + }; +}; diff --git a/test/client-getAll.test.ts b/test/client-dangerouslyGetAll.test.ts similarity index 57% rename from test/client-getAll.test.ts rename to test/client-dangerouslyGetAll.test.ts index 06e2fe7c..9de209a5 100644 --- a/test/client-getAll.test.ts +++ b/test/client-dangerouslyGetAll.test.ts @@ -3,12 +3,21 @@ import * as mswNode from "msw/node"; import { createMockQueryHandler } from "./__testutils__/createMockQueryHandler"; import { createMockRepositoryHandler } from "./__testutils__/createMockRepositoryHandler"; +import { createQueryResponse } from "./__testutils__/createQueryResponse"; import { createQueryResponsePages } from "./__testutils__/createQueryResponsePages"; import { createRepositoryResponse } from "./__testutils__/createRepositoryResponse"; import { createTestClient } from "./__testutils__/createClient"; import { getMasterRef } from "./__testutils__/getMasterRef"; import * as prismic from "../src"; +import { GET_ALL_QUERY_DELAY } from "../src/client"; + +/** + * Tolerance in number of milliseconds for the duration of a simulated network request. + * + * If tests are failing due to incorrect timed durations, increase the tolerance amount. + */ +const NETWORK_REQUEST_DURATION_TOLERANCE = 200; const server = mswNode.setupServer(); test.before(() => server.listen({ onUnhandledRequest: "error" })); @@ -31,7 +40,7 @@ test("returns all documents from paginated response", async (t) => { ); const client = createTestClient(t); - const res = await client.getAll(); + const res = await client.dangerouslyGetAll(); t.deepEqual(res, allDocs); t.is(res.length, 3 * 3); @@ -59,7 +68,7 @@ test("includes params if provided", async (t) => { ); const client = createTestClient(t); - const res = await client.getAll(params); + const res = await client.dangerouslyGetAll(params); t.deepEqual(res, allDocs); t.is(res.length, 3 * 3); @@ -87,7 +96,7 @@ test("includes default params if provided", async (t) => { ); const client = createTestClient(t, clientOptions); - const res = await client.getAll(); + const res = await client.dangerouslyGetAll(); t.deepEqual(res, allDocs); t.is(res.length, 3 * 3); @@ -119,7 +128,7 @@ test("merges params and default params if provided", async (t) => { ); const client = createTestClient(t, clientOptions); - const res = await client.getAll(params); + const res = await client.dangerouslyGetAll(params); t.deepEqual(res, allDocs); t.is(res.length, 3 * 3); @@ -146,7 +155,88 @@ test("uses the default pageSize when given a falsey pageSize param", async (t) = // `createMockQueryHandler` handler does not match the given `pageSize` of // 100. This is handled within createMockQueryHandler (see the `debug` option). - await client.getAll(); - await client.getAll({ pageSize: undefined }); - await client.getAll({ pageSize: 0 }); + await client.dangerouslyGetAll(); + await client.dangerouslyGetAll({ pageSize: undefined }); + await client.dangerouslyGetAll({ pageSize: 0 }); +}); + +test("throttles requests past first page", async (t) => { + const numPages = 3; + const repositoryResponse = createRepositoryResponse(); + const pagedResponses = createQueryResponsePages({ + numPages, + numDocsPerPage: 3, + }); + const queryDuration = 200; + + server.use( + createMockRepositoryHandler(t, repositoryResponse), + createMockQueryHandler( + t, + pagedResponses, + undefined, + { + ref: getMasterRef(repositoryResponse), + pageSize: 100, + }, + queryDuration, + ), + ); + + const client = createTestClient(t); + + const startTime = Date.now(); + await client.dangerouslyGetAll(); + const endTime = Date.now(); + + const totalTime = endTime - startTime; + const minTime = + numPages * queryDuration + (numPages - 1) * GET_ALL_QUERY_DELAY; + const maxTime = minTime + NETWORK_REQUEST_DURATION_TOLERANCE; + + // The total time should be the amount of time it takes to resolve all + // network requests in addition to a delay between requests (accounting for + // some tolerance). The last request does not start an artificial delay. + t.true( + minTime <= totalTime && totalTime <= maxTime, + `Total time should be between ${minTime}ms and ${maxTime}ms (inclusive), but was ${totalTime}ms`, + ); +}); + +test("does not throttle single page queries", async (t) => { + const repositoryResponse = createRepositoryResponse(); + const queryResponse = createQueryResponse(); + const queryDuration = 200; + + server.use( + createMockRepositoryHandler(t, repositoryResponse), + createMockQueryHandler( + t, + [queryResponse], + undefined, + { + ref: getMasterRef(repositoryResponse), + pageSize: 100, + }, + queryDuration, + ), + ); + + const client = createTestClient(t); + + const startTime = Date.now(); + await client.dangerouslyGetAll(); + const endTime = Date.now(); + + const totalTime = endTime - startTime; + const minTime = queryDuration; + const maxTime = minTime + NETWORK_REQUEST_DURATION_TOLERANCE; + + // The total time should only be the amount of time it takes to resolve the + // network request (accounting for some tolerance). In other words, there is + // no artificial delay in a single page `getAll` query. + t.true( + minTime <= totalTime && totalTime <= maxTime, + `Total time should be between ${minTime}ms and ${maxTime}ms (inclusive), but was ${totalTime}ms`, + ); });