Skip to content

Commit

Permalink
feat: throttle getAll* methods and rename getAll to dangerouslyGetAll (
Browse files Browse the repository at this point in the history
  • Loading branch information
angeloashmore authored Nov 8, 2021
1 parent 5c15ff0 commit 4efdfa0
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 39 deletions.
56 changes: 38 additions & 18 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*
Expand All @@ -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<TDocument extends prismicT.PrismicDocument>(
async dangerouslyGetAll<TDocument extends prismicT.PrismicDocument>(
params: Partial<Omit<BuildQueryURLArgs, "page">> & GetAllParams = {},
): Promise<TDocument[]> {
const { limit = Infinity, ...actualParams } = params;
Expand All @@ -487,15 +501,21 @@ export class Client {
pageSize: actualParams.pageSize || MAX_PAGE_SIZE,
};

const result = await this.get<TDocument>(resolvedParams);
const documents: TDocument[] = [];
let latestResult: prismicT.Query<TDocument> | 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<TDocument>({ ...resolvedParams, page });
documents = [...documents, ...result.results];
latestResult = await this.get<TDocument>({ ...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);
Expand Down Expand Up @@ -590,7 +610,7 @@ export class Client {
ids: string[],
params?: Partial<BuildQueryURLArgs>,
): Promise<TDocument[]> {
return await this.getAll<TDocument>(
return await this.dangerouslyGetAll<TDocument>(
appendPredicates(params, predicate.in("document.id", ids)),
);
}
Expand Down Expand Up @@ -696,7 +716,7 @@ export class Client {
uids: string[],
params?: Partial<BuildQueryURLArgs>,
): Promise<TDocument[]> {
return await this.getAll<TDocument>(
return await this.dangerouslyGetAll<TDocument>(
appendPredicates(params, [
typePredicate(documentType),
predicate.in(`my.${documentType}.uid`, uids),
Expand Down Expand Up @@ -781,7 +801,7 @@ export class Client {
documentType: string,
params?: Partial<Omit<BuildQueryURLArgs, "page">>,
): Promise<TDocument[]> {
return await this.getAll<TDocument>(
return await this.dangerouslyGetAll<TDocument>(
appendPredicates(params, typePredicate(documentType)),
);
}
Expand Down Expand Up @@ -833,7 +853,7 @@ export class Client {
tag: string,
params?: Partial<Omit<BuildQueryURLArgs, "page">>,
): Promise<TDocument[]> {
return await this.getAll<TDocument>(
return await this.dangerouslyGetAll<TDocument>(
appendPredicates(params, everyTagPredicate(tag)),
);
}
Expand Down Expand Up @@ -885,7 +905,7 @@ export class Client {
tags: string[],
params?: Partial<Omit<BuildQueryURLArgs, "page">>,
): Promise<TDocument[]> {
return await this.getAll<TDocument>(
return await this.dangerouslyGetAll<TDocument>(
appendPredicates(params, everyTagPredicate(tags)),
);
}
Expand Down Expand Up @@ -937,7 +957,7 @@ export class Client {
tags: string[],
params?: Partial<Omit<BuildQueryURLArgs, "page">>,
): Promise<TDocument[]> {
return await this.getAll<TDocument>(
return await this.dangerouslyGetAll<TDocument>(
appendPredicates(params, someTagsPredicate(tags)),
);
}
Expand Down
10 changes: 7 additions & 3 deletions test/__testutils__/createMockQueryHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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));
});
};
27 changes: 16 additions & 11 deletions test/__testutils__/createQueryResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ export const createQueryResponse = <
>(
docs: SetRequired<TDocument, "uid">[] = [createDocument(), createDocument()],
overrides?: Partial<prismicT.Query<TDocument>>,
): prismicT.Query<SetRequired<TDocument, "uid">> => ({
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<SetRequired<TDocument, "uid">> => {
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,
};
};
104 changes: 97 additions & 7 deletions test/client-getAll.test.ts → test/client-dangerouslyGetAll.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }));
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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`,
);
});

0 comments on commit 4efdfa0

Please sign in to comment.