UNPKG

axios-cache-interceptor

Version:
1,119 lines (1,104 loc) 39.2 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); let cache_parser = require("cache-parser"); let fast_defer = require("fast-defer"); let http_vary = require("http-vary"); let object_code = require("object-code"); let try$1 = require("try"); //#region src/header/headers.ts /** * @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 */ const Header = { IfModifiedSince: "if-modified-since", LastModified: "last-modified", IfNoneMatch: "if-none-match", CacheControl: "cache-control", Pragma: "pragma", ETag: "etag", Expires: "expires", Age: "age", XAxiosCacheEtag: "x-axios-cache-etag", XAxiosCacheLastModified: "x-axios-cache-last-modified", XAxiosCacheStaleIfError: "x-axios-cache-stale-if-error", Vary: "vary" }; //#endregion //#region src/header/interpreter.ts /** * @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 */ const defaultHeaderInterpreter = (headers, location) => { if (!headers) return "not enough headers"; const cacheControl = headers[Header.CacheControl]; if (cacheControl) { const cc = (0, cache_parser.parse)(String(cacheControl)); if (cc.noCache || cc.noStore || location === "server" && cc.private) return "dont cache"; if (cc.immutable) return { cache: 1e3 * 60 * 60 * 24 * 365 }; if (cc.maxAge !== void 0) { const age = headers[Header.Age]; return { cache: age ? (cc.maxAge - Number(age)) * 1e3 : cc.maxAge * 1e3, stale: cc.maxStale !== void 0 ? cc.maxStale * 1e3 : cc.staleWhileRevalidate !== void 0 ? cc.staleWhileRevalidate * 1e3 : void 0 }; } } const expires = headers[Header.Expires]; if (expires) { const milliseconds = Date.parse(String(expires)) - Date.now(); return milliseconds >= 0 ? { cache: milliseconds } : "dont cache"; } return "not enough headers"; }; //#endregion //#region src/header/extract.ts /** * Extracts specified header values from request headers. * Generic utility for extracting a subset of headers. * * @param requestHeaders The full request headers object * @param headerNames Array of header names to extract * @returns Object with extracted header values */ function extractHeaders(requestHeaders, headerNames) { const result = {}; for (const name of headerNames) result[name] = requestHeaders.get(name)?.toString(); return result; } //#endregion //#region src/util/cache-predicate.ts /** * Tests an response against a {@link CachePredicateObject}. * * @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 */ async function testCachePredicate(response, predicate) { if (typeof predicate === "function") return predicate(response); const { statusCheck, responseMatch, containsHeaders } = predicate; if (statusCheck && !await statusCheck(response.status) || responseMatch && !await responseMatch(response)) return false; if (containsHeaders) { for (const [header, predicate] of Object.entries(containsHeaders)) if (!await predicate(response.headers[header.toLowerCase()] ?? response.headers[header])) return false; } return true; } /** * Determines whether a given URL matches a specified pattern, which can be either a * string or a regular expression. * * @param matchPattern - The pattern to match against * * - If it's a regular expression, it will be reset to ensure consistent behavior for * stateful regular expressions. * - If it's a string, the function checks if the URL contains the string. * * @param configUrl - The URL to test against the provided pattern; normally `config.url`. * @returns `true` if the `configUrl` matches the `matchPattern` * * @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 */ function regexOrStringMatch(matchPattern, configUrl) { if (matchPattern instanceof RegExp) { matchPattern.lastIndex = 0; return matchPattern.test(configUrl); } return configUrl.includes(matchPattern); } //#endregion //#region src/interceptors/util.ts /** * Creates a new validateStatus function that will use the one already used and also * accept status code 304. * * @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 */ function createValidateStatus(oldValidate) { return oldValidate ? (status) => oldValidate(status) || status === 304 : (status) => status >= 200 && status < 300 || status === 304; } /** * Checks if the given method is in the methods array * * @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 */ function isMethodIn(requestMethod = "get", methodList = []) { requestMethod = requestMethod.toLowerCase(); return methodList.some((method) => method === requestMethod); } /** * This function updates the cache when the request is stale. So, the next request to the * server will be made with proper header / settings. * * @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 */ function updateStaleRequest(cache, config) { const { etag, modifiedSince } = config.cache; const revalidation = cache.data?.meta?.revalidation; if (etag) { let etagValue; if (revalidation?.etag) etagValue = revalidation.etag; else if (etag === true) etagValue = cache.data?.headers[Header.ETag]; else etagValue = etag; if (etagValue) config.headers.set(Header.IfNoneMatch, etagValue); } if (modifiedSince) { let lastModifiedValue; if (revalidation?.lastModified) lastModifiedValue = revalidation.lastModified === true ? new Date(cache.createdAt).toUTCString() : revalidation.lastModified; else if (modifiedSince === true) lastModifiedValue = cache.data.headers[Header.LastModified] || new Date(cache.createdAt).toUTCString(); else lastModifiedValue = modifiedSince.toUTCString(); config.headers.set(Header.IfModifiedSince, lastModifiedValue); } } /** * Creates the new date to the cache by the provided response. Also handles possible 304 * Not Modified by updating response properties. * * @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 */ function createCacheResponse(response, previousCache) { if (response.status === 304 && previousCache) { response.cached = true; response.data = previousCache.data; response.status = previousCache.status; response.statusText = previousCache.statusText; response.headers = { ...previousCache.headers, ...response.headers }; return previousCache; } return { data: response.data, status: response.status, statusText: response.statusText, headers: response.headers }; } //#endregion //#region src/interceptors/request.ts /** * @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 */ function defaultRequestInterceptor(axios) { const onFulfilled = async (config) => { config.id = axios.generateKey(config, { vary: config.cache && Array.isArray(config.cache.vary) ? extractHeaders(config.headers, config.cache.vary) : void 0 }); if (config.cache === false) { axios.debug({ id: config.id, msg: "Cache disabled: config.cache === false" }); return config; } config.cache = { ...axios.defaults.cache, ...config.cache }; if (config.cache.enabled === false) { axios.debug({ id: config.id, msg: "Cache disabled: config.cache.enabled === false" }); return config; } if (typeof config.cache.cachePredicate === "object" && config.cache.cachePredicate.ignoreUrls && config.url) { for (const url of config.cache.cachePredicate.ignoreUrls) if (regexOrStringMatch(url, config.url)) { axios.debug({ id: config.id, msg: `URL ignored: matches ignoreUrls pattern`, data: { url: config.url, pattern: url } }); return config; } } if (typeof config.cache.cachePredicate === "object" && config.cache.cachePredicate.allowUrls && config.url) { let matched = false; for (const url of config.cache.cachePredicate.allowUrls) if (regexOrStringMatch(url, config.url)) { matched = true; axios.debug({ id: config.id, msg: `URL allowed: matches allowUrls pattern`, data: { url: config.url, pattern: url } }); break; } if (!matched) { axios.debug({ id: config.id, msg: `URL rejected: not in allowUrls`, data: { url: config.url, allowUrls: config.cache.cachePredicate.allowUrls } }); return config; } } if (config.cache.cacheTakeover) { config.headers.set(Header.CacheControl, "no-cache, no-store, must-revalidate, max-age=0", false); config.headers.set(Header.Pragma, "no-cache", false); config.headers.set(Header.Expires, "0", false); } if (!isMethodIn(config.method, config.cache.methods)) { axios.debug({ id: config.id, msg: `Method ${config.method} not cacheable (allowed: ${config.cache.methods})` }); return config; } let cache = await axios.storage.get(config.id, config); const overrideCache = config.cache.override; if (config.cache.vary !== false && cache.data?.meta?.vary && cache.data.headers[Header.Vary]) { const vary = Array.isArray(config.cache.vary) ? config.cache.vary : (0, http_vary.parse)(cache.data.headers[Header.Vary]); if (vary && vary !== "*" && !(0, http_vary.compare)(vary, cache.data.meta?.vary, config.headers)) { const extractedHeaders = extractHeaders(config.headers, vary); const newKey = axios.generateKey({ ...config, id: void 0 }, { vary: extractedHeaders }); if (config.id !== newKey) { axios.debug({ id: config.id, msg: "Vary mismatch, switching to vary-aware key", data: { cachedHeaders: cache.data.meta.vary, currentHeaders: extractedHeaders, vary, newKey } }); config.id = newKey; cache = await axios.storage.get(newKey, config); } } } ignoreAndRequest: if (cache.state === "empty" || cache.state === "stale" || cache.state === "must-revalidate" || overrideCache) { if (axios.waiting.has(config.id) && !overrideCache) { cache = await axios.storage.get(config.id, config); if (cache.state !== "empty" && cache.state !== "must-revalidate") { axios.debug({ id: config.id, msg: "Concurrent request found, reusing result" }); break ignoreAndRequest; } } const def = (0, fast_defer.deferred)(); axios.waiting.set(config.id, def); def.catch(() => void 0); await axios.storage.set(config.id, { state: "loading", previous: overrideCache ? cache.data ? "stale" : "empty" : cache.state, data: cache.data, createdAt: overrideCache && !cache.createdAt ? Date.now() : cache.createdAt }, config); if ((cache.state === "stale" || cache.state === "must-revalidate") && !overrideCache) { updateStaleRequest(cache, { ...config, cache: config.cache }); axios.debug({ id: config.id, msg: "Stale revalidation: added conditional headers (If-None-Match/If-Modified-Since)" }); } config.validateStatus = createValidateStatus(config.validateStatus); axios.debug({ id: config.id, msg: "Making network request", data: { overrideCache, cacheState: cache.state } }); if (cache.state === "stale" || cache.data && cache.state !== "must-revalidate") await config.cache.hydrate?.(cache); return config; } let cachedResponse; if (cache.state === "loading") { const deferred = axios.waiting.get(config.id); if (!deferred) { if (cache.data) await config.cache.hydrate?.(cache); return config; } axios.debug({ id: config.id, msg: "Concurrent request detected, waiting..." }); try { await deferred; const state = await axios.storage.get(config.id, config); /* c8 ignore start */ if (!state.data) { axios.debug({ id: config.id, msg: "Concurrent request completed without data, retrying" }); return onFulfilled(config); } /* c8 ignore end */ if (config.cache.vary !== false && state.data.meta?.vary && state.data.headers[Header.Vary]) { const vary = Array.isArray(config.cache.vary) ? config.cache.vary : (0, http_vary.parse)(state.data.headers[Header.Vary]); if (vary && vary !== "*" && !(0, http_vary.compare)(vary, state.data.meta.vary, config.headers)) { axios.debug({ id: config.id, msg: "Vary mismatch after concurrent request, making own request", data: { cachedVary: state.data.meta.vary, currentVary: extractHeaders(config.headers, vary) } }); return onFulfilled(config); } } cachedResponse = state.data; } catch (err) { axios.debug({ id: config.id, msg: "Concurrent request failed, propagating error", data: err }); throw err; } } else cachedResponse = cache.data; config.transformResponse = void 0; config.adapter = function cachedAdapter() { return Promise.resolve({ config, data: cachedResponse.data, headers: cachedResponse.headers, status: cachedResponse.status, statusText: cachedResponse.statusText, cached: true, stale: cache.previous === "stale", id: config.id }); }; axios.debug({ id: config.id, msg: "Using cached response" }); return config; }; return { onFulfilled }; } //#endregion //#region src/util/update-cache.ts /** * Function to update all caches, from CacheProperties.update, with the new data. * * @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 */ async function updateCache(storage, data, cacheUpdater) { if (typeof cacheUpdater === "function") return cacheUpdater(data); for (const [cacheKey, updater] of Object.entries(cacheUpdater)) { if (updater === "delete") { await storage.remove(cacheKey, data.config); continue; } const value = await storage.get(cacheKey, data.config); if (value.state === "loading") continue; const newValue = await updater(value, data); if (newValue === "delete") { await storage.remove(cacheKey, data.config); continue; } if (newValue !== "ignore") await storage.set(cacheKey, newValue, data.config); } } //#endregion //#region src/interceptors/response.ts /** * @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 */ function defaultResponseInterceptor(axios) { /** * Replies a deferred stored in the axios waiting map. Use resolve to proceed checking the * previously updated cache or reject to abort deduplicated requests with error. */ const replyDeferred = (responseId, mode, error) => { const deferred = axios.waiting.get(responseId); if (deferred) { deferred[mode](error); axios.waiting.delete(responseId); axios.debug({ id: responseId, msg: `Found waiting deferred(s) and ${mode} them` }); } }; const onFulfilled = async (response) => { if (!response?.config) { axios.debug({ msg: "Unknown response received (not an Axios response)", data: response }); throw response; } response.id = response.config.id; response.cached ??= false; const config = response.config; const cacheConfig = config.cache; if (response.cached) { axios.debug({ id: response.id, msg: "Returned cached response" }); return response; } if (!cacheConfig) { axios.debug({ id: response.id, msg: "Response received without cache config", data: response }); response.cached = false; return response; } if (cacheConfig.update) await updateCache(axios.storage, response, cacheConfig.update); if (!isMethodIn(config.method, cacheConfig.methods)) { axios.debug({ id: response.id, msg: `Method ${config.method} not cacheable (allowed: ${cacheConfig.methods})` }); return response; } const cache = await axios.storage.get(response.id, config); if (cache.state !== "loading") { axios.debug({ id: response.id, msg: "Response received but storage not in loading state", data: { cacheState: cache.state } }); axios.waiting.delete(response.id); return response; } if (!cache.data && !await testCachePredicate(response, cacheConfig.cachePredicate)) { replyDeferred(response.id, "resolve"); axios.debug({ id: response.id, msg: "Cache predicate rejected this response" }); return response; } for (const header of Object.keys(response.headers)) if (header.startsWith("x-axios-cache")) delete response.headers[header]; let ttl = cacheConfig.ttl || -1; let staleTtl; if (cacheConfig.interpretHeader) { const expirationTime = axios.headerInterpreter(response.headers, axios.location); if (expirationTime === "dont cache") { replyDeferred(response.id, "resolve"); axios.debug({ id: response.id, msg: "Cache-Control header indicates: do not cache" }); return response; } if (expirationTime !== "not enough headers") if (typeof expirationTime === "number") ttl = expirationTime; else { ttl = expirationTime.cache; staleTtl = expirationTime.stale; } } if (typeof ttl === "function") ttl = await ttl(response); const data = createCacheResponse(response, cache.data); if (cacheConfig.etag || cacheConfig.modifiedSince) { data.meta ??= {}; data.meta.revalidation = {}; if (cacheConfig.etag) { const etag = cacheConfig.etag === true ? response.headers[Header.ETag] : cacheConfig.etag; if (etag) data.meta.revalidation.etag = etag; } if (cacheConfig.modifiedSince) data.meta.revalidation.lastModified = cacheConfig.modifiedSince === true ? response.headers[Header.LastModified] || true : cacheConfig.modifiedSince.toUTCString(); } if (cacheConfig.vary !== false && response.headers[Header.Vary]) { const vary = Array.isArray(cacheConfig.vary) ? cacheConfig.vary : (0, http_vary.parse)(response.headers[Header.Vary]); if (Array.isArray(vary)) { data.meta ??= {}; data.meta.vary = extractHeaders(config.headers, vary); axios.debug({ id: response.id, msg: "Storing response with Vary metadata", data: { vary, extracted: data.meta.vary } }); } else if (vary === "*") { axios.debug({ id: response.id, msg: "Vary: * detected, storing as stale" }); await axios.storage.set(response.id, { state: "stale", createdAt: Date.now(), data, ttl }, config); replyDeferred(response.id, "resolve"); return response; } } axios.debug({ id: response.id, msg: "Caching response", data: { ttl, staleTtl, interpretHeader: cacheConfig.interpretHeader } }); const newCache = { state: "cached", ttl, staleTtl, createdAt: Date.now(), data }; await axios.storage.set(response.id, newCache, config); replyDeferred(response.id, "resolve"); axios.debug({ id: response.id, msg: "Response cached successfully", data: { state: newCache.state, ttl: newCache.ttl } }); return response; }; const onRejected = async (error) => { if (!error.isAxiosError || !error.config) { axios.debug({ 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", data: error }); throw error; } const config = error.config; const id = config.id; const cacheConfig = config.cache; const response = error.response; if (!cacheConfig || !id) { axios.debug({ msg: "Request failed without cache config", data: { error } }); throw error; } if (!isMethodIn(config.method, cacheConfig.methods)) { axios.debug({ id, msg: `Method ${config.method} not cacheable (allowed: ${cacheConfig.methods})` }); await axios.storage.remove(id, config); replyDeferred(id, "reject", error); throw error; } const cache = await axios.storage.get(id, config); if (cache.state !== "loading" || cache.previous !== "stale") { axios.debug({ id, msg: "Request error with unexpected cache state", data: { cacheState: cache.state, previous: cache.state === "loading" ? cache.previous : void 0, errorCode: error.code } }); if (error.code !== "ERR_CANCELED" || error.code === "ERR_CANCELED" && cache.state !== "cached") await axios.storage.remove(id, config); if (error.code === "ERR_CANCELED") replyDeferred(id, "resolve"); else replyDeferred(id, "reject", error); throw error; } if (cacheConfig.staleIfError) { const cacheControl = String(response?.headers[Header.CacheControl]); const staleHeader = cacheControl && (0, cache_parser.parse)(cacheControl).staleIfError; const staleIfError = typeof cacheConfig.staleIfError === "function" ? await cacheConfig.staleIfError(response, cache, error) : cacheConfig.staleIfError === true && staleHeader ? staleHeader * 1e3 : cacheConfig.staleIfError; axios.debug({ id, msg: "staleIfError config found for failed request", data: { staleIfError, createdAt: cache.createdAt } }); if (staleIfError === true || typeof staleIfError === "number" && cache.createdAt + staleIfError > Date.now()) { await axios.storage.set(id, { state: "stale", createdAt: Date.now(), data: cache.data }, config); const waiting = axios.waiting.get(id); if (waiting) { waiting.resolve(); axios.waiting.delete(id); axios.debug({ id, msg: "Found waiting deferred(s) and resolved them" }); } axios.debug({ id, msg: "staleIfError: returning stale cache for failed request" }); return { cached: true, stale: true, config, id, data: cache.data.data, headers: cache.data.headers, status: cache.data.status, statusText: cache.data.statusText }; } } axios.debug({ id, msg: "Unhandled error, cleaning up", data: { errorCode: error.code, errorMessage: error.message } }); await axios.storage.remove(id, config); replyDeferred(id, "reject", error); throw error; }; return { onFulfilled, onRejected }; } //#endregion //#region src/storage/build.ts /** * Returns true if the provided object was created from {@link buildStorage} function. * * @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 */ const isStorage = (obj) => !!obj && !!obj["is-storage"]; /** * Migrates old header-based revalidation data to new meta.revalidation format. * This ensures backward compatibility with existing cache entries. * * @deprecated Internal migration function. Will be removed when all cache entries * have naturally expired and been recreated with new format. */ function migrateRevalidationHeaders(data) { if (data.meta?.revalidation) return; const oldEtag = data.headers[Header.XAxiosCacheEtag]; const oldLastModified = data.headers[Header.XAxiosCacheLastModified]; if (oldEtag || oldLastModified) { data.meta ??= {}; data.meta.revalidation = {}; if (oldEtag) data.meta.revalidation.etag = oldEtag; if (oldLastModified) data.meta.revalidation.lastModified = oldLastModified === "use-cache-timestamp" ? true : oldLastModified; delete data.headers[Header.XAxiosCacheEtag]; delete data.headers[Header.XAxiosCacheLastModified]; delete data.headers[Header.XAxiosCacheStaleIfError]; } } function hasRevalidationMetadata(value) { migrateRevalidationHeaders(value.data); const headers = value.data.headers; const revalidation = value.data.meta?.revalidation; return Header.ETag in headers || Header.LastModified in headers || !!(revalidation?.etag || revalidation?.lastModified); } /** Returns true if value must be revalidated */ function mustRevalidate(value) { return String(value.data.headers[Header.CacheControl]).includes("must-revalidate"); } /** Returns true if this has sufficient properties to stale instead of expire. */ function canStale(value) { if (hasRevalidationMetadata(value)) return true; return value.state === "cached" && value.staleTtl !== void 0 && Math.abs(Date.now() - (value.createdAt + value.ttl)) <= value.staleTtl; } /** * Checks if the provided cache is expired. You should also check if the cache * {@link canStale} and {@link mayUseStale} */ function isExpired(value) { return value.ttl !== void 0 && value.createdAt + value.ttl <= Date.now(); } /** * Defines which storage states are evicted first when cleaning up the storage. */ const StateEvictionOrder = { empty: 0, "must-revalidate": 1, stale: 2, cached: 3, loading: 4 }; /** * Is a comparator function that sorts storage entries by their eviction priority * and, in the same group, by older first. */ function storageEntriesSorter([, a], [, b]) { const stateDiff = StateEvictionOrder[a.state] - StateEvictionOrder[b.state]; if (stateDiff !== 0) return stateDiff; return (a.createdAt || 0) - (b.createdAt || 0); } /** * Returns true if the storage entry can be removed according to its state and the * provided maxStaleAge. */ function canRemoveStorageEntry(value, maxStaleAge) { switch (value.state) { case "loading": return false; case "empty": case "must-revalidate": return true; case "cached": return isExpired(value) && !canStale(value); case "stale": if (maxStaleAge !== void 0 && value.ttl !== void 0) return Date.now() > value.createdAt + value.ttl + maxStaleAge; return false; } } /** * All integrated storages are wrappers around the `buildStorage` function. External * libraries use it and if you want to build your own, `buildStorage` is the way to go! * * The exported `buildStorage` function abstracts the storage interface and requires a * super simple object to build the storage. * * **Note**: You can only create custom storages with this function. * * @example * * ```js * const myStorage = buildStorage({ * find: () => {...}, * set: () => {...}, * remove: () => {...}, * clear: () => {...} * }); * * const axios = setupCache(axios, { storage: myStorage }); * ``` * * @see https://axios-cache-interceptor.js.org/guide/storages#buildstorage */ function buildStorage({ set, find, remove, clear }) { return { "is-storage": 1, set, remove, clear, get: async (key, config) => { let value = await find(key, config); if (!value) return { state: "empty" }; if (value.state === "empty" || value.state === "loading" || value.state === "must-revalidate") return value; if (value.state === "cached" || value.state === "stale") migrateRevalidationHeaders(value.data); if (value.state === "cached") { if (!isExpired(value)) return value; if (!canStale(value)) { await remove(key, config); return { state: "empty" }; } value = { state: "stale", createdAt: value.createdAt, data: value.data, ttl: value.staleTtl !== void 0 ? value.staleTtl + value.ttl : void 0 }; await set(key, value, config); if (mustRevalidate(value)) return { ...value, state: "must-revalidate" }; } if (!isExpired(value)) return value; if (hasRevalidationMetadata(value)) return value; await remove(key, config); return { state: "empty" }; } }; } //#endregion //#region src/storage/memory.ts /* c8 ignore start */ /** * Clones an object using the structured clone algorithm if available, otherwise it uses * JSON.parse(JSON.stringify(value)). */ const clone = typeof structuredClone === "function" ? structuredClone : (value) => JSON.parse(JSON.stringify(value)); /* c8 ignore stop */ /** * Creates a simple in-memory storage. This means that if you need to persist data between * page or server reloads, this will not help. * * This is the storage used by default. * * If you need to modify it's data, you can do by the `data` property. * * @example * * ```js * const memoryStorage = buildMemoryStorage(); * * setupCache(axios, { storage: memoryStorage }); * * // Simple example to force delete the request cache * * const { id } = axios.get('url'); * * delete memoryStorage.data[id]; * ``` * * @param {boolean | 'double'} cloneData Use `true` if the data returned by `find()` * should be cloned to avoid mutating the original data outside the `set()` method. Use * `'double'` to also clone before saving value in storage using `set()`. Disabled is * default * @param {number | false} cleanupInterval The interval in milliseconds to run a * setInterval job of cleaning old entries. If false, the job will not be created. * 5 minutes (300_000) is default * @param {number | false} maxEntries The maximum number of entries to keep in the * storage. Its hard to determine the size of the entries, so a smart FIFO order is used * to determine eviction. If false, no check will be done and you may grow up memory * usage. 1024 is default * @param {number} maxStaleAge The maximum age in milliseconds a stale entry can stay * in the storage before being removed. Otherwise, stale-able entries would stay * indefinitely causing a memory leak eventually. 1 hour (3_600_000) is default */ function buildMemoryStorage(cloneData = false, cleanupInterval = 300 * 1e3, maxEntries = 1024, maxStaleAge = 3600 * 1e3) { function sortedEntries() { return Array.from(storage.data.entries()).sort(storageEntriesSorter); } const storage = buildStorage({ set: (key, value) => { if (maxEntries && storage.data.size >= maxEntries) { storage.cleanup(); if (storage.data.size >= maxEntries) for (const [key] of sortedEntries()) { storage.data.delete(key); if (storage.data.size < maxEntries) break; } } storage.data.set(key, cloneData === "double" ? clone(value) : value); }, remove: (key) => { storage.data.delete(key); }, find: (key) => { const value = storage.data.get(key); return cloneData && value !== void 0 ? clone(value) : value; }, clear: () => { storage.data.clear(); } }); storage.data = /* @__PURE__ */ new Map(); storage.cleanup = () => { for (const [key, value] of sortedEntries()) if (canRemoveStorageEntry(value, maxStaleAge)) storage.data.delete(key); }; if (cleanupInterval) { storage.cleaner = setInterval(storage.cleanup, cleanupInterval); if (typeof storage.cleaner === "object" && "unref" in storage.cleaner) storage.cleaner.unref(); } return storage; } //#endregion //#region src/util/key-generator.ts const SLASHES_REGEX = /^\/|\/$/g; /** * Builds an generator that receives a {@link CacheRequestConfig} and optional metadata, * and returns a value hashed by {@link hash}. * * The value is hashed into a signed integer when the returned value from the provided * generator is not a `string` or a `number`. * * You can return any type of data structure. * * @example * * ```js * // This generator will return a hash code. * // The code will only be the same if url, method and data are the same. * const generator = buildKeyGenerator(({ url, method, data }) => ({ * url, * method, * data * })); * ``` */ function buildKeyGenerator(generator) { return (request, meta) => { if (request.id) return request.id; const key = generator(request, meta); if (typeof key === "string" || typeof key === "number") return `${key}`; return `${(0, object_code.hash)(key)}`; }; } /** * @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 */ const defaultKeyGenerator = buildKeyGenerator(({ baseURL, url, method, params, data }, meta) => { if (baseURL !== void 0) baseURL = baseURL.replace(SLASHES_REGEX, ""); else baseURL = ""; if (url !== void 0) url = url.replace(SLASHES_REGEX, ""); else url = ""; if (method !== void 0) method = method.toLowerCase(); else method = "get"; return { url: baseURL + (baseURL && url ? "/" : "") + url, params, method, data, ...meta }; }); //#endregion //#region src/cache/create.ts /** * Apply the caching interceptors for a already created axios instance. * * ```ts * const axios = setupCache(axios, OPTIONS); * ``` * * The `setupCache` function receives global options and all [request * specifics](https://axios-cache-interceptor.js.org/config/request-specifics) ones too. * This way, you can customize the defaults for all requests. * * @param axios The already created axios instance * @param config The config for the caching interceptors * @returns The same instance with extended typescript types. * @see https://axios-cache-interceptor.js.org/config */ function setupCache(axios, options = {}) { const axiosCache = axios; if (axiosCache.defaults.cache) throw new Error("setupCache() should be called only once"); axiosCache.location = typeof window === "undefined" ? "server" : "client"; axiosCache.storage = options.storage || buildMemoryStorage(); if (!isStorage(axiosCache.storage)) throw new Error("Use buildStorage() function"); axiosCache.waiting = options.waiting || /* @__PURE__ */ new Map(); axiosCache.generateKey = options.generateKey || defaultKeyGenerator; axiosCache.headerInterpreter = options.headerInterpreter || defaultHeaderInterpreter; axiosCache.requestInterceptor = options.requestInterceptor || defaultRequestInterceptor(axiosCache); axiosCache.responseInterceptor = options.responseInterceptor || defaultResponseInterceptor(axiosCache); axiosCache.debug = options.debug || function noop() {}; axiosCache.defaults.cache = { enabled: options.enabled ?? true, update: options.update || {}, ttl: options.ttl ?? 1e3 * 60 * 5, methods: options.methods || ["get", "head"], cachePredicate: options.cachePredicate || { statusCheck: (status) => [ 200, 203, 300, 301, 302, 404, 405, 410, 414, 501 ].includes(status) }, etag: options.etag ?? true, modifiedSince: options.modifiedSince ?? options.etag === false, interpretHeader: options.interpretHeader ?? true, cacheTakeover: options.cacheTakeover ?? true, staleIfError: options.staleIfError ?? true, override: options.override ?? false, hydrate: options.hydrate ?? void 0, vary: options.vary ?? true }; if (options.register ?? true) { axiosCache.interceptors.request.use(axiosCache.requestInterceptor.onFulfilled, axiosCache.requestInterceptor.onRejected); axiosCache.interceptors.response.use(axiosCache.responseInterceptor.onFulfilled, axiosCache.responseInterceptor.onRejected); } return axiosCache; } //#endregion //#region src/storage/web-api.ts /** * Creates a simple storage. You can persist his data by using `sessionStorage` or * `localStorage` with it. * * **ImplNote**: Without polyfill, this storage only works on browser environments. * * @example * * ```js * const fromLocalStorage = buildWebStorage(localStorage); * const fromSessionStorage = buildWebStorage(sessionStorage); * * const myStorage = new Storage(); * const fromMyStorage = buildWebStorage(myStorage); * ``` * * @param storage The type of web storage to use. localStorage or sessionStorage. * @param prefix The prefix to index the storage. Useful to prevent collision between * multiple places using the same storage. * @param {number} maxStaleAge The maximum age in milliseconds a stale entry can stay * in the storage before being removed. Otherwise, stale-able entries would stay * indefinitely causing a memory leak eventually. 1 hour (3_600_000) is default */ function buildWebStorage(storage, prefix = "axios-cache-", maxStaleAge = 3600 * 1e3) { function save(key, value) { storage.setItem(prefix + key, JSON.stringify(value)); } return buildStorage({ clear: () => { for (const key in storage) if (key.startsWith(prefix)) storage.removeItem(key); }, find: (key) => { const json = storage.getItem(prefix + key); return json ? JSON.parse(json) : void 0; }, remove: (key) => { storage.removeItem(prefix + key); }, set: (key, value) => { const result = try$1.Result.try(save, key, value); if (result.ok) return; if (!isDomQuotaExceededError(result.error)) throw result.error; const allValues = Object.entries(storage).filter(([key]) => key.startsWith(prefix)).map(([key, value]) => [key, JSON.parse(value)]); for (const [key, value] of allValues) if (canRemoveStorageEntry(value, maxStaleAge)) storage.removeItem(key); const retry = try$1.Result.try(save, key, value); if (retry.ok) return; if (!isDomQuotaExceededError(retry.error)) throw retry.error; const descItems = allValues.sort((a, b) => (a[1].createdAt || 0) - (b[1].createdAt || 0)); for (const item of descItems) { storage.removeItem(item[0]); const lastTry = try$1.Result.try(save, key, value); if (lastTry.ok) return; if (!isDomQuotaExceededError(lastTry.error)) throw lastTry.error; } } }); } function isDomQuotaExceededError(error) { return (error instanceof DOMException || typeof error === "object" && error !== null && "name" in error && error.constructor?.name === "DOMException") && "name" in error && (error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED" || error.name === "QUOTA_EXCEEDED_ERR"); } //#endregion //#region src/index.ts console.error("You are using a development build. Make sure to use the correct build in production\nhttps://axios-cache-interceptor.js.org/guide/getting-started\n\n"); //#endregion exports.Header = Header; exports.buildKeyGenerator = buildKeyGenerator; exports.buildMemoryStorage = buildMemoryStorage; exports.buildStorage = buildStorage; exports.buildWebStorage = buildWebStorage; exports.canRemoveStorageEntry = canRemoveStorageEntry; exports.canStale = canStale; exports.createCacheResponse = createCacheResponse; exports.createValidateStatus = createValidateStatus; exports.defaultHeaderInterpreter = defaultHeaderInterpreter; exports.defaultKeyGenerator = defaultKeyGenerator; exports.defaultRequestInterceptor = defaultRequestInterceptor; exports.defaultResponseInterceptor = defaultResponseInterceptor; exports.isExpired = isExpired; exports.isMethodIn = isMethodIn; exports.isStorage = isStorage; exports.mustRevalidate = mustRevalidate; exports.regexOrStringMatch = regexOrStringMatch; exports.setupCache = setupCache; exports.storageEntriesSorter = storageEntriesSorter; exports.testCachePredicate = testCachePredicate; exports.updateCache = updateCache; exports.updateStaleRequest = updateStaleRequest; //# sourceMappingURL=index.cjs.map