refactor: interceptors in classes

This commit is contained in:
Hazork 2021-09-19 19:06:15 -03:00
parent be6e7d3e2d
commit f1033a5959
6 changed files with 96 additions and 40 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@
/ignore
/.vscode/settings.json
/package-lock.json
/coverage
/coverage
*.log

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
});
};
}

View File

@ -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;
});
};
}

View 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;
}