refactor: removed "abstract" buildInterceptor function

This commit is contained in:
arthurfiorette 2022-01-08 12:56:55 -03:00
parent d30b862605
commit f30e2622dd
No known key found for this signature in database
GPG Key ID: 9D190CD53C53C555
4 changed files with 211 additions and 233 deletions

View File

@ -1,35 +1,10 @@
import type {
AxiosCacheInstance,
CacheAxiosResponse,
CacheRequestConfig
} from '../cache/axios';
import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios';
export interface AxiosInterceptor<T> {
onFulfilled?(value: T): T | Promise<T>;
onRejected?(error: any): any;
apply: (axios: AxiosCacheInstance) => void;
apply: () => void;
}
export type RequestInterceptor = AxiosInterceptor<CacheRequestConfig<unknown, unknown>>;
export type ResponseInterceptor = AxiosInterceptor<CacheAxiosResponse<unknown, unknown>>;
export function buildInterceptor(
type: 'request',
interceptor: Omit<RequestInterceptor, 'apply'>
): RequestInterceptor;
export function buildInterceptor(
type: 'response',
interceptor: Omit<ResponseInterceptor, 'apply'>
): ResponseInterceptor;
export function buildInterceptor(
type: 'request' | 'response',
{ onFulfilled, onRejected }: Omit<AxiosInterceptor<unknown>, 'apply'>
): AxiosInterceptor<unknown> {
return {
onFulfilled,
onRejected,
apply: (axios) => axios.interceptors[type].use(onFulfilled, onRejected)
};
}

View File

