mirror of
https://github.com/arthurfiorette/axios-cache-interceptor.git
synced 2025-12-08 17:36:16 +00:00
feat: add staleIfError support
This commit is contained in:
parent
8273399746
commit
edb32bdea3
@ -11,7 +11,8 @@
|
|||||||
"plugin:prettier/recommended"
|
"plugin:prettier/recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/await-thenable": "off"
|
"@typescript-eslint/await-thenable": "off",
|
||||||
|
"@typescript-eslint/restrict-template-expressions": "off"
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": "latest",
|
"ecmaVersion": "latest",
|
||||||
|
|||||||
4
src/cache/axios.ts
vendored
4
src/cache/axios.ts
vendored
@ -77,8 +77,8 @@ export interface AxiosCacheInstance extends CacheInstance, AxiosInstance {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interceptors: {
|
interceptors: {
|
||||||
request: AxiosInterceptorManager<CacheRequestConfig<unknown, unknown>>;
|
request: AxiosInterceptorManager<CacheRequestConfig>;
|
||||||
response: AxiosInterceptorManager<CacheAxiosResponse<unknown, unknown>>;
|
response: AxiosInterceptorManager<CacheAxiosResponse>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @template D The type that the request body use */
|
/** @template D The type that the request body use */
|
||||||
|
|||||||
42
src/cache/cache.ts
vendored
42
src/cache/cache.ts
vendored
@ -3,7 +3,12 @@ import type { Deferred } from 'fast-defer';
|
|||||||
import type { HeadersInterpreter } from '../header/types';
|
import type { HeadersInterpreter } from '../header/types';
|
||||||
import type { AxiosInterceptor } from '../interceptors/build';
|
import type { AxiosInterceptor } from '../interceptors/build';
|
||||||
import type { AxiosStorage, CachedResponse } from '../storage/types';
|
import type { AxiosStorage, CachedResponse } from '../storage/types';
|
||||||
import type { CachePredicate, CacheUpdater, KeyGenerator } from '../util/types';
|
import type {
|
||||||
|
CachePredicate,
|
||||||
|
CacheUpdater,
|
||||||
|
KeyGenerator,
|
||||||
|
StaleIfErrorPredicate
|
||||||
|
} from '../util/types';
|
||||||
import type { CacheAxiosResponse, CacheRequestConfig } from './axios';
|
import type { CacheAxiosResponse, CacheRequestConfig } from './axios';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,6 +81,37 @@ export type CacheProperties<R = unknown, D = unknown> = {
|
|||||||
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
|
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
|
||||||
*/
|
*/
|
||||||
modifiedSince: Date | boolean;
|
modifiedSince: Date | boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables cache to be returned if the response comes with an error, either by invalid
|
||||||
|
* status code, network errors and etc. You can filter the type of error that should be
|
||||||
|
* stale by using a predicate function.
|
||||||
|
*
|
||||||
|
* **Note**: If this value ends up `false`, either by default or by a predicate function
|
||||||
|
* and there was an error, the request cache will be purged.
|
||||||
|
*
|
||||||
|
* **Note**: If the response is treated as error because of invalid status code *(like
|
||||||
|
* from AxiosRequestConfig#invalidateStatus)*, and this ends up `true`, the cache will
|
||||||
|
* be preserved over the "invalid" request. So, if you want to preserve the response,
|
||||||
|
* you can use this predicate:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* const customPredicate = (response, cache, error) => {
|
||||||
|
* // Return false if has a response
|
||||||
|
* return !response;
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Possible types:
|
||||||
|
*
|
||||||
|
* - `number` -> the max time (in seconds) that the cache can be reused.
|
||||||
|
* - `boolean` -> `false` disables and `true` enables with infinite time.
|
||||||
|
* - `function` -> a predicate that can return `number` or `boolean` as described above.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-if-error
|
||||||
|
*/
|
||||||
|
staleIfError: StaleIfErrorPredicate<R, D>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CacheInstance {
|
export interface CacheInstance {
|
||||||
@ -107,8 +143,8 @@ export interface CacheInstance {
|
|||||||
headerInterpreter: HeadersInterpreter;
|
headerInterpreter: HeadersInterpreter;
|
||||||
|
|
||||||
/** The request interceptor that will be used to handle the cache. */
|
/** The request interceptor that will be used to handle the cache. */
|
||||||
requestInterceptor: AxiosInterceptor<CacheRequestConfig<unknown, unknown>>;
|
requestInterceptor: AxiosInterceptor<CacheRequestConfig>;
|
||||||
|
|
||||||
/** The response interceptor that will be used to handle the cache. */
|
/** The response interceptor that will be used to handle the cache. */
|
||||||
responseInterceptor: AxiosInterceptor<CacheAxiosResponse<unknown, unknown>>;
|
responseInterceptor: AxiosInterceptor<CacheAxiosResponse>;
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/cache/create.ts
vendored
24
src/cache/create.ts
vendored
@ -67,19 +67,17 @@ export function setupCache(
|
|||||||
options.responseInterceptor || defaultResponseInterceptor(axiosCache);
|
options.responseInterceptor || defaultResponseInterceptor(axiosCache);
|
||||||
|
|
||||||
// CacheRequestConfig values
|
// CacheRequestConfig values
|
||||||
axiosCache.defaults = {
|
axiosCache.defaults.cache = {
|
||||||
...axios.defaults,
|
ttl: options.ttl ?? 1000 * 60 * 5,
|
||||||
cache: {
|
interpretHeader: options.interpretHeader ?? false,
|
||||||
ttl: options.ttl ?? 1000 * 60 * 5,
|
methods: options.methods || ['get'],
|
||||||
interpretHeader: options.interpretHeader ?? false,
|
cachePredicate: options.cachePredicate || {
|
||||||
methods: options.methods || ['get'],
|
statusCheck: (status) => status >= 200 && status < 400
|
||||||
cachePredicate: options.cachePredicate || {
|
},
|
||||||
statusCheck: (status) => status >= 200 && status < 400
|
etag: options.etag ?? false,
|
||||||
},
|
modifiedSince: options.modifiedSince ?? false,
|
||||||
etag: options.etag ?? false,
|
staleIfError: options.staleIfError ?? false,
|
||||||
modifiedSince: options.modifiedSince ?? false,
|
update: options.update || {}
|
||||||
update: options.update || {}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply interceptors
|
// Apply interceptors
|
||||||
|
|||||||
@ -3,9 +3,12 @@ import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios';
|
|||||||
/** See {@link AxiosInterceptorManager} */
|
/** See {@link AxiosInterceptorManager} */
|
||||||
export interface AxiosInterceptor<T> {
|
export interface AxiosInterceptor<T> {
|
||||||
onFulfilled?(value: T): T | Promise<T>;
|
onFulfilled?(value: T): T | Promise<T>;
|
||||||
onRejected?(error: unknown): unknown;
|
|
||||||
|
/** Returns a successful response or re-throws the error */
|
||||||
|
onRejected?(error: Record<string, unknown>): T | Promise<T>;
|
||||||
|
|
||||||
apply: () => void;
|
apply: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RequestInterceptor = AxiosInterceptor<CacheRequestConfig<unknown, unknown>>;
|
export type RequestInterceptor = AxiosInterceptor<CacheRequestConfig>;
|
||||||
export type ResponseInterceptor = AxiosInterceptor<CacheAxiosResponse<unknown, unknown>>;
|
export type ResponseInterceptor = AxiosInterceptor<CacheAxiosResponse>;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
ConfigWithCache,
|
ConfigWithCache,
|
||||||
createValidateStatus,
|
createValidateStatus,
|
||||||
isMethodIn,
|
isMethodIn,
|
||||||
setRevalidationHeaders
|
updateStaleRequest
|
||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
||||||
@ -56,11 +56,17 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
|||||||
|
|
||||||
await axios.storage.set(key, {
|
await axios.storage.set(key, {
|
||||||
state: 'loading',
|
state: 'loading',
|
||||||
data: cache.data
|
previous: cache.state,
|
||||||
|
|
||||||
|
// Eslint complains a lot :)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||||
|
data: cache.data as any,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||||
|
createdAt: cache.createdAt as any
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cache.state === 'stale') {
|
if (cache.state === 'stale') {
|
||||||
setRevalidationHeaders(cache, config as ConfigWithCache<unknown>);
|
updateStaleRequest(cache, config as ConfigWithCache<unknown>);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.validateStatus = createValidateStatus(config.validateStatus);
|
config.validateStatus = createValidateStatus(config.validateStatus);
|
||||||
@ -92,7 +98,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
|||||||
|
|
||||||
// Even though the response interceptor receives this one from here,
|
// Even though the response interceptor receives this one from here,
|
||||||
// it has been configured to ignore cached responses = true
|
// it has been configured to ignore cached responses = true
|
||||||
config.adapter = (): Promise<CacheAxiosResponse<unknown, unknown>> =>
|
config.adapter = (): Promise<CacheAxiosResponse> =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
config,
|
config,
|
||||||
data: cachedResponse.data,
|
data: cachedResponse.data,
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import type { AxiosCacheInstance } from '../cache/axios';
|
import type { CacheProperties } from '..';
|
||||||
import type { CacheProperties } from '../cache/cache';
|
import type {
|
||||||
|
AxiosCacheInstance,
|
||||||
|
CacheAxiosResponse,
|
||||||
|
CacheRequestConfig
|
||||||
|
} from '../cache/axios';
|
||||||
import type { CachedStorageValue } from '../storage/types';
|
import type { CachedStorageValue } from '../storage/types';
|
||||||
import { testCachePredicate } from '../util/cache-predicate';
|
import { testCachePredicate } from '../util/cache-predicate';
|
||||||
import { Header } from '../util/headers';
|
import { Header } from '../util/headers';
|
||||||
@ -15,15 +19,12 @@ export function defaultResponseInterceptor(
|
|||||||
*
|
*
|
||||||
* Also update the waiting list for this key by rejecting it.
|
* Also update the waiting list for this key by rejecting it.
|
||||||
*/
|
*/
|
||||||
const rejectResponse = async (
|
const rejectResponse = async (responseId: string) => {
|
||||||
{ storage, waiting }: AxiosCacheInstance,
|
|
||||||
responseId: string
|
|
||||||
) => {
|
|
||||||
// Update the cache to empty to prevent infinite loading state
|
// Update the cache to empty to prevent infinite loading state
|
||||||
await storage.remove(responseId);
|
await axios.storage.remove(responseId);
|
||||||
// Reject the deferred if present
|
// Reject the deferred if present
|
||||||
waiting[responseId]?.reject(null);
|
axios.waiting[responseId]?.reject(null);
|
||||||
delete waiting[responseId];
|
delete axios.waiting[responseId];
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFulfilled: ResponseInterceptor['onFulfilled'] = async (response) => {
|
const onFulfilled: ResponseInterceptor['onFulfilled'] = async (response) => {
|
||||||
@ -41,6 +42,7 @@ export function defaultResponseInterceptor(
|
|||||||
return { ...response, cached: false };
|
return { ...response, cached: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request interceptor merges defaults with per request configuration
|
||||||
const cacheConfig = response.config.cache as CacheProperties;
|
const cacheConfig = response.config.cache as CacheProperties;
|
||||||
|
|
||||||
const cache = await axios.storage.get(response.id);
|
const cache = await axios.storage.get(response.id);
|
||||||
@ -61,13 +63,18 @@ export function defaultResponseInterceptor(
|
|||||||
!cache.data &&
|
!cache.data &&
|
||||||
!(await testCachePredicate(response, cacheConfig.cachePredicate))
|
!(await testCachePredicate(response, cacheConfig.cachePredicate))
|
||||||
) {
|
) {
|
||||||
await rejectResponse(axios, response.id);
|
await rejectResponse(response.id);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// avoid remnant headers from remote server to break implementation
|
// avoid remnant headers from remote server to break implementation
|
||||||
delete response.headers[Header.XAxiosCacheEtag];
|
for (const header in Header) {
|
||||||
delete response.headers[Header.XAxiosCacheLastModified];
|
if (!header.startsWith('XAxiosCache')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete response.headers[header];
|
||||||
|
}
|
||||||
|
|
||||||
if (cacheConfig.etag && cacheConfig.etag !== true) {
|
if (cacheConfig.etag && cacheConfig.etag !== true) {
|
||||||
response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag;
|
response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag;
|
||||||
@ -87,7 +94,7 @@ export function defaultResponseInterceptor(
|
|||||||
|
|
||||||
// Cache should not be used
|
// Cache should not be used
|
||||||
if (expirationTime === 'dont cache') {
|
if (expirationTime === 'dont cache') {
|
||||||
await rejectResponse(axios, response.id);
|
await rejectResponse(response.id);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +107,15 @@ export function defaultResponseInterceptor(
|
|||||||
ttl = await ttl(response);
|
ttl = await ttl(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cacheConfig.staleIfError) {
|
||||||
|
response.headers[Header.XAxiosCacheStaleIfError] = String(ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update other entries before updating himself
|
||||||
|
if (cacheConfig?.update) {
|
||||||
|
await updateCache(axios.storage, response, cacheConfig.update);
|
||||||
|
}
|
||||||
|
|
||||||
const newCache: CachedStorageValue = {
|
const newCache: CachedStorageValue = {
|
||||||
state: 'cached',
|
state: 'cached',
|
||||||
ttl,
|
ttl,
|
||||||
@ -107,15 +123,8 @@ export function defaultResponseInterceptor(
|
|||||||
data
|
data
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update other entries before updating himself
|
|
||||||
if (cacheConfig?.update) {
|
|
||||||
await updateCache(axios.storage, response, cacheConfig.update);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deferred = axios.waiting[response.id];
|
|
||||||
|
|
||||||
// Resolve all other requests waiting for this response
|
// Resolve all other requests waiting for this response
|
||||||
deferred?.resolve(newCache.data);
|
axios.waiting[response.id]?.resolve(newCache.data);
|
||||||
delete axios.waiting[response.id];
|
delete axios.waiting[response.id];
|
||||||
|
|
||||||
// Define this key as cache on the storage
|
// Define this key as cache on the storage
|
||||||
@ -125,8 +134,74 @@ export function defaultResponseInterceptor(
|
|||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRejected: ResponseInterceptor['onRejected'] = async (error) => {
|
||||||
|
const config = error['config'] as CacheRequestConfig;
|
||||||
|
|
||||||
|
if (!config || config.cache === false || !config.id) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = await axios.storage.get(config.id);
|
||||||
|
const cacheConfig = config.cache;
|
||||||
|
|
||||||
|
if (
|
||||||
|
// This will only not be loading if the interceptor broke
|
||||||
|
cache.state !== 'loading' ||
|
||||||
|
cache.previous !== 'stale'
|
||||||
|
) {
|
||||||
|
await rejectResponse(config.id);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheConfig?.staleIfError) {
|
||||||
|
const staleIfError =
|
||||||
|
typeof cacheConfig.staleIfError === 'function'
|
||||||
|
? await cacheConfig.staleIfError(
|
||||||
|
error.response as CacheAxiosResponse,
|
||||||
|
cache,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
: cacheConfig.staleIfError;
|
||||||
|
|
||||||
|
if (
|
||||||
|
staleIfError === true ||
|
||||||
|
// staleIfError is the number of seconds that stale is allowed to be used
|
||||||
|
(typeof staleIfError === 'number' && cache.createdAt + staleIfError > Date.now())
|
||||||
|
) {
|
||||||
|
const newCache: CachedStorageValue = {
|
||||||
|
state: 'cached',
|
||||||
|
ttl: Number(cache.data.headers[Header.XAxiosCacheStaleIfError]),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: cache.data
|
||||||
|
};
|
||||||
|
|
||||||
|
const response: CacheAxiosResponse = {
|
||||||
|
cached: true,
|
||||||
|
config,
|
||||||
|
id: config.id,
|
||||||
|
data: cache.data?.data,
|
||||||
|
headers: cache.data?.headers,
|
||||||
|
status: cache.data.status,
|
||||||
|
statusText: cache.data.statusText
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve all other requests waiting for this response
|
||||||
|
axios.waiting[response.id]?.resolve(newCache.data);
|
||||||
|
delete axios.waiting[response.id];
|
||||||
|
|
||||||
|
// Valid response
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject this response and rethrows the error
|
||||||
|
await rejectResponse(config.id);
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onFulfilled,
|
onFulfilled,
|
||||||
apply: () => axios.interceptors.response.use(onFulfilled)
|
onRejected,
|
||||||
|
apply: () => axios.interceptors.response.use(onFulfilled, onRejected)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,11 @@ export type ConfigWithCache<D> = CacheRequestConfig<unknown, D> & {
|
|||||||
cache: Partial<CacheProperties>;
|
cache: Partial<CacheProperties>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setRevalidationHeaders<D>(
|
/**
|
||||||
|
* This function updates the cache when the request is stale. So, the next request to the
|
||||||
|
* server will be made with proper header / settings.
|
||||||
|
*/
|
||||||
|
export function updateStaleRequest<D>(
|
||||||
cache: StaleStorageValue,
|
cache: StaleStorageValue,
|
||||||
config: ConfigWithCache<D>
|
config: ConfigWithCache<D>
|
||||||
): void {
|
): void {
|
||||||
|
|||||||
@ -56,9 +56,11 @@ export function buildStorage({ set, find, remove }: BuildStorage): AxiosStorage
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
value.data.headers &&
|
value.data.headers &&
|
||||||
|
// Any header below allows the response to stale
|
||||||
(Header.ETag in value.data.headers ||
|
(Header.ETag in value.data.headers ||
|
||||||
Header.LastModified in value.data.headers ||
|
Header.LastModified in value.data.headers ||
|
||||||
Header.XAxiosCacheEtag in value.data.headers ||
|
Header.XAxiosCacheEtag in value.data.headers ||
|
||||||
|
Header.XAxiosCacheStaleIfError in value.data.headers ||
|
||||||
Header.XAxiosCacheLastModified in value.data.headers)
|
Header.XAxiosCacheLastModified in value.data.headers)
|
||||||
) {
|
) {
|
||||||
const stale: StaleStorageValue = {
|
const stale: StaleStorageValue = {
|
||||||
|
|||||||
@ -16,6 +16,12 @@ export type StorageValue =
|
|||||||
|
|
||||||
export type NotEmptyStorageValue = Exclude<StorageValue, EmptyStorageValue>;
|
export type NotEmptyStorageValue = Exclude<StorageValue, EmptyStorageValue>;
|
||||||
|
|
||||||
|
export type StorageMetadata = {
|
||||||
|
/** If the request can be stale */
|
||||||
|
shouldStale?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
export type StaleStorageValue = {
|
export type StaleStorageValue = {
|
||||||
data: CachedResponse;
|
data: CachedResponse;
|
||||||
ttl?: undefined;
|
ttl?: undefined;
|
||||||
@ -31,18 +37,21 @@ export type CachedStorageValue = {
|
|||||||
state: 'cached';
|
state: 'cached';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoadingStorageValue = {
|
export type LoadingStorageValue =
|
||||||
/**
|
| {
|
||||||
* Only present if the previous state was `stale`. So, in case the new response comes
|
data?: undefined;
|
||||||
* without a value, this data is used
|
ttl?: undefined;
|
||||||
*/
|
createdAt?: undefined;
|
||||||
data?: CachedResponse;
|
state: 'loading';
|
||||||
ttl?: number;
|
previous: 'empty';
|
||||||
|
}
|
||||||
/** Defined when the state is cached */
|
| {
|
||||||
createdAt?: undefined;
|
state: 'loading';
|
||||||
state: 'loading';
|
data: CachedResponse;
|
||||||
};
|
ttl?: undefined;
|
||||||
|
createdAt: number;
|
||||||
|
previous: 'stale';
|
||||||
|
};
|
||||||
|
|
||||||
export type EmptyStorageValue = {
|
export type EmptyStorageValue = {
|
||||||
data?: undefined;
|
data?: undefined;
|
||||||
|
|||||||
@ -28,7 +28,13 @@ export const Header = Object.freeze({
|
|||||||
*/
|
*/
|
||||||
IfNoneMatch: 'if-none-match',
|
IfNoneMatch: 'if-none-match',
|
||||||
|
|
||||||
/** @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control */
|
/**
|
||||||
|
* ```txt
|
||||||
|
* Cache-Control: max-age=604800
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||||
|
*/
|
||||||
CacheControl: 'cache-control',
|
CacheControl: 'cache-control',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,8 +76,8 @@ export const Header = Object.freeze({
|
|||||||
ContentType: 'content-type',
|
ContentType: 'content-type',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used internally to mark the cache item as being revalidatable and enabling stale
|
* Used internally as metadata to mark the cache item as revalidatable and enabling
|
||||||
* cache state Contains a string of ASCII characters that can be used as ETag for
|
* stale cache state Contains a string of ASCII characters that can be used as ETag for
|
||||||
* `If-Match` header Provided by user using `cache.etag` value.
|
* `If-Match` header Provided by user using `cache.etag` value.
|
||||||
*
|
*
|
||||||
* ```txt
|
* ```txt
|
||||||
@ -81,16 +87,27 @@ export const Header = Object.freeze({
|
|||||||
XAxiosCacheEtag: 'x-axios-cache-etag',
|
XAxiosCacheEtag: 'x-axios-cache-etag',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used internally to mark the cache item as being revalidatable and enabling stale
|
* Used internally as metadata to mark the cache item as revalidatable and enabling
|
||||||
* cache state may contain `'use-cache-timestamp'` if `cache.modifiedSince` is `true`,
|
* stale cache state may contain `'use-cache-timestamp'` if `cache.modifiedSince` is
|
||||||
* otherwise will contain a date from `cache.modifiedSince`. If a date is provided, it
|
* `true`, otherwise will contain a date from `cache.modifiedSince`. If a date is
|
||||||
* can be used for `If-Modified-Since` header, otherwise the cache timestamp can be used
|
* provided, it can be used for `If-Modified-Since` header, otherwise the cache
|
||||||
* for `If-Modified-Since` header.
|
* timestamp can be used for `If-Modified-Since` header.
|
||||||
*
|
*
|
||||||
* ```txt
|
* ```txt
|
||||||
* X-Axios-Cache-Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
|
* X-Axios-Cache-Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
|
||||||
* X-Axios-Cache-Last-Modified: use-cache-timestamp
|
* X-Axios-Cache-Last-Modified: use-cache-timestamp
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
XAxiosCacheLastModified: 'x-axios-cache-last-modified'
|
XAxiosCacheLastModified: 'x-axios-cache-last-modified',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used internally as metadata to mark the cache item able to be used if the server
|
||||||
|
* returns an error. The stale-if-error response directive indicates that the cache can
|
||||||
|
* reuse a stale response when any error occurs.
|
||||||
|
*
|
||||||
|
* ```txt
|
||||||
|
* XAxiosCacheStaleIfError: <seconds>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
XAxiosCacheStaleIfError: 'x-axios-cache-stale-if-error'
|
||||||
});
|
});
|
||||||
|
|||||||
@ -41,3 +41,20 @@ export type CacheUpdater<R, D> =
|
|||||||
cached: Exclude<StorageValue, LoadingStorageValue>,
|
cached: Exclude<StorageValue, LoadingStorageValue>,
|
||||||
response: CacheAxiosResponse<R, D>
|
response: CacheAxiosResponse<R, D>
|
||||||
) => MaybePromise<CachedStorageValue | 'delete' | 'ignore'>);
|
) => MaybePromise<CachedStorageValue | 'delete' | 'ignore'>);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can use a `number` to ensure an max time (in seconds) that the cache can be reused.
|
||||||
|
*
|
||||||
|
* You can use `true` to use the cache until a new response is received.
|
||||||
|
*
|
||||||
|
* You can use a `function` predicate to determine if the cache can be reused (`boolean`)
|
||||||
|
* or how much time the cache can be used (`number`)
|
||||||
|
*/
|
||||||
|
export type StaleIfErrorPredicate<R, D> =
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| ((
|
||||||
|
networkResponse: CacheAxiosResponse<R, D> | undefined,
|
||||||
|
cache: LoadingStorageValue & { previous: 'stale' },
|
||||||
|
error: Record<string, unknown>
|
||||||
|
) => MaybePromise<number | boolean>);
|
||||||
|
|||||||
288
test/interceptors/stale-if-error.test.ts
Normal file
288
test/interceptors/stale-if-error.test.ts
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
import Axios from 'axios';
|
||||||
|
import { setupCache } from '../../src/cache/create';
|
||||||
|
import { Header } from '../../src/util/headers';
|
||||||
|
import { mockAxios } from '../mocks/axios';
|
||||||
|
|
||||||
|
describe('Last-Modified handling', () => {
|
||||||
|
it('expects that error is thrown', async () => {
|
||||||
|
const instance = Axios.create({});
|
||||||
|
const axios = setupCache(instance, {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.get('http://unknown.url.lan:1234');
|
||||||
|
} catch (error) {
|
||||||
|
expect(Axios.isAxiosError(error)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.defaults.cache.staleIfError = 10e5;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.get('http://unknown.url.lan:1234');
|
||||||
|
} catch (error) {
|
||||||
|
expect(Axios.isAxiosError(error)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.defaults.cache.staleIfError = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.get('http://unknown.url.lan:1234');
|
||||||
|
} catch (error) {
|
||||||
|
expect(Axios.isAxiosError(error)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.assertions(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expects staleIfError does nothing without cache', async () => {
|
||||||
|
const axios = setupCache(Axios.create(), {
|
||||||
|
staleIfError: () => Promise.resolve(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.get('http://unknown.url.lan:1234');
|
||||||
|
} catch (error) {
|
||||||
|
expect(Axios.isAxiosError(error)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.assertions(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expects that XAxiosCacheStaleIfError is defined', async () => {
|
||||||
|
const axios = mockAxios();
|
||||||
|
|
||||||
|
const { headers } = await axios.get('url', {
|
||||||
|
cache: { staleIfError: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(headers).toHaveProperty(Header.XAxiosCacheStaleIfError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expects staleIfError is ignore if config.cache is false', async () => {
|
||||||
|
const axios = setupCache(Axios.create(), {
|
||||||
|
staleIfError: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const cache = {
|
||||||
|
data: true,
|
||||||
|
headers: {},
|
||||||
|
status: 200,
|
||||||
|
statusText: 'Ok'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fill the cache
|
||||||
|
const id = 'some-config-id';
|
||||||
|
await axios.storage.set(id, {
|
||||||
|
state: 'stale',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: cache
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.get('http://unknown-url.lan:9090', {
|
||||||
|
id,
|
||||||
|
cache: false
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
expect(Axios.isAxiosError(error)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.assertions(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tests staleIfError', async () => {
|
||||||
|
const axios = setupCache(Axios.create(), {
|
||||||
|
staleIfError: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const cache = {
|
||||||
|
data: true,
|
||||||
|
headers: {},
|
||||||
|
status: 200,
|
||||||
|
statusText: 'Ok'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fill the cache
|
||||||
|
const id = 'some-config-id';
|
||||||
|
await axios.storage.set(id, {
|
||||||
|
state: 'stale',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: cache
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await axios.get('http://unknown-url.lan:9090', {
|
||||||
|
id,
|
||||||
|
cache: { staleIfError: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toBeDefined();
|
||||||
|
expect(response.id).toBe(id);
|
||||||
|
expect(response.data).toBe(cache.data);
|
||||||
|
expect(response.status).toBe(cache.status);
|
||||||
|
expect(response.statusText).toBe(cache.statusText);
|
||||||
|
expect(response.headers).toBe(cache.headers);
|
||||||
|
expect(response.cached).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expects that staleIfError needs to be true', async () => {
|
||||||
|
const axios = setupCache(Axios.create(), {
|
||||||
|
staleIfError: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const cache = {
|
||||||
|
data: true,
|
||||||
|
headers: {},
|
||||||
|
status: 200,
|
||||||
|
statusText: 'Ok'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fill the cache
|
||||||
|
const id = 'some-config-id';
|
||||||
|
await axios.storage.set(id, {
|
||||||
|
state: 'stale',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: cache
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.get('http://unknown-url.lan:9090', {
|
||||||
|
id,
|
||||||
|
cache: { staleIfError: false }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
expect(Axios.isAxiosError(error)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.assertions(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tests staleIfError returning false', async () => {
|
||||||
|
const axios = setupCache(Axios.create(), {
|
||||||
|
staleIfError: () => false
|
||||||
|
});
|
||||||
|
|
||||||
|
const id = 'some-config-id';
|
||||||
|
const cache = {
|
||||||
|
data: true,
|
||||||
|
headers: {},
|
||||||
|
status: 200,
|
||||||
|
statusText: 'Ok'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fill the cache
|
||||||
|
await axios.storage.set(id, {
|
||||||
|
state: 'stale',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: cache
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.get('http://unknown-url.lan:9090', {
|
||||||
|
id
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
expect(Axios.isAxiosError(error)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.assertions(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tests staleIfError as function', async () => {
|
||||||
|
const axios = setupCache(Axios.create(), {
|
||||||
|
staleIfError: () => {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const id = 'some-config-id';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.get('http://unknown-url.lan:9090', { id });
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (error) {
|
||||||
|
expect(Axios.isAxiosError(error)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.get('http://unknown-url.lan:9090', {
|
||||||
|
id,
|
||||||
|
cache: {
|
||||||
|
staleIfError: () => 1 // past
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (error) {
|
||||||
|
expect(Axios.isAxiosError(error)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = {
|
||||||
|
data: true,
|
||||||
|
headers: {},
|
||||||
|
status: 200,
|
||||||
|
statusText: 'Ok'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fill the cache
|
||||||
|
await axios.storage.set(id, {
|
||||||
|
state: 'stale',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: cache
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await axios.get('http://unknown-url.lan:9090', {
|
||||||
|
id,
|
||||||
|
cache: {
|
||||||
|
staleIfError: () => 10e5 // nearly infinity :)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toBeDefined();
|
||||||
|
expect(response.id).toBe(id);
|
||||||
|
expect(response.data).toBe(cache.data);
|
||||||
|
expect(response.status).toBe(cache.status);
|
||||||
|
expect(response.statusText).toBe(cache.statusText);
|
||||||
|
expect(response.headers).toBe(cache.headers);
|
||||||
|
expect(response.cached).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tests staleIfError with real 50X status code', async () => {
|
||||||
|
const axios = setupCache(Axios.create(), { staleIfError: true });
|
||||||
|
|
||||||
|
const id = 'some-config-id';
|
||||||
|
|
||||||
|
const cache = {
|
||||||
|
data: true,
|
||||||
|
headers: {},
|
||||||
|
status: 200,
|
||||||
|
statusText: 'Ok'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fill the cache
|
||||||
|
await axios.storage.set(id, {
|
||||||
|
state: 'stale',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: cache
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await axios.get('https://httpbin.org/status/503', {
|
||||||
|
id
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toBeDefined();
|
||||||
|
expect(response.id).toBe(id);
|
||||||
|
expect(response.data).toBe(cache.data);
|
||||||
|
expect(response.status).toBe(cache.status);
|
||||||
|
expect(response.statusText).toBe(cache.statusText);
|
||||||
|
expect(response.headers).toBe(cache.headers);
|
||||||
|
expect(response.cached).toBe(true);
|
||||||
|
|
||||||
|
const newResponse = await axios.get('https://httpbin.org/status/503', {
|
||||||
|
id,
|
||||||
|
validateStatus: () => true // prevents error
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newResponse).toBeDefined();
|
||||||
|
expect(newResponse.id).toBe(id);
|
||||||
|
expect(newResponse.data).not.toBe(cache.data);
|
||||||
|
expect(newResponse.status).toBe(503);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user