diff --git a/src/interceptors/request.ts b/src/interceptors/request.ts index 18571688..b6ec7b2a 100644 --- a/src/interceptors/request.ts +++ b/src/interceptors/request.ts @@ -1,6 +1,4 @@ -import type { AxiosRequestConfig, Method } from 'axios'; import { deferred } from 'fast-defer'; -import type { CacheProperties } from '..'; import type { AxiosCacheInstance, CacheAxiosResponse, @@ -9,11 +7,15 @@ import type { import type { CachedResponse, CachedStorageValue, - LoadingStorageValue, - StaleStorageValue + LoadingStorageValue } from '../storage/types'; -import { Header } from '../util/headers'; import type { AxiosInterceptor } from './types'; +import { + ConfigWithCache, + createValidateStatus, + isMethodIn, + setRevalidationHeaders +} from './util'; export class CacheRequestInterceptor implements AxiosInterceptor> @@ -36,7 +38,7 @@ export class CacheRequestInterceptor if ( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - !CacheRequestInterceptor.isMethodAllowed(config.method!, config.cache) + !isMethodIn(config.method!, config.cache.methods) ) { return config; } @@ -75,13 +77,10 @@ export class CacheRequestInterceptor }); if (cache.state === 'stale') { - //@ts-expect-error type infer couldn't resolve this - CacheRequestInterceptor.setRevalidationHeaders(cache, config); + setRevalidationHeaders(cache, config as ConfigWithCache); } - config.validateStatus = CacheRequestInterceptor.createValidateStatus( - config.validateStatus - ); + config.validateStatus = createValidateStatus(config.validateStatus); return config; } @@ -125,58 +124,4 @@ export class CacheRequestInterceptor return config; }; - - static readonly isMethodAllowed = ( - method: Method, - properties: Partial - ): boolean => { - const requestMethod = method.toLowerCase(); - - for (const method of properties.methods || []) { - if (method.toLowerCase() === requestMethod) { - return true; - } - } - - return false; - }; - - static readonly setRevalidationHeaders = ( - cache: StaleStorageValue, - config: CacheRequestConfig & { cache: Partial } - ): void => { - config.headers ||= {}; - - const { etag, modifiedSince } = config.cache; - - if (etag) { - const etagValue = etag === true ? cache.data?.headers[Header.ETag] : etag; - if (etagValue) { - config.headers[Header.IfNoneMatch] = etagValue; - } - } - - if (modifiedSince) { - config.headers[Header.IfModifiedSince] = - modifiedSince === true - ? // If last-modified is not present, use the createdAt timestamp - cache.data.headers[Header.LastModified] || - new Date(cache.createdAt).toUTCString() - : modifiedSince.toUTCString(); - } - }; - - /** - * Creates a new validateStatus function that will use the one already used and also - * accept status code 304. - */ - static readonly createValidateStatus = ( - oldValidate?: AxiosRequestConfig['validateStatus'] - ) => { - return (status: number): boolean => { - return oldValidate - ? oldValidate(status) || status === 304 - : (status >= 200 && status < 300) || status === 304; - }; - }; } diff --git a/src/interceptors/response.ts b/src/interceptors/response.ts index 94631504..f5193a6e 100644 --- a/src/interceptors/response.ts +++ b/src/interceptors/response.ts @@ -1,11 +1,12 @@ import type { AxiosResponse } from 'axios'; import type { AxiosCacheInstance, CacheAxiosResponse } from '../cache/axios'; import type { CacheProperties } from '../cache/cache'; -import type { CachedResponse, CachedStorageValue } from '../storage/types'; +import type { CachedStorageValue } from '../storage/types'; import { shouldCacheResponse } from '../util/cache-predicate'; import { Header } from '../util/headers'; import { updateCache } from '../util/update-cache'; import type { AxiosInterceptor } from './types'; +import { setupCacheData } from './util'; export class CacheResponseInterceptor implements AxiosInterceptor> @@ -85,7 +86,7 @@ export class CacheResponseInterceptor ttl = expirationTime || expirationTime === 0 ? expirationTime : ttl; } - const data = CacheResponseInterceptor.setupCacheData(response, cache.data); + const data = setupCacheData(response, cache.data); const newCache: CachedStorageValue = { state: 'cached', @@ -130,38 +131,4 @@ export class CacheResponseInterceptor ...response }; }; - - /** - * Creates the new date to the cache by the provided response. Also handles possible 304 - * Not Modified by updating response properties. - */ - static readonly setupCacheData = ( - response: CacheAxiosResponse, - cache?: CachedResponse - ): CachedResponse => { - if (response.status === 304 && cache) { - // Set the cache information into the response object - response.cached = true; - response.data = cache.data; - response.status = cache.status; - response.statusText = cache.statusText; - - // Update possible new headers - response.headers = { - ...cache.headers, - ...response.headers - }; - - // return the old cache - return cache; - } - - // New Response - return { - data: response.data, - status: response.status, - statusText: response.statusText, - headers: response.headers - }; - }; } diff --git a/src/interceptors/util.ts b/src/interceptors/util.ts new file mode 100644 index 00000000..636a84ef --- /dev/null +++ b/src/interceptors/util.ts @@ -0,0 +1,92 @@ +import type { Method } from 'axios'; +import type { CachedResponse, CacheProperties, StaleStorageValue } from '..'; +import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios'; +import { Header } from '../util/headers'; + +/** + * Creates a new validateStatus function that will use the one already used and also + * accept status code 304. + */ +export function createValidateStatus( + oldValidate?: CacheRequestConfig['validateStatus'] +): (status: number) => boolean { + return oldValidate + ? (status) => oldValidate(status) || status === 304 + : (status) => (status >= 200 && status < 300) || status === 304; +} + +/** Checks if the given method is in the methods array */ +export function isMethodIn(requestMethod: Method, methodList: Method[] = []): boolean { + requestMethod = requestMethod.toLowerCase() as Lowercase; + + for (const method of methodList) { + if (method.toLowerCase() === requestMethod) { + return true; + } + } + + return false; +} + +export type ConfigWithCache = CacheRequestConfig & { + cache: Partial; +}; + +export function setRevalidationHeaders( + cache: StaleStorageValue, + config: ConfigWithCache +): void { + config.headers ||= {}; + + const { etag, modifiedSince } = config.cache; + + if (etag) { + const etagValue = etag === true ? cache.data?.headers[Header.ETag] : etag; + if (etagValue) { + config.headers[Header.IfNoneMatch] = etagValue; + } + } + + if (modifiedSince) { + config.headers[Header.IfModifiedSince] = + modifiedSince === true + ? // If last-modified is not present, use the createdAt timestamp + cache.data.headers[Header.LastModified] || + new Date(cache.createdAt).toUTCString() + : modifiedSince.toUTCString(); + } +} + +/** + * Creates the new date to the cache by the provided response. Also handles possible 304 + * Not Modified by updating response properties. + */ +export function setupCacheData( + response: CacheAxiosResponse, + cache?: CachedResponse +): CachedResponse { + if (response.status === 304 && cache) { + // Set the cache information into the response object + response.cached = true; + response.data = cache.data; + response.status = cache.status; + response.statusText = cache.statusText; + + // Update possible new headers + response.headers = { + ...cache.headers, + ...response.headers + }; + + // return the old cache + return cache; + } + + // New Response + return { + data: response.data, + status: response.status, + statusText: response.statusText, + headers: response.headers + }; +} diff --git a/test/interceptors/request.test.ts b/test/interceptors/request.test.ts index 2cfbc813..94b2d61d 100644 --- a/test/interceptors/request.test.ts +++ b/test/interceptors/request.test.ts @@ -1,4 +1,3 @@ -import { CacheRequestInterceptor } from '../../src/interceptors/request'; import { mockAxios } from '../mocks/axios'; import { sleep } from '../utils'; @@ -114,25 +113,4 @@ describe('test request interceptor', () => { // nothing to use for revalidation expect(response.cached).toBe(false); }); - - it('tests validate-status function', async () => { - const { createValidateStatus } = CacheRequestInterceptor; - - const def = createValidateStatus(); - expect(def(200)).toBe(true); - expect(def(345)).toBe(false); - expect(def(304)).toBe(true); - - const only200 = createValidateStatus((s) => s >= 200 && s < 300); - expect(only200(200)).toBe(true); - expect(only200(299)).toBe(true); - expect(only200(304)).toBe(true); - expect(only200(345)).toBe(false); - - const randomValue = createValidateStatus((s) => s >= 405 && s <= 410); - expect(randomValue(200)).toBe(false); - expect(randomValue(404)).toBe(false); - expect(randomValue(405)).toBe(true); - expect(randomValue(304)).toBe(true); - }); }); diff --git a/test/interceptors/util.test.ts b/test/interceptors/util.test.ts new file mode 100644 index 00000000..6089d667 --- /dev/null +++ b/test/interceptors/util.test.ts @@ -0,0 +1,22 @@ +import { createValidateStatus } from '../../src/interceptors/util'; + +describe('test util functions', () => { + it('tests validate-status function', async () => { + const def = createValidateStatus(); + expect(def(200)).toBe(true); + expect(def(345)).toBe(false); + expect(def(304)).toBe(true); + + const only200 = createValidateStatus((s) => s >= 200 && s < 300); + expect(only200(200)).toBe(true); + expect(only200(299)).toBe(true); + expect(only200(304)).toBe(true); + expect(only200(345)).toBe(false); + + const randomValue = createValidateStatus((s) => s >= 405 && s <= 410); + expect(randomValue(200)).toBe(false); + expect(randomValue(404)).toBe(false); + expect(randomValue(405)).toBe(true); + expect(randomValue(304)).toBe(true); + }); +});