UNPKG

axios-cache-interceptor

Version:
1 lines 86.7 kB
{"version":3,"file":"index.mjs","names":["parseVary","compareVary","parseVary"],"sources":["../src/header/headers.ts","../src/header/interpreter.ts","../src/header/extract.ts","../src/util/cache-predicate.ts","../src/interceptors/util.ts","../src/interceptors/request.ts","../src/util/update-cache.ts","../src/interceptors/response.ts","../src/storage/build.ts","../src/storage/memory.ts","../src/util/key-generator.ts","../src/cache/create.ts","../src/storage/web-api.ts","../src/index.ts"],"sourcesContent":["/**\n * @deprecated This constant will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport const Header = {\n /**\n * ```txt\n * If-Modified-Since: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT\n * ```\n *\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since\n */\n IfModifiedSince: 'if-modified-since',\n\n /**\n * ```txt\n * Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT\n * ```\n *\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified\n */\n LastModified: 'last-modified',\n\n /**\n * ```txt\n * If-None-Match: \"<etag_value>\"\n * If-None-Match: \"<etag_value>\", \"<etag_value>\", …\n * If-None-Match: *\n * ```\n *\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match\n */\n IfNoneMatch: 'if-none-match',\n\n /**\n * ```txt\n * Cache-Control: max-age=604800\n * ```\n *\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control\n */\n CacheControl: 'cache-control',\n\n /**\n * ```txt\n * Pragma: no - cache;\n * ```\n *\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pragma\n */\n Pragma: 'pragma',\n\n /**\n * ```txt\n * ETag: W / '<etag_value>';\n * ETag: '<etag_value>';\n * ```\n *\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag\n */\n ETag: 'etag',\n\n /**\n * ```txt\n * Expires: <http-date>\n * ```\n *\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires\n */\n Expires: 'expires',\n\n /**\n * ```txt\n * Age: <delta-seconds>\n * ```\n *\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Age\n */\n Age: 'age',\n\n /**\n * Used internally as metadata to mark the cache item as revalidatable and enabling\n * stale cache state Contains a string of ASCII characters that can be used as ETag for\n * `If-Match` header Provided by user using `cache.etag` value.\n *\n * ```txt\n * X-Axios-Cache-Etag: \"<etag_value>\"\n * ```\n */\n XAxiosCacheEtag: 'x-axios-cache-etag',\n\n /**\n * Used internally as metadata to mark the cache item as revalidatable and enabling\n * stale cache state may contain `'use-cache-timestamp'` if `cache.modifiedSince` is\n * `true`, otherwise will contain a date from `cache.modifiedSince`. If a date is\n * provided, it can be used for `If-Modified-Since` header, otherwise the cache\n * timestamp can be used for `If-Modified-Since` header.\n *\n * ```txt\n * X-Axios-Cache-Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT\n * X-Axios-Cache-Last-Modified: use-cache-timestamp\n * ```\n */\n XAxiosCacheLastModified: 'x-axios-cache-last-modified',\n\n /**\n * Used internally as metadata to mark the cache item able to be used if the server\n * returns an error. The stale-if-error response directive indicates that the cache can\n * reuse a stale response when any error occurs.\n *\n * ```txt\n * XAxiosCacheStaleIfError: <seconds>\n * ```\n */\n XAxiosCacheStaleIfError: 'x-axios-cache-stale-if-error',\n\n /**\n * Indicates which request headers affect the response content.\n * Used to prevent cache poisoning when responses differ based on request headers.\n *\n * ```txt\n * Vary: Authorization\n * Vary: Authorization, Accept-Language\n * Vary: *\n * ```\n *\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary\n */\n Vary: 'vary'\n} as const;\n","import { parse } from 'cache-parser';\nimport { Header } from './headers.ts';\nimport type { HeaderInterpreter } from './types.ts';\n\n/**\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport const defaultHeaderInterpreter: HeaderInterpreter = (headers, location) => {\n if (!headers) return 'not enough headers';\n\n const cacheControl: unknown = headers[Header.CacheControl];\n\n if (cacheControl) {\n const cc = parse(String(cacheControl));\n\n if (\n // Header told that this response should not be cached.\n cc.noCache ||\n cc.noStore ||\n // Server side handling private data\n (location === 'server' && cc.private)\n ) {\n return 'dont cache';\n }\n\n if (cc.immutable) {\n // 1 year is sufficient, as Infinity may cause problems with certain storages.\n // It might not be the best way, but a year is better than none. Facebook shows\n // that a browser session stays at the most 1 month.\n return {\n cache: 1000 * 60 * 60 * 24 * 365\n };\n }\n\n if (cc.maxAge !== undefined) {\n const age: unknown = headers[Header.Age];\n\n return {\n cache: age\n ? // If age is present, we must subtract it from maxAge\n (cc.maxAge - Number(age)) * 1000\n : cc.maxAge * 1000,\n // Already out of date, must be requested again\n stale:\n // I couldn't find any documentation about who should be used, as they\n // are not meant to overlap each other. But, as we cannot request in the\n // background, as the stale-while-revalidate says, and we just increase\n // its staleTtl when its present, max-stale is being preferred over\n // stale-while-revalidate.\n cc.maxStale !== undefined\n ? cc.maxStale * 1000\n : cc.staleWhileRevalidate !== undefined\n ? cc.staleWhileRevalidate * 1000\n : undefined\n };\n }\n }\n\n const expires: unknown = headers[Header.Expires];\n\n if (expires) {\n const milliseconds = Date.parse(String(expires)) - Date.now();\n return milliseconds >= 0 ? { cache: milliseconds } : 'dont cache';\n }\n\n return 'not enough headers';\n};\n","import type { AxiosRequestHeaders, AxiosResponseHeaders } from 'axios';\n\n/**\n * Extracts specified header values from request headers.\n * Generic utility for extracting a subset of headers.\n *\n * @param requestHeaders The full request headers object\n * @param headerNames Array of header names to extract\n * @returns Object with extracted header values\n */\nexport function extractHeaders(\n requestHeaders: AxiosRequestHeaders | AxiosResponseHeaders,\n headerNames: string[]\n): Record<string, string | undefined> {\n const result: Record<string, string | undefined> = {};\n\n for (const name of headerNames) {\n result[name] = requestHeaders.get(name)?.toString();\n }\n\n return result;\n}\n","import type { CacheAxiosResponse } from '../cache/axios.ts';\n\nimport type { CachePredicate, CachePredicateObject } from './types.ts';\n\n/**\n * Tests an response against a {@link CachePredicateObject}.\n *\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport async function testCachePredicate<R = unknown, D = unknown>(\n response: CacheAxiosResponse<R, D>,\n predicate: CachePredicate<R, D>\n): Promise<boolean> {\n if (typeof predicate === 'function') {\n return predicate(response);\n }\n\n const { statusCheck, responseMatch, containsHeaders } = predicate;\n\n if (\n (statusCheck && !(await statusCheck(response.status))) ||\n (responseMatch && !(await responseMatch(response)))\n ) {\n return false;\n }\n\n if (containsHeaders) {\n for (const [header, predicate] of Object.entries(containsHeaders)) {\n if (\n !(await predicate(\n // Avoid bugs in case the header is not in lower case\n response.headers[header.toLowerCase()] ?? response.headers[header]\n ))\n ) {\n return false;\n }\n }\n }\n\n return true;\n}\n\n/**\n * Determines whether a given URL matches a specified pattern, which can be either a\n * string or a regular expression.\n *\n * @param matchPattern - The pattern to match against\n *\n * - If it's a regular expression, it will be reset to ensure consistent behavior for\n * stateful regular expressions.\n * - If it's a string, the function checks if the URL contains the string.\n *\n * @param configUrl - The URL to test against the provided pattern; normally `config.url`.\n * @returns `true` if the `configUrl` matches the `matchPattern`\n *\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport function regexOrStringMatch(matchPattern: string | RegExp, configUrl: string) {\n if (matchPattern instanceof RegExp) {\n matchPattern.lastIndex = 0; // Reset the regex to ensure consistent matching\n return matchPattern.test(configUrl);\n }\n\n return configUrl.includes(matchPattern);\n}\n","import type { Method } from 'axios';\nimport type {\n CacheAxiosResponse,\n CacheRequestConfig,\n InternalCacheRequestConfig\n} from '../cache/axios.ts';\nimport type { CacheProperties } from '../cache/cache.ts';\nimport { Header } from '../header/headers.ts';\nimport type {\n CachedResponse,\n MustRevalidateStorageValue,\n StaleStorageValue\n} from '../storage/types.ts';\n\n/**\n * Creates a new validateStatus function that will use the one already used and also\n * accept status code 304.\n *\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport function createValidateStatus(\n oldValidate?: CacheRequestConfig['validateStatus']\n): (status: number) => boolean {\n return oldValidate\n ? (status) => oldValidate(status) || status === 304\n : (status) => (status >= 200 && status < 300) || status === 304;\n}\n\n/**\n * Checks if the given method is in the methods array\n *\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport function isMethodIn(\n requestMethod: Method | string = 'get',\n methodList: Method[] = []\n): boolean {\n requestMethod = requestMethod.toLowerCase() as Lowercase<Method>;\n return methodList.some((method) => method === requestMethod);\n}\n\n/**\n * @deprecated This interface will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport interface ConfigWithCache<D> extends InternalCacheRequestConfig<unknown, D> {\n cache: Partial<CacheProperties<unknown, D>>;\n}\n\n/**\n * This function updates the cache when the request is stale. So, the next request to the\n * server will be made with proper header / settings.\n *\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport function updateStaleRequest<D>(\n cache: StaleStorageValue | MustRevalidateStorageValue,\n config: ConfigWithCache<D>\n): void {\n const { etag, modifiedSince } = config.cache;\n const revalidation = cache.data?.meta?.revalidation;\n\n // Handle ETag revalidation\n if (etag) {\n let etagValue: string | undefined;\n\n if (revalidation?.etag) {\n // Prefer meta value (new format)\n etagValue = revalidation.etag;\n } else if (etag === true) {\n // Fallback to response ETag header (backward compatibility)\n etagValue = cache.data?.headers[Header.ETag];\n } else {\n // Custom value from config\n etagValue = etag;\n }\n\n if (etagValue) {\n config.headers.set(Header.IfNoneMatch, etagValue);\n }\n }\n\n // Handle Last-Modified revalidation\n if (modifiedSince) {\n let lastModifiedValue: string;\n\n if (revalidation?.lastModified) {\n // Prefer meta value (new format)\n lastModifiedValue =\n revalidation.lastModified === true\n ? new Date(cache.createdAt).toUTCString()\n : revalidation.lastModified;\n } else if (modifiedSince === true) {\n // Fallback to response Last-Modified header (backward compatibility)\n lastModifiedValue =\n cache.data.headers[Header.LastModified] || new Date(cache.createdAt).toUTCString();\n } else {\n // Custom Date from config\n lastModifiedValue = modifiedSince.toUTCString();\n }\n\n config.headers.set(Header.IfModifiedSince, lastModifiedValue);\n }\n}\n\n/**\n * Creates the new date to the cache by the provided response. Also handles possible 304\n * Not Modified by updating response properties.\n *\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport function createCacheResponse<R, D>(\n response: CacheAxiosResponse<R, D>,\n previousCache?: CachedResponse\n): CachedResponse {\n if (response.status === 304 && previousCache) {\n // Set the cache information into the response object\n response.cached = true;\n response.data = previousCache.data as R;\n response.status = previousCache.status;\n response.statusText = previousCache.statusText;\n\n // Update possible new headers\n response.headers = {\n ...previousCache.headers,\n ...response.headers\n };\n\n // return the old cache\n return previousCache;\n }\n\n // New Response\n return {\n data: response.data,\n status: response.status,\n statusText: response.statusText,\n headers: response.headers\n };\n}\n","import { deferred } from 'fast-defer';\nimport { compare as compareVary, parse as parseVary } from 'http-vary';\nimport type { AxiosCacheInstance, CacheAxiosResponse } from '../cache/axios.ts';\nimport { extractHeaders } from '../header/extract.ts';\nimport { Header } from '../header/headers.ts';\nimport type { CachedResponse, LoadingStorageValue } from '../storage/types.ts';\nimport { regexOrStringMatch } from '../util/cache-predicate.ts';\nimport type { RequestInterceptor } from './build.ts';\nimport { createValidateStatus, isMethodIn, updateStaleRequest } from './util.ts';\n\n/**\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport function defaultRequestInterceptor(axios: AxiosCacheInstance): RequestInterceptor {\n const onFulfilled: RequestInterceptor['onFulfilled'] = async (config) => {\n config.id = axios.generateKey(config, {\n vary:\n config.cache && Array.isArray(config.cache.vary)\n ? extractHeaders(config.headers, config.cache.vary)\n : undefined\n });\n\n if (config.cache === false) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Cache disabled: config.cache === false'\n });\n }\n\n return config;\n }\n\n // merge defaults with per request configuration\n config.cache = { ...axios.defaults.cache, ...config.cache };\n\n // Check if cache is disabled via enabled flag\n if (config.cache.enabled === false) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Cache disabled: config.cache.enabled === false'\n });\n }\n\n return config;\n }\n\n // ignoreUrls (blacklist)\n if (\n typeof config.cache.cachePredicate === 'object' &&\n config.cache.cachePredicate.ignoreUrls &&\n config.url\n ) {\n for (const url of config.cache.cachePredicate.ignoreUrls) {\n if (regexOrStringMatch(url, config.url)) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: `URL ignored: matches ignoreUrls pattern`,\n data: { url: config.url, pattern: url }\n });\n }\n\n return config;\n }\n }\n }\n\n // allowUrls\n if (\n typeof config.cache.cachePredicate === 'object' &&\n config.cache.cachePredicate.allowUrls &&\n config.url\n ) {\n let matched = false;\n\n for (const url of config.cache.cachePredicate.allowUrls) {\n if (regexOrStringMatch(url, config.url)) {\n matched = true;\n\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: `URL allowed: matches allowUrls pattern`,\n data: { url: config.url, pattern: url }\n });\n }\n break;\n }\n }\n\n if (!matched) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: `URL rejected: not in allowUrls`,\n data: { url: config.url, allowUrls: config.cache.cachePredicate.allowUrls }\n });\n }\n return config;\n }\n }\n\n // Applies sufficient headers to prevent other cache systems to work along with this one\n //\n // Its currently used before isMethodIn because if the isMethodIn returns false, the request\n // shouldn't be cached an therefore neither in the browser.\n // https://stackoverflow.com/a/2068407\n if (config.cache.cacheTakeover) {\n config.headers.set(\n Header.CacheControl,\n 'no-cache, no-store, must-revalidate, max-age=0',\n false\n );\n config.headers.set(Header.Pragma, 'no-cache', false);\n config.headers.set(Header.Expires, '0', false);\n }\n\n if (!isMethodIn(config.method, config.cache.methods)) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: `Method ${config.method} not cacheable (allowed: ${config.cache.methods})`\n });\n }\n\n return config;\n }\n\n // Assumes that the storage handled staled responses\n let cache = await axios.storage.get(config.id, config);\n const overrideCache = config.cache.override;\n\n // Checks for vary mismatches in cached responses before proceeding\n // If a vary mismatch is detected, it will generate a new key based on the\n // current request headers and re-fetch the cache.\n if (\n // Vary enabled\n config.cache.vary !== false &&\n // Had vary headers in cached response (cached or stale)\n cache.data?.meta?.vary &&\n // Previous response had Vary header to use\n cache.data.headers[Header.Vary]\n ) {\n const vary = Array.isArray(config.cache.vary)\n ? config.cache.vary\n : parseVary(cache.data.headers[Header.Vary]);\n\n // Compares current request headers with cached vary headers (meta.vary)\n if (vary && vary !== '*' && !compareVary(vary, cache.data.meta?.vary, config.headers)) {\n // Generate base key without id field (otherwise returns config.id)\n const extractedHeaders = extractHeaders(config.headers, vary);\n const newKey = axios.generateKey({ ...config, id: undefined }, { vary: extractedHeaders });\n\n // If ends up being a new key, change the cache to the new one\n if (config.id !== newKey) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Vary mismatch, switching to vary-aware key',\n data: {\n cachedHeaders: cache.data.meta.vary,\n currentHeaders: extractedHeaders,\n vary,\n newKey\n }\n });\n }\n\n config.id = newKey;\n cache = await axios.storage.get(newKey, config);\n }\n }\n }\n\n // Not cached, continue the request, and mark it as fetching\n // biome-ignore lint/suspicious/noConfusingLabels: required to break condition in simultaneous accesses\n ignoreAndRequest: if (\n cache.state === 'empty' ||\n cache.state === 'stale' ||\n cache.state === 'must-revalidate' ||\n overrideCache\n ) {\n // This checks for simultaneous access to a new key. The js event loop jumps on the\n // first await statement, so the second (asynchronous call) request may have already\n // started executing.\n if (axios.waiting.has(config.id) && !overrideCache) {\n cache = await axios.storage.get(config.id, config);\n\n // This check is required when a request has it own cache deleted manually, lets\n // say by a `axios.storage.delete(key)` and has a concurrent loading request.\n // Because in this case, the cache will be empty and may still has a pending key\n // on waiting map.\n if (cache.state !== 'empty' && cache.state !== 'must-revalidate') {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Concurrent request found, reusing result'\n });\n }\n\n break ignoreAndRequest;\n }\n }\n\n // Create a deferred to resolve other requests for the same key when it's completed\n const def = deferred<void>();\n axios.waiting.set(config.id, def);\n\n // Adds a default reject handler to catch when the request gets aborted without\n // others waiting for it.\n def.catch(() => undefined);\n\n await axios.storage.set(\n config.id,\n {\n state: 'loading',\n previous: overrideCache\n ? // Simply determine if the request is stale or not\n // based if it had previous data or not\n cache.data\n ? 'stale'\n : 'empty'\n : // Typescript doesn't know that cache.state here can only be 'empty' or 'stale'\n (cache.state as 'stale' | 'must-revalidate'),\n\n data: cache.data as any,\n\n // If the cache is empty and asked to override it, use the current timestamp\n createdAt: overrideCache && !cache.createdAt ? Date.now() : (cache.createdAt as any)\n },\n config\n );\n\n // Skip adding conditional headers (If-None-Match, If-Modified-Since) when override is true.\n // The override option is meant to bypass cache and get fresh data, not revalidate existing cache.\n // Adding conditional headers would cause the server to return 304 Not Modified instead of fresh data.\n if ((cache.state === 'stale' || cache.state === 'must-revalidate') && !overrideCache) {\n updateStaleRequest(cache, { ...config, cache: config.cache });\n\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Stale revalidation: added conditional headers (If-None-Match/If-Modified-Since)'\n });\n }\n }\n\n config.validateStatus = createValidateStatus(config.validateStatus);\n\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Making network request',\n data: { overrideCache, cacheState: cache.state }\n });\n }\n\n // Hydrates any UI temporarily, if cache is available\n if (cache.state === 'stale' || (cache.data && cache.state !== 'must-revalidate')) {\n await config.cache.hydrate?.(cache);\n }\n\n return config;\n }\n\n let cachedResponse: CachedResponse;\n\n if (cache.state === 'loading') {\n const deferred = axios.waiting.get(config.id);\n\n // The deferred may not exists when the process is using a persistent\n // storage and cancelled in the middle of a request, this would result in\n // a pending loading state in the storage but no current promises to resolve\n if (!deferred) {\n // Hydrates any UI temporarily, if cache is available\n if (cache.data) {\n await config.cache.hydrate?.(cache);\n }\n\n return config;\n }\n\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Concurrent request detected, waiting...'\n });\n }\n\n try {\n // Deferred can't reuse the value because the user's storage might clone\n // or mutate the value, so we need to ask it again.\n // For example with memoryStorage + cloneData\n await deferred;\n const state = await axios.storage.get(config.id, config);\n\n // This is a cache mismatch and should never happen, but in case it does,\n // we need to redo the request all over again.\n /* c8 ignore start */\n if (!state.data) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Concurrent request completed without data, retrying'\n });\n }\n\n return onFulfilled!(config);\n }\n /* c8 ignore end */\n\n // After waiting, check if this request's vary headers match the cached variant\n // If mismatch, don't use the cache - make own request to prevent cache poisoning\n if (\n config.cache.vary !== false &&\n state.data.meta?.vary &&\n state.data.headers[Header.Vary]\n ) {\n const vary = Array.isArray(config.cache.vary)\n ? config.cache.vary\n : parseVary(state.data.headers[Header.Vary]);\n\n // Compare vary headers - if mismatch, make own request\n if (vary && vary !== '*' && !compareVary(vary, state.data.meta.vary, config.headers)) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Vary mismatch after concurrent request, making own request',\n data: {\n cachedVary: state.data.meta.vary,\n currentVary: extractHeaders(config.headers, vary)\n }\n });\n }\n\n // Don't use cached response - rerun interceptor logic but with new key\n return onFulfilled!(config);\n }\n }\n\n cachedResponse = state.data;\n } catch (err) {\n // The deferred was rejected by the first request that encountered an error.\n // All deduplicated requests waiting on this deferred should fail with the same error\n // to maintain consistency and prevent multiple network retries for the same resource.\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Concurrent request failed, propagating error',\n data: err\n });\n }\n\n throw err;\n }\n } else {\n cachedResponse = cache.data;\n }\n\n // The cached data is already transformed after receiving the response from the server.\n // Reapplying the transformation on the transformed data will have an unintended effect.\n // Since the cached data is already in the desired format, there is no need to apply the transformation function again.\n config.transformResponse = undefined;\n\n // Even though the response interceptor receives this one from here,\n // it has been configured to ignore cached responses = true\n config.adapter = function cachedAdapter(): Promise<CacheAxiosResponse> {\n return Promise.resolve({\n config,\n data: cachedResponse.data,\n headers: cachedResponse.headers,\n status: cachedResponse.status,\n statusText: cachedResponse.statusText,\n cached: true,\n stale: (cache as LoadingStorageValue).previous === 'stale',\n id: config.id!\n });\n };\n\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Using cached response'\n });\n }\n\n return config;\n };\n\n return {\n onFulfilled\n };\n}\n","import type { CacheAxiosResponse } from '../cache/axios.ts';\nimport type { AxiosStorage } from '../storage/types.ts';\nimport type { CacheUpdater } from './types.ts';\n\n/**\n * Function to update all caches, from CacheProperties.update, with the new data.\n *\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport async function updateCache<R, D>(\n storage: AxiosStorage,\n data: CacheAxiosResponse<R, D>,\n cacheUpdater: CacheUpdater<R, D>\n): Promise<void> {\n // Global cache update function.\n if (typeof cacheUpdater === 'function') {\n return cacheUpdater(data);\n }\n\n for (const [cacheKey, updater] of Object.entries(cacheUpdater)) {\n if (updater === 'delete') {\n await storage.remove(cacheKey, data.config);\n continue;\n }\n\n const value = await storage.get(cacheKey, data.config);\n\n if (value.state === 'loading') {\n continue;\n }\n\n const newValue = await updater(value, data);\n\n if (newValue === 'delete') {\n await storage.remove(cacheKey, data.config);\n continue;\n }\n\n if (newValue !== 'ignore') {\n await storage.set(cacheKey, newValue, data.config);\n }\n }\n}\n","import type { AxiosResponseHeaders } from 'axios';\nimport { parse } from 'cache-parser';\nimport { parse as parseVary } from 'http-vary';\nimport type { AxiosCacheInstance, CacheAxiosResponse, CacheRequestConfig } from '../cache/axios.ts';\nimport type { CacheProperties } from '../cache/cache.ts';\nimport { extractHeaders } from '../header/extract.ts';\nimport { Header } from '../header/headers.ts';\nimport type { CachedStorageValue } from '../storage/types.ts';\nimport { testCachePredicate } from '../util/cache-predicate.ts';\nimport { updateCache } from '../util/update-cache.ts';\nimport type { ResponseInterceptor } from './build.ts';\nimport { createCacheResponse, isMethodIn } from './util.ts';\n\n/**\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport function defaultResponseInterceptor(axios: AxiosCacheInstance): ResponseInterceptor {\n /**\n * Replies a deferred stored in the axios waiting map. Use resolve to proceed checking the\n * previously updated cache or reject to abort deduplicated requests with error.\n */\n const replyDeferred = (responseId: string, mode: 'reject' | 'resolve', error?: any) => {\n // Rejects the deferred, if present\n const deferred = axios.waiting.get(responseId);\n\n if (deferred) {\n deferred[mode](error);\n axios.waiting.delete(responseId);\n\n if (__ACI_DEV__) {\n axios.debug({\n id: responseId,\n msg: `Found waiting deferred(s) and ${mode} them`\n });\n }\n }\n };\n\n const onFulfilled: ResponseInterceptor['onFulfilled'] = async (response) => {\n // When response.config is not present, the response is indeed a error.\n if (!response?.config) {\n if (__ACI_DEV__) {\n axios.debug({\n msg: 'Unknown response received (not an Axios response)',\n data: response\n });\n }\n\n // Re-throws the error\n throw response;\n }\n\n response.id = response.config.id!;\n response.cached ??= false;\n\n const config = response.config;\n // Request interceptor merges defaults with per request configuration\n const cacheConfig = config.cache as CacheProperties;\n\n // Response is already cached\n if (response.cached) {\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Returned cached response'\n });\n }\n\n return response;\n }\n\n // Skip cache: either false or weird behavior\n // config.cache should always exists, at least from global config merge.\n if (!cacheConfig) {\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Response received without cache config',\n data: response\n });\n }\n\n response.cached = false;\n return response;\n }\n\n // Update other entries before updating himself\n if (cacheConfig.update) {\n await updateCache(axios.storage, response, cacheConfig.update);\n }\n\n if (!isMethodIn(config.method, cacheConfig.methods)) {\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: `Method ${config.method} not cacheable (allowed: ${cacheConfig.methods})`\n });\n }\n\n return response;\n }\n\n const cache = await axios.storage.get(response.id, config);\n\n if (\n // If the request interceptor had a problem or it wasn't cached\n cache.state !== 'loading'\n ) {\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Response received but storage not in loading state',\n data: { cacheState: cache.state }\n });\n }\n\n // On limited storage scenarios, its possible the request was evicted while waiting\n // for the response, in this case, state will be 'empty' again instead of loading.\n // https://github.com/arthurfiorette/axios-cache-interceptor/issues/833\n axios.waiting.delete(response.id);\n return response;\n }\n\n // Config told that this response should not be cached.\n if (\n // For 'loading' values (previous: stale), this check already ran in the past.\n !cache.data &&\n !(await testCachePredicate(response, cacheConfig.cachePredicate))\n ) {\n replyDeferred(response.id, 'resolve');\n\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Cache predicate rejected this response'\n });\n }\n\n return response;\n }\n\n // Avoid remnant headers from remote server to break implementation\n for (const header of Object.keys(response.headers)) {\n if (header.startsWith('x-axios-cache')) {\n delete response.headers[header];\n }\n }\n\n let ttl = cacheConfig.ttl || -1; // always set from global config\n let staleTtl: number | undefined;\n\n if (cacheConfig.interpretHeader) {\n const expirationTime = axios.headerInterpreter(response.headers, axios.location);\n\n // Cache should not be used\n if (expirationTime === 'dont cache') {\n replyDeferred(response.id, 'resolve');\n\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Cache-Control header indicates: do not cache'\n });\n }\n\n return response;\n }\n\n if (expirationTime !== 'not enough headers') {\n if (typeof expirationTime === 'number') {\n ttl = expirationTime;\n } else {\n ttl = expirationTime.cache;\n staleTtl = expirationTime.stale;\n }\n }\n }\n\n if (typeof ttl === 'function') {\n ttl = await ttl(response);\n }\n\n const data = createCacheResponse(response, cache.data);\n\n // Store revalidation metadata in meta.revalidation (single source of truth)\n if (cacheConfig.etag || cacheConfig.modifiedSince) {\n data.meta ??= {};\n data.meta.revalidation = {};\n\n // ETag: store response's ETag or custom value\n if (cacheConfig.etag) {\n const etag = cacheConfig.etag === true ? response.headers[Header.ETag] : cacheConfig.etag;\n if (etag) {\n data.meta.revalidation.etag = etag;\n }\n }\n\n // Last-Modified: store response's Last-Modified, cache timestamp (true), or custom date\n if (cacheConfig.modifiedSince) {\n data.meta.revalidation.lastModified =\n cacheConfig.modifiedSince === true\n ? response.headers[Header.LastModified] || true\n : cacheConfig.modifiedSince.toUTCString();\n }\n }\n\n // Either stales response (Vary *) or sets request Vary headers into metadata\n if (cacheConfig.vary !== false && response.headers[Header.Vary]) {\n const vary = Array.isArray(cacheConfig.vary)\n ? cacheConfig.vary\n : parseVary(response.headers[Header.Vary]);\n\n // For valid values, store the subset of request headers in the cache response\n if (Array.isArray(vary)) {\n data.meta ??= {};\n data.meta.vary = extractHeaders(config.headers, vary);\n\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Storing response with Vary metadata',\n data: { vary, extracted: data.meta.vary }\n });\n }\n\n // RFC States * must revalidate every time per RFC 9110.\n } else if (vary === '*') {\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Vary: * detected, storing as stale'\n });\n }\n\n // Marks cache as stale immediately\n await axios.storage.set(\n response.id,\n {\n state: 'stale',\n createdAt: Date.now(),\n data,\n ttl\n },\n config\n );\n\n replyDeferred(response.id, 'resolve');\n return response;\n }\n }\n\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Caching response',\n data: { ttl, staleTtl, interpretHeader: cacheConfig.interpretHeader }\n });\n }\n\n const newCache: CachedStorageValue = {\n state: 'cached',\n ttl,\n staleTtl,\n createdAt: Date.now(),\n data\n };\n\n // Define this key as cache on the storage\n await axios.storage.set(response.id, newCache, config);\n replyDeferred(response.id, 'resolve');\n\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Response cached successfully',\n data: { state: newCache.state, ttl: newCache.ttl }\n });\n }\n\n // Return the response with cached as false, because it was not cached at all\n return response;\n };\n\n const onRejected: ResponseInterceptor['onRejected'] = async (error) => {\n // When response.config is not present, the response is indeed a error.\n if (!error.isAxiosError || !error.config) {\n if (__ACI_DEV__) {\n axios.debug({\n msg: 'A non-AxiosError was thrown and the cache interceptor could not identify the failing request. The deferred and loading cache entry cannot be cleaned up. Custom adapters must always throw an AxiosError. See https://axios-cache-interceptor.js.org/guide/interceptors#custom-adapters',\n data: error\n });\n }\n\n // We should probably re-request the response to avoid an infinite loading state here\n // but, since this is an unknown error, we cannot figure out what request ID to use.\n // And the only solution is to let the storage actively reject the current loading state.\n throw error;\n }\n\n const config = error.config as CacheRequestConfig & { headers: AxiosResponseHeaders };\n const id = config.id;\n const cacheConfig = config.cache as CacheProperties;\n const response = error.response as CacheAxiosResponse | undefined;\n\n // config.cache should always exist, at least from global config merge.\n if (!cacheConfig || !id) {\n if (__ACI_DEV__) {\n axios.debug({\n msg: 'Request failed without cache config',\n data: { error }\n });\n }\n\n throw error;\n }\n\n if (!isMethodIn(config.method, cacheConfig.methods)) {\n if (__ACI_DEV__) {\n axios.debug({\n id,\n msg: `Method ${config.method} not cacheable (allowed: ${cacheConfig.methods})`\n });\n }\n\n // Rejects all other requests waiting for this response\n await axios.storage.remove(id, config);\n replyDeferred(id, 'reject', error);\n\n throw error;\n }\n\n const cache = await axios.storage.get(id, config);\n\n if (\n // This will only not be loading if the interceptor broke\n cache.state !== 'loading' ||\n cache.previous !== 'stale'\n ) {\n if (__ACI_DEV__) {\n axios.debug({\n id,\n msg: 'Request error with unexpected cache state',\n data: {\n cacheState: cache.state,\n previous: cache.state === 'loading' ? cache.previous : undefined,\n errorCode: error.code\n }\n });\n }\n\n // Do not clear cache if this request is cached, but the request was cancelled before returning the cached response\n if (\n error.code !== 'ERR_CANCELED' ||\n (error.code === 'ERR_CANCELED' && cache.state !== 'cached')\n ) {\n await axios.storage.remove(id, config);\n }\n\n // Handle canceled requests differently from other errors\n // Canceled requests should not propagate the error to waiting deduplicated requests\n // Instead, resolve the deferred so waiting requests can make their own network call\n if (error.code === 'ERR_CANCELED') {\n replyDeferred(id, 'resolve');\n } else {\n // Rejects all other requests waiting for this response\n replyDeferred(id, 'reject', error);\n }\n\n throw error;\n }\n\n if (cacheConfig.staleIfError) {\n const cacheControl = String(response?.headers[Header.CacheControl]);\n const staleHeader = cacheControl && parse(cacheControl).staleIfError;\n\n const staleIfError =\n typeof cacheConfig.staleIfError === 'function'\n ? await cacheConfig.staleIfError(response, cache, error)\n : cacheConfig.staleIfError === true && staleHeader\n ? staleHeader * 1000 //staleIfError is in seconds\n : cacheConfig.staleIfError;\n\n if (__ACI_DEV__) {\n axios.debug({\n id,\n msg: 'staleIfError config found for failed request',\n data: { staleIfError, createdAt: cache.createdAt }\n });\n }\n\n if (\n staleIfError === true ||\n // staleIfError is the number of seconds that stale is allowed to be used\n (typeof staleIfError === 'number' && cache.createdAt + staleIfError > Date.now())\n ) {\n // re-mark the cache as stale\n await axios.storage.set(\n id,\n {\n state: 'stale',\n createdAt: Date.now(),\n data: cache.data\n },\n config\n );\n // Resolve all other requests waiting for this response\n const waiting = axios.waiting.get(id);\n\n if (waiting) {\n waiting.resolve();\n axios.waiting.delete(id);\n\n if (__ACI_DEV__) {\n axios.debug({\n id,\n msg: 'Found waiting deferred(s) and resolved them'\n });\n }\n }\n\n if (__ACI_DEV__) {\n axios.debug({\n id,\n msg: 'staleIfError: returning stale cache for failed request'\n });\n }\n\n return {\n cached: true,\n stale: true,\n config,\n id,\n data: cache.data.data,\n headers: cache.data.headers,\n status: cache.data.status,\n statusText: cache.data.statusText\n };\n }\n }\n\n if (__ACI_DEV__) {\n axios.debug({\n id,\n msg: 'Unhandled error, cleaning up',\n data: { errorCode: error.code, errorMessage: error.message }\n });\n }\n\n // Rejects all other requests waiting for this response\n await axios.storage.remove(id, config);\n replyDeferred(id, 'reject', error);\n\n throw error;\n };\n\n return {\n onFulfilled,\n onRejected\n };\n}\n","import type { CacheRequestConfig } from '../cache/axios.ts';\nimport { Header } from '../header/headers.ts';\nimport type { MaybePromise } from '../util/types.ts';\nimport type {\n AxiosStorage,\n CachedResponse,\n CachedStorageValue,\n StaleStorageValue,\n StorageValue\n} from './types.ts';\n\n/**\n * Returns true if the provided object was created from {@link buildStorage} function.\n *\n * @deprecated This function will be hidden in future versions. Please tell us why you need it at https://github.com/arthurfiorette/axios-cache-interceptor/issues/1158\n */\nexport const isStorage = (obj: unknown): obj is AxiosStorage =>\n !!obj && !!(obj as Record<string, boolean>)['is-storage'];\n\n/**\n * Migrates old header-based revalidation data to new meta.revalidation format.\n * This ensures backward compatibility with existing cache entries.\n *\n * @deprecated Internal migration function. Will be removed when all cache entries\n * have naturally expired and been recreated with new format.\n */\nfunction migrateRevalidationHeaders(data: CachedResponse): void {\n // Skip if already has meta.revalidation\n if (data.meta?.revalidation) {\n return;\n }\n\n const oldEtag = data.headers[Header.XAxiosCacheEtag];\n const oldLastModified = data.headers[Header.XAxiosCacheLastModified];\n\n if (oldEtag || oldLastModified) {\n data.meta ??= {};\n data.meta.revalidation = {};\n\n if (oldEtag) {\n data.meta.revalidation.etag = oldEtag;\n }\n\n if (oldLastModified) {\n data.meta.revalidation.lastModified =\n oldLastModified === 'use-cache-timestamp' ? true : oldLastModified;\n }\n\n delete data.headers[Header.XAxiosCacheEtag];\n delete data.headers[Header.XAxiosCacheLastModified];\n delete data.headers[Header.XAxiosCacheStaleIfError];\n }\n}\n\nfunction hasRevalidationMetadata(value: CachedStorageValue | StaleStorageValue): boolean {\n // Migrate old entries on-the-fly\n migrateRevalidationHeaders(value.data);\n\n const headers = value.data.headers;\n const revalidation = value.data.meta?.revalidation;\n\n return (\n // Standard HTTP revalidation headers\n Header.ETag in headers ||\n Header.LastModified in headers ||\n // Revalidation metadata (new format)\n !!(revalidation?.etag || revalidation?.lastModified)\n );\n}\n\n/** Returns true if value must be revalidated */\nexport function mustRevalidate(value: CachedStorageValue | StaleStorageValue): boolean {\n // Must revalidate is a special case and should not serve stale values\n // We could use cache-control's parse function, but this is way faster and simpler\n return String(value.data.headers[Header.CacheControl]).includes('must-revalidate');\n}\n\n/** Returns true if this has sufficient properties to stale instead of expire. */\nexport function canStale(value: CachedStorageValue): boolean {\n if (hasRevalidationMetadata(value)) {\n return true;\n }\n\n return (\n value.state === 'cached' &&\n value.staleTtl !== undefined &&\n // Only allow stale values after the ttl is already in the past and the staleTtl is in the future.\n // In cases that just createdAt + ttl > Date.now(), isn't enough because the staleTtl could be <= 0.\n // This logic only returns true when Date.now() is between the (createdAt + ttl) and (createdAt + ttl + staleTtl).\n // Following the example below:\n // |--createdAt--:--ttl--:---staleTtl--->\n // [ past ][now is in here]\n Math.abs(Date.now() - (value.createdAt + value.ttl)) <= value.staleTtl\n );\n}\n\n/**\n * Checks if the provided cache is expired. You should also check if the cache\n * {@link canStale} and {@link mayUseStale}\n */\nexport function isExpired(value: CachedStorageValue | StaleStorageValue): boolean {\n return value.ttl !== undefined && value.createdAt + value.ttl <= Date.now();\n}\n\n/**\n * Defines which storage states are evicted first when cleaning up the storage.\n */\nconst StateEvictionOrder: Record<StorageValue['state'], number> = {\n empty: 0,\n 'must-revalidate': 1,\n stale: 2,\n cached: 3,\n // loading states usually don't have any data and are the most important ones\n // to keep around\n loading: 4\n};\n\n/**\n * Is a comparator function that sorts storage entries by their eviction priority\n * and, in the same group, by older first.\n */\nexport function storageEntriesSorter(\n [, a]: [string, StorageValue],\n [, b]: [string, StorageValue]\n): number {\n const stateDiff = StateEvictionOrder[a.state] - StateEvictionOrder[b.state];\n if (stateDiff !== 0) return stateDiff;\n return (a.createdAt || 0) - (b.createdAt || 0);\n}\n\n/**\n * Returns true if the storage entry can be removed according to its state and the\n * provided maxStaleAge.\n */\nexport function canRemoveStorageEntry(value: StorageValue, maxStaleAge: number): boolean {\n switch (value.state) {\n case 'loading':\n return false;\n\n case 'empty':\n case 'must-revalidate':\n return true;\n\n case 'cached':\n return isExpired(value) && !canStale(value);\n\n case 'stale':\n if (maxStaleAge !== undefined && value.ttl !== undefined) {\n return Date.now() > value.createdAt + value.ttl + maxStaleAge;\n }\n return false;\n }\n}\n\nexport interface BuildStorage extends Omit<AxiosStorage, 'get'> {\n /**\n * Returns the value for the given key. This method does not have to make checks for\n * cache invalidation or anything. It just returns what was previous saved, if present.\n *\n * @param key The key to look for\n * @param currentRequest The current {@link CacheRequestConfig}, if any\n * @see https://axios-cache-interceptor.js.org/guide/storages#buildstorage\n */\n find: (\n key: strin