diff --git a/.gitignore b/.gitignore index a2bed90..414dfa2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /ignore /.vscode/settings.json /package-lock.json -/coverage \ No newline at end of file +/coverage +*.log diff --git a/src/axios/cache.ts b/src/axios/cache.ts index 26b62bb..9909a45 100644 --- a/src/axios/cache.ts +++ b/src/axios/cache.ts @@ -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 & Partial = {} + { + storage, + generateKey, + waiting, + headerInterpreter, + requestInterceptor, + responseInterceptor, + ...cacheOptions + }: Partial & Partial = {} ): 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; } diff --git a/src/axios/types.ts b/src/axios/types.ts index e0a41d6..bfe1dbe 100644 --- a/src/axios/types.ts +++ b/src/axios/types.ts @@ -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; }; +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; + + /** + * The response interceptor that will be used to handle the cache. + */ + responseInterceptor: AxiosInterceptor; } /** @@ -134,7 +149,7 @@ export interface AxiosCacheInstance extends AxiosInstance, CacheInstance { interceptors: { request: AxiosInterceptorManager; - response: AxiosInterceptorManager; + response: AxiosInterceptorManager; }; getUri(config?: CacheRequestConfig): string; diff --git a/src/interceptors/request.ts b/src/interceptors/request.ts index bf38219..0783dac 100644 --- a/src/interceptors/request.ts +++ b/src/interceptors/request.ts @@ -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 { + constructor(readonly axios: AxiosCacheInstance) {} + + apply = (): void => { + this.axios.interceptors.request.use(this.onFulfilled); + }; + + onFulfilled = async (config: CacheRequestConfig): Promise => { // 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; - }); + }; } diff --git a/src/interceptors/response.ts b/src/interceptors/response.ts index 743a320..35923a6 100644 --- a/src/interceptors/response.ts +++ b/src/interceptors/response.ts @@ -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 }; -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 { + 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 => { // 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; - }); + }; } diff --git a/src/interceptors/types.ts b/src/interceptors/types.ts new file mode 100644 index 0000000..dcf940c --- /dev/null +++ b/src/interceptors/types.ts @@ -0,0 +1,9 @@ +export interface AxiosInterceptor { + onFulfilled?(value: T): T | Promise; + onRejected?(error: any): any; + + /** + * Should apply this interceptor to an already provided axios instance + */ + apply(): void; +}