mirror of
https://github.com/arthurfiorette/axios-cache-interceptor.git
synced 2025-12-08 17:36:16 +00:00
refactor: interceptors in classes
This commit is contained in:
parent
be6e7d3e2d
commit
f1033a5959
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,4 +3,5 @@
|
|||||||
/ignore
|
/ignore
|
||||||
/.vscode/settings.json
|
/.vscode/settings.json
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
/coverage
|
/coverage
|
||||||
|
*.log
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import Axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
import Axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
import { defaultHeaderInterpreter } from '../header';
|
import { defaultHeaderInterpreter } from '../header';
|
||||||
import { applyRequestInterceptor } from '../interceptors/request';
|
import { CacheRequestInterceptor } from '../interceptors/request';
|
||||||
import { applyResponseInterceptor } from '../interceptors/response';
|
import { CacheResponseInterceptor } from '../interceptors/response';
|
||||||
import { MemoryStorage } from '../storage/memory';
|
import { MemoryStorage } from '../storage/memory';
|
||||||
import { defaultKeyGenerator } from '../util/key-generator';
|
import { defaultKeyGenerator } from '../util/key-generator';
|
||||||
import CacheInstance, { AxiosCacheInstance, CacheProperties } from './types';
|
import CacheInstance, { AxiosCacheInstance, CacheProperties } from './types';
|
||||||
@ -15,14 +15,24 @@ import CacheInstance, { AxiosCacheInstance, CacheProperties } from './types';
|
|||||||
*/
|
*/
|
||||||
export function applyCache(
|
export function applyCache(
|
||||||
axios: AxiosInstance,
|
axios: AxiosInstance,
|
||||||
config: Partial<CacheInstance> & Partial<CacheProperties> = {}
|
{
|
||||||
|
storage,
|
||||||
|
generateKey,
|
||||||
|
waiting,
|
||||||
|
headerInterpreter,
|
||||||
|
requestInterceptor,
|
||||||
|
responseInterceptor,
|
||||||
|
...cacheOptions
|
||||||
|
}: Partial<CacheInstance> & Partial<CacheProperties> = {}
|
||||||
): AxiosCacheInstance {
|
): AxiosCacheInstance {
|
||||||
const axiosCache = axios as AxiosCacheInstance;
|
const axiosCache = axios as AxiosCacheInstance;
|
||||||
|
|
||||||
axiosCache.storage = config.storage || new MemoryStorage();
|
axiosCache.storage = storage || new MemoryStorage();
|
||||||
axiosCache.generateKey = config.generateKey || defaultKeyGenerator;
|
axiosCache.generateKey = generateKey || defaultKeyGenerator;
|
||||||
axiosCache.waiting = config.waiting || {};
|
axiosCache.waiting = waiting || {};
|
||||||
axiosCache.headerInterpreter = config.headerInterpreter || defaultHeaderInterpreter;
|
axiosCache.headerInterpreter = headerInterpreter || defaultHeaderInterpreter;
|
||||||
|
axiosCache.requestInterceptor = requestInterceptor || new CacheRequestInterceptor(axiosCache);
|
||||||
|
axiosCache.responseInterceptor = responseInterceptor || new CacheResponseInterceptor(axiosCache);
|
||||||
|
|
||||||
// CacheRequestConfig values
|
// CacheRequestConfig values
|
||||||
axiosCache.defaults = {
|
axiosCache.defaults = {
|
||||||
@ -31,15 +41,17 @@ export function applyCache(
|
|||||||
ttl: 1000 * 60 * 5,
|
ttl: 1000 * 60 * 5,
|
||||||
interpretHeader: false,
|
interpretHeader: false,
|
||||||
methods: ['get'],
|
methods: ['get'],
|
||||||
cachePredicate: { statusCheck: [200, 399] },
|
cachePredicate: {
|
||||||
|
statusCheck: [200, 399]
|
||||||
|
},
|
||||||
update: {},
|
update: {},
|
||||||
...config
|
...cacheOptions
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply interceptors
|
// Apply interceptors
|
||||||
applyRequestInterceptor(axiosCache);
|
axiosCache.requestInterceptor.apply();
|
||||||
applyResponseInterceptor(axiosCache);
|
axiosCache.responseInterceptor.apply();
|
||||||
|
|
||||||
return axiosCache;
|
return axiosCache;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
Method
|
Method
|
||||||
} from 'axios';
|
} from 'axios';
|
||||||
import { HeaderInterpreter } from '../header';
|
import { HeaderInterpreter } from '../header';
|
||||||
|
import { AxiosInterceptor } from '../interceptors/types';
|
||||||
import {
|
import {
|
||||||
CachedResponse,
|
CachedResponse,
|
||||||
CachedStorageValue,
|
CachedStorageValue,
|
||||||
@ -72,6 +73,10 @@ export type CacheProperties = {
|
|||||||
update: Record<string, CacheUpdater | undefined>;
|
update: Record<string, CacheUpdater | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CacheAxiosResponse = AxiosResponse & {
|
||||||
|
config: CacheRequestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options that can be overridden per request
|
* Options that can be overridden per request
|
||||||
*/
|
*/
|
||||||
@ -117,6 +122,16 @@ export default interface CacheInstance {
|
|||||||
* Only used if cache.interpretHeader is true.
|
* Only used if cache.interpretHeader is true.
|
||||||
*/
|
*/
|
||||||
headerInterpreter: HeaderInterpreter;
|
headerInterpreter: HeaderInterpreter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request interceptor that will be used to handle the cache.
|
||||||
|
*/
|
||||||
|
requestInterceptor: AxiosInterceptor<CacheRequestConfig>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response interceptor that will be used to handle the cache.
|
||||||
|
*/
|
||||||
|
responseInterceptor: AxiosInterceptor<CacheAxiosResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,7 +149,7 @@ export interface AxiosCacheInstance extends AxiosInstance, CacheInstance {
|
|||||||
|
|
||||||
interceptors: {
|
interceptors: {
|
||||||
request: AxiosInterceptorManager<CacheRequestConfig>;
|
request: AxiosInterceptorManager<CacheRequestConfig>;
|
||||||
response: AxiosInterceptorManager<AxiosResponse & { config: CacheRequestConfig }>;
|
response: AxiosInterceptorManager<CacheAxiosResponse>;
|
||||||
};
|
};
|
||||||
|
|
||||||
getUri(config?: CacheRequestConfig): string;
|
getUri(config?: CacheRequestConfig): string;
|
||||||
|
|||||||
@ -1,33 +1,40 @@
|
|||||||
import { AxiosCacheInstance } from '../axios/types';
|
import { AxiosCacheInstance, CacheRequestConfig } from '../axios/types';
|
||||||
import { CachedResponse } from '../storage/types';
|
import { CachedResponse } from '../storage/types';
|
||||||
import { Deferred } from '../util/deferred';
|
import { Deferred } from '../util/deferred';
|
||||||
import { CACHED_RESPONSE_STATUS, CACHED_RESPONSE_STATUS_TEXT } from '../util/status-codes';
|
import { CACHED_RESPONSE_STATUS, CACHED_RESPONSE_STATUS_TEXT } from '../util/status-codes';
|
||||||
|
import { AxiosInterceptor } from './types';
|
||||||
|
|
||||||
export function applyRequestInterceptor(axios: AxiosCacheInstance): void {
|
export class CacheRequestInterceptor implements AxiosInterceptor<CacheRequestConfig> {
|
||||||
axios.interceptors.request.use(async (config) => {
|
constructor(readonly axios: AxiosCacheInstance) {}
|
||||||
|
|
||||||
|
apply = (): void => {
|
||||||
|
this.axios.interceptors.request.use(this.onFulfilled);
|
||||||
|
};
|
||||||
|
|
||||||
|
onFulfilled = async (config: CacheRequestConfig): Promise<CacheRequestConfig> => {
|
||||||
// Ignore caching
|
// Ignore caching
|
||||||
if (config.cache === false) {
|
if (config.cache === false) {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only cache specified methods
|
// Only cache specified methods
|
||||||
const allowedMethods = config.cache?.methods || axios.defaults.cache.methods;
|
const allowedMethods = config.cache?.methods || this.axios.defaults.cache.methods;
|
||||||
|
|
||||||
if (!allowedMethods.some((method) => (config.method || 'get').toLowerCase() == method)) {
|
if (!allowedMethods.some((method) => (config.method || 'get').toLowerCase() == method)) {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = axios.generateKey(config);
|
const key = this.axios.generateKey(config);
|
||||||
|
|
||||||
// Assumes that the storage handled staled responses
|
// Assumes that the storage handled staled responses
|
||||||
const cache = await axios.storage.get(key);
|
const cache = await this.axios.storage.get(key);
|
||||||
|
|
||||||
// Not cached, continue the request, and mark it as fetching
|
// Not cached, continue the request, and mark it as fetching
|
||||||
if (cache.state == 'empty') {
|
if (cache.state == 'empty') {
|
||||||
// Create a deferred to resolve other requests for the same key when it's completed
|
// Create a deferred to resolve other requests for the same key when it's completed
|
||||||
axios.waiting[key] = new Deferred();
|
this.axios.waiting[key] = new Deferred();
|
||||||
|
|
||||||
await axios.storage.set(key, {
|
await this.axios.storage.set(key, {
|
||||||
state: 'loading',
|
state: 'loading',
|
||||||
ttl: config.cache?.ttl
|
ttl: config.cache?.ttl
|
||||||
});
|
});
|
||||||
@ -38,12 +45,12 @@ export function applyRequestInterceptor(axios: AxiosCacheInstance): void {
|
|||||||
let data: CachedResponse = {};
|
let data: CachedResponse = {};
|
||||||
|
|
||||||
if (cache.state === 'loading') {
|
if (cache.state === 'loading') {
|
||||||
const deferred = axios.waiting[key];
|
const deferred = this.axios.waiting[key];
|
||||||
|
|
||||||
// If the deferred is undefined, means that the
|
// If the deferred is undefined, means that the
|
||||||
// outside has removed that key from the waiting list
|
// outside has removed that key from the waiting list
|
||||||
if (!deferred) {
|
if (!deferred) {
|
||||||
await axios.storage.remove(key);
|
await this.axios.storage.remove(key);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,5 +69,5 @@ export function applyRequestInterceptor(axios: AxiosCacheInstance): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,26 @@
|
|||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { AxiosCacheInstance, CacheProperties, CacheRequestConfig } from '../axios/types';
|
import {
|
||||||
|
AxiosCacheInstance,
|
||||||
|
CacheAxiosResponse,
|
||||||
|
CacheProperties,
|
||||||
|
CacheRequestConfig
|
||||||
|
} from '../axios/types';
|
||||||
import { CachedStorageValue } from '../storage/types';
|
import { CachedStorageValue } from '../storage/types';
|
||||||
import { checkPredicateObject } from '../util/cache-predicate';
|
import { checkPredicateObject } from '../util/cache-predicate';
|
||||||
import { updateCache } from '../util/update-cache';
|
import { updateCache } from '../util/update-cache';
|
||||||
|
import { AxiosInterceptor } from './types';
|
||||||
|
|
||||||
type CacheConfig = CacheRequestConfig & { cache?: Partial<CacheProperties> };
|
type CacheConfig = CacheRequestConfig & { cache?: Partial<CacheProperties> };
|
||||||
|
|
||||||
export function applyResponseInterceptor(axios: AxiosCacheInstance): void {
|
export class CacheResponseInterceptor implements AxiosInterceptor<CacheAxiosResponse> {
|
||||||
const testCachePredicate = (response: AxiosResponse, config: CacheConfig): boolean => {
|
constructor(readonly axios: AxiosCacheInstance) {}
|
||||||
const cachePredicate = config.cache?.cachePredicate || axios.defaults.cache.cachePredicate;
|
|
||||||
|
apply = (): void => {
|
||||||
|
this.axios.interceptors.response.use(this.onFulfilled);
|
||||||
|
};
|
||||||
|
|
||||||
|
testCachePredicate = (response: AxiosResponse, { cache }: CacheConfig): boolean => {
|
||||||
|
const cachePredicate = cache?.cachePredicate || this.axios.defaults.cache.cachePredicate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(typeof cachePredicate === 'function' && cachePredicate(response)) ||
|
(typeof cachePredicate === 'function' && cachePredicate(response)) ||
|
||||||
@ -16,14 +28,14 @@ export function applyResponseInterceptor(axios: AxiosCacheInstance): void {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
axios.interceptors.response.use(async (response) => {
|
onFulfilled = async (response: CacheAxiosResponse): Promise<CacheAxiosResponse> => {
|
||||||
// Ignore caching
|
// Ignore caching
|
||||||
if (response.config.cache === false) {
|
if (response.config.cache === false) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = axios.generateKey(response.config);
|
const key = this.axios.generateKey(response.config);
|
||||||
const cache = await axios.storage.get(key);
|
const cache = await this.axios.storage.get(key);
|
||||||
|
|
||||||
// Response shouldn't be cached or was already cached
|
// Response shouldn't be cached or was already cached
|
||||||
if (cache.state !== 'loading') {
|
if (cache.state !== 'loading') {
|
||||||
@ -31,21 +43,21 @@ export function applyResponseInterceptor(axios: AxiosCacheInstance): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Config told that this response should be cached.
|
// Config told that this response should be cached.
|
||||||
if (!testCachePredicate(response, response.config as CacheConfig)) {
|
if (!this.testCachePredicate(response, response.config as CacheConfig)) {
|
||||||
// Update the cache to empty to prevent infinite loading state
|
// Update the cache to empty to prevent infinite loading state
|
||||||
await axios.storage.remove(key);
|
await this.axios.storage.remove(key);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ttl = response.config.cache?.ttl || axios.defaults.cache.ttl;
|
let ttl = response.config.cache?.ttl || this.axios.defaults.cache.ttl;
|
||||||
|
|
||||||
if (response.config.cache?.interpretHeader) {
|
if (response.config.cache?.interpretHeader) {
|
||||||
const expirationTime = axios.headerInterpreter(response.headers);
|
const expirationTime = this.axios.headerInterpreter(response.headers);
|
||||||
|
|
||||||
// Cache should not be used
|
// Cache should not be used
|
||||||
if (expirationTime === false) {
|
if (expirationTime === false) {
|
||||||
// Update the cache to empty to prevent infinite loading state
|
// Update the cache to empty to prevent infinite loading state
|
||||||
await axios.storage.remove(key);
|
await this.axios.storage.remove(key);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,18 +73,18 @@ export function applyResponseInterceptor(axios: AxiosCacheInstance): void {
|
|||||||
|
|
||||||
// Update other entries before updating himself
|
// Update other entries before updating himself
|
||||||
if (response.config.cache?.update) {
|
if (response.config.cache?.update) {
|
||||||
updateCache(axios, response.data, response.config.cache.update);
|
updateCache(this.axios, response.data, response.config.cache.update);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deferred = axios.waiting[key];
|
const deferred = this.axios.waiting[key];
|
||||||
|
|
||||||
// Resolve all other requests waiting for this response
|
// Resolve all other requests waiting for this response
|
||||||
if (deferred) {
|
if (deferred) {
|
||||||
await deferred.resolve(newCache.data);
|
await deferred.resolve(newCache.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.storage.set(key, newCache);
|
await this.axios.storage.set(key, newCache);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/interceptors/types.ts
Normal file
9
src/interceptors/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface AxiosInterceptor<T> {
|
||||||
|
onFulfilled?(value: T): T | Promise<T>;
|
||||||
|
onRejected?(error: any): any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should apply this interceptor to an already provided axios instance
|
||||||
|
*/
|
||||||
|
apply(): void;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user