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
|
||||
/.vscode/settings.json
|
||||
/package-lock.json
|
||||
/coverage
|
||||
/coverage
|
||||
*.log
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import { defaultHeaderInterpreter } from '../header';
|
||||
import { applyRequestInterceptor } from '../interceptors/request';
|
||||
import { applyResponseInterceptor } from '../interceptors/response';
|
||||
import { CacheRequestInterceptor } from '../interceptors/request';
|
||||
import { CacheResponseInterceptor } from '../interceptors/response';
|
||||
import { MemoryStorage } from '../storage/memory';
|
||||
import { defaultKeyGenerator } from '../util/key-generator';
|
||||
import CacheInstance, { AxiosCacheInstance, CacheProperties } from './types';
|
||||
@ -15,14 +15,24 @@ import CacheInstance, { AxiosCacheInstance, CacheProperties } from './types';
|
||||
*/
|
||||
export function applyCache(
|
||||
axios: AxiosInstance,
|
||||
config: Partial<CacheInstance> & Partial<CacheProperties> = {}
|
||||
{
|
||||
storage,
|
||||
generateKey,
|
||||
waiting,
|
||||
headerInterpreter,
|
||||
requestInterceptor,
|
||||
responseInterceptor,
|
||||
...cacheOptions
|
||||
}: Partial<CacheInstance> & Partial<CacheProperties> = {}
|
||||
): AxiosCacheInstance {
|
||||
const axiosCache = axios as AxiosCacheInstance;
|
||||
|
||||
axiosCache.storage = config.storage || new MemoryStorage();
|
||||
axiosCache.generateKey = config.generateKey || defaultKeyGenerator;
|
||||
axiosCache.waiting = config.waiting || {};
|
||||
axiosCache.headerInterpreter = config.headerInterpreter || defaultHeaderInterpreter;
|
||||
axiosCache.storage = storage || new MemoryStorage();
|
||||
axiosCache.generateKey = generateKey || defaultKeyGenerator;
|
||||
axiosCache.waiting = waiting || {};
|
||||
axiosCache.headerInterpreter = headerInterpreter || defaultHeaderInterpreter;
|
||||
axiosCache.requestInterceptor = requestInterceptor || new CacheRequestInterceptor(axiosCache);
|
||||
axiosCache.responseInterceptor = responseInterceptor || new CacheResponseInterceptor(axiosCache);
|
||||
|
||||
// CacheRequestConfig values
|
||||
axiosCache.defaults = {
|
||||
@ -31,15 +41,17 @@ export function applyCache(
|
||||
ttl: 1000 * 60 * 5,
|
||||
interpretHeader: false,
|
||||
methods: ['get'],
|
||||
cachePredicate: { statusCheck: [200, 399] },
|
||||
cachePredicate: {
|
||||
statusCheck: [200, 399]
|
||||
},
|
||||
update: {},
|
||||
...config
|
||||
...cacheOptions
|
||||
}
|
||||
};
|
||||
|
||||
// Apply interceptors
|
||||
applyRequestInterceptor(axiosCache);
|
||||
applyResponseInterceptor(axiosCache);
|
||||
axiosCache.requestInterceptor.apply();
|
||||
axiosCache.responseInterceptor.apply();
|
||||
|
||||
return axiosCache;
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import type {
|
||||
Method
|
||||
} from 'axios';
|
||||
import { HeaderInterpreter } from '../header';
|
||||
import { AxiosInterceptor } from '../interceptors/types';
|
||||
import {
|
||||
CachedResponse,
|
||||
CachedStorageValue,
|
||||
@ -72,6 +73,10 @@ export type CacheProperties = {
|
||||
update: Record<string, CacheUpdater | undefined>;
|
||||
};
|
||||
|
||||
export type CacheAxiosResponse = AxiosResponse & {
|
||||
config: CacheRequestConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options that can be overridden per request
|
||||
*/
|
||||
@ -117,6 +122,16 @@ export default interface CacheInstance {
|
||||
* Only used if cache.interpretHeader is true.
|
||||
*/
|
||||
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: {
|
||||
request: AxiosInterceptorManager<CacheRequestConfig>;
|
||||
response: AxiosInterceptorManager<AxiosResponse & { config: CacheRequestConfig }>;
|
||||
response: AxiosInterceptorManager<CacheAxiosResponse>;
|
||||
};
|
||||
|
||||
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 { Deferred } from '../util/deferred';
|
||||
import { CACHED_RESPONSE_STATUS, CACHED_RESPONSE_STATUS_TEXT } from '../util/status-codes';
|
||||
import { AxiosInterceptor } from './types';
|
||||
|
||||
export function applyRequestInterceptor(axios: AxiosCacheInstance): void {
|
||||
axios.interceptors.request.use(async (config) => {
|
||||
export class CacheRequestInterceptor implements AxiosInterceptor<CacheRequestConfig> {
|
||||
constructor(readonly axios: AxiosCacheInstance) {}
|
||||
|
||||
apply = (): void => {
|
||||
this.axios.interceptors.request.use(this.onFulfilled);
|
||||
};
|
||||
|
||||
onFulfilled = async (config: CacheRequestConfig): Promise<CacheRequestConfig> => {
|
||||
// Ignore caching
|
||||
if (config.cache === false) {
|
||||
return config;
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const key = axios.generateKey(config);
|
||||
const key = this.axios.generateKey(config);
|
||||
|
||||
// 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
|
||||
if (cache.state == 'empty') {
|
||||
// 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',
|
||||
ttl: config.cache?.ttl
|
||||
});
|
||||
@ -38,12 +45,12 @@ export function applyRequestInterceptor(axios: AxiosCacheInstance): void {
|
||||
let data: CachedResponse = {};
|
||||
|
||||
if (cache.state === 'loading') {
|
||||
const deferred = axios.waiting[key];
|
||||
const deferred = this.axios.waiting[key];
|
||||
|
||||
// If the deferred is undefined, means that the
|
||||
// outside has removed that key from the waiting list
|
||||
if (!deferred) {
|
||||
await axios.storage.remove(key);
|
||||
await this.axios.storage.remove(key);
|
||||
return config;
|
||||
}
|
||||
|
||||
@ -62,5 +69,5 @@ export function applyRequestInterceptor(axios: AxiosCacheInstance): void {
|
||||
});
|
||||
|
||||
return config;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,14 +1,26 @@
|
||||
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 { checkPredicateObject } from '../util/cache-predicate';
|
||||
import { updateCache } from '../util/update-cache';
|
||||
import { AxiosInterceptor } from './types';
|
||||
|
||||
type CacheConfig = CacheRequestConfig & { cache?: Partial<CacheProperties> };
|
||||
|
||||
export function applyResponseInterceptor(axios: AxiosCacheInstance): void {
|
||||
const testCachePredicate = (response: AxiosResponse, config: CacheConfig): boolean => {
|
||||
const cachePredicate = config.cache?.cachePredicate || axios.defaults.cache.cachePredicate;
|
||||
export class CacheResponseInterceptor implements AxiosInterceptor<CacheAxiosResponse> {
|
||||
constructor(readonly axios: AxiosCacheInstance) {}
|
||||
|
||||
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 (
|
||||
(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
|
||||
if (response.config.cache === false) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const key = axios.generateKey(response.config);
|
||||
const cache = await axios.storage.get(key);
|
||||
const key = this.axios.generateKey(response.config);
|
||||
const cache = await this.axios.storage.get(key);
|
||||
|
||||
// Response shouldn't be cached or was already cached
|
||||
if (cache.state !== 'loading') {
|
||||
@ -31,21 +43,21 @@ export function applyResponseInterceptor(axios: AxiosCacheInstance): void {
|
||||
}
|
||||
|
||||
// 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
|
||||
await axios.storage.remove(key);
|
||||
await this.axios.storage.remove(key);
|
||||
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) {
|
||||
const expirationTime = axios.headerInterpreter(response.headers);
|
||||
const expirationTime = this.axios.headerInterpreter(response.headers);
|
||||
|
||||
// Cache should not be used
|
||||
if (expirationTime === false) {
|
||||
// Update the cache to empty to prevent infinite loading state
|
||||
await axios.storage.remove(key);
|
||||
await this.axios.storage.remove(key);
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -61,18 +73,18 @@ export function applyResponseInterceptor(axios: AxiosCacheInstance): void {
|
||||
|
||||
// Update other entries before updating himself
|
||||
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
|
||||
if (deferred) {
|
||||
await deferred.resolve(newCache.data);
|
||||
}
|
||||
|
||||
await axios.storage.set(key, newCache);
|
||||
await this.axios.storage.set(key, newCache);
|
||||
|
||||
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