/** * External dependencies */ import apiFetch from '@wordpress/api-fetch'; /** * Internal dependencies */ import { Product, ProductType, SearchAPIProductType, SearchAPIJSONType, } from '../components/product-list/types'; import { MARKETPLACE_HOST, MARKETPLACE_CATEGORY_API_PATH, MARKETPLACE_SEARCH_API_PATH, } from '../components/constants'; import { CategoryAPIItem } from '../components/category-selector/types'; import { LOCALE } from '../../utils/admin-settings'; interface ProductGroup { id: string; title: string; items: Product[]; url: string; itemType: ProductType; } // The fetchCache stores the results of GET fetch/apiFetch calls from the Marketplace, in RAM, for performance const maxFetchCacheSize = 100; const fetchCache = new Map(); function maybePruneFetchCache() { while ( fetchCache.size > maxFetchCacheSize ) { fetchCache.delete( fetchCache.keys().next().value ); } } // Wrapper around apiFetch() that caches results in memory async function apiFetchWithCache( params: object ): Promise< object > { // Attempt to fetch from cache: const cacheKey = JSON.stringify( params ); if ( fetchCache.get( cacheKey ) ) { return new Promise( ( resolve ) => { resolve( fetchCache.get( cacheKey ) ); } ); } // Failing that, fetch using apiCache: return new Promise( ( resolve, reject ) => { apiFetch( params ) .then( ( json ) => { fetchCache.set( cacheKey, json ); maybePruneFetchCache(); resolve( json as object ); } ) .catch( () => { reject(); } ); } ); } // Wrapper around fetch() that caches results in memory async function fetchJsonWithCache( url: string, abortSignal?: AbortSignal ): Promise< object > { // Attempt to fetch from cache: if ( fetchCache.get( url ) ) { return new Promise( ( resolve ) => { resolve( fetchCache.get( url ) ); } ); } // Failing that, fetch from net: return new Promise( ( resolve, reject ) => { fetch( url, { signal: abortSignal } ) .then( ( response ) => { if ( ! response.ok ) { throw new Error( response.statusText ); } return response.json(); } ) .then( ( json ) => { fetchCache.set( url, json ); maybePruneFetchCache(); resolve( json ); } ) .catch( () => { reject(); } ); } ); } // Fetch search results for a given set of URLSearchParams from the WooCommerce.com API async function fetchSearchResults( params: URLSearchParams, abortSignal?: AbortSignal ): Promise< Product[] > { const url = MARKETPLACE_HOST + MARKETPLACE_SEARCH_API_PATH + '?' + params.toString(); // Fetch data from WCCOM API return new Promise( ( resolve, reject ) => { fetchJsonWithCache( url, abortSignal ) .then( ( json ) => { /** * Product card component expects a Product type. * So we build that object from the API response. */ const products = ( json as SearchAPIJSONType ).products.map( ( product: SearchAPIProductType ): Product => { return { id: product.id, title: product.title, image: product.image, type: product.type, description: product.excerpt, vendorName: product.vendor_name, vendorUrl: product.vendor_url, icon: product.icon, url: product.link, // Due to backwards compatibility, raw_price is from search API, price is from featured API price: product.raw_price ?? product.price, averageRating: product.rating ?? 0, reviewsCount: product.reviews_count ?? 0, }; } ); resolve( products ); } ) .catch( () => reject ); } ); } // Fetch data for the discover page from the WooCommerce.com API async function fetchDiscoverPageData(): Promise< ProductGroup[] > { let url = '/wc/v3/marketplace/featured'; if ( LOCALE.userLocale ) { url = `${ url }?locale=${ LOCALE.userLocale }`; } try { return ( await apiFetchWithCache( { path: url.toString(), } ) ) as Promise< ProductGroup[] >; } catch ( error ) { return []; } } function fetchCategories( type: ProductType ): Promise< CategoryAPIItem[] > { const url = new URL( MARKETPLACE_HOST + MARKETPLACE_CATEGORY_API_PATH ); if ( LOCALE.userLocale ) { url.searchParams.set( 'locale', LOCALE.userLocale ); } // We don't define parent for extensions since that is provided by default // This is to ensure the old marketplace continues to work when this isn't defined if ( type === ProductType.theme ) { url.searchParams.set( 'parent', 'themes' ); } return ( fetchJsonWithCache( url.toString() ) as Promise< CategoryAPIItem[] > ) .then( ( json ) => { return json; } ) .catch( () => { return []; } ); } // Append UTM parameters to a URL, being aware of existing query parameters const appendURLParams = ( url: string, utmParams: Array< [ string, string ] > ): string => { if ( ! url ) { return url; } const urlObject = new URL( url ); if ( ! urlObject ) { return url; } utmParams.forEach( ( [ key, value ] ) => { urlObject.searchParams.set( key, value ); } ); return urlObject.toString(); }; export { fetchSearchResults, fetchDiscoverPageData, fetchCategories, ProductGroup, appendURLParams, };