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 /ignore
/.vscode/settings.json /.vscode/settings.json
/package-lock.json /package-lock.json
/coverage /coverage
*.log

View File

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

View File

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

View File

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

View File

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

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