18d307f52SEvan Bacon/** 28d307f52SEvan Bacon * Copyright (c) 2021 Expo, Inc. 38d307f52SEvan Bacon * Copyright (c) 2020 mistval. 48d307f52SEvan Bacon * 58d307f52SEvan Bacon * This source code is licensed under the MIT license found in the 68d307f52SEvan Bacon * LICENSE file in the root directory of this source tree. 78d307f52SEvan Bacon * 88d307f52SEvan Bacon * Based on https://github.com/mistval/node-fetch-cache/blob/9c40ddf786b0de22ce521d8bdaa6347bc44dd629/src/index.js#L1 98d307f52SEvan Bacon * But with TypeScript support to fix Jest tests, and removed unused code. 108d307f52SEvan Bacon */ 118d307f52SEvan Baconimport crypto from 'crypto'; 128d307f52SEvan Baconimport FormData from 'form-data'; 138d307f52SEvan Baconimport fs from 'fs'; 148d307f52SEvan Baconimport { Request, RequestInfo, RequestInit, Response } from 'node-fetch'; 158d307f52SEvan Baconimport { URLSearchParams } from 'url'; 168d307f52SEvan Bacon 178d307f52SEvan Baconimport { FileSystemCache } from './FileSystemCache'; 188d307f52SEvan Baconimport { NFCResponse } from './response'; 19*8a424bebSJames Ideimport { FetchLike } from '../client.types'; 208d307f52SEvan Bacon 218d307f52SEvan Baconconst CACHE_VERSION = 3; 228d307f52SEvan Bacon 238d307f52SEvan Baconconst lockPromiseForKey: Record<string, Promise<any>> = {}; 248d307f52SEvan Baconconst unlockFunctionForKey: Record<string, any> = {}; 258d307f52SEvan Bacon 268d307f52SEvan Bacon/** 278d307f52SEvan Bacon * Take out a lock. When this function returns (asynchronously), 288d307f52SEvan Bacon * you have the lock. 298d307f52SEvan Bacon * @param {string} key - The key to lock on. Anyone else who 308d307f52SEvan Bacon * tries to lock on the same key will need to wait for it to 318d307f52SEvan Bacon * be unlocked. 328d307f52SEvan Bacon */ 338d307f52SEvan Baconasync function lock(key: string) { 348d307f52SEvan Bacon if (!lockPromiseForKey[key]) { 358d307f52SEvan Bacon lockPromiseForKey[key] = Promise.resolve(); 368d307f52SEvan Bacon } 378d307f52SEvan Bacon 388d307f52SEvan Bacon const takeLockPromise = lockPromiseForKey[key]; 398d307f52SEvan Bacon lockPromiseForKey[key] = takeLockPromise.then( 408d307f52SEvan Bacon () => 418d307f52SEvan Bacon new Promise((fulfill) => { 428d307f52SEvan Bacon unlockFunctionForKey[key] = fulfill; 438d307f52SEvan Bacon }) 448d307f52SEvan Bacon ); 458d307f52SEvan Bacon 468d307f52SEvan Bacon return takeLockPromise; 478d307f52SEvan Bacon} 488d307f52SEvan Bacon 498d307f52SEvan Bacon/** 508d307f52SEvan Bacon * Release a lock. 518d307f52SEvan Bacon * @param {string} key - The key to release the lock for. 528d307f52SEvan Bacon * The next person in line will now be able to take out 538d307f52SEvan Bacon * the lock for that key. 548d307f52SEvan Bacon */ 558d307f52SEvan Baconfunction unlock(key: string) { 568d307f52SEvan Bacon if (unlockFunctionForKey[key]) { 578d307f52SEvan Bacon unlockFunctionForKey[key](); 588d307f52SEvan Bacon delete unlockFunctionForKey[key]; 598d307f52SEvan Bacon } 608d307f52SEvan Bacon} 618d307f52SEvan Bacon 628d307f52SEvan Baconfunction md5(str: string) { 638d307f52SEvan Bacon return crypto.createHash('md5').update(str).digest('hex'); 648d307f52SEvan Bacon} 658d307f52SEvan Bacon 668d307f52SEvan Bacon// Since the boundary in FormData is random, 678d307f52SEvan Bacon// we ignore it for purposes of calculating 688d307f52SEvan Bacon// the cache key. 698d307f52SEvan Baconfunction getFormDataCacheKey(formData: FormData) { 708d307f52SEvan Bacon const cacheKey = { ...formData }; 718d307f52SEvan Bacon const boundary = formData.getBoundary(); 728d307f52SEvan Bacon 738d307f52SEvan Bacon // @ts-expect-error 748d307f52SEvan Bacon delete cacheKey._boundary; 758d307f52SEvan Bacon 768d307f52SEvan Bacon const boundaryReplaceRegex = new RegExp(boundary, 'g'); 778d307f52SEvan Bacon 788d307f52SEvan Bacon // @ts-expect-error 798d307f52SEvan Bacon cacheKey._streams = cacheKey._streams.map((s) => { 808d307f52SEvan Bacon if (typeof s === 'string') { 818d307f52SEvan Bacon return s.replace(boundaryReplaceRegex, ''); 828d307f52SEvan Bacon } 838d307f52SEvan Bacon 848d307f52SEvan Bacon return s; 858d307f52SEvan Bacon }); 868d307f52SEvan Bacon 878d307f52SEvan Bacon return cacheKey; 888d307f52SEvan Bacon} 898d307f52SEvan Bacon 908d307f52SEvan Baconfunction getBodyCacheKeyJson(body: any) { 918d307f52SEvan Bacon if (!body) { 928d307f52SEvan Bacon return body; 938d307f52SEvan Bacon } 948d307f52SEvan Bacon if (typeof body === 'string') { 958d307f52SEvan Bacon return body; 968d307f52SEvan Bacon } 978d307f52SEvan Bacon if (body instanceof URLSearchParams) { 988d307f52SEvan Bacon return body.toString(); 998d307f52SEvan Bacon } 1008d307f52SEvan Bacon if (body instanceof fs.ReadStream) { 1018d307f52SEvan Bacon return body.path; 1028d307f52SEvan Bacon } 1038d307f52SEvan Bacon if (body.toString && body.toString() === '[object FormData]') { 1048d307f52SEvan Bacon return getFormDataCacheKey(body); 1058d307f52SEvan Bacon } 1068d307f52SEvan Bacon if (body instanceof Buffer) { 1078d307f52SEvan Bacon return body.toString(); 1088d307f52SEvan Bacon } 1098d307f52SEvan Bacon 1108d307f52SEvan Bacon throw new Error( 1118d307f52SEvan Bacon 'Unsupported body type. Supported body types are: string, number, undefined, null, url.URLSearchParams, fs.ReadStream, FormData' 1128d307f52SEvan Bacon ); 1138d307f52SEvan Bacon} 1148d307f52SEvan Bacon 1158d307f52SEvan Baconfunction getRequestCacheKey(req: any) { 1168d307f52SEvan Bacon return { 1178d307f52SEvan Bacon cache: req.cache, 1188d307f52SEvan Bacon credentials: req.credentials, 1198d307f52SEvan Bacon destination: req.destination, 1208d307f52SEvan Bacon headers: req.headers, 1218d307f52SEvan Bacon integrity: req.integrity, 1228d307f52SEvan Bacon method: req.method, 1238d307f52SEvan Bacon redirect: req.redirect, 1248d307f52SEvan Bacon referrer: req.referrer, 1258d307f52SEvan Bacon referrerPolicy: req.referrerPolicy, 1268d307f52SEvan Bacon url: req.url, 1278d307f52SEvan Bacon body: getBodyCacheKeyJson(req.body), 1288d307f52SEvan Bacon }; 1298d307f52SEvan Bacon} 1308d307f52SEvan Bacon 1318d307f52SEvan Baconfunction getCacheKey(requestArguments: any[]) { 1328d307f52SEvan Bacon const resource = requestArguments[0]; 1338d307f52SEvan Bacon const init = requestArguments[1] || {}; 1348d307f52SEvan Bacon 1358d307f52SEvan Bacon const resourceCacheKeyJson = 1368d307f52SEvan Bacon resource instanceof Request ? getRequestCacheKey(resource) : { url: resource }; 1378d307f52SEvan Bacon 1388d307f52SEvan Bacon const initCacheKeyJson = { ...init }; 1398d307f52SEvan Bacon 1408d307f52SEvan Bacon // @ts-ignore 1418d307f52SEvan Bacon resourceCacheKeyJson.body = getBodyCacheKeyJson(resourceCacheKeyJson.body); 1428d307f52SEvan Bacon initCacheKeyJson.body = getBodyCacheKeyJson(initCacheKeyJson.body); 1438d307f52SEvan Bacon 1448d307f52SEvan Bacon delete initCacheKeyJson.agent; 1458d307f52SEvan Bacon 1468d307f52SEvan Bacon return md5(JSON.stringify([resourceCacheKeyJson, initCacheKeyJson, CACHE_VERSION])); 1478d307f52SEvan Bacon} 1488d307f52SEvan Bacon 1498d307f52SEvan Baconexport function wrapFetchWithCache( 1508d307f52SEvan Bacon fetch: FetchLike, 1518d307f52SEvan Bacon cache: FileSystemCache 1528d307f52SEvan Bacon): (url: RequestInfo, init?: RequestInit | undefined) => Promise<Response> { 1538d307f52SEvan Bacon async function getResponse( 1548d307f52SEvan Bacon cache: FileSystemCache, 1558d307f52SEvan Bacon url: RequestInfo, 1568d307f52SEvan Bacon init?: RequestInit | undefined 1578d307f52SEvan Bacon ) { 1588d307f52SEvan Bacon const cacheKey = getCacheKey([url, init]); 1598d307f52SEvan Bacon let cachedValue = await cache.get(cacheKey); 1608d307f52SEvan Bacon 1618d307f52SEvan Bacon const ejectSelfFromCache = () => cache.remove(cacheKey); 1628d307f52SEvan Bacon 1638d307f52SEvan Bacon if (cachedValue) { 1648d307f52SEvan Bacon return new NFCResponse( 1658d307f52SEvan Bacon cachedValue.bodyStream, 1668d307f52SEvan Bacon cachedValue.metaData, 1678d307f52SEvan Bacon ejectSelfFromCache, 1688d307f52SEvan Bacon true 1698d307f52SEvan Bacon ); 1708d307f52SEvan Bacon } 1718d307f52SEvan Bacon 1728d307f52SEvan Bacon await lock(cacheKey); 1738d307f52SEvan Bacon try { 1748d307f52SEvan Bacon cachedValue = await cache.get(cacheKey); 1758d307f52SEvan Bacon if (cachedValue) { 1768d307f52SEvan Bacon return new NFCResponse( 1778d307f52SEvan Bacon cachedValue.bodyStream, 1788d307f52SEvan Bacon cachedValue.metaData, 1798d307f52SEvan Bacon ejectSelfFromCache, 1808d307f52SEvan Bacon true 1818d307f52SEvan Bacon ); 1828d307f52SEvan Bacon } 1838d307f52SEvan Bacon 1848d307f52SEvan Bacon const fetchResponse = await fetch(url, init); 1858d307f52SEvan Bacon const serializedMeta = NFCResponse.serializeMetaFromNodeFetchResponse(fetchResponse); 1868d307f52SEvan Bacon 1878d307f52SEvan Bacon const newlyCachedData = await cache.set( 1888d307f52SEvan Bacon cacheKey, 1898d307f52SEvan Bacon // @ts-expect-error 1908d307f52SEvan Bacon fetchResponse.body, 1918d307f52SEvan Bacon serializedMeta 1928d307f52SEvan Bacon ); 1938d307f52SEvan Bacon 1948d307f52SEvan Bacon return new NFCResponse( 1958d307f52SEvan Bacon newlyCachedData!.bodyStream, 1968d307f52SEvan Bacon newlyCachedData!.metaData, 1978d307f52SEvan Bacon ejectSelfFromCache, 1988d307f52SEvan Bacon false 1998d307f52SEvan Bacon ); 2008d307f52SEvan Bacon } finally { 2018d307f52SEvan Bacon unlock(cacheKey); 2028d307f52SEvan Bacon } 2038d307f52SEvan Bacon } 2048d307f52SEvan Bacon return (url: RequestInfo, init?: RequestInit | undefined) => getResponse(cache, url, init); 2058d307f52SEvan Bacon} 206