diff --git a/.gitignore b/.gitignore index a0caaaaac9..959b63145c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ coverage/ .history .unlighthouse .bigcommerce +.worktrees diff --git a/core/app/[locale]/(default)/(auth)/change-password/page-data.ts b/core/app/[locale]/(default)/(auth)/change-password/page-data.ts index 43a72f2d3a..c48ab1d1a4 100644 --- a/core/app/[locale]/(default)/(auth)/change-password/page-data.ts +++ b/core/app/[locale]/(default)/(auth)/change-password/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -24,16 +25,24 @@ const ChangePasswordQuery = graphql(` } `); -export const getChangePasswordQuery = cache(async () => { - const response = await client.fetch({ - document: ChangePasswordQuery, - fetchOptions: { next: { revalidate } }, - }); +const getCachedChangePasswordQuery = unstable_cache( + async (locale: string) => { + const response = await client.fetch({ + document: ChangePasswordQuery, + locale, + }); - const passwordComplexitySettings = - response.data.site.settings?.customers?.passwordComplexitySettings; + const passwordComplexitySettings = + response.data.site.settings?.customers?.passwordComplexitySettings; - return { - passwordComplexitySettings, - }; + return { + passwordComplexitySettings, + }; + }, + ['get-change-password-query'], + { revalidate }, +); + +export const getChangePasswordQuery = cache(async (locale: string) => { + return getCachedChangePasswordQuery(locale); }); diff --git a/core/app/[locale]/(default)/(auth)/change-password/page.tsx b/core/app/[locale]/(default)/(auth)/change-password/page.tsx index 1e7e251a6a..fe8f70832d 100644 --- a/core/app/[locale]/(default)/(auth)/change-password/page.tsx +++ b/core/app/[locale]/(default)/(auth)/change-password/page.tsx @@ -38,7 +38,7 @@ export default async function ChangePassword({ params, searchParams }: Props) { return redirect({ href: '/login', locale }); } - const { passwordComplexitySettings } = await getChangePasswordQuery(); + const { passwordComplexitySettings } = await getChangePasswordQuery(locale); return ( { - const customerAccessToken = await getSessionCustomerAccessToken(); +const getCachedRegisterCustomerQuery = unstable_cache( + async (locale: string, { address, customer }: Props) => { + const response = await client.fetch({ + document: RegisterCustomerQuery, + variables: { + addressFilters: address?.filters, + addressSortBy: address?.sortBy, + customerFilters: customer?.filters, + customerSortBy: customer?.sortBy, + }, + fetchOptions: { cache: 'no-store' }, + locale, + }); - const response = await client.fetch({ - document: RegisterCustomerQuery, - variables: { - addressFilters: address?.filters, - addressSortBy: address?.sortBy, - customerFilters: customer?.filters, - customerSortBy: customer?.sortBy, - }, - fetchOptions: { cache: 'no-store' }, - customerAccessToken, - }); + const addressFields = response.data.site.settings?.formFields.shippingAddress; + const customerFields = response.data.site.settings?.formFields.customer; + const countries = response.data.geography.countries; + const passwordComplexitySettings = + response.data.site.settings?.customers?.passwordComplexitySettings; - const addressFields = response.data.site.settings?.formFields.shippingAddress; - const customerFields = response.data.site.settings?.formFields.customer; - const countries = response.data.geography.countries; - const passwordComplexitySettings = - response.data.site.settings?.customers?.passwordComplexitySettings; + if (!addressFields || !customerFields) { + return null; + } - if (!addressFields || !customerFields) { - return null; - } + return { + addressFields, + customerFields, + countries, + passwordComplexitySettings, + }; + }, + ['get-register-customer-query'], + { revalidate }, +); - return { - addressFields, - customerFields, - countries, - passwordComplexitySettings, - }; +export const getRegisterCustomerQuery = cache(async (locale: string, props: Props) => { + return getCachedRegisterCustomerQuery(locale, props); }); diff --git a/core/app/[locale]/(default)/(auth)/register/page.tsx b/core/app/[locale]/(default)/(auth)/register/page.tsx index bf8ebf1357..ea5fdc8a91 100644 --- a/core/app/[locale]/(default)/(auth)/register/page.tsx +++ b/core/app/[locale]/(default)/(auth)/register/page.tsx @@ -51,7 +51,7 @@ export default async function Register({ params }: Props) { const t = await getTranslations('Auth.Register'); - const registerCustomerData = await getRegisterCustomerQuery({ + const registerCustomerData = await getRegisterCustomerQuery(locale, { address: { sortBy: 'SORT_ORDER' }, customer: { sortBy: 'SORT_ORDER' }, }); diff --git a/core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts b/core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts index 9bb605d215..c7e0296921 100644 --- a/core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts +++ b/core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -38,13 +39,35 @@ const BrandPageQuery = graphql(` } `); -export const getBrandPageData = cache(async (entityId: number, customerAccessToken?: string) => { - const response = await client.fetch({ - document: BrandPageQuery, - variables: { entityId }, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } }, - }); +const getCachedBrandPageData = unstable_cache( + async (locale: string, entityId: number) => { + const response = await client.fetch({ + document: BrandPageQuery, + variables: { entityId }, + locale, + fetchOptions: { cache: 'no-store' }, + }); - return response.data.site; -}); + return response.data.site; + }, + ['get-brand-page-data'], + { revalidate }, +); + +export const getBrandPageData = cache( + async (locale: string, entityId: number, customerAccessToken?: string) => { + if (customerAccessToken) { + const response = await client.fetch({ + document: BrandPageQuery, + variables: { entityId }, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return response.data.site; + } + + return getCachedBrandPageData(locale, entityId); + }, +); diff --git a/core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx b/core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx index 4278def31c..ba091c43ff 100644 --- a/core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx +++ b/core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx @@ -30,9 +30,14 @@ const getCachedBrand = cache((brandId: string) => { const compareLoader = createCompareLoader(); const createBrandSearchParamsLoader = cache( - async (brandId: string, customerAccessToken?: string) => { + async (locale: string, brandId: string, customerAccessToken?: string) => { const cachedBrand = getCachedBrand(brandId); - const brandSearch = await fetchFacetedSearch(cachedBrand, undefined, customerAccessToken); + const brandSearch = await fetchFacetedSearch( + locale, + cachedBrand, + undefined, + customerAccessToken, + ); const brandFacets = brandSearch.facets.items.filter( (facet) => facet.__typename !== 'BrandSearchFilter', ); @@ -73,7 +78,7 @@ export async function generateMetadata(props: Props): Promise { const brandId = Number(slug); - const { brand } = await getBrandPageData(brandId, customerAccessToken); + const { brand } = await getBrandPageData(locale, brandId, customerAccessToken); if (!brand) { return notFound(); @@ -99,7 +104,7 @@ export default async function Brand(props: Props) { const brandId = Number(slug); - const { brand, settings } = await getBrandPageData(brandId, customerAccessToken); + const { brand, settings } = await getBrandPageData(locale, brandId, customerAccessToken); if (!brand) { return notFound(); @@ -114,10 +119,11 @@ export default async function Brand(props: Props) { const searchParams = await props.searchParams; const currencyCode = await getPreferredCurrencyCode(); - const loadSearchParams = await createBrandSearchParamsLoader(slug, customerAccessToken); + const loadSearchParams = await createBrandSearchParamsLoader(locale, slug, customerAccessToken); const parsedSearchParams = loadSearchParams?.(searchParams) ?? {}; const search = await fetchFacetedSearch( + locale, { ...searchParams, ...parsedSearchParams, @@ -162,10 +168,15 @@ export default async function Brand(props: Props) { const streamableFilters = Streamable.from(async () => { const searchParams = await props.searchParams; - const loadSearchParams = await createBrandSearchParamsLoader(slug, customerAccessToken); + const loadSearchParams = await createBrandSearchParamsLoader(locale, slug, customerAccessToken); const parsedSearchParams = loadSearchParams?.(searchParams) ?? {}; const cachedBrand = getCachedBrand(slug); - const categorySearch = await fetchFacetedSearch(cachedBrand, undefined, customerAccessToken); + const categorySearch = await fetchFacetedSearch( + locale, + cachedBrand, + undefined, + customerAccessToken, + ); const refinedSearch = await streamableFacetedSearch; const allFacets = categorySearch.facets.items.filter( @@ -195,7 +206,7 @@ export default async function Brand(props: Props) { const compareIds = { entityIds: compare ? compare.map((id: string) => Number(id)) : [] }; - const products = await getCompareProductsData(compareIds, customerAccessToken); + const products = await getCompareProductsData(locale, compareIds, customerAccessToken); return products.map((product) => ({ id: product.entityId.toString(), diff --git a/core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts b/core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts index 6c2c4633fe..2e49f085d5 100644 --- a/core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts +++ b/core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -58,13 +59,35 @@ const CategoryPageQuery = graphql( [BreadcrumbsCategoryFragment], ); -export const getCategoryPageData = cache(async (entityId: number, customerAccessToken?: string) => { - const response = await client.fetch({ - document: CategoryPageQuery, - variables: { entityId }, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } }, - }); +const getCachedCategoryPageData = unstable_cache( + async (locale: string, entityId: number) => { + const response = await client.fetch({ + document: CategoryPageQuery, + variables: { entityId }, + locale, + fetchOptions: { cache: 'no-store' }, + }); - return response.data.site; -}); + return response.data.site; + }, + ['get-category-page-data'], + { revalidate }, +); + +export const getCategoryPageData = cache( + async (locale: string, entityId: number, customerAccessToken?: string) => { + if (customerAccessToken) { + const response = await client.fetch({ + document: CategoryPageQuery, + variables: { entityId }, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return response.data.site; + } + + return getCachedCategoryPageData(locale, entityId); + }, +); diff --git a/core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx b/core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx index ee143281b5..74c532cbb7 100644 --- a/core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx +++ b/core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx @@ -32,9 +32,14 @@ const getCachedCategory = cache((categoryId: number) => { const compareLoader = createCompareLoader(); const createCategorySearchParamsLoader = cache( - async (categoryId: number, customerAccessToken?: string) => { + async (locale: string, categoryId: number, customerAccessToken?: string) => { const cachedCategory = getCachedCategory(categoryId); - const categorySearch = await fetchFacetedSearch(cachedCategory, undefined, customerAccessToken); + const categorySearch = await fetchFacetedSearch( + locale, + cachedCategory, + undefined, + customerAccessToken, + ); const categoryFacets = categorySearch.facets.items.filter( (facet) => facet.__typename !== 'CategorySearchFilter', ); @@ -75,7 +80,7 @@ export async function generateMetadata(props: Props): Promise { const categoryId = Number(slug); - const { category } = await getCategoryPageData(categoryId, customerAccessToken); + const { category } = await getCategoryPageData(locale, categoryId, customerAccessToken); if (!category) { return notFound(); @@ -107,6 +112,7 @@ export default async function Category(props: Props) { const categoryId = Number(slug); const { category, settings, categoryTree } = await getCategoryPageData( + locale, categoryId, customerAccessToken, ); @@ -130,12 +136,14 @@ export default async function Category(props: Props) { const currencyCode = await getPreferredCurrencyCode(); const loadSearchParams = await createCategorySearchParamsLoader( + locale, categoryId, customerAccessToken, ); const parsedSearchParams = loadSearchParams?.(searchParams) ?? {}; const search = await fetchFacetedSearch( + locale, { ...searchParams, ...parsedSearchParams, @@ -182,12 +190,18 @@ export default async function Category(props: Props) { const searchParams = await props.searchParams; const loadSearchParams = await createCategorySearchParamsLoader( + locale, categoryId, customerAccessToken, ); const parsedSearchParams = loadSearchParams?.(searchParams) ?? {}; const cachedCategory = getCachedCategory(categoryId); - const categorySearch = await fetchFacetedSearch(cachedCategory, undefined, customerAccessToken); + const categorySearch = await fetchFacetedSearch( + locale, + cachedCategory, + undefined, + customerAccessToken, + ); const refinedSearch = await streamableFacetedSearch; const allFacets = categorySearch.facets.items.filter( @@ -234,7 +248,7 @@ export default async function Category(props: Props) { const compareIds = { entityIds: compare ? compare.map((id: string) => Number(id)) : [] }; - const products = await getCompareProducts(compareIds, customerAccessToken); + const products = await getCompareProducts(locale, compareIds, customerAccessToken); return products.map((product) => ({ id: product.entityId.toString(), diff --git a/core/app/[locale]/(default)/(faceted)/fetch-compare-products.ts b/core/app/[locale]/(default)/(faceted)/fetch-compare-products.ts index 8033dc8235..af4f0139ce 100644 --- a/core/app/[locale]/(default)/(faceted)/fetch-compare-products.ts +++ b/core/app/[locale]/(default)/(faceted)/fetch-compare-products.ts @@ -1,5 +1,6 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; import { VariablesOf } from 'gql.tada'; +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { z } from 'zod'; @@ -42,8 +43,8 @@ const CompareProductsQuery = graphql(` type Variables = VariablesOf; -export const getCompareProducts = cache( - async (variables: Variables, customerAccessToken?: string) => { +const getCachedCompareProducts = unstable_cache( + async (locale: string, variables: Variables) => { const parsedVariables = CompareProductsSchema.parse(variables); if (parsedVariables.entityIds.length === 0) { @@ -53,10 +54,36 @@ export const getCompareProducts = cache( const response = await client.fetch({ document: CompareProductsQuery, variables: { ...parsedVariables, first: MAX_COMPARE_LIMIT }, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } }, + locale, + fetchOptions: { cache: 'no-store' }, }); return removeEdgesAndNodes(response.data.site.products); }, + ['get-compare-products'], + { revalidate }, +); + +export const getCompareProducts = cache( + async (locale: string, variables: Variables, customerAccessToken?: string) => { + if (customerAccessToken) { + const parsedVariables = CompareProductsSchema.parse(variables); + + if (parsedVariables.entityIds.length === 0) { + return []; + } + + const response = await client.fetch({ + document: CompareProductsQuery, + variables: { ...parsedVariables, first: MAX_COMPARE_LIMIT }, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return removeEdgesAndNodes(response.data.site.products); + } + + return getCachedCompareProducts(locale, variables); + }, ); diff --git a/core/app/[locale]/(default)/(faceted)/fetch-faceted-search.ts b/core/app/[locale]/(default)/(faceted)/fetch-faceted-search.ts index 115d639d1c..6c230abdc7 100644 --- a/core/app/[locale]/(default)/(faceted)/fetch-faceted-search.ts +++ b/core/app/[locale]/(default)/(faceted)/fetch-faceted-search.ts @@ -1,4 +1,5 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { z } from 'zod'; @@ -178,11 +179,11 @@ interface ProductSearch { filters: SearchProductsFiltersInput; } -const getProductSearchResults = cache( +const getCachedProductSearchResults = unstable_cache( async ( + locale: string, { limit = 9, after, before, sort, filters }: ProductSearch, - currencyCode?: CurrencyCode, - customerAccessToken?: string, + currencyCode: CurrencyCode | undefined, ) => { const filterArgs = { filters, sort }; const paginationArgs = before ? { last: limit, before } : { first: limit, after }; @@ -190,12 +191,11 @@ const getProductSearchResults = cache( const response = await client.fetch({ document: GetProductSearchResultsQuery, variables: { ...filterArgs, ...paginationArgs, currencyCode }, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate: 300 } }, + locale, + fetchOptions: { cache: 'no-store' }, }); const { site } = response.data; - const searchResults = site.search.searchProducts; const items = removeEdgesAndNodes(searchResults.products).map((product) => ({ @@ -242,6 +242,84 @@ const getProductSearchResults = cache( }, }; }, + ['get-product-search-results'], + { revalidate: 300 }, +); + +const getProductSearchResults = cache( + // We need to make sure the reference passed into this function is the same if we want it to be memoized. + async ( + locale: string, + { limit = 9, after, before, sort, filters }: ProductSearch, + currencyCode?: CurrencyCode, + customerAccessToken?: string, + ) => { + if (customerAccessToken) { + const filterArgs = { filters, sort }; + const paginationArgs = before ? { last: limit, before } : { first: limit, after }; + + const response = await client.fetch({ + document: GetProductSearchResultsQuery, + variables: { ...filterArgs, ...paginationArgs, currencyCode }, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + const { site } = response.data; + const searchResults = site.search.searchProducts; + + const items = removeEdgesAndNodes(searchResults.products).map((product) => ({ + ...product, + })); + + return { + facets: { + items: removeEdgesAndNodes(searchResults.filters).map((node) => { + switch (node.__typename) { + case 'BrandSearchFilter': + return { + ...node, + brands: removeEdgesAndNodes(node.brands), + }; + + case 'CategorySearchFilter': + return { + ...node, + categories: removeEdgesAndNodes(node.categories), + }; + + case 'ProductAttributeSearchFilter': + return { + ...node, + attributes: removeEdgesAndNodes(node.attributes), + }; + + case 'RatingSearchFilter': + return { + ...node, + ratings: removeEdgesAndNodes(node.ratings), + }; + + default: + return node; + } + }), + }, + products: { + collectionInfo: searchResults.products.collectionInfo, + pageInfo: searchResults.products.pageInfo, + items, + }, + }; + } + + return getCachedProductSearchResults( + locale, + { limit, after, before, sort, filters }, + currencyCode, + ); + }, ); const SearchParamSchema = z.union([z.string(), z.array(z.string()), z.undefined()]); @@ -406,6 +484,7 @@ export const PublicToPrivateParams = PublicSearchParamsSchema.catchall(SearchPar export const fetchFacetedSearch = cache( // We need to make sure the reference passed into this function is the same if we want it to be memoized. async ( + locale: string, params: z.input, currencyCode?: CurrencyCode, customerAccessToken?: string, @@ -413,6 +492,7 @@ export const fetchFacetedSearch = cache( const { after, before, limit = 9, sort, filters } = PublicToPrivateParams.parse(params); return getProductSearchResults( + locale, { after, before, diff --git a/core/app/[locale]/(default)/(faceted)/search/page-data.ts b/core/app/[locale]/(default)/(faceted)/search/page-data.ts index 37f571c49b..607832f973 100644 --- a/core/app/[locale]/(default)/(faceted)/search/page-data.ts +++ b/core/app/[locale]/(default)/(faceted)/search/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -29,11 +30,20 @@ const SearchPageQuery = graphql(` } `); -export const getSearchPageData = cache(async () => { - const response = await client.fetch({ - document: SearchPageQuery, - fetchOptions: { next: { revalidate } }, - }); +const getCachedSearchPageData = unstable_cache( + async (locale: string) => { + const response = await client.fetch({ + document: SearchPageQuery, + locale, + fetchOptions: { cache: 'no-store' }, + }); - return response.data.site; + return response.data.site; + }, + ['get-search-page-data'], + { revalidate }, +); + +export const getSearchPageData = cache(async (locale: string) => { + return getCachedSearchPageData(locale); }); diff --git a/core/app/[locale]/(default)/(faceted)/search/page.tsx b/core/app/[locale]/(default)/(faceted)/search/page.tsx index bc86471c32..d5f8007a34 100644 --- a/core/app/[locale]/(default)/(faceted)/search/page.tsx +++ b/core/app/[locale]/(default)/(faceted)/search/page.tsx @@ -22,14 +22,14 @@ import { getSearchPageData } from './page-data'; const compareLoader = createCompareLoader(); const createSearchSearchParamsLoader = cache( - async (searchParams: SearchParams, customerAccessToken?: string) => { + async (locale: string, searchParams: SearchParams, customerAccessToken?: string) => { const searchTerm = typeof searchParams.term === 'string' ? searchParams.term : ''; if (!searchTerm) { return null; } - const search = await fetchFacetedSearch(searchParams, undefined, customerAccessToken); + const search = await fetchFacetedSearch(locale, searchParams, undefined, customerAccessToken); const searchFacets = search.facets.items; const transformedSearchFacets = await facetsTransformer({ refinedFacets: searchFacets, @@ -76,7 +76,7 @@ export default async function Search(props: Props) { const t = await getTranslations('Faceted'); - const { settings } = await getSearchPageData(); + const { settings } = await getSearchPageData(locale); const showRating = Boolean(settings?.reviews.enabled && settings.display.showProductRating); @@ -89,12 +89,14 @@ export default async function Search(props: Props) { const currencyCode = await getPreferredCurrencyCode(); const loadSearchParams = await createSearchSearchParamsLoader( + locale, searchParams, customerAccessToken, ); const parsedSearchParams = loadSearchParams?.(searchParams) ?? {}; const search = await fetchFacetedSearch( + locale, { ...searchParams, ...parsedSearchParams, @@ -186,11 +188,12 @@ export default async function Search(props: Props) { } const loadSearchParams = await createSearchSearchParamsLoader( + locale, searchParams, customerAccessToken, ); const parsedSearchParams = loadSearchParams?.(searchParams) ?? {}; - const categorySearch = await fetchFacetedSearch({}, undefined, customerAccessToken); + const categorySearch = await fetchFacetedSearch(locale, {}, undefined, customerAccessToken); const refinedSearch = await streamableFacetedSearch; const allFacets = categorySearch.facets.items.filter( @@ -221,7 +224,7 @@ export default async function Search(props: Props) { const compareIds = { entityIds: compare ? compare.map((id: string) => Number(id)) : [] }; - const products = await getCompareProductsData(compareIds, customerAccessToken); + const products = await getCompareProductsData(locale, compareIds, customerAccessToken); return products.map((product) => ({ id: product.entityId.toString(), diff --git a/core/app/[locale]/(default)/account/addresses/page-data.ts b/core/app/[locale]/(default)/account/addresses/page-data.ts index 66433985ff..f44e833967 100644 --- a/core/app/[locale]/(default)/account/addresses/page-data.ts +++ b/core/app/[locale]/(default)/account/addresses/page-data.ts @@ -1,7 +1,6 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; import { cache } from 'react'; -import { getSessionCustomerAccessToken } from '~/auth'; import { client } from '~/client'; import { PaginationFragment } from '~/client/fragments/pagination'; import { graphql } from '~/client/graphql'; @@ -70,13 +69,17 @@ interface Pagination { } export const getCustomerAddresses = cache( - async ({ before = '', after = '', limit = 10 }: Pagination) => { - const customerAccessToken = await getSessionCustomerAccessToken(); + async ( + locale: string, + { before = '', after = '', limit = 10 }: Pagination, + customerAccessToken?: string, + ) => { const paginationArgs = before ? { last: limit, before } : { first: limit, after }; const response = await client.fetch({ document: GetCustomerAddressesQuery, variables: { ...paginationArgs }, + locale, customerAccessToken, fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } }, }); diff --git a/core/app/[locale]/(default)/account/addresses/page.tsx b/core/app/[locale]/(default)/account/addresses/page.tsx index 49c02451a6..865ba4f575 100644 --- a/core/app/[locale]/(default)/account/addresses/page.tsx +++ b/core/app/[locale]/(default)/account/addresses/page.tsx @@ -3,6 +3,7 @@ import { notFound } from 'next/navigation'; import { getTranslations, setRequestLocale } from 'next-intl/server'; import { Address, AddressListSection } from '@/vibes/soul/sections/address-list-section'; +import { getSessionCustomerAccessToken } from '~/auth'; import { formFieldTransformer, injectCountryCodeOptions, @@ -41,13 +42,20 @@ export default async function Addresses({ params, searchParams }: Props) { setRequestLocale(locale); - const t = await getTranslations('Account.Addresses'); - const { before, after } = await searchParams; - - const data = await getCustomerAddresses({ - ...(after && { after }), - ...(before && { before }), - }); + const [customerAccessToken, t, { before, after }] = await Promise.all([ + getSessionCustomerAccessToken(), + getTranslations('Account.Addresses'), + searchParams, + ]); + + const data = await getCustomerAddresses( + locale, + { + ...(after && { after }), + ...(before && { before }), + }, + customerAccessToken, + ); if (!data) { notFound(); diff --git a/core/app/[locale]/(default)/account/orders/page-data.ts b/core/app/[locale]/(default)/account/orders/page-data.ts index a2b240f4f4..bf67b08311 100644 --- a/core/app/[locale]/(default)/account/orders/page-data.ts +++ b/core/app/[locale]/(default)/account/orders/page-data.ts @@ -89,13 +89,10 @@ interface CustomerOrdersArgs { } export const getCustomerOrders = cache( - async ({ - before = '', - after = '', - filterByStatus, - filterByDateRange, - limit = 5, - }: CustomerOrdersArgs) => { + async ( + locale: string, + { before = '', after = '', filterByStatus, filterByDateRange, limit = 5 }: CustomerOrdersArgs, + ) => { const customerAccessToken = await getSessionCustomerAccessToken(); const paginationArgs = before ? { last: limit, before } : { first: limit, after }; const filtersArgs = { @@ -107,6 +104,7 @@ export const getCustomerOrders = cache( const response = await client.fetch({ document: CustomerAllOrders, variables: { ...paginationArgs, ...filtersArgs }, + locale, customerAccessToken, fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } }, errorPolicy: 'auth', diff --git a/core/app/[locale]/(default)/account/orders/page.tsx b/core/app/[locale]/(default)/account/orders/page.tsx index 65d4167426..aad96c1f18 100644 --- a/core/app/[locale]/(default)/account/orders/page.tsx +++ b/core/app/[locale]/(default)/account/orders/page.tsx @@ -15,9 +15,9 @@ interface Props { }>; } -async function getOrders(after?: string, before?: string): Promise { +async function getOrders(locale: string, after?: string, before?: string): Promise { const format = await getFormatter(); - const customerOrdersDetails = await getCustomerOrders({ + const customerOrdersDetails = await getCustomerOrders(locale, { ...(after && { after }), ...(before && { before }), }); @@ -31,8 +31,8 @@ async function getOrders(after?: string, before?: string): Promise { return ordersTransformer(orders, format); } -async function getPaginationInfo(after?: string, before?: string) { - const customerOrdersDetails = await getCustomerOrders({ +async function getPaginationInfo(locale: string, after?: string, before?: string) { + const customerOrdersDetails = await getCustomerOrders(locale, { ...(after && { after }), ...(before && { before }), }); @@ -53,8 +53,8 @@ export default async function Orders({ params, searchParams }: Props) { emptyStateActionLabel={t('EmptyState.cta')} emptyStateTitle={t('EmptyState.title')} orderNumberLabel={t('orderNumber')} - orders={getOrders(after, before)} - paginationInfo={getPaginationInfo(after, before)} + orders={getOrders(locale, after, before)} + paginationInfo={getPaginationInfo(locale, after, before)} title={t('title')} totalLabel={t('totalPrice')} viewDetailsLabel={t('viewDetails')} diff --git a/core/app/[locale]/(default)/account/wishlists/[id]/page-data.ts b/core/app/[locale]/(default)/account/wishlists/[id]/page-data.ts index ae14ab4173..602f3729bc 100644 --- a/core/app/[locale]/(default)/account/wishlists/[id]/page-data.ts +++ b/core/app/[locale]/(default)/account/wishlists/[id]/page-data.ts @@ -1,11 +1,10 @@ import { cache } from 'react'; -import { getSessionCustomerAccessToken } from '~/auth'; import { client } from '~/client'; import { graphql } from '~/client/graphql'; import { TAGS } from '~/client/tags'; +import type { CurrencyCode } from '~/components/header/fragment'; import { WishlistPaginatedItemsFragment } from '~/components/wishlist/fragment'; -import { getPreferredCurrencyCode } from '~/lib/currency'; const WishlistDetailsQuery = graphql( ` @@ -37,23 +36,30 @@ interface Pagination { after: string | null; } -export const getCustomerWishlist = cache(async (entityId: number, pagination: Pagination) => { - const { before, after, limit = 9 } = pagination; - const customerAccessToken = await getSessionCustomerAccessToken(); - const currencyCode = await getPreferredCurrencyCode(); - const paginationArgs = before ? { last: limit, before } : { first: limit, after }; - const response = await client.fetch({ - document: WishlistDetailsQuery, - variables: { ...paginationArgs, currencyCode, entityId }, - customerAccessToken, - fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } }, - }); +export const getCustomerWishlist = cache( + async ( + locale: string, + entityId: number, + pagination: Pagination, + customerAccessToken?: string, + currencyCode?: CurrencyCode, + ) => { + const { before, after, limit = 9 } = pagination; + const paginationArgs = before ? { last: limit, before } : { first: limit, after }; + const response = await client.fetch({ + document: WishlistDetailsQuery, + variables: { ...paginationArgs, currencyCode, entityId }, + locale, + customerAccessToken, + fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } }, + }); - const wishlist = response.data.customer?.wishlists.edges?.[0]?.node; + const wishlist = response.data.customer?.wishlists.edges?.[0]?.node; - if (!wishlist) { - return null; - } + if (!wishlist) { + return null; + } - return wishlist; -}); + return wishlist; + }, +); diff --git a/core/app/[locale]/(default)/account/wishlists/[id]/page.tsx b/core/app/[locale]/(default)/account/wishlists/[id]/page.tsx index 6ea2baa869..b55be1d869 100644 --- a/core/app/[locale]/(default)/account/wishlists/[id]/page.tsx +++ b/core/app/[locale]/(default)/account/wishlists/[id]/page.tsx @@ -6,10 +6,13 @@ import { createSearchParamsCache, parseAsInteger, parseAsString } from 'nuqs/ser import { Streamable } from '@/vibes/soul/lib/streamable'; import { CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination'; import { Wishlist, WishlistDetails } from '@/vibes/soul/sections/wishlist-details'; +import { getSessionCustomerAccessToken } from '~/auth'; import { ExistingResultType } from '~/client/util'; +import type { CurrencyCode } from '~/components/header/fragment'; import { defaultPageInfo, pageInfoTransformer } from '~/data-transformers/page-info-transformer'; import { wishlistDetailsTransformer } from '~/data-transformers/wishlists-transformer'; import { redirect } from '~/i18n/routing'; +import { getPreferredCurrencyCode } from '~/lib/currency'; import { isMobileUser } from '~/lib/user-agent'; import { removeWishlistItem } from '../_actions/remove-wishlist-item'; @@ -39,11 +42,19 @@ async function getWishlist( pt: ExistingResultType>, searchParamsPromise: Promise, locale: string, + customerAccessToken?: string, + currencyCode?: CurrencyCode, ): Promise { const entityId = Number(id); const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise); const formatter = await getFormatter(); - const wishlist = await getCustomerWishlist(entityId, searchParamsParsed); + const wishlist = await getCustomerWishlist( + locale, + entityId, + searchParamsParsed, + customerAccessToken, + currencyCode, + ); if (!wishlist) { return redirect({ href: '/account/wishlists/', locale }); @@ -52,10 +63,22 @@ async function getWishlist( return wishlistDetailsTransformer(wishlist, t, pt, formatter); } -const getAnalyticsData = async (id: string, searchParamsPromise: Promise) => { +const getAnalyticsData = async ( + locale: string, + id: string, + searchParamsPromise: Promise, + customerAccessToken?: string, + currencyCode?: CurrencyCode, +) => { const entityId = Number(id); const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise); - const wishlist = await getCustomerWishlist(entityId, searchParamsParsed); + const wishlist = await getCustomerWishlist( + locale, + entityId, + searchParamsParsed, + customerAccessToken, + currencyCode, + ); if (!wishlist) { return []; @@ -77,12 +100,21 @@ const getAnalyticsData = async (id: string, searchParamsPromise: Promise, + customerAccessToken?: string, + currencyCode?: CurrencyCode, ): Promise { const entityId = Number(id); const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise); - const wishlist = await getCustomerWishlist(entityId, searchParamsParsed); + const wishlist = await getCustomerWishlist( + locale, + entityId, + searchParamsParsed, + customerAccessToken, + currencyCode, + ); return pageInfoTransformer(wishlist?.items.pageInfo ?? defaultPageInfo); } @@ -92,8 +124,12 @@ export default async function WishlistPage({ params, searchParams }: Props) { setRequestLocale(locale); - const t = await getTranslations('Wishlist'); - const pt = await getTranslations('Product.ProductDetails'); + const [t, pt, customerAccessToken, currencyCode] = await Promise.all([ + getTranslations('Wishlist'), + getTranslations('Product.ProductDetails'), + getSessionCustomerAccessToken(), + getPreferredCurrencyCode(), + ]); const wishlistActions = (wishlist?: Wishlist) => { if (!wishlist) { return ; @@ -127,16 +163,24 @@ export default async function WishlistPage({ params, searchParams }: Props) { }; return ( - getAnalyticsData(id, searchParams))}> + + getAnalyticsData(locale, id, searchParams, customerAccessToken, currencyCode), + )} + > getPaginationInfo(id, searchParams))} + paginationInfo={Streamable.from(() => + getPaginationInfo(locale, id, searchParams, customerAccessToken, currencyCode), + )} prevHref="/account/wishlists" removeAction={removeWishlistItem} removeButtonTitle={t('removeButtonTitle')} - wishlist={Streamable.from(() => getWishlist(id, t, pt, searchParams, locale))} + wishlist={Streamable.from(() => + getWishlist(id, t, pt, searchParams, locale, customerAccessToken, currencyCode), + )} /> ); diff --git a/core/app/[locale]/(default)/account/wishlists/page-data.ts b/core/app/[locale]/(default)/account/wishlists/page-data.ts index 02a1c3f7b1..fef85399e0 100644 --- a/core/app/[locale]/(default)/account/wishlists/page-data.ts +++ b/core/app/[locale]/(default)/account/wishlists/page-data.ts @@ -1,11 +1,10 @@ import { cache } from 'react'; -import { getSessionCustomerAccessToken } from '~/auth'; import { client } from '~/client'; import { graphql } from '~/client/graphql'; import { TAGS } from '~/client/tags'; +import type { CurrencyCode } from '~/components/header/fragment'; import { WishlistsFragment } from '~/components/wishlist/fragment'; -import { getPreferredCurrencyCode } from '~/lib/currency'; const WishlistsPageQuery = graphql( ` @@ -33,22 +32,28 @@ interface Pagination { after: string | null; } -export const getCustomerWishlists = cache(async ({ limit = 9, before, after }: Pagination) => { - const customerAccessToken = await getSessionCustomerAccessToken(); - const currencyCode = await getPreferredCurrencyCode(); - const paginationArgs = before ? { last: limit, before } : { first: limit, after }; - const response = await client.fetch({ - document: WishlistsPageQuery, - variables: { ...paginationArgs, currencyCode }, - customerAccessToken, - fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } }, - }); +export const getCustomerWishlists = cache( + async ( + locale: string, + { limit = 9, before, after }: Pagination, + customerAccessToken?: string, + currencyCode?: CurrencyCode, + ) => { + const paginationArgs = before ? { last: limit, before } : { first: limit, after }; + const response = await client.fetch({ + document: WishlistsPageQuery, + variables: { ...paginationArgs, currencyCode }, + locale, + customerAccessToken, + fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } }, + }); - const wishlists = response.data.customer?.wishlists; + const wishlists = response.data.customer?.wishlists; - if (!wishlists) { - return null; - } + if (!wishlists) { + return null; + } - return wishlists; -}); + return wishlists; + }, +); diff --git a/core/app/[locale]/(default)/account/wishlists/page.tsx b/core/app/[locale]/(default)/account/wishlists/page.tsx index 5fba791cfa..26e9b738c0 100644 --- a/core/app/[locale]/(default)/account/wishlists/page.tsx +++ b/core/app/[locale]/(default)/account/wishlists/page.tsx @@ -7,9 +7,12 @@ import { CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination' import * as Skeleton from '@/vibes/soul/primitives/skeleton'; import { Wishlist } from '@/vibes/soul/sections/wishlist-details'; import { WishlistsSection } from '@/vibes/soul/sections/wishlists-section'; +import { getSessionCustomerAccessToken } from '~/auth'; import { ExistingResultType } from '~/client/util'; +import type { CurrencyCode } from '~/components/header/fragment'; import { defaultPageInfo, pageInfoTransformer } from '~/data-transformers/page-info-transformer'; import { wishlistsTransformer } from '~/data-transformers/wishlists-transformer'; +import { getPreferredCurrencyCode } from '~/lib/currency'; import { isMobileUser } from '~/lib/user-agent'; import { NewWishlistButton } from './_components/new-wishlist-button'; @@ -36,12 +39,20 @@ const searchParamsCache = createSearchParamsCache({ }); async function listWishlists( + locale: string, searchParamsPromise: Promise, t: ExistingResultType>, + customerAccessToken?: string, + currencyCode?: CurrencyCode, ): Promise { const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise); const formatter = await getFormatter(); - const wishlists = await getCustomerWishlists(searchParamsParsed); + const wishlists = await getCustomerWishlists( + locale, + searchParamsParsed, + customerAccessToken, + currencyCode, + ); if (!wishlists) { return []; @@ -51,10 +62,18 @@ async function listWishlists( } async function getPaginationInfo( + locale: string, searchParamsPromise: Promise, + customerAccessToken?: string, + currencyCode?: CurrencyCode, ): Promise { const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise); - const wishlists = await getCustomerWishlists(searchParamsParsed); + const wishlists = await getCustomerWishlists( + locale, + searchParamsParsed, + customerAccessToken, + currencyCode, + ); return pageInfoTransformer(wishlists?.pageInfo ?? defaultPageInfo); } @@ -64,8 +83,12 @@ export default async function Wishlists({ params, searchParams }: Props) { setRequestLocale(locale); - const t = await getTranslations('Wishlist'); - const isMobile = await isMobileUser(); + const [t, isMobile, customerAccessToken, currencyCode] = await Promise.all([ + getTranslations('Wishlist'), + isMobileUser(), + getSessionCustomerAccessToken(), + getPreferredCurrencyCode(), + ]); const newWishlistModal = getNewWishlistModal(t); return ( @@ -121,10 +144,14 @@ export default async function Wishlists({ params, searchParams }: Props) { ); }, }} - paginationInfo={Streamable.from(() => getPaginationInfo(searchParams))} + paginationInfo={Streamable.from(() => + getPaginationInfo(locale, searchParams, customerAccessToken, currencyCode), + )} title={t('title')} viewWishlistLabel={t('viewWishlist')} - wishlists={Streamable.from(() => listWishlists(searchParams, t))} + wishlists={Streamable.from(() => + listWishlists(locale, searchParams, t, customerAccessToken, currencyCode), + )} /> ); } diff --git a/core/app/[locale]/(default)/blog/[blogId]/page-data.ts b/core/app/[locale]/(default)/blog/[blogId]/page-data.ts index 472c44059a..3acda342f3 100644 --- a/core/app/[locale]/(default)/blog/[blogId]/page-data.ts +++ b/core/app/[locale]/(default)/blog/[blogId]/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -38,18 +39,27 @@ const BlogPageQuery = graphql(` type Variables = VariablesOf; -export const getBlogPageData = cache(async (variables: Variables) => { - const response = await client.fetch({ - document: BlogPageQuery, - variables, - fetchOptions: { next: { revalidate } }, - }); +const getCachedBlogPageData = unstable_cache( + async (locale: string, variables: Variables) => { + const response = await client.fetch({ + document: BlogPageQuery, + variables, + locale, + fetchOptions: { cache: 'no-store' }, + }); - const { blog } = response.data.site.content; + const { blog } = response.data.site.content; - if (!blog?.post) { - return null; - } + if (!blog?.post) { + return null; + } + + return blog; + }, + ['get-blog-page-data'], + { revalidate }, +); - return blog; +export const getBlogPageData = cache(async (locale: string, variables: Variables) => { + return getCachedBlogPageData(locale, variables); }); diff --git a/core/app/[locale]/(default)/blog/[blogId]/page.tsx b/core/app/[locale]/(default)/blog/[blogId]/page.tsx index 6b8da45019..49537ac9ef 100644 --- a/core/app/[locale]/(default)/blog/[blogId]/page.tsx +++ b/core/app/[locale]/(default)/blog/[blogId]/page.tsx @@ -23,7 +23,7 @@ export async function generateMetadata({ params }: Props): Promise { const variables = cachedBlogPageDataVariables(blogId); - const blog = await getBlogPageData(variables); + const blog = await getBlogPageData(locale, variables); const blogPost = blog?.post; if (!blogPost) { @@ -45,11 +45,11 @@ export async function generateMetadata({ params }: Props): Promise { async function getBlogPost(props: Props): Promise { const format = await getFormatter(); - const { blogId } = await props.params; + const { blogId, locale } = await props.params; const variables = cachedBlogPageDataVariables(blogId); - const blog = await getBlogPageData(variables); + const blog = await getBlogPageData(locale, variables); const blogPost = blog?.post; if (!blog || !blogPost) { @@ -76,11 +76,11 @@ async function getBlogPost(props: Props): Promise { async function getBlogPostBreadcrumbs(props: Props): Promise { const t = await getTranslations('Blog'); - const { blogId } = await props.params; + const { blogId, locale } = await props.params; const variables = cachedBlogPageDataVariables(blogId); - const blog = await getBlogPageData(variables); + const blog = await getBlogPageData(locale, variables); const blogPost = blog?.post; if (!blog || !blogPost) { diff --git a/core/app/[locale]/(default)/blog/page-data.ts b/core/app/[locale]/(default)/blog/page-data.ts index d51cf024cd..bd62608475 100644 --- a/core/app/[locale]/(default)/blog/page-data.ts +++ b/core/app/[locale]/(default)/blog/page-data.ts @@ -1,4 +1,5 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; +import { unstable_cache } from 'next/cache'; import { getFormatter } from 'next-intl/server'; import { cache } from 'react'; @@ -72,24 +73,34 @@ interface Pagination { after: string | null; } -export const getBlog = cache(async () => { - const response = await client.fetch({ - document: BlogQuery, - fetchOptions: { next: { revalidate } }, - }); +const getCachedBlog = unstable_cache( + async (locale: string) => { + const response = await client.fetch({ + document: BlogQuery, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return response.data.site.content.blog; + }, + ['get-blog'], + { revalidate }, +); - return response.data.site.content.blog; +export const getBlog = cache(async (locale: string) => { + return getCachedBlog(locale); }); -export const getBlogPosts = cache( - async ({ tag, limit = 9, before, after }: BlogPostsFiltersInput & Pagination) => { +const getCachedBlogPosts = unstable_cache( + async (locale: string, { tag, limit = 9, before, after }: BlogPostsFiltersInput & Pagination) => { const filterArgs = tag ? { filters: { tags: [tag] } } : {}; const paginationArgs = before ? { last: limit, before } : { first: limit, after }; const response = await client.fetch({ document: BlogPostsPageQuery, variables: { ...filterArgs, ...paginationArgs }, - fetchOptions: { next: { revalidate } }, + locale, + fetchOptions: { cache: 'no-store' }, }); const { blog } = response.data.site.content; @@ -98,15 +109,13 @@ export const getBlogPosts = cache( return null; } - const format = await getFormatter(); - return { pageInfo: blog.posts.pageInfo, posts: removeEdgesAndNodes(blog.posts).map((post) => ({ id: String(post.entityId), author: post.author, content: post.plainTextSummary, - date: format.dateTime(new Date(post.publishedDate.utc)), + dateUtc: post.publishedDate.utc, image: post.thumbnailImage ? { src: post.thumbnailImage.url, @@ -118,4 +127,26 @@ export const getBlogPosts = cache( })), }; }, + ['get-blog-posts'], + { revalidate }, +); + +export const getBlogPosts = cache( + async (locale: string, { tag, limit = 9, before, after }: BlogPostsFiltersInput & Pagination) => { + const raw = await getCachedBlogPosts(locale, { tag, limit, before, after }); + + if (!raw) { + return null; + } + + const format = await getFormatter(); + + return { + pageInfo: raw.pageInfo, + posts: raw.posts.map(({ dateUtc, ...post }) => ({ + ...post, + date: format.dateTime(new Date(dateUtc)), + })), + }; + }, ); diff --git a/core/app/[locale]/(default)/blog/page.tsx b/core/app/[locale]/(default)/blog/page.tsx index 1b8d90cf91..f7b972f709 100644 --- a/core/app/[locale]/(default)/blog/page.tsx +++ b/core/app/[locale]/(default)/blog/page.tsx @@ -29,7 +29,7 @@ export async function generateMetadata({ params }: Props): Promise { const { locale } = await params; const t = await getTranslations({ locale, namespace: 'Blog' }); - const blog = await getBlog(); + const blog = await getBlog(locale); const description = blog?.description && blog.description.length > 150 @@ -43,9 +43,9 @@ export async function generateMetadata({ params }: Props): Promise { }; } -async function listBlogPosts(searchParamsPromise: Promise) { +async function listBlogPosts(locale: string, searchParamsPromise: Promise) { const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise); - const blogPosts = await getBlogPosts(searchParamsParsed); + const blogPosts = await getBlogPosts(locale, searchParamsParsed); const posts = blogPosts?.posts ?? []; return posts; @@ -63,9 +63,9 @@ async function getEmptyStateSubtitle(): Promise { return t('subtitle'); } -async function getPaginationInfo(searchParamsPromise: Promise) { +async function getPaginationInfo(locale: string, searchParamsPromise: Promise) { const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise); - const blogPosts = await getBlogPosts(searchParamsParsed); + const blogPosts = await getBlogPosts(locale, searchParamsParsed); return pageInfoTransformer(blogPosts?.pageInfo ?? defaultPageInfo); } @@ -79,7 +79,7 @@ export default async function Blog(props: Props) { const searchParamsParsed = searchParamsCache.parse(await props.searchParams); const { tag } = searchParamsParsed; - const blog = await getBlog(); + const blog = await getBlog(locale); if (!blog) { return notFound(); @@ -103,9 +103,9 @@ export default async function Blog(props: Props) { description={blog.description} emptyStateSubtitle={Streamable.from(getEmptyStateSubtitle)} emptyStateTitle={Streamable.from(getEmptyStateTitle)} - paginationInfo={Streamable.from(() => getPaginationInfo(props.searchParams))} + paginationInfo={Streamable.from(() => getPaginationInfo(locale, props.searchParams))} placeholderCount={6} - posts={Streamable.from(() => listBlogPosts(props.searchParams))} + posts={Streamable.from(() => listBlogPosts(locale, props.searchParams))} title={blog.name} /> ); diff --git a/core/app/[locale]/(default)/cart/_actions/update-coupon-code.ts b/core/app/[locale]/(default)/cart/_actions/update-coupon-code.ts index 01611007f7..55ffc640dd 100644 --- a/core/app/[locale]/(default)/cart/_actions/update-coupon-code.ts +++ b/core/app/[locale]/(default)/cart/_actions/update-coupon-code.ts @@ -3,9 +3,10 @@ import { BigCommerceGQLError } from '@bigcommerce/catalyst-client'; import { SubmissionResult } from '@conform-to/react'; import { parseWithZod } from '@conform-to/zod'; -import { getTranslations } from 'next-intl/server'; +import { getLocale, getTranslations } from 'next-intl/server'; import { couponCodeActionFormDataSchema } from '@/vibes/soul/sections/cart/schema'; +import { getSessionCustomerAccessToken } from '~/auth'; import { getCartId } from '~/lib/cart'; import { getCart } from '../page-data'; @@ -34,7 +35,11 @@ export const updateCouponCode = async ( return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) }; } - const cart = await getCart({ cartId }); + const [locale, customerAccessToken] = await Promise.all([ + getLocale(), + getSessionCustomerAccessToken(), + ]); + const cart = await getCart(locale, { cartId }, customerAccessToken); const checkout = cart.site.checkout; if (!checkout) { diff --git a/core/app/[locale]/(default)/cart/_actions/update-gift-certificate.ts b/core/app/[locale]/(default)/cart/_actions/update-gift-certificate.ts index b7f2c52ac3..bf1aa88e42 100644 --- a/core/app/[locale]/(default)/cart/_actions/update-gift-certificate.ts +++ b/core/app/[locale]/(default)/cart/_actions/update-gift-certificate.ts @@ -3,9 +3,10 @@ import { BigCommerceGQLError } from '@bigcommerce/catalyst-client'; import { SubmissionResult } from '@conform-to/react'; import { parseWithZod } from '@conform-to/zod'; -import { getTranslations } from 'next-intl/server'; +import { getLocale, getTranslations } from 'next-intl/server'; import { giftCertificateCodeActionFormDataSchema } from '@/vibes/soul/sections/cart/schema'; +import { getSessionCustomerAccessToken } from '~/auth'; import { getCartId } from '~/lib/cart'; import { getCart } from '../page-data'; @@ -36,7 +37,11 @@ export const updateGiftCertificate = async ( return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) }; } - const cart = await getCart({ cartId }); + const [locale, customerAccessToken] = await Promise.all([ + getLocale(), + getSessionCustomerAccessToken(), + ]); + const cart = await getCart(locale, { cartId }, customerAccessToken); const checkout = cart.site.checkout; if (!checkout) { diff --git a/core/app/[locale]/(default)/cart/_actions/update-shipping-info.ts b/core/app/[locale]/(default)/cart/_actions/update-shipping-info.ts index 662ba0f7a6..809a3e6d43 100644 --- a/core/app/[locale]/(default)/cart/_actions/update-shipping-info.ts +++ b/core/app/[locale]/(default)/cart/_actions/update-shipping-info.ts @@ -2,10 +2,11 @@ import { BigCommerceGQLError } from '@bigcommerce/catalyst-client'; import { parseWithZod } from '@conform-to/zod'; -import { getTranslations } from 'next-intl/server'; +import { getLocale, getTranslations } from 'next-intl/server'; import { shippingActionFormDataSchema } from '@/vibes/soul/sections/cart/schema'; import { ShippingFormState } from '@/vibes/soul/sections/cart/shipping-form'; +import { getSessionCustomerAccessToken } from '~/auth'; import { getCartId } from '~/lib/cart'; import { getCart } from '../page-data'; @@ -32,7 +33,11 @@ export const updateShippingInfo = async ( return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) }; } - const cart = await getCart({ cartId }); + const [locale, customerAccessToken] = await Promise.all([ + getLocale(), + getSessionCustomerAccessToken(), + ]); + const cart = await getCart(locale, { cartId }, customerAccessToken); const checkout = cart.site.checkout; if (!checkout || !cart.site.cart) { diff --git a/core/app/[locale]/(default)/cart/page-data.ts b/core/app/[locale]/(default)/cart/page-data.ts index c6e47636dc..19d9d58d6f 100644 --- a/core/app/[locale]/(default)/cart/page-data.ts +++ b/core/app/[locale]/(default)/cart/page-data.ts @@ -1,6 +1,6 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; -import { getSessionCustomerAccessToken } from '~/auth'; import { client } from '~/client'; import { graphql, VariablesOf } from '~/client/graphql'; import { revalidate } from '~/client/revalidate-target'; @@ -295,12 +295,15 @@ const CartPageQuery = graphql( type Variables = VariablesOf; -export const getCart = async (variables: Variables) => { - const customerAccessToken = await getSessionCustomerAccessToken(); - +export const getCart = async ( + locale: string, + variables: Variables, + customerAccessToken?: string, +) => { const { data } = await client.fetch({ document: CartPageQuery, variables, + locale, customerAccessToken, fetchOptions: { cache: 'no-store', @@ -336,11 +339,20 @@ const SupportedShippingDestinationsQuery = graphql(` } `); -export const getShippingCountries = cache(async () => { - const { data } = await client.fetch({ - document: SupportedShippingDestinationsQuery, - fetchOptions: { next: { revalidate } }, - }); +const getCachedShippingCountries = unstable_cache( + async (locale: string) => { + const { data } = await client.fetch({ + document: SupportedShippingDestinationsQuery, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return data.site.settings?.shipping?.supportedShippingDestinations.countries ?? []; + }, + ['get-shipping-countries'], + { revalidate }, +); - return data.site.settings?.shipping?.supportedShippingDestinations.countries ?? []; +export const getShippingCountries = cache(async (locale: string) => { + return getCachedShippingCountries(locale); }); diff --git a/core/app/[locale]/(default)/cart/page.tsx b/core/app/[locale]/(default)/cart/page.tsx index bc07f3d473..9967347516 100644 --- a/core/app/[locale]/(default)/cart/page.tsx +++ b/core/app/[locale]/(default)/cart/page.tsx @@ -4,6 +4,7 @@ import { getFormatter, getTranslations, setRequestLocale } from 'next-intl/serve import { Streamable } from '@/vibes/soul/lib/streamable'; import { Cart as CartComponent, CartEmptyState } from '@/vibes/soul/sections/cart'; import { CartAnalyticsProvider } from '~/app/[locale]/(default)/cart/_components/cart-analytics-provider'; +import { getSessionCustomerAccessToken } from '~/auth'; import { getCartId } from '~/lib/cart'; import { getPreferredCurrencyCode } from '~/lib/currency'; import { exists } from '~/lib/utils'; @@ -32,8 +33,8 @@ export async function generateMetadata({ params }: Props): Promise { }; } -const getAnalyticsData = async (cartId: string) => { - const data = await getCart({ cartId }); +const getAnalyticsData = async (locale: string, cartId: string, customerAccessToken?: string) => { + const data = await getCart(locale, { cartId }, customerAccessToken); const cart = data.site.cart; @@ -65,10 +66,7 @@ export default async function Cart({ params }: Props) { setRequestLocale(locale); - const t = await getTranslations('Cart'); - const tGiftCertificates = await getTranslations('GiftCertificates'); - const format = await getFormatter(); - const cartId = await getCartId(); + const [t, cartId] = await Promise.all([getTranslations('Cart'), getCartId()]); if (!cartId) { return ( @@ -80,8 +78,13 @@ export default async function Cart({ params }: Props) { ); } - const currencyCode = await getPreferredCurrencyCode(); - const data = await getCart({ cartId, currencyCode }); + const [tGiftCertificates, format, currencyCode, customerAccessToken] = await Promise.all([ + getTranslations('GiftCertificates'), + getFormatter(), + getPreferredCurrencyCode(), + getSessionCustomerAccessToken(), + ]); + const data = await getCart(locale, { cartId, currencyCode }, customerAccessToken); const cart = data.site.cart; const checkout = data.site.checkout; @@ -227,7 +230,7 @@ export default async function Cart({ params }: Props) { checkout?.shippingConsignments?.find((consignment) => consignment.selectedShippingOption) || checkout?.shippingConsignments?.[0]; - const shippingCountries = await getShippingCountries(); + const shippingCountries = await getShippingCountries(locale); const countries = shippingCountries.map((country) => ({ value: country.code, @@ -260,7 +263,9 @@ export default async function Cart({ params }: Props) { return ( <> - getAnalyticsData(cartId))}> + getAnalyticsData(locale, cartId, customerAccessToken))} + > {checkoutUrl ? : null} { +const getCachedComparedProducts = unstable_cache( + async (locale: string, productIds: number[], currencyCode: CurrencyCode | undefined) => { if (productIds.length === 0) { return []; } @@ -68,10 +69,43 @@ export const getComparedProducts = cache( first: productIds.length ? MAX_COMPARE_LIMIT : 0, currencyCode, }, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } }, + locale, + fetchOptions: { cache: 'no-store' }, }); return removeEdgesAndNodes(data.site.products); }, + ['get-compared-products'], + { revalidate }, +); + +export const getComparedProducts = cache( + async ( + locale: string, + productIds: number[] = [], + currencyCode?: CurrencyCode, + customerAccessToken?: string, + ) => { + if (customerAccessToken) { + if (productIds.length === 0) { + return []; + } + + const { data } = await client.fetch({ + document: ComparedProductsQuery, + variables: { + entityIds: productIds, + first: productIds.length ? MAX_COMPARE_LIMIT : 0, + currencyCode, + }, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return removeEdgesAndNodes(data.site.products); + } + + return getCachedComparedProducts(locale, productIds, currencyCode); + }, ); diff --git a/core/app/[locale]/(default)/compare/page.tsx b/core/app/[locale]/(default)/compare/page.tsx index ac851ddb1a..e99acf7944 100644 --- a/core/app/[locale]/(default)/compare/page.tsx +++ b/core/app/[locale]/(default)/compare/page.tsx @@ -64,7 +64,12 @@ export default async function Compare(props: Props) { const parsed = CompareParamsSchema.parse(searchParams); const productIds = parsed.ids?.filter((id) => !Number.isNaN(id)); - const products = await getComparedProducts(productIds, currencyCode, customerAccessToken); + const products = await getComparedProducts( + locale, + productIds, + currencyCode, + customerAccessToken, + ); const format = await getFormatter(); return products.map((product) => ({ @@ -97,7 +102,12 @@ export default async function Compare(props: Props) { const parsed = CompareParamsSchema.parse(searchParams); const productIds = parsed.ids?.filter((id) => !Number.isNaN(id)); - const products = await getComparedProducts(productIds, currencyCode, customerAccessToken); + const products = await getComparedProducts( + locale, + productIds, + currencyCode, + customerAccessToken, + ); return products.map((product) => { return { diff --git a/core/app/[locale]/(default)/gift-certificates/balance/page.tsx b/core/app/[locale]/(default)/gift-certificates/balance/page.tsx index 0320e48acb..e6eca42d97 100644 --- a/core/app/[locale]/(default)/gift-certificates/balance/page.tsx +++ b/core/app/[locale]/(default)/gift-certificates/balance/page.tsx @@ -32,7 +32,7 @@ export default async function GiftCertificates(props: Props) { const t = await getTranslations('GiftCertificates'); const currencyCode = await getPreferredCurrencyCode(); - const data = await getGiftCertificatesData(currencyCode); + const data = await getGiftCertificatesData(locale, currencyCode); if (!data.giftCertificatesEnabled) { return redirect({ href: '/', locale }); diff --git a/core/app/[locale]/(default)/gift-certificates/page-data.ts b/core/app/[locale]/(default)/gift-certificates/page-data.ts index 6905ee0b7f..5ece542997 100644 --- a/core/app/[locale]/(default)/gift-certificates/page-data.ts +++ b/core/app/[locale]/(default)/gift-certificates/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -26,16 +27,27 @@ const GiftCertificatesRootQuery = graphql( [StoreLogoFragment], ); -export const getGiftCertificatesData = cache(async (currencyCode?: CurrencyCode) => { - const response = await client.fetch({ - document: GiftCertificatesRootQuery, - variables: { currencyCode }, - fetchOptions: { next: { revalidate } }, - }); +const getCachedGiftCertificatesData = unstable_cache( + async (locale: string, currencyCode?: CurrencyCode) => { + const response = await client.fetch({ + document: GiftCertificatesRootQuery, + variables: { currencyCode }, + locale, + fetchOptions: { cache: 'no-store' }, + }); - return { - giftCertificatesEnabled: response.data.site.settings?.giftCertificates?.isEnabled ?? false, - defaultCurrency: response.data.site.settings?.currency.defaultCurrency ?? undefined, - logo: response.data.site.settings ? logoTransformer(response.data.site.settings) : '', - }; -}); + return { + giftCertificatesEnabled: response.data.site.settings?.giftCertificates?.isEnabled ?? false, + defaultCurrency: response.data.site.settings?.currency.defaultCurrency ?? undefined, + logo: response.data.site.settings ? logoTransformer(response.data.site.settings) : '', + }; + }, + ['get-gift-certificates-data'], + { revalidate }, +); + +export const getGiftCertificatesData = cache( + async (locale: string, currencyCode?: CurrencyCode) => { + return getCachedGiftCertificatesData(locale, currencyCode); + }, +); diff --git a/core/app/[locale]/(default)/gift-certificates/page.tsx b/core/app/[locale]/(default)/gift-certificates/page.tsx index 5c50984fb3..67e62827c0 100644 --- a/core/app/[locale]/(default)/gift-certificates/page.tsx +++ b/core/app/[locale]/(default)/gift-certificates/page.tsx @@ -31,7 +31,7 @@ export default async function GiftCertificates(props: Props) { const t = await getTranslations('GiftCertificates'); const format = await getFormatter(); const currencyCode = await getPreferredCurrencyCode(); - const data = await getGiftCertificatesData(currencyCode); + const data = await getGiftCertificatesData(locale, currencyCode); if (!data.giftCertificatesEnabled) { return redirect({ href: '/', locale }); diff --git a/core/app/[locale]/(default)/gift-certificates/purchase/page-data.ts b/core/app/[locale]/(default)/gift-certificates/purchase/page-data.ts index 609584722a..76b763f086 100644 --- a/core/app/[locale]/(default)/gift-certificates/purchase/page-data.ts +++ b/core/app/[locale]/(default)/gift-certificates/purchase/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -29,17 +30,28 @@ const GiftCertificatePurchaseSettingsQuery = graphql( [GiftCertificateSettingsFragment, StoreLogoFragment], ); -export const getGiftCertificatePurchaseData = cache(async (currencyCode?: CurrencyCode) => { - const response = await client.fetch({ - document: GiftCertificatePurchaseSettingsQuery, - variables: { currencyCode }, - fetchOptions: { next: { revalidate } }, - }); +const getCachedGiftCertificatePurchaseData = unstable_cache( + async (locale: string, currencyCode?: CurrencyCode) => { + const response = await client.fetch({ + document: GiftCertificatePurchaseSettingsQuery, + variables: { currencyCode }, + locale, + fetchOptions: { cache: 'no-store' }, + }); - return { - giftCertificateSettings: response.data.site.settings?.giftCertificates ?? null, - logo: response.data.site.settings ? logoTransformer(response.data.site.settings) : '', - storeName: response.data.site.settings?.storeName ?? undefined, - defaultCurrency: response.data.site.settings?.currency.defaultCurrency ?? undefined, - }; -}); + return { + giftCertificateSettings: response.data.site.settings?.giftCertificates ?? null, + logo: response.data.site.settings ? logoTransformer(response.data.site.settings) : '', + storeName: response.data.site.settings?.storeName ?? undefined, + defaultCurrency: response.data.site.settings?.currency.defaultCurrency ?? undefined, + }; + }, + ['get-gift-certificate-purchase-data'], + { revalidate }, +); + +export const getGiftCertificatePurchaseData = cache( + async (locale: string, currencyCode?: CurrencyCode) => { + return getCachedGiftCertificatePurchaseData(locale, currencyCode); + }, +); diff --git a/core/app/[locale]/(default)/gift-certificates/purchase/page.tsx b/core/app/[locale]/(default)/gift-certificates/purchase/page.tsx index 29e4158c75..06d5bdc30e 100644 --- a/core/app/[locale]/(default)/gift-certificates/purchase/page.tsx +++ b/core/app/[locale]/(default)/gift-certificates/purchase/page.tsx @@ -153,7 +153,7 @@ export default async function GiftCertificatePurchasePage({ params }: Props) { const t = await getTranslations({ locale, namespace: 'GiftCertificates' }); const format = await getFormatter(); const currencyCode = await getPreferredCurrencyCode(); - const data = await getGiftCertificatePurchaseData(currencyCode); + const data = await getGiftCertificatePurchaseData(locale, currencyCode); if (!data.giftCertificateSettings?.isEnabled) { return redirect({ href: '/', locale }); diff --git a/core/app/[locale]/(default)/page-data.ts b/core/app/[locale]/(default)/page-data.ts index ab78d520a6..4f529eeb40 100644 --- a/core/app/[locale]/(default)/page-data.ts +++ b/core/app/[locale]/(default)/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -77,15 +78,35 @@ const HomePageQuery = graphql( [FeaturedProductsCarouselFragment, FeaturedProductsListFragment], ); -export const getPageData = cache( - async (currencyCode?: CurrencyCode, customerAccessToken?: string) => { +const getCachedPageData = unstable_cache( + async (locale: string, currencyCode: CurrencyCode | undefined) => { const { data } = await client.fetch({ document: HomePageQuery, - customerAccessToken, variables: { currencyCode }, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } }, + locale, + fetchOptions: { cache: 'no-store' }, }); return data; }, + ['get-page-data'], + { revalidate }, +); + +export const getPageData = cache( + async (locale: string, currencyCode?: CurrencyCode, customerAccessToken?: string) => { + if (customerAccessToken) { + const { data } = await client.fetch({ + document: HomePageQuery, + customerAccessToken, + variables: { currencyCode }, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return data; + } + + return getCachedPageData(locale, currencyCode); + }, ); diff --git a/core/app/[locale]/(default)/page.tsx b/core/app/[locale]/(default)/page.tsx index 77cf4db613..ebb7c52655 100644 --- a/core/app/[locale]/(default)/page.tsx +++ b/core/app/[locale]/(default)/page.tsx @@ -38,7 +38,7 @@ export default async function Home({ params }: Props) { const customerAccessToken = await getSessionCustomerAccessToken(); const currencyCode = await getPreferredCurrencyCode(); - return getPageData(currencyCode, customerAccessToken); + return getPageData(locale, currencyCode, customerAccessToken); }); const streamableFeaturedProducts = Streamable.from(async () => { diff --git a/core/app/[locale]/(default)/product/[slug]/page-data.ts b/core/app/[locale]/(default)/product/[slug]/page-data.ts index 02a8293735..ac6d3fcdda 100644 --- a/core/app/[locale]/(default)/product/[slug]/page-data.ts +++ b/core/app/[locale]/(default)/product/[slug]/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -154,17 +155,37 @@ const ProductPageMetadataQuery = graphql(` } `); -export const getProductPageMetadata = cache( - async (entityId: number, customerAccessToken?: string) => { +const getCachedProductPageMetadata = unstable_cache( + async (locale: string, entityId: number) => { const { data } = await client.fetch({ document: ProductPageMetadataQuery, variables: { entityId }, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } }, + locale, + fetchOptions: { cache: 'no-store' }, }); return data.site.product; }, + ['get-product-page-metadata'], + { revalidate }, +); + +export const getProductPageMetadata = cache( + async (locale: string, entityId: number, customerAccessToken?: string) => { + if (customerAccessToken) { + const { data } = await client.fetch({ + document: ProductPageMetadataQuery, + variables: { entityId }, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return data.site.product; + } + + return getCachedProductPageMetadata(locale, entityId); + }, ); const ProductQuery = graphql( @@ -200,16 +221,38 @@ const ProductQuery = graphql( [ProductOptionsFragment], ); -export const getProduct = cache(async (entityId: number, customerAccessToken?: string) => { - const { data } = await client.fetch({ - document: ProductQuery, - variables: { entityId }, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } }, - }); +const getCachedProduct = unstable_cache( + async (locale: string, entityId: number) => { + const { data } = await client.fetch({ + document: ProductQuery, + variables: { entityId }, + locale, + fetchOptions: { cache: 'no-store' }, + }); - return data.site; -}); + return data.site; + }, + ['get-product'], + { revalidate }, +); + +export const getProduct = cache( + async (locale: string, entityId: number, customerAccessToken?: string) => { + if (customerAccessToken) { + const { data } = await client.fetch({ + document: ProductQuery, + variables: { entityId }, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return data.site; + } + + return getCachedProduct(locale, entityId); + }, +); const StreamableProductVariantInventoryBySkuQuery = graphql(` query ProductVariantBySkuQuery($productId: Int!, $sku: String!) { @@ -249,17 +292,37 @@ const StreamableProductVariantInventoryBySkuQuery = graphql(` type VariantInventoryVariables = VariablesOf; -export const getStreamableProductVariantInventory = cache( - async (variables: VariantInventoryVariables, customerAccessToken?: string) => { +const getCachedStreamableProductVariantInventory = unstable_cache( + async (locale: string, variables: VariantInventoryVariables) => { const { data } = await client.fetch({ document: StreamableProductVariantInventoryBySkuQuery, variables, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate: 60 } }, + locale, + fetchOptions: { cache: 'no-store' }, }); return data.site.product?.variants; }, + ['get-streamable-product-variant-inventory'], + { revalidate: 60 }, +); + +export const getStreamableProductVariantInventory = cache( + async (locale: string, variables: VariantInventoryVariables, customerAccessToken?: string) => { + if (customerAccessToken) { + const { data } = await client.fetch({ + document: StreamableProductVariantInventoryBySkuQuery, + variables, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return data.site.product?.variants; + } + + return getCachedStreamableProductVariantInventory(locale, variables); + }, ); const StreamableProductQuery = graphql( @@ -322,17 +385,37 @@ const StreamableProductQuery = graphql( type Variables = VariablesOf; -export const getStreamableProduct = cache( - async (variables: Variables, customerAccessToken?: string) => { +const getCachedStreamableProduct = unstable_cache( + async (locale: string, variables: Variables) => { const { data } = await client.fetch({ document: StreamableProductQuery, variables, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } }, + locale, + fetchOptions: { cache: 'no-store' }, }); return data.site.product; }, + ['get-streamable-product'], + { revalidate }, +); + +export const getStreamableProduct = cache( + async (locale: string, variables: Variables, customerAccessToken?: string) => { + if (customerAccessToken) { + const { data } = await client.fetch({ + document: StreamableProductQuery, + variables, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return data.site.product; + } + + return getCachedStreamableProduct(locale, variables); + }, ); const StreamableProductInventoryQuery = graphql( @@ -365,17 +448,37 @@ const StreamableProductInventoryQuery = graphql( type ProductInventoryVariables = VariablesOf; -export const getStreamableProductInventory = cache( - async (variables: ProductInventoryVariables, customerAccessToken?: string) => { +const getCachedStreamableProductInventory = unstable_cache( + async (locale: string, variables: ProductInventoryVariables) => { const { data } = await client.fetch({ document: StreamableProductInventoryQuery, variables, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate: 60 } }, + locale, + fetchOptions: { cache: 'no-store' }, }); return data.site.product; }, + ['get-streamable-product-inventory'], + { revalidate: 60 }, +); + +export const getStreamableProductInventory = cache( + async (locale: string, variables: ProductInventoryVariables, customerAccessToken?: string) => { + if (customerAccessToken) { + const { data } = await client.fetch({ + document: StreamableProductInventoryQuery, + variables, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return data.site.product; + } + + return getCachedStreamableProductInventory(locale, variables); + }, ); // Fields that require currencyCode as a query variable @@ -409,17 +512,37 @@ const ProductPricingAndRelatedProductsQuery = graphql( [PricingFragment, FeaturedProductsCarouselFragment], ); -export const getProductPricingAndRelatedProducts = cache( - async (variables: Variables, customerAccessToken?: string) => { +const getCachedProductPricingAndRelatedProducts = unstable_cache( + async (locale: string, variables: Variables) => { const { data } = await client.fetch({ document: ProductPricingAndRelatedProductsQuery, variables, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } }, + locale, + fetchOptions: { cache: 'no-store' }, }); return data.site.product; }, + ['get-product-pricing-and-related-products'], + { revalidate }, +); + +export const getProductPricingAndRelatedProducts = cache( + async (locale: string, variables: Variables, customerAccessToken?: string) => { + if (customerAccessToken) { + const { data } = await client.fetch({ + document: ProductPricingAndRelatedProductsQuery, + variables, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return data.site.product; + } + + return getCachedProductPricingAndRelatedProducts(locale, variables); + }, ); const InventorySettingsQuery = graphql(` @@ -440,12 +563,33 @@ const InventorySettingsQuery = graphql(` } `); -export const getStreamableInventorySettingsQuery = cache(async (customerAccessToken?: string) => { - const { data } = await client.fetch({ - document: InventorySettingsQuery, - customerAccessToken, - fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } }, - }); +const getCachedStreamableInventorySettingsQuery = unstable_cache( + async (locale: string) => { + const { data } = await client.fetch({ + document: InventorySettingsQuery, + locale, + fetchOptions: { cache: 'no-store' }, + }); - return data.site.settings?.inventory; -}); + return data.site.settings?.inventory; + }, + ['get-streamable-inventory-settings'], + { revalidate }, +); + +export const getStreamableInventorySettingsQuery = cache( + async (locale: string, customerAccessToken?: string) => { + if (customerAccessToken) { + const { data } = await client.fetch({ + document: InventorySettingsQuery, + customerAccessToken, + locale, + fetchOptions: { cache: 'no-store' }, + }); + + return data.site.settings?.inventory; + } + + return getCachedStreamableInventorySettingsQuery(locale); + }, +); diff --git a/core/app/[locale]/(default)/product/[slug]/page.tsx b/core/app/[locale]/(default)/product/[slug]/page.tsx index c1e8a96bf7..07d1f446e1 100644 --- a/core/app/[locale]/(default)/product/[slug]/page.tsx +++ b/core/app/[locale]/(default)/product/[slug]/page.tsx @@ -44,7 +44,7 @@ export async function generateMetadata({ params }: Props): Promise { const productId = Number(slug); - const product = await getProductPageMetadata(productId, customerAccessToken); + const product = await getProductPageMetadata(locale, productId, customerAccessToken); if (!product) { return notFound(); @@ -76,7 +76,11 @@ export default async function Product({ params, searchParams }: Props) { const productId = Number(slug); - const { product: baseProduct, settings } = await getProduct(productId, customerAccessToken); + const { product: baseProduct, settings } = await getProduct( + locale, + productId, + customerAccessToken, + ); const reviewsEnabled = Boolean(settings?.reviews.enabled && !settings.display.showProductRating); const showRating = Boolean(settings?.reviews.enabled && settings.display.showProductRating); @@ -103,7 +107,7 @@ export default async function Product({ params, searchParams }: Props) { useDefaultOptionSelections: true, }; - const product = await getStreamableProduct(variables, customerAccessToken); + const product = await getStreamableProduct(locale, variables, customerAccessToken); if (!product) { return notFound(); @@ -119,7 +123,7 @@ export default async function Product({ params, searchParams }: Props) { entityId: Number(productId), }; - const product = await getStreamableProductInventory(variables, customerAccessToken); + const product = await getStreamableProductInventory(locale, variables, customerAccessToken); if (!product) { return notFound(); @@ -140,7 +144,11 @@ export default async function Product({ params, searchParams }: Props) { sku: product.sku, }; - const variants = await getStreamableProductVariantInventory(variables, customerAccessToken); + const variants = await getStreamableProductVariantInventory( + locale, + variables, + customerAccessToken, + ); if (!variants) { return undefined; @@ -170,7 +178,7 @@ export default async function Product({ params, searchParams }: Props) { currencyCode, }; - return await getProductPricingAndRelatedProducts(variables, customerAccessToken); + return await getProductPricingAndRelatedProducts(locale, variables, customerAccessToken); }); const streamablePrices = Streamable.from(async () => { @@ -238,7 +246,7 @@ export default async function Product({ params, searchParams }: Props) { }); const streamableInventorySettings = Streamable.from(async () => { - return await getStreamableInventorySettingsQuery(customerAccessToken); + return await getStreamableInventorySettingsQuery(locale, customerAccessToken); }); const getBackorderAvailabilityPrompt = ({ diff --git a/core/app/[locale]/(default)/webpages/[id]/contact/page-data.ts b/core/app/[locale]/(default)/webpages/[id]/contact/page-data.ts index 191e5f56ac..c1952ca187 100644 --- a/core/app/[locale]/(default)/webpages/[id]/contact/page-data.ts +++ b/core/app/[locale]/(default)/webpages/[id]/contact/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -31,12 +32,21 @@ const ContactPageQuery = graphql( type Variables = VariablesOf; -export const getWebpageData = cache(async (variables: Variables) => { - const { data } = await client.fetch({ - document: ContactPageQuery, - variables, - fetchOptions: { next: { revalidate } }, - }); +const getCachedWebpageData = unstable_cache( + async (locale: string, variables: Variables) => { + const { data } = await client.fetch({ + document: ContactPageQuery, + variables, + locale, + fetchOptions: { cache: 'no-store' }, + }); - return data; + return data; + }, + ['get-contact-webpage-data'], + { revalidate }, +); + +export const getWebpageData = cache(async (locale: string, variables: Variables) => { + return getCachedWebpageData(locale, variables); }); diff --git a/core/app/[locale]/(default)/webpages/[id]/contact/page.tsx b/core/app/[locale]/(default)/webpages/[id]/contact/page.tsx index b8d1f2e759..291068b27c 100644 --- a/core/app/[locale]/(default)/webpages/[id]/contact/page.tsx +++ b/core/app/[locale]/(default)/webpages/[id]/contact/page.tsx @@ -40,8 +40,8 @@ const fieldMapping = { type ContactField = keyof typeof fieldMapping; -const getWebPage = cache(async (id: string): Promise => { - const data = await getWebpageData({ id: decodeURIComponent(id) }); +const getWebPage = cache(async (locale: string, id: string): Promise => { + const data = await getWebpageData(locale, { id: decodeURIComponent(id) }); const webpage = data.node?.__typename === 'ContactPage' ? data.node : null; if (!webpage) { @@ -61,10 +61,10 @@ const getWebPage = cache(async (id: string): Promise => { }; }); -async function getWebPageBreadcrumbs(id: string): Promise { +async function getWebPageBreadcrumbs(locale: string, id: string): Promise { const t = await getTranslations('WebPages.ContactUs'); - const webpage = await getWebPage(id); + const webpage = await getWebPage(locale, id); const [, ...rest] = webpage.breadcrumbs.reverse(); const breadcrumbs = [ { @@ -81,8 +81,8 @@ async function getWebPageBreadcrumbs(id: string): Promise { return truncateBreadcrumbs(breadcrumbs, 5); } -async function getWebPageWithSuccessContent(id: string, message: string) { - const webpage = await getWebPage(id); +async function getWebPageWithSuccessContent(locale: string, id: string, message: string) { + const webpage = await getWebPage(locale, id); return { ...webpage, @@ -90,9 +90,9 @@ async function getWebPageWithSuccessContent(id: string, message: string) { }; } -async function getContactFields(id: string) { +async function getContactFields(locale: string, id: string) { const t = await getTranslations('WebPages.ContactUs.Form'); - const { entityId, path, contactFields } = await getWebPage(id); + const { entityId, path, contactFields } = await getWebPage(locale, id); const toGroupsOfTwo = (fields: Field[]) => fields.reduce>>((acc, _, i) => { if (i % 2 === 0) { @@ -155,7 +155,7 @@ async function getContactFields(id: string) { export async function generateMetadata({ params }: Props): Promise { const { id, locale } = await params; - const webpage = await getWebPage(id); + const webpage = await getWebPage(locale, id); const { pageTitle, metaDescription, metaKeywords } = webpage.seo; return { @@ -179,8 +179,8 @@ export default async function ContactPage({ params, searchParams }: Props) { if (success === 'true') { return ( getWebPageBreadcrumbs(id))} - webPage={Streamable.from(() => getWebPageWithSuccessContent(id, t('success')))} + breadcrumbs={Streamable.from(() => getWebPageBreadcrumbs(locale, id))} + webPage={Streamable.from(() => getWebPageWithSuccessContent(locale, id, t('success')))} > getWebPageBreadcrumbs(id))} - webPage={Streamable.from(() => getWebPage(id))} + breadcrumbs={Streamable.from(() => getWebPageBreadcrumbs(locale, id))} + webPage={Streamable.from(() => getWebPage(locale, id))} >
diff --git a/core/app/[locale]/(default)/webpages/[id]/normal/page-data.ts b/core/app/[locale]/(default)/webpages/[id]/normal/page-data.ts index eb87a7884d..1a6bc521f1 100644 --- a/core/app/[locale]/(default)/webpages/[id]/normal/page-data.ts +++ b/core/app/[locale]/(default)/webpages/[id]/normal/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -29,12 +30,21 @@ const NormalPageQuery = graphql( type Variables = VariablesOf; -export const getWebpageData = cache(async (variables: Variables) => { - const { data } = await client.fetch({ - document: NormalPageQuery, - variables, - fetchOptions: { next: { revalidate } }, - }); +const getCachedWebpageData = unstable_cache( + async (locale: string, variables: Variables) => { + const { data } = await client.fetch({ + document: NormalPageQuery, + variables, + locale, + fetchOptions: { cache: 'no-store' }, + }); - return data; + return data; + }, + ['get-normal-webpage-data'], + { revalidate }, +); + +export const getWebpageData = cache(async (locale: string, variables: Variables) => { + return getCachedWebpageData(locale, variables); }); diff --git a/core/app/[locale]/(default)/webpages/[id]/normal/page.tsx b/core/app/[locale]/(default)/webpages/[id]/normal/page.tsx index a222ea18c8..3b55299d9e 100644 --- a/core/app/[locale]/(default)/webpages/[id]/normal/page.tsx +++ b/core/app/[locale]/(default)/webpages/[id]/normal/page.tsx @@ -19,8 +19,8 @@ interface Props { params: Promise<{ locale: string; id: string }>; } -const getWebPage = cache(async (id: string): Promise => { - const data = await getWebpageData({ id: decodeURIComponent(id) }); +const getWebPage = cache(async (locale: string, id: string): Promise => { + const data = await getWebpageData(locale, { id: decodeURIComponent(id) }); const webpage = data.node?.__typename === 'NormalPage' ? data.node : null; if (!webpage) { @@ -37,10 +37,10 @@ const getWebPage = cache(async (id: string): Promise => { }; }); -async function getWebPageBreadcrumbs(id: string): Promise { +async function getWebPageBreadcrumbs(locale: string, id: string): Promise { const t = await getTranslations('WebPages.Normal'); - const webpage = await getWebPage(id); + const webpage = await getWebPage(locale, id); const [, ...rest] = webpage.breadcrumbs.reverse(); const breadcrumbs = [ { @@ -59,7 +59,7 @@ async function getWebPageBreadcrumbs(id: string): Promise { export async function generateMetadata({ params }: Props): Promise { const { id, locale } = await params; - const webpage = await getWebPage(id); + const webpage = await getWebPage(locale, id); const { pageTitle, metaDescription, metaKeywords } = webpage.seo; // Get the path from the last breadcrumb @@ -80,8 +80,8 @@ export default async function WebPage({ params }: Props) { return ( getWebPageBreadcrumbs(id))} - webPage={Streamable.from(() => getWebPage(id))} + breadcrumbs={Streamable.from(() => getWebPageBreadcrumbs(locale, id))} + webPage={Streamable.from(() => getWebPage(locale, id))} /> ); } diff --git a/core/app/[locale]/(default)/wishlist/[token]/page-data.ts b/core/app/[locale]/(default)/wishlist/[token]/page-data.ts index 4c3e56de3d..d03424ea59 100644 --- a/core/app/[locale]/(default)/wishlist/[token]/page-data.ts +++ b/core/app/[locale]/(default)/wishlist/[token]/page-data.ts @@ -1,3 +1,4 @@ +import { unstable_cache } from 'next/cache'; import { cache } from 'react'; import { client } from '~/client'; @@ -5,9 +6,9 @@ import { PaginationFragment } from '~/client/fragments/pagination'; import { graphql } from '~/client/graphql'; import { revalidate } from '~/client/revalidate-target'; import { TAGS } from '~/client/tags'; +import type { CurrencyCode } from '~/components/header/fragment'; import { ProductCardFragment } from '~/components/product-card/fragment'; import { WishlistItemFragment } from '~/components/wishlist/fragment'; -import { getPreferredCurrencyCode } from '~/lib/currency'; const PublicWishlistQuery = graphql( ` @@ -50,22 +51,27 @@ interface Pagination { after?: string | null; } -export const getPublicWishlist = cache(async (token: string, pagination: Pagination) => { - const { before, after, limit = 9 } = pagination; - const currencyCode = await getPreferredCurrencyCode(); - const paginationArgs = before ? { last: limit, before } : { first: limit, after }; - const response = await client.fetch({ - document: PublicWishlistQuery, - variables: { ...paginationArgs, currencyCode, token }, - // Since the wishlist is public, it's okay that we cache this request - fetchOptions: { next: { revalidate, tags: [TAGS.customer] } }, - }); +const getCachedPublicWishlist = unstable_cache( + async (locale: string, token: string, pagination: Pagination, currencyCode?: CurrencyCode) => { + const { before, after, limit = 9 } = pagination; + const paginationArgs = before ? { last: limit, before } : { first: limit, after }; + const response = await client.fetch({ + document: PublicWishlistQuery, + variables: { ...paginationArgs, currencyCode, token }, + locale, + fetchOptions: { cache: 'no-store' }, + }); - const wishlist = response.data.site.publicWishlist; + const wishlist = response.data.site.publicWishlist; - if (!wishlist) { - return null; - } + return wishlist; + }, + ['get-public-wishlist'], + { revalidate, tags: [TAGS.customer] }, +); - return wishlist; -}); +export const getPublicWishlist = cache( + async (locale: string, token: string, pagination: Pagination, currencyCode?: CurrencyCode) => { + return getCachedPublicWishlist(locale, token, pagination, currencyCode); + }, +); diff --git a/core/app/[locale]/(default)/wishlist/[token]/page.tsx b/core/app/[locale]/(default)/wishlist/[token]/page.tsx index e6fe52378f..ab4d7bcc50 100644 --- a/core/app/[locale]/(default)/wishlist/[token]/page.tsx +++ b/core/app/[locale]/(default)/wishlist/[token]/page.tsx @@ -13,12 +13,14 @@ import { Wishlist, WishlistDetails } from '@/vibes/soul/sections/wishlist-detail import { addWishlistItemToCart } from '~/app/[locale]/(default)/account/wishlists/[id]/_actions/add-to-cart'; import { WishlistAnalyticsProvider } from '~/app/[locale]/(default)/account/wishlists/[id]/_components/wishlist-analytics-provider'; import { ExistingResultType } from '~/client/util'; +import type { CurrencyCode } from '~/components/header/fragment'; import { WishlistShareButton, WishlistShareButtonSkeleton, } from '~/components/wishlist/share-button'; import { defaultPageInfo, pageInfoTransformer } from '~/data-transformers/page-info-transformer'; import { publicWishlistDetailsTransformer } from '~/data-transformers/wishlists-transformer'; +import { getPreferredCurrencyCode } from '~/lib/currency'; import { getMetadataAlternates } from '~/lib/seo/canonical'; import { isMobileUser } from '~/lib/user-agent'; @@ -38,14 +40,16 @@ const searchParamsCache = createSearchParamsCache({ }); async function getWishlist( + locale: string, token: string, t: ExistingResultType>, pt: ExistingResultType>, searchParams: Promise, + currencyCode: CurrencyCode | undefined, ): Promise { const searchParamsParsed = searchParamsCache.parse(await searchParams); const formatter = await getFormatter(); - const wishlist = await getPublicWishlist(token, searchParamsParsed); + const wishlist = await getPublicWishlist(locale, token, searchParamsParsed, currencyCode); if (!wishlist) { return notFound(); @@ -55,11 +59,13 @@ async function getWishlist( } async function getPaginationInfo( + locale: string, token: string, searchParams: Promise, + currencyCode: CurrencyCode | undefined, ): Promise { const searchParamsParsed = searchParamsCache.parse(await searchParams); - const wishlist = await getPublicWishlist(token, searchParamsParsed); + const wishlist = await getPublicWishlist(locale, token, searchParamsParsed, currencyCode); return pageInfoTransformer(wishlist?.items.pageInfo ?? defaultPageInfo); } @@ -70,7 +76,8 @@ export async function generateMetadata({ params, searchParams }: Props): Promise // to make sure we aren't bypassing an existing cache just for the metadata generation. const searchParamsParsed = searchParamsCache.parse(await searchParams); const t = await getTranslations({ locale, namespace: 'PublicWishlist' }); - const wishlist = await getPublicWishlist(token, searchParamsParsed); + const currencyCode = await getPreferredCurrencyCode(); + const wishlist = await getPublicWishlist(locale, token, searchParamsParsed, currencyCode); return { title: wishlist?.name ?? t('title'), @@ -78,9 +85,14 @@ export async function generateMetadata({ params, searchParams }: Props): Promise }; } -const getAnalyticsData = async (token: string, searchParamsPromise: Promise) => { +const getAnalyticsData = async ( + locale: string, + token: string, + searchParamsPromise: Promise, + currencyCode: CurrencyCode | undefined, +) => { const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise); - const wishlist = await getPublicWishlist(token, searchParamsParsed); + const wishlist = await getPublicWishlist(locale, token, searchParamsParsed, currencyCode); if (!wishlist) { return []; @@ -102,12 +114,14 @@ const getAnalyticsData = async (token: string, searchParamsPromise: Promise, + currencyCode: CurrencyCode | undefined, ): Promise { const t = await getTranslations('PublicWishlist'); const searchParamsParsed = searchParamsCache.parse(await searchParams); - const wishlist = await getPublicWishlist(token, searchParamsParsed); + const wishlist = await getPublicWishlist(locale, token, searchParamsParsed, currencyCode); return [ { href: '/', label: 'Home' }, @@ -120,6 +134,7 @@ export default async function PublicWishlist({ params, searchParams }: Props) { setRequestLocale(locale); + const currencyCode = await getPreferredCurrencyCode(); const t = await getTranslations('Wishlist'); const pwt = await getTranslations('PublicWishlist'); const pt = await getTranslations('Product.ProductDetails'); @@ -159,17 +174,27 @@ export default async function PublicWishlist({ params, searchParams }: Props) { }; return ( - getAnalyticsData(token, searchParams))}> + getAnalyticsData(locale, token, searchParams, currencyCode))} + > - getBreadcrumbs(token, searchParams))} /> + + getBreadcrumbs(locale, token, searchParams, currencyCode), + )} + /> getPaginationInfo(token, searchParams))} - wishlist={Streamable.from(() => getWishlist(token, t, pt, searchParams))} + paginationInfo={Streamable.from(() => + getPaginationInfo(locale, token, searchParams, currencyCode), + )} + wishlist={Streamable.from(() => + getWishlist(locale, token, t, pt, searchParams, currencyCode), + )} /> diff --git a/core/client/correlation-id.ts b/core/client/correlation-id.ts new file mode 100644 index 0000000000..f3ce208e1a --- /dev/null +++ b/core/client/correlation-id.ts @@ -0,0 +1,8 @@ +import { cache } from 'react'; + +/** + * Returns a stable correlation ID for the current request. + * React.cache ensures the same UUID is returned for all fetches within a + * single page render, while being unique across renders/requests. + */ +export const getCorrelationId = cache((): string => crypto.randomUUID()); diff --git a/core/client/index.ts b/core/client/index.ts index d0b059b994..29a20de68d 100644 --- a/core/client/index.ts +++ b/core/client/index.ts @@ -2,28 +2,11 @@ import { BigCommerceAuthError, createClient } from '@bigcommerce/catalyst-client import { headers } from 'next/headers'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { redirect } from 'next/navigation'; -import { getLocale as getServerLocale } from 'next-intl/server'; import { getChannelIdFromLocale } from '../channels.config'; import { backendUserAgent } from '../user-agent'; -const getLocale = async () => { - try { - const locale = await getServerLocale(); - - return locale; - } catch { - /** - * Next-intl `getLocale` only works on the server, and when middleware has run. - * - * Instances when `getLocale` will not work: - * - Requests in middlewares - * - Requests in `generateStaticParams` - * - Request in api routes - * - Requests in static sites without `setRequestLocale` - */ - } -}; +import { getCorrelationId } from './correlation-id'; export const client = createClient({ storefrontToken: process.env.BIGCOMMERCE_STOREFRONT_TOKEN ?? '', @@ -33,16 +16,12 @@ export const client = createClient({ logger: (process.env.NODE_ENV !== 'production' && process.env.CLIENT_LOGGER !== 'false') || process.env.CLIENT_LOGGER === 'true', - getChannelId: async (defaultChannelId: string) => { - const locale = await getLocale(); - - // We use the default channelId as a fallback, but it is not ideal in some scenarios. + getChannelId: (defaultChannelId: string, locale?: string) => { return getChannelIdFromLocale(locale) ?? defaultChannelId; }, beforeRequest: async (fetchOptions) => { // We can't serialize a `Headers` object within this method so we have to opt into using a plain object const requestHeaders: Record = {}; - const locale = await getLocale(); if (fetchOptions?.cache && ['no-store', 'no-cache'].includes(fetchOptions.cache)) { const ipAddress = (await headers()).get('X-Forwarded-For'); @@ -53,9 +32,7 @@ export const client = createClient({ } } - if (locale) { - requestHeaders['Accept-Language'] = locale; - } + requestHeaders['X-Correlation-ID'] = getCorrelationId(); return { headers: requestHeaders, diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index dd25618110..6930069969 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -20,7 +20,7 @@ interface Config { platform?: string; backendUserAgentExtensions?: string; logger?: boolean; - getChannelId?: (defaultChannelId: string) => Promise | string; + getChannelId?: (defaultChannelId: string, locale?: string) => Promise | string; beforeRequest?: ( fetchOptions?: FetcherRequestInit, ) => Promise | undefined> | Partial | undefined; @@ -49,7 +49,7 @@ type GraphQLErrorPolicy = 'none' | 'all' | 'auth' | 'ignore'; class Client { private backendUserAgent: string; private readonly defaultChannelId: string; - private getChannelId: (defaultChannelId: string) => Promise | string; + private getChannelId: (defaultChannelId: string, locale?: string) => Promise | string; private beforeRequest?: ( fetchOptions?: FetcherRequestInit, ) => Promise | undefined> | Partial | undefined; @@ -85,6 +85,7 @@ class Client { customerAccessToken?: string; fetchOptions?: FetcherRequestInit; channelId?: string; + locale?: string; errorPolicy?: GraphQLErrorPolicy; validateCustomerAccessToken?: boolean; }): Promise>; @@ -96,6 +97,7 @@ class Client { customerAccessToken?: string; fetchOptions?: FetcherRequestInit; channelId?: string; + locale?: string; errorPolicy?: GraphQLErrorPolicy; validateCustomerAccessToken?: boolean; }): Promise>; @@ -106,6 +108,7 @@ class Client { customerAccessToken, fetchOptions = {} as FetcherRequestInit, channelId, + locale, errorPolicy = 'none', validateCustomerAccessToken = true, }: { @@ -114,6 +117,7 @@ class Client { customerAccessToken?: string; fetchOptions?: FetcherRequestInit; channelId?: string; + locale?: string; errorPolicy?: GraphQLErrorPolicy; validateCustomerAccessToken?: boolean; }): Promise> { @@ -126,6 +130,7 @@ class Client { channelId, operationInfo.name, operationInfo.type, + locale, ); const { headers: additionalFetchHeaders = {}, ...additionalFetchOptions } = (await this.beforeRequest?.(fetchOptions)) ?? {}; @@ -136,6 +141,7 @@ class Client { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.storefrontToken}`, 'User-Agent': this.backendUserAgent, + ...(locale && { 'Accept-Language': locale }), ...(customerAccessToken && { 'X-Bc-Customer-Access-Token': customerAccessToken }), ...(validateCustomerAccessToken && { 'X-Bc-Error-On-Invalid-Customer-Access-Token': 'true', @@ -210,8 +216,8 @@ class Client { return response.text(); } - private async getCanonicalUrl(channelId?: string) { - const resolvedChannelId = channelId ?? (await this.getChannelId(this.defaultChannelId)); + private async getCanonicalUrl(channelId?: string, locale?: string) { + const resolvedChannelId = channelId ?? (await this.getChannelId(this.defaultChannelId, locale)); return `https://store-${this.config.storeHash}-${resolvedChannelId}.${graphqlApiDomain}`; } @@ -220,8 +226,9 @@ class Client { channelId?: string, operationName?: string, operationType?: string, + locale?: string, ) { - const baseUrl = new URL(`${await this.getCanonicalUrl(channelId)}/graphql`); + const baseUrl = new URL(`${await this.getCanonicalUrl(channelId, locale)}/graphql`); if (operationName) { baseUrl.searchParams.set('operation', operationName);