(
url: RequestInfo,
options: RequestInit = {},
timeout: number = getRequestTimeoutMs(),
format: 'json' | 'text' = 'json',
bustOrOptions: boolean | CacheOptions | undefined = false,
maxRetries?: number,
)
| 747 | * @see enableCache / disableCache for cache control |
| 748 | */ |
| 749 | export async function fetchWithCache<T = unknown>( |
| 750 | url: RequestInfo, |
| 751 | options: RequestInit = {}, |
| 752 | timeout: number = getRequestTimeoutMs(), |
| 753 | format: 'json' | 'text' = 'json', |
| 754 | bustOrOptions: boolean | CacheOptions | undefined = false, |
| 755 | maxRetries?: number, |
| 756 | ): Promise<FetchWithCacheResult<T>> { |
| 757 | const cacheOptions: CacheOptions = |
| 758 | typeof bustOrOptions === 'boolean' ? { bust: bustOrOptions } : (bustOrOptions ?? {}); |
| 759 | const { bust = false, repeatIndex } = cacheOptions; |
| 760 | |
| 761 | // Only retry body-read for idempotent methods to avoid double-submitting |
| 762 | // POST/PATCH requests (the server already processed the request once |
| 763 | // headers arrived; only the response body stream failed). |
| 764 | const method = (options.method ?? (url instanceof Request ? url.method : 'GET')).toUpperCase(); |
| 765 | const isIdempotent = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'].includes(method); |
| 766 | |
| 767 | const cacheEnabled = getEffectiveCacheEnabled(); |
| 768 | const cacheKey = |
| 769 | cacheEnabled && !bust ? getFetchCacheKey(url, options, method, format, repeatIndex) : null; |
| 770 | |
| 771 | if (!cacheEnabled || bust || cacheKey == null) { |
| 772 | const { respText, resp, fetchLatencyMs } = await fetchAndReadBody( |
| 773 | url, |
| 774 | options, |
| 775 | timeout, |
| 776 | maxRetries, |
| 777 | isIdempotent, |
| 778 | ); |
| 779 | try { |
| 780 | return { |
| 781 | cached: false, |
| 782 | data: format === 'json' ? JSON.parse(respText) : respText, |
| 783 | status: resp.status, |
| 784 | statusText: resp.statusText, |
| 785 | headers: Object.fromEntries(resp.headers.entries()), |
| 786 | latencyMs: fetchLatencyMs, |
| 787 | deleteFromCache: async () => { |
| 788 | // No-op when cache is disabled |
| 789 | }, |
| 790 | }; |
| 791 | } catch (err) { |
| 792 | throw new Error( |
| 793 | `Error parsing response from ${url}: ${ |
| 794 | (err as Error).message |
| 795 | }. HTTP ${resp.status} ${resp.statusText}. Received text: ${respText}`, |
| 796 | ); |
| 797 | } |
| 798 | } |
| 799 | |
| 800 | const cache = getCacheInstance(); |
| 801 | |
| 802 | const cachedResponse = await cache.get<SerializedFetchResponse>(cacheKey); |
| 803 | if (cachedResponse != null) { |
| 804 | logger.debug(`Returning cached response for ${url}: ${cachedResponse}`); |
| 805 | return deserializeFetchResponse<T>(cachedResponse, true, cache, cacheKey); |
| 806 | } |
no test coverage detected
searching dependent graphs…