From 0abc6a5ad8117961d49dce22764ad06a4508733c Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 11 Jan 2022 19:18:18 -0800 Subject: [PATCH] feat: added pagination fix: only cache 200 responses --- app/containers/cdp/cdp.component.tsx | 30 ++++++++- app/containers/cdp/cdp.server.ts | 11 +++- app/containers/wishlist/wishlist.server.ts | 1 - app/models/ecommerce-provider.server.ts | 12 +++- .../ecommerce-providers/shopify.server.ts | 63 ++++++++++++------- .../swr-redis-cache.server.ts | 53 +++++++++------- 6 files changed, 118 insertions(+), 52 deletions(-) diff --git a/app/containers/cdp/cdp.component.tsx b/app/containers/cdp/cdp.component.tsx index 11a9d34..3bc3665 100644 --- a/app/containers/cdp/cdp.component.tsx +++ b/app/containers/cdp/cdp.component.tsx @@ -86,8 +86,16 @@ function ThreeProductGridItem({ product }: { product: CDPProduct }) { } export default function CDP() { - let { category, sort, categories, search, sortByOptions, products } = - useLoaderData(); + let { + category, + sort, + categories, + search, + sortByOptions, + products, + hasNextPage, + nextPageCursor, + } = useLoaderData(); let submit = useSubmit(); let location = useLocation(); @@ -114,6 +122,7 @@ export default function CDP() { prefetch="intent" to={(() => { let params = new URLSearchParams(location.search); + params.delete("cursor"); params.delete("q"); params.set("category", cat.slug); params.sort(); @@ -142,6 +151,7 @@ export default function CDP() { prefetch="intent" to={(() => { let params = new URLSearchParams(location.search); + params.delete("cursor"); params.set("sort", sortBy.value); return location.pathname + "?" + params.toString(); })()} @@ -157,6 +167,7 @@ export default function CDP() { action={(() => { let params = new URLSearchParams(location.search); params.delete("category"); + params.delete("cursor"); params.delete("q"); let search = params.toString(); search = search ? "?" + search : ""; @@ -190,6 +201,7 @@ export default function CDP() { className="px-4 pb-2 border-b border-zinc-700 lg:hidden" action={(() => { let params = new URLSearchParams(location.search); + params.delete("cursor"); params.delete("sort"); let search = params.toString(); search = search ? "?" + search : ""; @@ -230,6 +242,20 @@ export default function CDP() { ))} + {hasNextPage && nextPageCursor && ( +

+ { + let params = new URLSearchParams(location.search); + params.set("cursor", nextPageCursor); + return location.pathname + "?" + params.toString(); + })()} + > + Load more + +

+ )} ); diff --git a/app/containers/cdp/cdp.server.ts b/app/containers/cdp/cdp.server.ts index 0ff683e..1f12f63 100644 --- a/app/containers/cdp/cdp.server.ts +++ b/app/containers/cdp/cdp.server.ts @@ -25,6 +25,8 @@ export type LoaderData = { search?: string; sortByOptions: SortByOption[]; products: CDPProduct[]; + hasNextPage: boolean; + nextPageCursor?: string; }; export let loader: LoaderFunction = async ({ request, params }) => { @@ -35,11 +37,12 @@ export let loader: LoaderFunction = async ({ request, params }) => { let category = url.searchParams.get("category") || undefined; let sort = url.searchParams.get("sort") || undefined; let search = url.searchParams.get("q") || undefined; + let cursor = url.searchParams.get("cursor") || undefined; - let [categories, sortByOptions, products, wishlist] = await Promise.all([ + let [categories, sortByOptions, productsPage, wishlist] = await Promise.all([ commerce.getCategories(lang, 250), commerce.getSortByOptions(lang), - commerce.getProducts(lang, category, sort, search), + commerce.getProducts(lang, category, sort, search, cursor), session.getWishlist(), ]); @@ -53,7 +56,9 @@ export let loader: LoaderFunction = async ({ request, params }) => { categories, search, sortByOptions, - products: products.map((product) => ({ + hasNextPage: productsPage.hasNextPage, + nextPageCursor: productsPage.nextPageCursor, + products: productsPage.products.map((product) => ({ favorited: wishlistHasProduct.has(product.id), formattedPrice: product.formattedPrice, id: product.id, diff --git a/app/containers/wishlist/wishlist.server.ts b/app/containers/wishlist/wishlist.server.ts index 65319f3..178dcd4 100644 --- a/app/containers/wishlist/wishlist.server.ts +++ b/app/containers/wishlist/wishlist.server.ts @@ -44,7 +44,6 @@ export let action: ActionFunction = async ({ request, params }) => { let productId = formData.get("productId"); let variantId = formData.get("variantId"); let quantityStr = formData.get("quantity"); - console.log({ productId, variantId, quantityStr }); if (!productId || !variantId || !quantityStr) { break; } diff --git a/app/models/ecommerce-provider.server.ts b/app/models/ecommerce-provider.server.ts index 1a1e15c..97a2bed 100644 --- a/app/models/ecommerce-provider.server.ts +++ b/app/models/ecommerce-provider.server.ts @@ -77,6 +77,12 @@ export interface SelectedProductOption { value: string; } +export interface ProductsResult { + hasNextPage: boolean; + nextPageCursor?: string; + products: Product[]; +} + export interface EcommerceProvider { getCartInfo( locale: Language, @@ -96,8 +102,10 @@ export interface EcommerceProvider { language: Language, category?: string, sort?: string, - search?: string - ): Promise; + search?: string, + cursor?: string, + perPage?: number + ): Promise; getSortByOptions(language: Language): Promise; getWishlistInfo( locale: Language, diff --git a/app/models/ecommerce-providers/shopify.server.ts b/app/models/ecommerce-providers/shopify.server.ts index 83de6aa..349a914 100644 --- a/app/models/ecommerce-providers/shopify.server.ts +++ b/app/models/ecommerce-providers/shopify.server.ts @@ -26,7 +26,7 @@ export function createShopifyProvider({ storefrontAccessToken, }: ShopifyProviderOptions): EcommerceProvider { let href = `https://${shop}.myshopify.com/api/2021-10/graphql.json`; - function query(locale: string, query: string, variables?: any) { + async function query(locale: string, query: string, variables?: any) { let request = new Request(href, { method: "POST", headers: { @@ -62,7 +62,7 @@ export function createShopifyProvider({ let itemsMap = new Map(items.map((item) => [item.variantId, item])); let fullItems: FullCartItem[] = []; for (let item of json.data.nodes) { - let itemInput = itemsMap.get(item.id); + let itemInput = !!item && itemsMap.get(item.id); if (!itemInput) { continue; } @@ -94,7 +94,9 @@ export function createShopifyProvider({ let formattedSubTotal = formatPrice({ amount: subtotal.toDecimalPlaces(2).toString(), - currencyCode: json.data.nodes[0].priceV2.currencyCode, + currencyCode: json.data.nodes.find( + (n: any) => !!n?.priceV2?.currencyCode + ).priceV2.currencyCode, }); let translations = getTranslations(locale, ["Calculated at checkout"]); @@ -254,7 +256,7 @@ export function createShopifyProvider({ })), }; }, - async getProducts(locale, category, sort, search) { + async getProducts(locale, category, sort, search, cursor, perPage = 30) { let q = ""; if (search) { q += `product_type:${search} OR title:${search} OR tag:${search} `; @@ -294,31 +296,39 @@ export function createShopifyProvider({ category ? getCollectionProductsQuery : getAllProductsQuery, { ...sortVariables, - first: 250, + first: perPage, query: q, collection: category, + cursor, } ); - let edges = category - ? json.data.collections.edges[0]?.node.products.edges - : json.data.products.edges; + let productsInfo = category + ? json.data.collections.edges[0]?.node.products + : json.data.products; + let { edges, pageInfo } = productsInfo; + + let nextPageCursor: string | undefined = undefined; let products = edges?.map( ({ + cursor, node: { id, handle, title, images, priceRange, variants }, - }: any): Product => ({ - formattedPrice: formatPrice(priceRange.minVariantPrice), - id, - defaultVariantId: variants.edges[0].node.id, - image: images.edges[0].node.originalSrc, - slug: handle, - title, - }) + }: any): Product => { + nextPageCursor = cursor; + return { + formattedPrice: formatPrice(priceRange.minVariantPrice), + id, + defaultVariantId: variants.edges[0].node.id, + image: images.edges[0].node.originalSrc, + slug: handle, + title, + }; + } ) || []; - return products; + return { hasNextPage: pageInfo.hasNextPage, nextPageCursor, products }; }, async getSortByOptions(locale) { let translations = getTranslations(locale, [ @@ -358,7 +368,7 @@ export function createShopifyProvider({ let itemsMap = new Map(items.map((item) => [item.variantId, item])); let fullItems: FullWishlistItem[] = []; for (let item of json.data.nodes) { - let itemInput = itemsMap.get(item.id); + let itemInput = !!item && itemsMap.get(item.id); if (!itemInput) { continue; } @@ -490,6 +500,7 @@ let productConnectionFragment = /* GraphQL */ ` hasPreviousPage } edges { + cursor node { id title @@ -529,16 +540,18 @@ let productConnectionFragment = /* GraphQL */ ` let getAllProductsQuery = /* GraphQL */ ` query getAllProducts( - $first: Int = 250 + $first: Int = 20 $query: String = "" $sortKey: ProductSortKeys = RELEVANCE $reverse: Boolean = false + $cursor: String ) { products( first: $first sortKey: $sortKey reverse: $reverse query: $query + after: $cursor ) { ...productConnection } @@ -549,15 +562,21 @@ let getAllProductsQuery = /* GraphQL */ ` let getCollectionProductsQuery = /* GraphQL */ ` query getProductsFromCollection( $collection: String - $first: Int = 250 + $first: Int = 20 $sortKey: ProductCollectionSortKeys = RELEVANCE $reverse: Boolean = false + $cursor: String ) { collections(first: 1, query: $collection) { edges { node { handle - products(first: $first, sortKey: $sortKey, reverse: $reverse) { + products( + first: $first + sortKey: $sortKey + reverse: $reverse + after: $cursor + ) { ...productConnection } } @@ -618,7 +637,7 @@ let getProductQuery = /* GraphQL */ ` } } } - images(first: 250) { + images(first: 20) { pageInfo { hasNextPage hasPreviousPage diff --git a/app/models/request-response-caches/swr-redis-cache.server.ts b/app/models/request-response-caches/swr-redis-cache.server.ts index 7eb74c5..7b3d0d4 100644 --- a/app/models/request-response-caches/swr-redis-cache.server.ts +++ b/app/models/request-response-caches/swr-redis-cache.server.ts @@ -64,6 +64,11 @@ export let createSwrRedisCache = ({ let cachedResponseJson = JSON.parse( cachedResponseString ) as CachedResponse; + + if (cachedResponseJson.status !== 200) { + return null; + } + let cachedResponse = new Response(cachedResponseJson.body, { status: cachedResponseJson.status, statusText: cachedResponseJson.statusText, @@ -77,15 +82,17 @@ export let createSwrRedisCache = ({ (async () => { let responseToCache = await fetch(request.clone()); - let toCache: CachedResponse = { - status: responseToCache.status, - statusText: responseToCache.statusText, - headers: Array.from(responseToCache.headers), - body: await responseToCache.text(), - }; - - await redisClient.set(responseKey, JSON.stringify(toCache)); - await redisClient.setEx(stillGoodKey, maxAgeSeconds, "true"); + if (responseToCache.status === 200) { + let toCache: CachedResponse = { + status: responseToCache.status, + statusText: responseToCache.statusText, + headers: Array.from(responseToCache.headers), + body: await responseToCache.text(), + }; + + await redisClient.set(responseKey, JSON.stringify(toCache)); + await redisClient.setEx(stillGoodKey, maxAgeSeconds, "true"); + } })().catch((error) => { console.error("Failed to revalidate", error); }); @@ -100,19 +107,21 @@ export let createSwrRedisCache = ({ let responseToCache = response.clone(); response.headers.set("X-SWR-Cache", "miss"); - (async () => { - let toCache: CachedResponse = { - status: responseToCache.status, - statusText: responseToCache.statusText, - headers: Array.from(responseToCache.headers), - body: await responseToCache.text(), - }; - - await redisClient.set(responseKey, JSON.stringify(toCache)); - await redisClient.setEx(stillGoodKey, maxAgeSeconds, "true"); - })().catch((error) => { - console.error("Failed to seed cache", error); - }); + if (responseToCache.status === 200) { + (async () => { + let toCache: CachedResponse = { + status: responseToCache.status, + statusText: responseToCache.statusText, + headers: Array.from(responseToCache.headers), + body: await responseToCache.text(), + }; + + await redisClient.set(responseKey, JSON.stringify(toCache)); + await redisClient.setEx(stillGoodKey, maxAgeSeconds, "true"); + })().catch((error) => { + console.error("Failed to seed cache", error); + }); + } } return response;