Skip to content

Commit 99b4ec3

Browse files
committed
fix(vnda): optimize /api/v2/tags/:name calls to prevent rate limiting on PLP loader
1 parent 0b95d39 commit 99b4ec3

1 file changed

Lines changed: 66 additions & 50 deletions

File tree

vnda/loaders/productListingPage.ts

Lines changed: 66 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
getSEOFromTag,
1414
toFilters,
1515
toProduct,
16-
typeTagExtractor,
1716
} from "../utils/transform.ts";
1817

1918
export const VNDA_SORT_OPTIONS: SortOption[] = [
@@ -87,6 +86,42 @@ const handleOperator = (
8786
[`${key}_operator`]: filterOperators?.[key] ?? defaultValue ?? "and",
8887
});
8988

89+
const fetchTag = (
90+
api: AppContext["api"],
91+
name: string,
92+
): Promise<Tag | undefined> =>
93+
api["GET /api/v2/tags/:name"]({ name }, STALE)
94+
.then((res) => res.json() as Promise<Tag>)
95+
.catch((): undefined => undefined);
96+
97+
interface TypeTag {
98+
key: string;
99+
value: string;
100+
isProperty: boolean;
101+
}
102+
103+
const parseTypeTagsFromUrl = (url: URL): { typeTags: TypeTag[]; cleanUrl: URL } => {
104+
const TYPE_TAG_PATTERN = /^type_tags\[(.+)\]\[\]$/;
105+
106+
const typeTags = [...url.searchParams.entries()]
107+
.filter(([key]) => TYPE_TAG_PATTERN.test(key))
108+
.map(([key, value]) => {
109+
const keyName = key.match(TYPE_TAG_PATTERN)?.[1] ?? "";
110+
return {
111+
key,
112+
value,
113+
isProperty: /^property\d+$/.test(keyName),
114+
};
115+
});
116+
117+
const cleanUrl = new URL(url.href);
118+
[...cleanUrl.searchParams.keys()]
119+
.filter((k) => k.startsWith("type_tags"))
120+
.forEach((k) => cleanUrl.searchParams.delete(k));
121+
122+
return { typeTags, cleanUrl };
123+
};
124+
90125
/**
91126
* @title VNDA Integration
92127
* @description Product Listing Page loader
@@ -108,44 +143,40 @@ const searchLoader = async (
108143
const isSearchPage = ctx.searchPagePath
109144
? ctx.searchPagePath === url.pathname
110145
: url.pathname === "/busca" || url.pathname === "/s";
146+
111147
const qQueryString = url.searchParams.get("q");
112-
const term = props.term || props.slug || qQueryString ||
113-
undefined;
148+
const term = props.term || props.slug || qQueryString || undefined;
114149

115150
const priceFilterRegex = /de-(\d+)-a-(\d+)/;
116151
const filterMatch = url.href.match(priceFilterRegex) ?? [];
117152

118-
const categoryTagName = (props.term || url.pathname.slice(1) || "").split(
119-
"/",
120-
);
153+
const categoryTagName = (props.term || url.pathname.slice(1) || "").split("/");
121154

122155
const properties1 = url.searchParams.getAll("type_tags[property1][]");
123156
const properties2 = url.searchParams.getAll("type_tags[property2][]");
124157
const properties3 = url.searchParams.getAll("type_tags[property3][]");
125158

126-
const categoryTagNames = Array.from(url.searchParams.values());
159+
const uniquePathNames = [
160+
...new Set(
161+
categoryTagName.filter((item): item is string => typeof item === "string"),
162+
),
163+
];
127164

128-
const tags = await Promise.all([
129-
...categoryTagNames,
130-
...categoryTagName.filter((item): item is string =>
131-
typeof item === "string"
165+
const tagByName = new Map<string, Tag | undefined>(
166+
await Promise.all(
167+
uniquePathNames.map(
168+
async (name) => [name, await fetchTag(api, name)] as const,
169+
),
132170
),
133-
].map((name) =>
134-
api["GET /api/v2/tags/:name"]({ name }, STALE)
135-
.then((res) => res.json())
136-
.catch(() => undefined)
137-
));
138-
139-
const categories = tags
140-
.slice(-categoryTagName.length)
171+
);
172+
173+
const categories = categoryTagName
174+
.map((name) => tagByName.get(name))
141175
.filter((tag): tag is Tag =>
142176
typeof tag !== "undefined" && typeof tag.name !== "undefined"
143177
);
144178

145-
const filteredTags = tags
146-
.filter((tag): tag is Tag => typeof tag !== "undefined");
147-
148-
const { cleanUrl, typeTags } = typeTagExtractor(url, filteredTags);
179+
const { typeTags, cleanUrl } = parseTypeTagsFromUrl(url);
149180

150181
const initialTags = props.tags && props.tags?.length > 0
151182
? props.tags
@@ -165,7 +196,7 @@ const searchLoader = async (
165196
const tag = categories.at(-1);
166197

167198
const [response, seo = []] = await Promise.all([
168-
await api["GET /api/v2/products/search"]({
199+
api["GET /api/v2/products/search"]({
169200
term: term ?? preference,
170201
sort,
171202
page,
@@ -211,19 +242,15 @@ const searchLoader = async (
211242
) as ProductSearchResult["pagination"] | null;
212243

213244
const search = await response.json();
214-
215245
const { results: searchResults = [] } = search;
216246

217-
const validProducts = searchResults.filter(({ variants }) => {
218-
return variants.length !== 0;
219-
});
247+
const validProducts = searchResults.filter(({ variants }) =>
248+
variants.length !== 0
249+
);
220250

221-
const products = validProducts.map((product) => {
222-
return toProduct(product, null, {
223-
url,
224-
priceCurrency: "BRL",
225-
});
226-
});
251+
const products = validProducts.map((product) =>
252+
toProduct(product, null, { url, priceCurrency: "BRL" })
253+
);
227254

228255
const nextPage = new URLSearchParams(url.searchParams);
229256
const previousPage = new URLSearchParams(url.searchParams);
@@ -247,11 +274,7 @@ const searchLoader = async (
247274
"@type": "ProductListingPage",
248275
seo: getSEOFromTag(categories, url, seo.at(-1), hasTypeTags, isSearchPage),
249276
breadcrumb: isSearchPage
250-
? {
251-
"@type": "BreadcrumbList",
252-
itemListElement: [],
253-
numberOfItems: 0,
254-
}
277+
? { "@type": "BreadcrumbList", itemListElement: [], numberOfItems: 0 }
255278
: getBreadcrumbList(categories, url),
256279
filters: toFilters(search.aggregations, typeTags, cleanUrl),
257280
products,
@@ -270,12 +293,9 @@ export const cache = "stale-while-revalidate";
270293
export const cacheKey = (props: Props, req: Request, _ctx: AppContext) => {
271294
const url = new URL(props.pageHref || req.url);
272295
const qQueryString = url.searchParams.get("q");
273-
const term = props.term || qQueryString ||
274-
undefined;
296+
const term = props.term || qQueryString || undefined;
275297

276-
if (term) {
277-
return null;
278-
}
298+
if (term) return null;
279299

280300
const typeTags = [...url.searchParams.entries()]
281301
.filter(([key]) => key.includes("type_tags"))
@@ -299,19 +319,15 @@ export const cacheKey = (props: Props, req: Request, _ctx: AppContext) => {
299319
["sort", url.searchParams.get("sort") ?? props.sort ?? ""],
300320
["type_tags", typeTags],
301321
["tags", props?.tags?.join("\\") ?? ""],
302-
[
303-
"price",
304-
filterMatch ? `min:${filterMatch[1]}_max:${filterMatch[2]}` : "",
305-
],
322+
["price", filterMatch ? `min:${filterMatch[1]}_max:${filterMatch[2]}` : ""],
306323
["filterByTags", props.filterByTags ? "true" : "false"],
307324
["filterOperator", filterOperators.join("\\")],
308325
["page", (url.searchParams.get("page") ?? 1).toString()],
309326
]);
310327

311328
params.sort();
312-
313329
url.search = params.toString();
314330
return url.href;
315331
};
316332

317-
export default searchLoader;
333+
export default searchLoader;

0 commit comments

Comments
 (0)