Skip to content

Commit

Permalink
refactor: interceptors in classes
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurfiorette committed Sep 19, 2021
1 parent be6e7d3 commit f1033a5
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 40 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
/ignore
/.vscode/settings.json
/package-lock.json
/coverage
/coverage
*.log
34 changes: 23 additions & 11 deletions src/axios/cache.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 = {
Expand All @@ -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;
}
Expand Down
17 changes: 16 additions & 1 deletion src/axios/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
Method
} from 'axios';
import { HeaderInterpreter } from '../header';
import { AxiosInterceptor } from '../interceptors/types';
import {
CachedResponse,
CachedStorageValue,
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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>;
}

/**
Expand All @@ -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;
Expand Down
29 changes: 18 additions & 11 deletions src/interceptors/request.ts
Original file line number Diff line number Diff line change
@@ -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
});
Expand All @@ -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;
}

Expand All @@ -62,5 +69,5 @@ export function applyRequestInterceptor(axios: AxiosCacheInstance): void {
});

return config;
});
};
}
44 changes: 28 additions & 16 deletions src/interceptors/response.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,63 @@
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)) ||
(typeof cachePredicate === 'object' && checkPredicateObject(response, cachePredicate))
);
};

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') {
return response;
}

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

Expand All @@ -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 changes: 9 additions & 0 deletions src/interceptors/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit f1033a5

Please sign in to comment.