mirror of
https://github.com/arthurfiorette/axios-cache-interceptor.git
synced 2025-12-08 17:36:16 +00:00
refactor: removed "abstract" buildInterceptor function
This commit is contained in:
parent
d30b862605
commit
f30e2622dd
@ -1,35 +1,10 @@
|
|||||||
import type {
|
import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios';
|
||||||
AxiosCacheInstance,
|
|
||||||
CacheAxiosResponse,
|
|
||||||
CacheRequestConfig
|
|
||||||
} from '../cache/axios';
|
|
||||||
|
|
||||||
export interface AxiosInterceptor<T> {
|
export interface AxiosInterceptor<T> {
|
||||||
onFulfilled?(value: T): T | Promise<T>;
|
onFulfilled?(value: T): T | Promise<T>;
|
||||||
onRejected?(error: any): any;
|
onRejected?(error: any): any;
|
||||||
apply: (axios: AxiosCacheInstance) => void;
|
apply: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RequestInterceptor = AxiosInterceptor<CacheRequestConfig<unknown, unknown>>;
|
export type RequestInterceptor = AxiosInterceptor<CacheRequestConfig<unknown, unknown>>;
|
||||||
export type ResponseInterceptor = AxiosInterceptor<CacheAxiosResponse<unknown, unknown>>;
|
export type ResponseInterceptor = AxiosInterceptor<CacheAxiosResponse<unknown, unknown>>;
|
||||||
|
|
||||||
export function buildInterceptor(
|
|
||||||
type: 'request',
|
|
||||||
interceptor: Omit<RequestInterceptor, 'apply'>
|
|
||||||
): RequestInterceptor;
|
|
||||||
|
|
||||||
export function buildInterceptor(
|
|
||||||
type: 'response',
|
|
||||||
interceptor: Omit<ResponseInterceptor, 'apply'>
|
|
||||||
): ResponseInterceptor;
|
|
||||||
|
|
||||||
export function buildInterceptor(
|
|
||||||
type: 'request' | 'response',
|
|
||||||
{ onFulfilled, onRejected }: Omit<AxiosInterceptor<unknown>, 'apply'>
|
|
||||||
): AxiosInterceptor<unknown> {
|
|
||||||
return {
|
|
||||||
onFulfilled,
|
|
||||||
onRejected,
|
|
||||||
apply: (axios) => axios.interceptors[type].use(onFulfilled, onRejected)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { deferred } from 'fast-defer';
|
import { deferred } from 'fast-defer';
|
||||||
import { buildInterceptor } from '..';
|
|
||||||
import type { AxiosCacheInstance, CacheAxiosResponse } from '../cache/axios';
|
import type { AxiosCacheInstance, CacheAxiosResponse } from '../cache/axios';
|
||||||
import type {
|
import type {
|
||||||
CachedResponse,
|
CachedResponse,
|
||||||
CachedStorageValue,
|
CachedStorageValue,
|
||||||
LoadingStorageValue
|
LoadingStorageValue
|
||||||
} from '../storage/types';
|
} from '../storage/types';
|
||||||
|
import type { RequestInterceptor } from './build';
|
||||||
import {
|
import {
|
||||||
ConfigWithCache,
|
ConfigWithCache,
|
||||||
createValidateStatus,
|
createValidateStatus,
|
||||||
@ -14,99 +14,102 @@ import {
|
|||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
||||||
return buildInterceptor('request', {
|
const onFulfilled: RequestInterceptor['onFulfilled'] = async (config) => {
|
||||||
onFulfilled: async (config) => {
|
if (config.cache === false) {
|
||||||
if (config.cache === false) {
|
return config;
|
||||||
return config;
|
}
|
||||||
|
|
||||||
|
// merge defaults with per request configuration
|
||||||
|
config.cache = { ...axios.defaults.cache, ...config.cache };
|
||||||
|
|
||||||
|
if (!isMethodIn(config.method, config.cache.methods)) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = axios.generateKey(config);
|
||||||
|
|
||||||
|
// Assumes that the storage handled staled responses
|
||||||
|
let cache = await axios.storage.get(key);
|
||||||
|
|
||||||
|
// Not cached, continue the request, and mark it as fetching
|
||||||
|
emptyOrStale: if (cache.state == 'empty' || cache.state === 'stale') {
|
||||||
|
/**
|
||||||
|
* This checks for simultaneous access to a new key. The js event loop jumps on the
|
||||||
|
* first await statement, so the second (asynchronous call) request may have already
|
||||||
|
* started executing.
|
||||||
|
*/
|
||||||
|
if (axios.waiting[key]) {
|
||||||
|
cache = (await axios.storage.get(key)) as
|
||||||
|
| CachedStorageValue
|
||||||
|
| LoadingStorageValue;
|
||||||
|
break emptyOrStale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge defaults with per request configuration
|
// Create a deferred to resolve other requests for the same key when it's completed
|
||||||
config.cache = { ...axios.defaults.cache, ...config.cache };
|
axios.waiting[key] = deferred();
|
||||||
|
|
||||||
if (!isMethodIn(config.method, config.cache.methods)) {
|
/**
|
||||||
return config;
|
* Add a default reject handler to catch when the request is aborted without others
|
||||||
|
* waiting for it.
|
||||||
|
*/
|
||||||
|
axios.waiting[key]?.catch(() => undefined);
|
||||||
|
|
||||||
|
await axios.storage.set(key, {
|
||||||
|
state: 'loading',
|
||||||
|
data: cache.data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cache.state === 'stale') {
|
||||||
|
setRevalidationHeaders(cache, config as ConfigWithCache<unknown>);
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = axios.generateKey(config);
|
config.validateStatus = createValidateStatus(config.validateStatus);
|
||||||
|
|
||||||
// Assumes that the storage handled staled responses
|
|
||||||
let cache = await axios.storage.get(key);
|
|
||||||
|
|
||||||
// Not cached, continue the request, and mark it as fetching
|
|
||||||
emptyOrStale: if (cache.state == 'empty' || cache.state === 'stale') {
|
|
||||||
/**
|
|
||||||
* This checks for simultaneous access to a new key. The js event loop jumps on
|
|
||||||
* the first await statement, so the second (asynchronous call) request may have
|
|
||||||
* already started executing.
|
|
||||||
*/
|
|
||||||
if (axios.waiting[key]) {
|
|
||||||
cache = (await axios.storage.get(key)) as
|
|
||||||
| CachedStorageValue
|
|
||||||
| LoadingStorageValue;
|
|
||||||
break emptyOrStale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a deferred to resolve other requests for the same key when it's completed
|
|
||||||
axios.waiting[key] = deferred();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a default reject handler to catch when the request is aborted without
|
|
||||||
* others waiting for it.
|
|
||||||
*/
|
|
||||||
axios.waiting[key]?.catch(() => undefined);
|
|
||||||
|
|
||||||
await axios.storage.set(key, {
|
|
||||||
state: 'loading',
|
|
||||||
data: cache.data
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cache.state === 'stale') {
|
|
||||||
setRevalidationHeaders(cache, config as ConfigWithCache<unknown>);
|
|
||||||
}
|
|
||||||
|
|
||||||
config.validateStatus = createValidateStatus(config.validateStatus);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cachedResponse: CachedResponse;
|
|
||||||
|
|
||||||
if (cache.state === 'loading') {
|
|
||||||
const deferred = axios.waiting[key];
|
|
||||||
|
|
||||||
// Just in case, the deferred doesn't exists.
|
|
||||||
/* istanbul ignore if 'really hard to test' */
|
|
||||||
if (!deferred) {
|
|
||||||
await axios.storage.remove(key);
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
cachedResponse = await deferred;
|
|
||||||
} catch {
|
|
||||||
// The deferred is rejected when the request that we are waiting rejected cache.
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cachedResponse = cache.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.adapter = () =>
|
|
||||||
/**
|
|
||||||
* Even though the response interceptor receives this one from here, it has been
|
|
||||||
* configured to ignore cached responses: true
|
|
||||||
*/
|
|
||||||
Promise.resolve<CacheAxiosResponse<unknown, unknown>>({
|
|
||||||
config,
|
|
||||||
data: cachedResponse.data,
|
|
||||||
headers: cachedResponse.headers,
|
|
||||||
status: cachedResponse.status,
|
|
||||||
statusText: cachedResponse.statusText,
|
|
||||||
cached: true,
|
|
||||||
id: key
|
|
||||||
});
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
let cachedResponse: CachedResponse;
|
||||||
|
|
||||||
|
if (cache.state === 'loading') {
|
||||||
|
const deferred = axios.waiting[key];
|
||||||
|
|
||||||
|
// Just in case, the deferred doesn't exists.
|
||||||
|
/* istanbul ignore if 'really hard to test' */
|
||||||
|
if (!deferred) {
|
||||||
|
await axios.storage.remove(key);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cachedResponse = await deferred;
|
||||||
|
} catch {
|
||||||
|
// The deferred is rejected when the request that we are waiting rejected cache.
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cachedResponse = cache.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.adapter = () =>
|
||||||
|
/**
|
||||||
|
* Even though the response interceptor receives this one from here, it has been
|
||||||
|
* configured to ignore cached responses: true
|
||||||
|
*/
|
||||||
|
Promise.resolve<CacheAxiosResponse<unknown, unknown>>({
|
||||||
|
config,
|
||||||
|
data: cachedResponse.data,
|
||||||
|
headers: cachedResponse.headers,
|
||||||
|
status: cachedResponse.status,
|
||||||
|
statusText: cachedResponse.statusText,
|
||||||
|
cached: true,
|
||||||
|
id: key
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onFulfilled,
|
||||||
|
apply: () => axios.interceptors.request.use(onFulfilled)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,111 +1,132 @@
|
|||||||
import { buildInterceptor } from '..';
|
|
||||||
import type { AxiosCacheInstance } from '../cache/axios';
|
import type { AxiosCacheInstance } from '../cache/axios';
|
||||||
import type { CacheProperties } from '../cache/cache';
|
import type { CacheProperties } from '../cache/cache';
|
||||||
import type { CachedStorageValue } from '../storage/types';
|
import type { CachedStorageValue } from '../storage/types';
|
||||||
import { shouldCacheResponse } from '../util/cache-predicate';
|
import { shouldCacheResponse } from '../util/cache-predicate';
|
||||||
import { Header } from '../util/headers';
|
import { Header } from '../util/headers';
|
||||||
import { updateCache } from '../util/update-cache';
|
import { updateCache } from '../util/update-cache';
|
||||||
import { rejectResponse, setupCacheData } from './util';
|
import type { ResponseInterceptor } from './build';
|
||||||
|
import { setupCacheData } from './util';
|
||||||
|
|
||||||
export function defaultResponseInterceptor(axios: AxiosCacheInstance) {
|
export function defaultResponseInterceptor(
|
||||||
return buildInterceptor('response', {
|
axios: AxiosCacheInstance
|
||||||
onFulfilled: async (response) => {
|
): ResponseInterceptor {
|
||||||
response.id ??= axios.generateKey(response.config);
|
/**
|
||||||
response.cached ??= false;
|
* Rejects cache for an response response.
|
||||||
|
*
|
||||||
|
* Also update the waiting list for this key by rejecting it.
|
||||||
|
*/
|
||||||
|
const rejectResponse = async (
|
||||||
|
{ storage, waiting }: AxiosCacheInstance,
|
||||||
|
responseId: string
|
||||||
|
) => {
|
||||||
|
// Update the cache to empty to prevent infinite loading state
|
||||||
|
await storage.remove(responseId);
|
||||||
|
// Reject the deferred if present
|
||||||
|
waiting[responseId]?.reject(null);
|
||||||
|
delete waiting[responseId];
|
||||||
|
};
|
||||||
|
|
||||||
// Response is already cached
|
const onFulfilled: ResponseInterceptor['onFulfilled'] = async (response) => {
|
||||||
if (response.cached) {
|
response.id ??= axios.generateKey(response.config);
|
||||||
return response;
|
response.cached ??= false;
|
||||||
}
|
|
||||||
|
|
||||||
// Skip cache: either false or weird behavior
|
// Response is already cached
|
||||||
// config.cache should always exists, at least from global config merge.
|
if (response.cached) {
|
||||||
if (!response.config.cache) {
|
return response;
|
||||||
return { ...response, cached: false };
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const cacheConfig = response.config.cache as CacheProperties;
|
// Skip cache: either false or weird behavior
|
||||||
|
// config.cache should always exists, at least from global config merge.
|
||||||
|
if (!response.config.cache) {
|
||||||
|
return { ...response, cached: false };
|
||||||
|
}
|
||||||
|
|
||||||
const cache = await axios.storage.get(response.id);
|
const cacheConfig = response.config.cache as CacheProperties;
|
||||||
|
|
||||||
if (
|
const cache = await axios.storage.get(response.id);
|
||||||
// If the request interceptor had a problem
|
|
||||||
cache.state === 'stale' ||
|
|
||||||
cache.state === 'empty' ||
|
|
||||||
// Should not hit here because of previous response.cached check
|
|
||||||
cache.state === 'cached'
|
|
||||||
) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config told that this response should be cached.
|
if (
|
||||||
if (
|
// If the request interceptor had a problem
|
||||||
// For 'loading' values (post stale), this check was already run in the past.
|
cache.state === 'stale' ||
|
||||||
!cache.data &&
|
cache.state === 'empty' ||
|
||||||
!shouldCacheResponse(response, cacheConfig)
|
// Should not hit here because of previous response.cached check
|
||||||
) {
|
cache.state === 'cached'
|
||||||
|
) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config told that this response should be cached.
|
||||||
|
if (
|
||||||
|
// For 'loading' values (post stale), this check was already run in the past.
|
||||||
|
!cache.data &&
|
||||||
|
!shouldCacheResponse(response, cacheConfig)
|
||||||
|
) {
|
||||||
|
await rejectResponse(axios, response.id);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid remnant headers from remote server to break implementation
|
||||||
|
delete response.headers[Header.XAxiosCacheEtag];
|
||||||
|
delete response.headers[Header.XAxiosCacheLastModified];
|
||||||
|
|
||||||
|
if (cacheConfig.etag && cacheConfig.etag !== true) {
|
||||||
|
response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheConfig.modifiedSince) {
|
||||||
|
response.headers[Header.XAxiosCacheLastModified] =
|
||||||
|
cacheConfig.modifiedSince === true
|
||||||
|
? 'use-cache-timestamp'
|
||||||
|
: cacheConfig.modifiedSince.toUTCString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let ttl = cacheConfig.ttl || -1; // always set from global config
|
||||||
|
|
||||||
|
if (cacheConfig?.interpretHeader) {
|
||||||
|
const expirationTime = axios.headerInterpreter(response.headers);
|
||||||
|
|
||||||
|
// Cache should not be used
|
||||||
|
if (expirationTime === 'dont cache') {
|
||||||
await rejectResponse(axios, response.id);
|
await rejectResponse(axios, response.id);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// avoid remnant headers from remote server to break implementation
|
ttl = expirationTime === 'not enough headers' ? ttl : expirationTime;
|
||||||
delete response.headers[Header.XAxiosCacheEtag];
|
|
||||||
delete response.headers[Header.XAxiosCacheLastModified];
|
|
||||||
|
|
||||||
if (cacheConfig.etag && cacheConfig.etag !== true) {
|
|
||||||
response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheConfig.modifiedSince) {
|
|
||||||
response.headers[Header.XAxiosCacheLastModified] =
|
|
||||||
cacheConfig.modifiedSince === true
|
|
||||||
? 'use-cache-timestamp'
|
|
||||||
: cacheConfig.modifiedSince.toUTCString();
|
|
||||||
}
|
|
||||||
|
|
||||||
let ttl = cacheConfig.ttl || -1; // always set from global config
|
|
||||||
|
|
||||||
if (cacheConfig?.interpretHeader) {
|
|
||||||
const expirationTime = axios.headerInterpreter(response.headers);
|
|
||||||
|
|
||||||
// Cache should not be used
|
|
||||||
if (expirationTime === 'dont cache') {
|
|
||||||
await rejectResponse(axios, response.id);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
ttl = expirationTime === 'not enough headers' ? ttl : expirationTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = setupCacheData(response, cache.data);
|
|
||||||
|
|
||||||
if (typeof ttl === 'function') {
|
|
||||||
ttl = await ttl(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCache: CachedStorageValue = {
|
|
||||||
state: 'cached',
|
|
||||||
ttl,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
data
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update other entries before updating himself
|
|
||||||
if (cacheConfig?.update) {
|
|
||||||
updateCache(axios.storage, response, cacheConfig.update);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deferred = axios.waiting[response.id];
|
|
||||||
|
|
||||||
// Resolve all other requests waiting for this response
|
|
||||||
await deferred?.resolve(newCache.data);
|
|
||||||
delete axios.waiting[response.id];
|
|
||||||
|
|
||||||
// Define this key as cache on the storage
|
|
||||||
await axios.storage.set(response.id, newCache);
|
|
||||||
|
|
||||||
// Return the response with cached as false, because it was not cached at all
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const data = setupCacheData(response, cache.data);
|
||||||
|
|
||||||
|
if (typeof ttl === 'function') {
|
||||||
|
ttl = await ttl(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCache: CachedStorageValue = {
|
||||||
|
state: 'cached',
|
||||||
|
ttl,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update other entries before updating himself
|
||||||
|
if (cacheConfig?.update) {
|
||||||
|
updateCache(axios.storage, response, cacheConfig.update);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deferred = axios.waiting[response.id];
|
||||||
|
|
||||||
|
// Resolve all other requests waiting for this response
|
||||||
|
await deferred?.resolve(newCache.data);
|
||||||
|
delete axios.waiting[response.id];
|
||||||
|
|
||||||
|
// Define this key as cache on the storage
|
||||||
|
await axios.storage.set(response.id, newCache);
|
||||||
|
|
||||||
|
// Return the response with cached as false, because it was not cached at all
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onFulfilled,
|
||||||
|
apply: () => axios.interceptors.response.use(onFulfilled)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
import type { Method } from 'axios';
|
import type { Method } from 'axios';
|
||||||
import type {
|
import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios';
|
||||||
AxiosCacheInstance,
|
|
||||||
CacheAxiosResponse,
|
|
||||||
CacheRequestConfig
|
|
||||||
} from '../cache/axios';
|
|
||||||
import type { CacheProperties } from '../cache/cache';
|
import type { CacheProperties } from '../cache/cache';
|
||||||
import type { CachedResponse, StaleStorageValue } from '../storage/types';
|
import type { CachedResponse, StaleStorageValue } from '../storage/types';
|
||||||
import { Header } from '../util/headers';
|
import { Header } from '../util/headers';
|
||||||
@ -21,11 +17,10 @@ export function createValidateStatus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Checks if the given method is in the methods array */
|
/** Checks if the given method is in the methods array */
|
||||||
export function isMethodIn(requestMethod?: Method, methodList?: Method[]): boolean {
|
export function isMethodIn(
|
||||||
if (!requestMethod || !methodList) {
|
requestMethod: Method = 'get',
|
||||||
return false;
|
methodList: Method[] = []
|
||||||
}
|
): boolean {
|
||||||
|
|
||||||
requestMethod = requestMethod.toLowerCase() as Lowercase<Method>;
|
requestMethod = requestMethod.toLowerCase() as Lowercase<Method>;
|
||||||
|
|
||||||
for (const method of methodList) {
|
for (const method of methodList) {
|
||||||
@ -99,19 +94,3 @@ export function setupCacheData<R, D>(
|
|||||||
headers: response.headers
|
headers: response.headers
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Rejects cache for an response response.
|
|
||||||
*
|
|
||||||
* Also update the waiting list for this key by rejecting it.
|
|
||||||
*/
|
|
||||||
export async function rejectResponse(
|
|
||||||
{ storage, waiting }: AxiosCacheInstance,
|
|
||||||
responseId: string
|
|
||||||
) {
|
|
||||||
// Update the cache to empty to prevent infinite loading state
|
|
||||||
await storage.remove(responseId);
|
|
||||||
// Reject the deferred if present
|
|
||||||
waiting[responseId]?.reject(null);
|
|
||||||
delete waiting[responseId];
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user