axios-cache-interceptor
Version:
Cache interceptor for axios
892 lines (878 loc) • 34.1 kB
JavaScript
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) return config;
config.cache = {
...axios.defaults.cache,
...config.cache
};
if (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)) 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;
break;
}
if (!matched) 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)) 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) {
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") 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
});
config.validateStatus = createValidateStatus(config.validateStatus);
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;
}
try {
await deferred;
const state = await axios.storage.get(config.id, config);
/* c8 ignore start */
if (!state.data) 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)) return onFulfilled(config);
}
cachedResponse = state.data;
} catch (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
});
};
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);
}
};
const onFulfilled = async (response) => {
if (!response?.config) throw response;
response.id = response.config.id;
response.cached ??= false;
const config = response.config;
const cacheConfig = config.cache;
if (response.cached) return response;
if (!cacheConfig) {
response.cached = false;
return response;
}
if (cacheConfig.update) await updateCache(axios.storage, response, cacheConfig.update);
if (!isMethodIn(config.method, cacheConfig.methods)) return response;
const cache = await axios.storage.get(response.id, config);
if (cache.state !== "loading") {
axios.waiting.delete(response.id);
return response;
}
if (!cache.data && !await testCachePredicate(response, cacheConfig.cachePredicate)) {
replyDeferred(response.id, "resolve");
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");
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);
} else if (vary === "*") {
await axios.storage.set(response.id, {
state: "stale",
createdAt: Date.now(),
data,
ttl
}, config);
replyDeferred(response.id, "resolve");
return response;
}
}
const newCache = {
state: "cached",
ttl,
staleTtl,
createdAt: Date.now(),
data
};
await axios.storage.set(response.id, newCache, config);
replyDeferred(response.id, "resolve");
return response;
};
const onRejected = async (error) => {
if (!error.isAxiosError || !error.config) throw error;
const config = error.config;
const id = config.id;
const cacheConfig = config.cache;
const response = error.response;
if (!cacheConfig || !id) throw error;
if (!isMethodIn(config.method, 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") {
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;
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);
}
return {
cached: true,
stale: true,
config,
id,
data: cache.data.data,
headers: cache.data.headers,
status: cache.data.status,
statusText: cache.data.statusText
};
}
}
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
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