@ -1,11 +1,11 @@
import { deferred } from 'fast-defer';
import { buildInterceptor } from '..';
import type { AxiosCacheInstance, CacheAxiosResponse } from '../cache/axios';
import type {
CachedResponse,
CachedStorageValue,
LoadingStorageValue
} from '../storage/types';
import type { RequestInterceptor } from './build';
import {
ConfigWithCache,
createValidateStatus,
@ -14,99 +14,102 @@ import {
} from './util';
export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
return buildInterceptor('request', {
onFulfilled: async (config) => {
if (config.cache === false) {
return config;
const onFulfilled: RequestInterceptor['onFulfilled'] = async (config) => {
if (config.cache === false) {
return config;
}
// merge defaults with per request configuration
config.cache = { ...axios.defaults.cache, ...config.cache };
if (!isMethodIn(config.method, config.cache.methods)) {
return config;
}
const key = axios.generateKey(config);
// Assumes that the storage handled staled responses
let cache = await axios.storage.get(key);
// Not cached, continue the request, and mark it as fetching
emptyOrStale: if (cache.state == 'empty' || cache.state === 'stale') {
/**
* This checks for simultaneous access to a new key. The js event loop jumps on the
* first await statement, so the second (asynchronous call) request may have already
* started executing.
*/
if (axios.waiting[key]) {
cache = (await axios.storage.get(key)) as
| CachedStorageValue
| LoadingStorageValue;
break emptyOrStale;
}
// merge defaults with per request configuration
config.cache = { ...axios.defaults.cache, ...config.cache };
// Create a deferred to resolve other requests for the same key when it's completed
axios.waiting[key] = deferred();
if (!isMethodIn(config.method, config.cache.methods)) {
return config;
/**
* Add a default reject handler to catch when the request is aborted without others
* waiting for it.
*/
axios.waiting[key]?.catch(() => undefined);
await axios.storage.set(key, {
state: 'loading',
data: cache.data
});
if (cache.state === 'stale') {
setRevalidationHeaders(cache, config as ConfigWithCache<unknown>);
}
const key = axios.generateKey(config);
// Assumes that the storage handled staled responses
let cache = await axios.storage.get(key);
// Not cached, continue the request, and mark it as fetching
emptyOrStale: if (cache.state == 'empty' || cache.state === 'stale') {
/**
* This checks for simultaneous access to a new key. The js event loop jumps on
* the first await statement, so the second (asynchronous call) request may have
* already started executing.
*/
if (axios.waiting[key]) {
cache = (await axios.storage.get(key)) as
| CachedStorageValue
| LoadingStorageValue;
break emptyOrStale;
}
// Create a deferred to resolve other requests for the same key when it's completed
axios.waiting[key] = deferred();
/**
* Add a default reject handler to catch when the request is aborted without
* others waiting for it.
*/
axios.waiting[key]?.catch(() => undefined);
await axios.storage.set(key, {
state: 'loading',
data: cache.data
});
if (cache.state === 'stale') {
setRevalidationHeaders(cache, config as ConfigWithCache<unknown>);
}
config.validateStatus = createValidateStatus(config.validateStatus);
return config;
}
let cachedResponse: CachedResponse;
if (cache.state === 'loading') {
const deferred = axios.waiting[key];
// Just in case, the deferred doesn't exists.
/* istanbul ignore if 'really hard to test' */
if (!deferred) {
await axios.storage.remove(key);
return config;
}
try {
cachedResponse = await deferred;
} catch {
// The deferred is rejected when the request that we are waiting rejected cache.
return config;
}
} else {
cachedResponse = cache.data;
}
config.adapter = () =>
/**
* Even though the response interceptor receives this one from here, it has been
* configured to ignore cached responses: true
*/
Promise.resolve<CacheAxiosResponse<unknown, unknown>>({
config,
data: cachedResponse.data,
headers: cachedResponse.headers,
status: cachedResponse.status,
statusText: cachedResponse.statusText,
cached: true,
id: key
});
config.validateStatus = createValidateStatus(config.validateStatus);
return config;
}
});
let cachedResponse: CachedResponse;
if (cache.state === 'loading') {
const deferred = axios.waiting[key];
// Just in case, the deferred doesn't exists.
/* istanbul ignore if 'really hard to test' */
if (!deferred) {
await axios.storage.remove(key);
return config;
}
try {
cachedResponse = await deferred;
} catch {
// The deferred is rejected when the request that we are waiting rejected cache.
return config;
}
} else {
cachedResponse = cache.data;
}
config.adapter = () =>
/**
* Even though the response interceptor receives this one from here, it has been
* configured to ignore cached responses: true
*/
Promise.resolve<CacheAxiosResponse<unknown, unknown>>({
config,
data: cachedResponse.data,
headers: cachedResponse.headers,
status: cachedResponse.status,
statusText: cachedResponse.statusText,
cached: true,
id: key
});
return config;
};
return {
onFulfilled,
apply: () => axios.interceptors.request.use(onFulfilled)
};
}

View File

@ -1,111 +1,132 @@
import { buildInterceptor } from '..';
import type { AxiosCacheInstance } from '../cache/axios';
import type { CacheProperties } from '../cache/cache';
import type { CachedStorageValue } from '../storage/types';
import { shouldCacheResponse } from '../util/cache-predicate';
import { Header } from '../util/headers';
import { updateCache } from '../util/update-cache';
import { rejectResponse, setupCacheData } from './util';
import type { ResponseInterceptor } from './build';
import { setupCacheData } from './util';
export function defaultResponseInterceptor(axios: AxiosCacheInstance) {
return buildInterceptor('response', {
onFulfilled: async (response) => {
response.id ??= axios.generateKey(response.config);
response.cached ??= false;
export function defaultResponseInterceptor(
axios: AxiosCacheInstance
): ResponseInterceptor {
/**
* Rejects cache for an response response.
*
* Also update the waiting list for this key by rejecting it.
*/
const rejectResponse = async (
{ storage, waiting }: AxiosCacheInstance,
responseId: string
) => {
// Update the cache to empty to prevent infinite loading state
await storage.remove(responseId);
// Reject the deferred if present
waiting[responseId]?.reject(null);
delete waiting[responseId];
};
// Response is already cached
if (response.cached) {
return response;
}
const onFulfilled: ResponseInterceptor['onFulfilled'] = async (response) => {
response.id ??= axios.generateKey(response.config);
response.cached ??= false;
// Skip cache: either false or weird behavior
// config.cache should always exists, at least from global config merge.
if (!response.config.cache) {
return { ...response, cached: false };
}
// Response is already cached
if (response.cached) {
return response;
}
const cacheConfig = response.config.cache as CacheProperties;
// Skip cache: either false or weird behavior
// config.cache should always exists, at least from global config merge.
if (!response.config.cache) {
return { ...response, cached: false };
}
const cache = await axios.storage.get(response.id);
const cacheConfig = response.config.cache as CacheProperties;
if (
// If the request interceptor had a problem
cache.state === 'stale' ||
cache.state === 'empty' ||
// Should not hit here because of previous response.cached check
cache.state === 'cached'
) {
return response;
}
const cache = await axios.storage.get(response.id);
// Config told that this response should be cached.
if (
// For 'loading' values (post stale), this check was already run in the past.
!cache.data &&
!shouldCacheResponse(response, cacheConfig)
) {
if (
// If the request interceptor had a problem
cache.state === 'stale' ||
cache.state === 'empty' ||
// Should not hit here because of previous response.cached check
cache.state === 'cached'
) {
return response;
}
// Config told that this response should be cached.
if (
// For 'loading' values (post stale), this check was already run in the past.
!cache.data &&
!shouldCacheResponse(response, cacheConfig)
) {
await rejectResponse(axios, response.id);
return response;
}
// avoid remnant headers from remote server to break implementation
delete response.headers[Header.XAxiosCacheEtag];
delete response.headers[Header.XAxiosCacheLastModified];
if (cacheConfig.etag && cacheConfig.etag !== true) {
response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag;
}
if (cacheConfig.modifiedSince) {
response.headers[Header.XAxiosCacheLastModified] =
cacheConfig.modifiedSince === true
? 'use-cache-timestamp'
: cacheConfig.modifiedSince.toUTCString();
}
let ttl = cacheConfig.ttl || -1; // always set from global config
if (cacheConfig?.interpretHeader) {
const expirationTime = axios.headerInterpreter(response.headers);
// Cache should not be used
if (expirationTime === 'dont cache') {
await rejectResponse(axios, response.id);
return response;
}
// avoid remnant headers from remote server to break implementation
delete response.headers[Header.XAxiosCacheEtag];
delete response.headers[Header.XAxiosCacheLastModified];
if (cacheConfig.etag && cacheConfig.etag !== true) {
response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag;
}
if (cacheConfig.modifiedSince) {
response.headers[Header.XAxiosCacheLastModified] =
cacheConfig.modifiedSince === true
? 'use-cache-timestamp'
: cacheConfig.modifiedSince.toUTCString();
}
let ttl = cacheConfig.ttl || -1; // always set from global config
if (cacheConfig?.interpretHeader) {
const expirationTime = axios.headerInterpreter(response.headers);
// Cache should not be used
if (expirationTime === 'dont cache') {
await rejectResponse(axios, response.id);
return response;
}
ttl = expirationTime === 'not enough headers' ? ttl : expirationTime;
}
const data = setupCacheData(response, cache.data);
if (typeof ttl === 'function') {
ttl = await ttl(response);
}
const newCache: CachedStorageValue = {
state: 'cached',
ttl,
createdAt: Date.now(),
data
};
// Update other entries before updating himself
if (cacheConfig?.update) {
updateCache(axios.storage, response, cacheConfig.update);
}
const deferred = axios.waiting[response.id];
// Resolve all other requests waiting for this response
await deferred?.resolve(newCache.data);
delete axios.waiting[response.id];
// Define this key as cache on the storage
await axios.storage.set(response.id, newCache);
// Return the response with cached as false, because it was not cached at all
return response;
ttl = expirationTime === 'not enough headers' ? ttl : expirationTime;
}
});
const data = setupCacheData(response, cache.data);
if (typeof ttl === 'function') {
ttl = await ttl(response);
}
const newCache: CachedStorageValue = {
state: 'cached',
ttl,
createdAt: Date.now(),
data
};
// Update other entries before updating himself
if (cacheConfig?.update) {
updateCache(axios.storage, response, cacheConfig.update);
}
const deferred = axios.waiting[response.id];
// Resolve all other requests waiting for this response
await deferred?.resolve(newCache.data);
delete axios.waiting[response.id];
// Define this key as cache on the storage
await axios.storage.set(response.id, newCache);
// Return the response with cached as false, because it was not cached at all
return response;
};
return {
onFulfilled,
apply: () => axios.interceptors.response.use(onFulfilled)
};
}

View File

@ -1,9 +1,5 @@
import type { Method } from 'axios';
import type {
AxiosCacheInstance,
CacheAxiosResponse,
CacheRequestConfig
} from '../cache/axios';
import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios';
import type { CacheProperties } from '../cache/cache';
import type { CachedResponse, StaleStorageValue } from '../storage/types';
import { Header } from '../util/headers';
@ -21,11 +17,10 @@ export function createValidateStatus(
}
/** Checks if the given method is in the methods array */
export function isMethodIn(requestMethod?: Method, methodList?: Method[]): boolean {
if (!requestMethod || !methodList) {
return false;
}
export function isMethodIn(
requestMethod: Method = 'get',
methodList: Method[] = []
): boolean {
requestMethod = requestMethod.toLowerCase() as Lowercase<Method>;
for (const method of methodList) {
@ -99,19 +94,3 @@ export function setupCacheData<R, D>(
headers: response.headers
};
}
/**
* Rejects cache for an response response.
*
* Also update the waiting list for this key by rejecting it.
*/
export async function rejectResponse(
{ storage, waiting }: AxiosCacheInstance,
responseId: string
) {
// Update the cache to empty to prevent infinite loading state
await storage.remove(responseId);
// Reject the deferred if present
waiting[responseId]?.reject(null);
delete waiting[responseId];
}