UNPKG

axios-cache-interceptor

Version:
1 lines 81.6 kB
{"version":3,"file":"index.mjs","sources":["../src/header/headers.ts","../src/header/interpreter.ts","../src/interceptors/util.ts","../src/interceptors/request.ts","../src/util/cache-predicate.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"],"sourcesContent":["export const Header = Object.freeze({\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","import { parse } from 'cache-parser';\nimport { Header } from './headers.js';\nimport type { HeaderInterpreter } from './types.js';\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 { Method } from 'axios';\nimport type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios.js';\nimport type { CacheProperties } from '../cache/cache.js';\nimport { Header } from '../header/headers.js';\nimport type {\n CachedResponse,\n MustRevalidateStorageValue,\n StaleStorageValue\n} from '../storage/types.js';\n\n/**\n * Creates a new validateStatus function that will use the one already used and also\n * accept status code 304.\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/** Checks if the given method is in the methods array */\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\nexport interface ConfigWithCache<D> extends CacheRequestConfig<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 */\nexport function updateStaleRequest<D>(\n cache: StaleStorageValue | MustRevalidateStorageValue,\n config: ConfigWithCache<D>\n): void {\n config.headers ||= {};\n\n const { etag, modifiedSince } = config.cache;\n\n if (etag) {\n const etagValue = etag === true ? (cache.data?.headers[Header.ETag] as unknown) : etag;\n\n if (etagValue) {\n config.headers[Header.IfNoneMatch] = etagValue;\n }\n }\n\n if (modifiedSince) {\n config.headers[Header.IfModifiedSince] =\n modifiedSince === true\n ? // If last-modified is not present, use the createdAt timestamp\n (cache.data.headers[Header.LastModified] as unknown) ||\n new Date(cache.createdAt).toUTCString()\n : modifiedSince.toUTCString();\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 */\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 type { AxiosCacheInstance, CacheAxiosResponse } from '../cache/axios.js';\nimport { Header } from '../header/headers.js';\nimport type { CachedResponse, CachedStorageValue, LoadingStorageValue } from '../storage/types.js';\nimport type { RequestInterceptor } from './build.js';\nimport {\n type ConfigWithCache,\n createValidateStatus,\n isMethodIn,\n updateStaleRequest\n} from './util.js';\n\nexport function defaultRequestInterceptor(axios: AxiosCacheInstance): RequestInterceptor {\n const onFulfilled: RequestInterceptor['onFulfilled'] = async (config) => {\n config.id = axios.generateKey(config);\n\n if (config.cache === false) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Ignoring cache because config.cache === false',\n data: config\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 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 (\n url instanceof RegExp\n ? // Handles stateful regexes\n // biome-ignore lint: reduces the number of checks\n ((url.lastIndex = 0), url.test(config.url))\n : config.url.includes(url)\n ) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: `Ignored because url (${config.url}) matches ignoreUrls (${config.cache.cachePredicate.ignoreUrls})`,\n data: {\n url: config.url,\n cachePredicate: config.cache.cachePredicate\n }\n });\n }\n\n return config;\n }\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 if (config.cache.cacheTakeover) {\n config.headers[Header.CacheControl] ??= 'no-cache';\n config.headers[Header.Pragma] ??= 'no-cache';\n config.headers[Header.Expires] ??= '0';\n }\n\n if (!isMethodIn(config.method, config.cache.methods)) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: `Ignored because method (${config.method}) is not in cache.methods (${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 // 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)) as\n | CachedStorageValue\n | LoadingStorageValue;\n\n // @ts-expect-error 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: 'Waiting list had an deferred for this key, waiting for it to finish'\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 if (cache.state === 'stale' || cache.state === 'must-revalidate') {\n updateStaleRequest(cache, config as ConfigWithCache<unknown>);\n\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Updated stale request'\n });\n }\n }\n\n config.validateStatus = createValidateStatus(config.validateStatus);\n\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Sending request, waiting for response',\n data: {\n overrideCache,\n state: cache.state\n }\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: 'Detected concurrent request, waiting for it to finish'\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: 'Deferred resolved, but no data was found, requesting again'\n });\n }\n\n return onFulfilled!(config);\n }\n /* c8 ignore end */\n\n cachedResponse = state.data;\n } catch (err) {\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Deferred rejected, requesting again',\n data: err\n });\n }\n\n // Hydrates any UI temporarily, if cache is available\n /* c8 ignore start */\n if (cache.data) {\n await config.cache.hydrate?.(cache);\n }\n /* c8 ignore end */\n\n // The deferred is rejected when the request that we are waiting rejects its cache.\n // In this case, we need to redo the request all over again.\n return onFulfilled!(config);\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\n id: config.id!\n });\n };\n\n if (__ACI_DEV__) {\n axios.debug({\n id: config.id,\n msg: 'Returning cached response'\n });\n }\n\n return config;\n };\n\n return {\n onFulfilled\n };\n}\n","import type { CacheAxiosResponse } from '../cache/axios.js';\n\nimport type { CachePredicate, CachePredicateObject } from './types.js';\n\n/** Tests an response against a {@link CachePredicateObject}. */\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","import type { CacheAxiosResponse } from '../cache/axios.js';\nimport type { AxiosStorage } from '../storage/types.js';\nimport type { CacheUpdater } from './types.js';\n\n/** Function to update all caches, from CacheProperties.update, with the new data. */\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 type { AxiosCacheInstance, CacheAxiosResponse, CacheRequestConfig } from '../cache/axios.js';\nimport type { CacheProperties } from '../cache/cache.js';\nimport { Header } from '../header/headers.js';\nimport type { CachedStorageValue } from '../storage/types.js';\nimport { testCachePredicate } from '../util/cache-predicate.js';\nimport { updateCache } from '../util/update-cache.js';\nimport type { ResponseInterceptor } from './build.js';\nimport { createCacheResponse, isMethodIn } from './util.js';\n\nexport function defaultResponseInterceptor(axios: AxiosCacheInstance): ResponseInterceptor {\n /**\n * Rejects cache for an response response.\n *\n * Also update the waiting list for this key by rejecting it.\n */\n const rejectResponse = async (\n responseId: string,\n config: CacheRequestConfig,\n clearCache: boolean\n ) => {\n // Updates the cache to empty to prevent infinite loading state\n if (clearCache) {\n await axios.storage.remove(responseId, config);\n }\n\n // Rejects the deferred, if present\n const deferred = axios.waiting.get(responseId);\n\n if (deferred) {\n deferred.reject();\n axios.waiting.delete(responseId);\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: 'Response interceptor received an unknown 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 with config.cache falsy',\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: `Ignored because method (${config.method}) is not in cache.methods (${cacheConfig.methods})`,\n data: { config, cacheConfig }\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 not cached and storage isn't loading\",\n data: { cache, response }\n });\n }\n\n return response;\n }\n\n // Config told that this response should 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 await rejectResponse(response.id, config, true);\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 if (cacheConfig.etag && cacheConfig.etag !== true) {\n response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag;\n }\n\n if (cacheConfig.modifiedSince) {\n response.headers[Header.XAxiosCacheLastModified] =\n cacheConfig.modifiedSince === true\n ? 'use-cache-timestamp'\n : cacheConfig.modifiedSince.toUTCString();\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 await rejectResponse(response.id, config, true);\n\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: `Cache header interpreted as 'dont cache'`,\n data: { cache, response, expirationTime }\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 const data = createCacheResponse(response, cache.data);\n\n if (typeof ttl === 'function') {\n ttl = await ttl(response);\n }\n\n if (cacheConfig.staleIfError) {\n response.headers[Header.XAxiosCacheStaleIfError] = String(ttl);\n }\n\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Useful response configuration found',\n data: { cacheConfig, cacheResponse: data }\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\n // Resolve all other requests waiting for this response\n const waiting = axios.waiting.get(response.id);\n\n if (waiting) {\n waiting.resolve();\n axios.waiting.delete(response.id);\n\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Found waiting deferred(s) and resolved them'\n });\n }\n }\n\n if (__ACI_DEV__) {\n axios.debug({\n id: response.id,\n msg: 'Response cached',\n data: { cache: newCache, response }\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: 'FATAL: Received an non axios error in the rejected response interceptor, ignoring.',\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: 'Web request returned an error but cache handling is not enabled',\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: `Ignored because method (${config.method}) is not in cache.methods (${cacheConfig.methods})`,\n data: { config, cacheConfig }\n });\n }\n\n // Rejects all other requests waiting for this response\n await rejectResponse(id, config, true);\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: 'Caught an error in the request interceptor',\n data: { cache, error, config }\n });\n }\n\n // Rejects all other requests waiting for this response\n await rejectResponse(\n id,\n config,\n // Do not clear cache if this request is cached, but the request was cancelled before returning the cached response\n error.code !== 'ERR_CANCELED' || (error.code === 'ERR_CANCELED' && cache.state !== 'cached')\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: 'Found cache if stale config for rejected response',\n data: { error, config, staleIfError }\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 resolved this response with cached data',\n data: { error, config, cache }\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: 'Received an unknown error that could not be handled',\n data: { error, config }\n });\n }\n\n // Rejects all other requests waiting for this response\n await rejectResponse(id, config, true);\n\n throw error;\n };\n\n return {\n onFulfilled,\n onRejected\n };\n}\n","import type { CacheRequestConfig } from '../cache/axios.js';\nimport { Header } from '../header/headers.js';\nimport type { MaybePromise } from '../util/types.js';\nimport type { AxiosStorage, CachedStorageValue, StaleStorageValue, StorageValue } from './types.js';\n\n/** Returns true if the provided object was created from {@link buildStorage} function. */\nexport const isStorage = (obj: unknown): obj is AxiosStorage =>\n !!obj && !!(obj as Record<string, boolean>)['is-storage'];\n\nfunction hasUniqueIdentifierHeader(value: CachedStorageValue | StaleStorageValue): boolean {\n const headers = value.data.headers;\n\n return (\n Header.ETag in headers ||\n Header.LastModified in headers ||\n Header.XAxiosCacheEtag in headers ||\n Header.XAxiosCacheLastModified in headers\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 (hasUniqueIdentifierHeader(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\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: string,\n currentRequest?: CacheRequestConfig\n ) => MaybePromise<StorageValue | undefined>;\n}\n\n/**\n * All integrated storages are wrappers around the `buildStorage` function. External\n * libraries use it and if you want to build your own, `buildStorage` is the way to go!\n *\n * The exported `buildStorage` function abstracts the storage interface and requires a\n * super simple object to build the storage.\n *\n * **Note**: You can only create an custom storage with this function.\n *\n * @example\n *\n * ```js\n * const myStorage = buildStorage({\n * find: () => {...},\n * set: () => {...},\n * remove: () => {...},\n * clear: () => {...}\n * });\n *\n * const axios = setupCache(axios, { storage: myStorage });\n * ```\n *\n * @see https://axios-cache-interceptor.js.org/guide/storages#buildstorage\n */\nexport function buildStorage({ set, find, remove, clear }: BuildStorage): AxiosStorage {\n return {\n //@ts-expect-error - we don't want to expose this\n 'is-storage': 1,\n set,\n remove,\n clear,\n get: async (key, config) => {\n let value = await find(key, config);\n\n if (!value) {\n return { state: 'empty' };\n }\n\n if (\n value.state === 'empty' ||\n value.state === 'loading' ||\n value.state === 'must-revalidate'\n ) {\n return value;\n }\n\n // Handle cached values\n if (value.state === 'cached') {\n if (!isExpired(value)) {\n return value;\n }\n\n // Tries to stale expired value\n if (!canStale(value)) {\n await remove(key, config);\n return { state: 'empty' };\n }\n\n value = {\n state: 'stale',\n createdAt: value.createdAt,\n data: value.data,\n ttl: value.staleTtl !== undefined ? value.staleTtl + value.ttl : undefined\n };\n\n await set(key, value, config);\n\n // Must revalidate is a special case and should not serve stale values\n if (mustRevalidate(value)) {\n return { ...value, state: 'must-revalidate' };\n }\n }\n\n // A second check in case the new stale value was created already expired.\n if (!isExpired(value)) {\n return value;\n }\n\n if (hasUniqueIdentifierHeader(value)) {\n return value;\n }\n\n await remove(key, config);\n return { state: 'empty' };\n }\n };\n}\n","import { buildStorage, canStale, isExpired } from './build.js';\nimport type { AxiosStorage, StorageValue } from './types.js';\n\n/* c8 ignore start */\n/**\n * Clones an object using the structured clone algorithm if available, otherwise\n * it uses JSON.parse(JSON.stringify(value)).\n */\nconst clone: <T>(value: T) => T =\n // https://caniuse.com/mdn-api_structuredclone (10/18/2023 92.51%)\n typeof structuredClone === 'function'\n ? structuredClone\n : (value) => JSON.parse(JSON.stringify(value));\n/* c8 ignore stop */\n\n/**\n * Creates a simple in-memory storage. This means that if you need to persist data between\n * page or server reloads, this will not help.\n *\n * This is the storage used by default.\n *\n * If you need to modify it's data, you can do by the `data` property.\n *\n * @example\n *\n * ```js\n * const memoryStorage = buildMemoryStorage();\n *\n * setupCache(axios, { storage: memoryStorage });\n *\n * // Simple example to force delete the request cache\n *\n * const { id } = axios.get('url');\n *\n * delete memoryStorage.data[id];\n * ```\n *\n * @param {boolean | 'double'} cloneData Use `true` if the data returned by `find()`\n * should be cloned to avoid mutating the original data outside the `set()` method. Use\n * `'double'` to also clone before saving value in storage using `set()`. Disabled is\n * default\n * @param {number | false} cleanupInterval The interval in milliseconds to run a\n * setInterval job of cleaning old entries. If false, the job will not be created.\n * Disabled is default\n * @param {number | false} maxEntries The maximum number of entries to keep in the\n * storage. Its hard to determine the size of the entries, so a smart FIFO order is used\n * to determine eviction. If false, no check will be done and you may grow up memory\n * usage. Disabled is default\n */\nexport function buildMemoryStorage(\n cloneData: boolean | 'double' = false,\n cleanupInterval: number | false = false,\n maxEntries: number | false = false\n) {\n const storage = buildStorage({\n set: (key, value) => {\n if (maxEntries) {\n let keys = Object.keys(storage.data);\n\n // Tries to cleanup first\n if (keys.length >= maxEntries) {\n storage.cleanup();\n\n // Recalculates the keys\n keys = Object.keys(storage.data);\n\n // Keeps deleting until there's space\n while (keys.length >= maxEntries) {\n // There's always at least one key here, otherwise it would not be\n // in the loop.\n\n delete storage.data[keys.shift()!];\n }\n }\n }\n\n // Clone the value before storing to prevent future mutations\n // from affecting cached data.\n storage.data[key] = cloneData === 'double' ? clone(value) : value;\n },\n\n remove: (key) => {\n delete storage.data[key];\n },\n\n find: (key) => {\n const value = storage.data[key];\n\n return cloneData && value !== undefined ? clone(value) : value;\n },\n\n clear: () => {\n storage.data = Object.create(null);\n }\n }) as MemoryStorage;\n\n storage.data = Object.create(null) as Record<string, StorageValue>;\n\n // When this program gets running for more than the specified interval, there's a good\n // chance of it being a long-running process or at least have a lot of entries. Therefore,\n // \"faster\" loop is more important than code readability.\n storage.cleanup = () => {\n const keys = Object.keys(storage.data);\n\n let i = -1;\n let value: StorageValue;\n let key: string;\n\n // Looping forward, as older entries are more likely to be expired\n // than newer ones.\n while (++i < keys.length) {\n key = keys[i]!;\n value = storage.data[key]!;\n\n if (value.state === 'empty') {\n storage.remove(key);\n continue;\n }\n\n // If the value is expired and can't be stale, remove it\n if (value.state === 'cached' && isExpired(value) && !canStale(value)) {\n // this storage returns void.\n\n storage.remove(key);\n }\n }\n };\n\n if (cleanupInterval) {\n storage.cleaner = setInterval(storage.cleanup, cleanupInterval);\n }\n\n return storage;\n}\n\nexport interface MemoryStorage extends AxiosStorage {\n data: Record<string, StorageValue>;\n /** The job responsible to cleaning old entries */\n cleaner: ReturnType<typeof setInterval>;\n /** Tries to remove any invalid entry from the memory */\n cleanup: () => void;\n}\n","import type { Method } from 'axios';\nimport { hash } from 'object-code';\nimport type { CacheRequestConfig } from '../cache/axios.js';\nimport type { KeyGenerator } from './types.js';\n\n// Remove first and last '/' char, if present\nconst SLASHES_REGEX = /^\\/|\\/$/g;\n\n/**\n * Builds an generator that receives a {@link CacheRequestConfig} and returns a value\n * hashed by {@link hash}.\n *\n * The value is hashed into a signed integer when the returned value from the provided\n * generator is not a `string` or a `number`.\n *\n * You can return any type of data structure.\n *\n * @example\n *\n * ```js\n * // This generator will return a hash code.\n * // The code will only be the same if url, method and data are the same.\n * const generator = buildKeyGenerator(({ url, method, data }) => ({\n * url,\n * method,\n * data\n * }));\n * ```\n */\nexport function buildKeyGenerator<R = unknown, D = unknown>(\n generator: (request: CacheRequestConfig<R, D>) => unknown\n): KeyGenerator<R, D> {\n return (request) => {\n if (request.id) {\n return request.id;\n }\n\n const key = generator(request);\n\n if (typeof key === 'string' || typeof key === 'number') {\n return `${key}`;\n }\n\n return `${hash(key)}`;\n };\n}\n\nexport const defaultKeyGenerator = buildKeyGenerator(({ baseURL, url, method, params, data }) => {\n // Remove trailing slashes to avoid generating different keys for the \"same\" final url.\n if (baseURL !== undefined) {\n baseURL = baseURL.replace(SLASHES_REGEX, '');\n } else {\n // just to have a consistent hash\n baseURL = '';\n }\n\n if (url !== undefined) {\n url = url.replace(SLASHES_REGEX, '');\n } else {\n // just to have a consistent hash\n url = '';\n }\n\n if (method !== undefined) {\n method = method.toLowerCase() as Method;\n } else {\n // just to have a consistent hash\n method = 'get';\n }\n\n return {\n url: baseURL + (baseURL && url ? '/' : '') + url,\n params: params,\n method: method,\n data: data\n };\n});\n","import type { AxiosInstance } from 'axios';\nimport { defaultHeaderInterpreter } from '../header/interpreter.js';\nimport { defaultRequestInterceptor } from '../interceptors/request.js';\nimport { defaultResponseInterceptor } from '../interceptors/response.js';\nimport { isStorage } from '../storage/build.js';\nimport { buildMemoryStorage } from '../storage/memory.js';\nimport { defaultKeyGenerator } from '../util/key-generator.js';\nimport type { AxiosCacheInstance } from './axios.js';\nimport type { CacheInstance, CacheProperties } from './cache.js';\n\nexport interface CacheOptions extends Partial<CacheInstance>, Partial<CacheProperties> {}\n\n/**\n * Apply the caching interceptors for a already created axios instance.\n *\n * ```ts\n * const axios = setupCache(axios, OPTIONS);\n * ```\n *\n * The `setupCache` function receives global options and all [request\n * specifics](https://axios-cache-interceptor.js.org/config/request-specifics) ones too.\n * This way, you can customize the defaults for all requests.\n *\n * @param axios The already created axios instance\n * @param config The config for the caching interceptors\n * @returns The same instance with extended typescript types.\n * @see https://axios-cache-interceptor.js.org/config\n */\nexport function setupCache(axios: AxiosInstance, options: CacheOptions = {}): AxiosCacheInstance {\n const axiosCache = axios as AxiosCacheInstance;\n\n if (axiosCache.defaults.cache) {\n throw new Error('setupCache() should be called only once');\n }\n\n axiosCache.location = typeof window === 'undefined' ? 'server' : 'client';\n\n axiosCache.storage = options.storage || buildMemoryStorage();\n\n if (!isStorage(axiosCache.storage)) {\n throw new Error('Use buildStorage() function');\n }\n\n axiosCache.waiting = options.waiting || new Map();\n\n axiosCache.generateKey = options.generateKey || defaultKeyGenerator;\n\n axiosCache.headerInterpreter = options.headerInterpreter || defaultHeaderInterpreter;\n\n axiosCache.requestInterceptor =\n options.requestInterceptor || defaultRequestInterceptor(axiosCache);\n\n axiosCache.responseInterceptor =\n options.responseInterceptor || defaultResponseInterceptor(axiosCache);\n\n axiosCache.debug = options.debug || function noop() {};\n\n // CacheRequestConfig values\n axiosCache.defaults.cache = {\n update: options.update || {},\n\n ttl: options.ttl ?? 1000 * 60 * 5,\n\n // Although RFC 7231 also marks POST as cacheable, most users don't know that\n // and may have problems about why their \"create X\" route not working.\n methods: options.methods || ['get', 'head'],\n\n cachePredicate: options.cachePredicate || {\n // All cacheable status codes defined in RFC 7231\n statusCheck: (status) => [200, 203, 300, 301, 302, 404, 405, 410, 414, 501].includes(status)\n },\n\n etag: options.etag ?? true,\n\n // This option is going to be ignored by servers when ETag is enabled\n // Checks strict equality to false to avoid undefined-ish values\n modifiedSince: options.modifiedSince ?? options.etag === false,\n\n interpretHeader: options.interpretHeader ?? true,\n\n cacheTakeover: options.cacheTakeover ?? true,\n\n staleIfError: options.staleIfError ?? true,\n\n override: options.override ?? false,\n\n hydrate: options.hydrate ?? undefined\n };\n\n // Apply interceptors\n axiosCache.interceptors.request.use(\n axiosCache.requestInterceptor.onFulfilled,\n axiosCache.requestInterceptor.onRejected\n );\n axiosCache.interceptors.response.use(\n axiosCache.responseInterceptor.onFulfilled,\n axiosCache.responseInterceptor.onRejected\n );\n\n return axiosCache;\n}\n","import { buildStorage, canStale, isExpired } from './build.js';\nimport type { StorageValue } from './types.js';\n\n/**\n * Creates a simple storage. You can persist his data by using `sessionStorage` or\n * `localStorage` with it.\n *\n * **ImplNote**: Without polyfill, this storage only works on browser environments.\n *\n * @example\n *\n * ```js\n * const fromLocalStorage = buildWebStorage(localStorage);\n * const fromSessionStorage = buildWebStorage(sessionStorage);\n *\n * const myStorage = new Storage();\n * const fromMyStorage = buildWebStorage(myStorage);\n * ```\n *\n * @param storage The type of web storage to use. localStorage or sessionStorage.\n * @param prefix The prefix to index the storage. Useful to prevent collision between\n * multiple places using the same storage.\n */\nexport function buildWebStorage(storage: Storage, prefix = 'axios-cache-') {\n return buildStorage({\n clear: () => {\n for (const key in storage) {\n if (key.startsWith(prefix)) {\n storage.removeItem(key);\n }\n }\n },\n\n find: (key) => {\n const json = storage.getItem(prefix + key);\n return json ? (JSON.parse(json) as StorageValue) : undefined;\n },\n\n remove: (key) => {\n storage.removeItem(prefix + key);\n },\n\n set: (key, value) => {\n const save = () => storage.setItem(prefix + key, JSON.stringify(value));\n\n try {\n return save();\n } catch (error) {\n const allValues: [string, StorageValue][] = Object.entries(\n storage as Record<string, string>\n )\n .filter((item) => item[0].startsWith(prefix))\n .map((item) => [item[0], JSON.parse(item[1]) as StorageValue]);\n\n // Remove all expired values\n for (const value of allValues) {\n if (value[1].state === 'cached' && isExpired(