mirror of
https://github.com/arthurfiorette/axios-cache-interceptor.git
synced 2025-12-08 17:36:16 +00:00
* feat: initial working code * fix: better usage of currentRequest on storages * feat: added tests
This commit is contained in:
parent
55f9599788
commit
17682ca0b9
@ -41,7 +41,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
|||||||
const key = (config.id = axios.generateKey(config));
|
const key = (config.id = axios.generateKey(config));
|
||||||
|
|
||||||
// Assumes that the storage handled staled responses
|
// Assumes that the storage handled staled responses
|
||||||
let cache = await axios.storage.get(key);
|
let cache = await axios.storage.get(key, config);
|
||||||
|
|
||||||
// Not cached, continue the request, and mark it as fetching
|
// Not cached, continue the request, and mark it as fetching
|
||||||
emptyOrStale: if (cache.state === 'empty' || cache.state === 'stale') {
|
emptyOrStale: if (cache.state === 'empty' || cache.state === 'stale') {
|
||||||
@ -51,7 +51,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
|||||||
* started executing.
|
* started executing.
|
||||||
*/
|
*/
|
||||||
if (axios.waiting[key]) {
|
if (axios.waiting[key]) {
|
||||||
cache = (await axios.storage.get(key)) as
|
cache = (await axios.storage.get(key, config)) as
|
||||||
| CachedStorageValue
|
| CachedStorageValue
|
||||||
| LoadingStorageValue;
|
| LoadingStorageValue;
|
||||||
|
|
||||||
@ -83,7 +83,9 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
|||||||
*/
|
*/
|
||||||
axios.waiting[key]?.catch(() => undefined);
|
axios.waiting[key]?.catch(() => undefined);
|
||||||
|
|
||||||
await axios.storage.set(key, {
|
await axios.storage.set(
|
||||||
|
key,
|
||||||
|
{
|
||||||
state: 'loading',
|
state: 'loading',
|
||||||
previous: cache.state,
|
previous: cache.state,
|
||||||
|
|
||||||
@ -93,7 +95,9 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||||
createdAt: cache.createdAt as any
|
createdAt: cache.createdAt as any
|
||||||
});
|
},
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
if (cache.state === 'stale') {
|
if (cache.state === 'stale') {
|
||||||
updateStaleRequest(cache, config as ConfigWithCache<unknown>);
|
updateStaleRequest(cache, config as ConfigWithCache<unknown>);
|
||||||
@ -126,7 +130,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
|||||||
// Just in case, the deferred doesn't exists.
|
// Just in case, the deferred doesn't exists.
|
||||||
/* istanbul ignore if 'really hard to test' */
|
/* istanbul ignore if 'really hard to test' */
|
||||||
if (!deferred) {
|
if (!deferred) {
|
||||||
await axios.storage.remove(key);
|
await axios.storage.remove(key, config);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,9 +19,9 @@ export function defaultResponseInterceptor(
|
|||||||
*
|
*
|
||||||
* Also update the waiting list for this key by rejecting it.
|
* Also update the waiting list for this key by rejecting it.
|
||||||
*/
|
*/
|
||||||
const rejectResponse = async (responseId: string) => {
|
const rejectResponse = async (responseId: string, config: CacheRequestConfig) => {
|
||||||
// Update the cache to empty to prevent infinite loading state
|
// Update the cache to empty to prevent infinite loading state
|
||||||
await axios.storage.remove(responseId);
|
await axios.storage.remove(responseId, config);
|
||||||
// Reject the deferred if present
|
// Reject the deferred if present
|
||||||
axios.waiting[responseId]?.reject(null);
|
axios.waiting[responseId]?.reject(null);
|
||||||
delete axios.waiting[responseId];
|
delete axios.waiting[responseId];
|
||||||
@ -59,8 +59,9 @@ export function defaultResponseInterceptor(
|
|||||||
|
|
||||||
// Request interceptor merges defaults with per request configuration
|
// Request interceptor merges defaults with per request configuration
|
||||||
const cacheConfig = response.config.cache as CacheProperties;
|
const cacheConfig = response.config.cache as CacheProperties;
|
||||||
|
const config = response.config;
|
||||||
|
|
||||||
const cache = await axios.storage.get(id);
|
const cache = await axios.storage.get(id, config);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
// If the request interceptor had a problem
|
// If the request interceptor had a problem
|
||||||
@ -86,7 +87,7 @@ export function defaultResponseInterceptor(
|
|||||||
!cache.data &&
|
!cache.data &&
|
||||||
!(await testCachePredicate(response, cacheConfig.cachePredicate))
|
!(await testCachePredicate(response, cacheConfig.cachePredicate))
|
||||||
) {
|
) {
|
||||||
await rejectResponse(id);
|
await rejectResponse(id, config);
|
||||||
|
|
||||||
if (__ACI_DEV__) {
|
if (__ACI_DEV__) {
|
||||||
axios.debug?.({
|
axios.debug?.({
|
||||||
@ -125,17 +126,13 @@ export function defaultResponseInterceptor(
|
|||||||
|
|
||||||
// Cache should not be used
|
// Cache should not be used
|
||||||
if (expirationTime === 'dont cache') {
|
if (expirationTime === 'dont cache') {
|
||||||
await rejectResponse(id);
|
await rejectResponse(id, config);
|
||||||
|
|
||||||
if (__ACI_DEV__) {
|
if (__ACI_DEV__) {
|
||||||
axios.debug?.({
|
axios.debug?.({
|
||||||
id,
|
id,
|
||||||
msg: `Cache header interpreted as 'dont cache'`,
|
msg: `Cache header interpreted as 'dont cache'`,
|
||||||
data: {
|
data: { cache, response, expirationTime }
|
||||||
cache,
|
|
||||||
response,
|
|
||||||
expirationTime
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,7 +188,7 @@ export function defaultResponseInterceptor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define this key as cache on the storage
|
// Define this key as cache on the storage
|
||||||
await axios.storage.set(id, newCache);
|
await axios.storage.set(id, newCache, config);
|
||||||
|
|
||||||
if (__ACI_DEV__) {
|
if (__ACI_DEV__) {
|
||||||
axios.debug?.({
|
axios.debug?.({
|
||||||
@ -206,7 +203,7 @@ export function defaultResponseInterceptor(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onRejected: ResponseInterceptor['onRejected'] = async (error) => {
|
const onRejected: ResponseInterceptor['onRejected'] = async (error) => {
|
||||||
const config = error['config'] as CacheRequestConfig;
|
const config = error.config as CacheRequestConfig;
|
||||||
|
|
||||||
// config.cache should always exists, at least from global config merge.
|
// config.cache should always exists, at least from global config merge.
|
||||||
if (!config?.cache || !config.id) {
|
if (!config?.cache || !config.id) {
|
||||||
@ -220,7 +217,7 @@ export function defaultResponseInterceptor(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cache = await axios.storage.get(config.id);
|
const cache = await axios.storage.get(config.id, config);
|
||||||
const cacheConfig = config.cache;
|
const cacheConfig = config.cache;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -228,7 +225,7 @@ export function defaultResponseInterceptor(
|
|||||||
cache.state !== 'loading' ||
|
cache.state !== 'loading' ||
|
||||||
cache.previous !== 'stale'
|
cache.previous !== 'stale'
|
||||||
) {
|
) {
|
||||||
await rejectResponse(config.id);
|
await rejectResponse(config.id, config);
|
||||||
|
|
||||||
if (__ACI_DEV__) {
|
if (__ACI_DEV__) {
|
||||||
axios.debug?.({
|
axios.debug?.({
|
||||||
@ -267,11 +264,15 @@ export function defaultResponseInterceptor(
|
|||||||
delete axios.waiting[config.id];
|
delete axios.waiting[config.id];
|
||||||
|
|
||||||
// re-mark the cache as stale
|
// re-mark the cache as stale
|
||||||
await axios.storage.set(config.id, {
|
await axios.storage.set(
|
||||||
|
config.id,
|
||||||
|
{
|
||||||
state: 'stale',
|
state: 'stale',
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
data: cache.data
|
data: cache.data
|
||||||
});
|
},
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
if (__ACI_DEV__) {
|
if (__ACI_DEV__) {
|
||||||
axios.debug?.({
|
axios.debug?.({
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { CacheRequestConfig } from '../cache/axios';
|
||||||
import { Header } from '../header/headers';
|
import { Header } from '../header/headers';
|
||||||
import type { MaybePromise } from '../util/types';
|
import type { MaybePromise } from '../util/types';
|
||||||
import type {
|
import type {
|
||||||
@ -34,8 +35,14 @@ export type BuildStorage = Omit<AxiosStorage, 'get'> & {
|
|||||||
/**
|
/**
|
||||||
* Returns the value for the given key. This method does not have to make checks for
|
* Returns the value for the given key. This method does not have to make checks for
|
||||||
* cache invalidation or anything. It just returns what was previous saved, if present.
|
* cache invalidation or anything. It just returns what was previous saved, if present.
|
||||||
|
*
|
||||||
|
* @param key The key to look for
|
||||||
|
* @param currentRequest The current {@link CacheRequestConfig}, if any
|
||||||
*/
|
*/
|
||||||
find: (key: string) => MaybePromise<StorageValue | undefined>;
|
find: (
|
||||||
|
key: string,
|
||||||
|
currentRequest?: CacheRequestConfig
|
||||||
|
) => MaybePromise<StorageValue | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,8 +68,8 @@ export function buildStorage({ set, find, remove }: BuildStorage): AxiosStorage
|
|||||||
[storage]: 1,
|
[storage]: 1,
|
||||||
set,
|
set,
|
||||||
remove,
|
remove,
|
||||||
get: async (key) => {
|
get: async (key, config) => {
|
||||||
const value = await find(key);
|
const value = await find(key, config);
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return { state: 'empty' };
|
return { state: 'empty' };
|
||||||
@ -83,11 +90,11 @@ export function buildStorage({ set, find, remove }: BuildStorage): AxiosStorage
|
|||||||
data: value.data
|
data: value.data
|
||||||
};
|
};
|
||||||
|
|
||||||
await set(key, stale);
|
await set(key, stale, config);
|
||||||
return stale;
|
return stale;
|
||||||
}
|
}
|
||||||
|
|
||||||
await remove(key);
|
await remove(key, config);
|
||||||
return { state: 'empty' };
|
return { state: 'empty' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { AxiosResponseHeaders } from 'axios';
|
import type { AxiosResponseHeaders } from 'axios';
|
||||||
|
import type { CacheRequestConfig } from '../cache/axios';
|
||||||
import type { MaybePromise } from '../util/types';
|
import type { MaybePromise } from '../util/types';
|
||||||
|
|
||||||
export type CachedResponse = {
|
export type CachedResponse = {
|
||||||
@ -78,13 +79,34 @@ export type AxiosStorage = {
|
|||||||
/**
|
/**
|
||||||
* Sets a new value for the given key
|
* Sets a new value for the given key
|
||||||
*
|
*
|
||||||
* Use CacheStorage.remove(key) to define a key to 'empty' state.
|
* Use {@link AxiosStorage.remove} to define a key with `'empty'` state.
|
||||||
|
*
|
||||||
|
* @param key The key to look for
|
||||||
|
* @param value The value to save.
|
||||||
|
* @param currentRequest The current {@link CacheRequestConfig}, if any
|
||||||
*/
|
*/
|
||||||
set: (key: string, value: NotEmptyStorageValue) => MaybePromise<void>;
|
set: (
|
||||||
|
key: string,
|
||||||
|
value: NotEmptyStorageValue,
|
||||||
|
currentRequest?: CacheRequestConfig
|
||||||
|
) => MaybePromise<void>;
|
||||||
|
|
||||||
/** Removes the value for the given key */
|
/**
|
||||||
remove: (key: string) => MaybePromise<void>;
|
* Removes the value for the given key
|
||||||
|
*
|
||||||
|
* @param key The key to look for
|
||||||
|
* @param currentRequest The current {@link CacheRequestConfig}, if any
|
||||||
|
*/
|
||||||
|
remove: (key: string, currentRequest?: CacheRequestConfig) => MaybePromise<void>;
|
||||||
|
|
||||||
/** Returns the value for the given key. This method make checks for cache invalidation or etc. */
|
/**
|
||||||
get: (key: string) => MaybePromise<StorageValue>;
|
* Returns the value for the given key. This method make checks for cache invalidation or etc.
|
||||||
|
*
|
||||||
|
* If the provided `find()` method returned null, this will map it to a `'empty'` storage value.
|
||||||
|
*
|
||||||
|
* @param key The key to look for
|
||||||
|
* @param currentRequest The current {@link CacheRequestConfig}, if any
|
||||||
|
* @returns The saved value for the given key.
|
||||||
|
*/
|
||||||
|
get: (key: string, currentRequest?: CacheRequestConfig) => MaybePromise<StorageValue>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,11 +13,11 @@ export async function updateCache<T, D>(
|
|||||||
const updater = entries[cacheKey]!;
|
const updater = entries[cacheKey]!;
|
||||||
|
|
||||||
if (updater === 'delete') {
|
if (updater === 'delete') {
|
||||||
await storage.remove(cacheKey);
|
await storage.remove(cacheKey, data.config);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = await storage.get(cacheKey);
|
const value = await storage.get(cacheKey, data.config);
|
||||||
|
|
||||||
if (value.state === 'loading') {
|
if (value.state === 'loading') {
|
||||||
continue;
|
continue;
|
||||||
@ -26,12 +26,12 @@ export async function updateCache<T, D>(
|
|||||||
const newValue = await updater(value, data);
|
const newValue = await updater(value, data);
|
||||||
|
|
||||||
if (newValue === 'delete') {
|
if (newValue === 'delete') {
|
||||||
await storage.remove(cacheKey);
|
await storage.remove(cacheKey, data.config);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValue !== 'ignore') {
|
if (newValue !== 'ignore') {
|
||||||
await storage.set(cacheKey, newValue);
|
await storage.set(cacheKey, newValue, data.config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
/** @jest-environment jsdom */
|
|
||||||
|
|
||||||
import { Axios } from 'axios';
|
|
||||||
import { isStorage } from '../../src/storage/build';
|
|
||||||
import { buildMemoryStorage } from '../../src/storage/memory';
|
|
||||||
import type { AxiosStorage } from '../../src/storage/types';
|
|
||||||
import { buildWebStorage } from '../../src/storage/web-api';
|
|
||||||
import { mockAxios } from '../mocks/axios';
|
|
||||||
|
|
||||||
it('tests isStorage function', () => {
|
|
||||||
expect(isStorage(void 0)).toBe(false);
|
|
||||||
expect(isStorage(1)).toBe(false);
|
|
||||||
expect(isStorage('a')).toBe(false);
|
|
||||||
expect(isStorage({})).toBe(false);
|
|
||||||
expect(isStorage(Axios)).toBe(false);
|
|
||||||
expect(isStorage(() => 0)).toBe(false);
|
|
||||||
expect(isStorage(null)).toBe(false);
|
|
||||||
expect(isStorage(undefined)).toBe(false);
|
|
||||||
expect(isStorage({ a: 1, b: 'a' })).toBe(false);
|
|
||||||
|
|
||||||
expect(isStorage(buildMemoryStorage())).toBe(true);
|
|
||||||
expect(isStorage(buildWebStorage(localStorage))).toBe(true);
|
|
||||||
expect(isStorage(buildWebStorage(sessionStorage))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('tests setupCache without proper storage', () => {
|
|
||||||
expect(() =>
|
|
||||||
mockAxios({
|
|
||||||
storage: {} as AxiosStorage
|
|
||||||
})
|
|
||||||
).toThrowError();
|
|
||||||
});
|
|
||||||
80
test/storage/storages.test.ts
Normal file
80
test/storage/storages.test.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/** @jest-environment jsdom */
|
||||||
|
|
||||||
|
import { Axios } from 'axios';
|
||||||
|
import { buildStorage, isStorage } from '../../src/storage/build';
|
||||||
|
import { buildMemoryStorage } from '../../src/storage/memory';
|
||||||
|
import type { AxiosStorage, StorageValue } from '../../src/storage/types';
|
||||||
|
import { buildWebStorage } from '../../src/storage/web-api';
|
||||||
|
import { mockAxios } from '../mocks/axios';
|
||||||
|
|
||||||
|
it('tests isStorage function', () => {
|
||||||
|
expect(isStorage(void 0)).toBe(false);
|
||||||
|
expect(isStorage(1)).toBe(false);
|
||||||
|
expect(isStorage('a')).toBe(false);
|
||||||
|
expect(isStorage({})).toBe(false);
|
||||||
|
expect(isStorage(Axios)).toBe(false);
|
||||||
|
expect(isStorage(() => 0)).toBe(false);
|
||||||
|
expect(isStorage(null)).toBe(false);
|
||||||
|
expect(isStorage(undefined)).toBe(false);
|
||||||
|
expect(isStorage({ a: 1, b: 'a' })).toBe(false);
|
||||||
|
|
||||||
|
expect(isStorage(buildMemoryStorage())).toBe(true);
|
||||||
|
expect(isStorage(buildWebStorage(localStorage))).toBe(true);
|
||||||
|
expect(isStorage(buildWebStorage(sessionStorage))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tests setupCache without proper storage', () => {
|
||||||
|
expect(() =>
|
||||||
|
mockAxios({
|
||||||
|
storage: {} as AxiosStorage
|
||||||
|
})
|
||||||
|
).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tests that a normal request workflow will always have a currentRequest', async () => {
|
||||||
|
const memory: Record<string, StorageValue> = {};
|
||||||
|
const isCR = 'unique-RANDOM-key-8ya5re28364ri';
|
||||||
|
|
||||||
|
const storage = buildStorage({
|
||||||
|
find(key, cr) {
|
||||||
|
//@ts-expect-error ignore
|
||||||
|
expect(cr[isCR]).toBe(true);
|
||||||
|
return memory[key];
|
||||||
|
},
|
||||||
|
set(key, value, cr) {
|
||||||
|
//@ts-expect-error ignore
|
||||||
|
expect(cr[isCR]).toBe(true);
|
||||||
|
memory[key] = value;
|
||||||
|
},
|
||||||
|
remove(key, cr) {
|
||||||
|
//@ts-expect-error ignore
|
||||||
|
expect(cr[isCR]).toBe(true);
|
||||||
|
delete memory[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const axios = mockAxios({ storage });
|
||||||
|
//@ts-expect-error ignore
|
||||||
|
axios.defaults[isCR] = true;
|
||||||
|
|
||||||
|
const req1 = axios.get('https://api.example.com/');
|
||||||
|
const req2 = axios.get('https://api.example.com/');
|
||||||
|
|
||||||
|
const [res1, res2] = await Promise.all([req1, req2]);
|
||||||
|
|
||||||
|
expect(res1.status).toBe(200);
|
||||||
|
expect(res1.cached).toBeFalsy();
|
||||||
|
|
||||||
|
expect(res2.status).toBe(200);
|
||||||
|
expect(res2.cached).toBeTruthy();
|
||||||
|
|
||||||
|
expect(res1.id).toBe(res2.id);
|
||||||
|
|
||||||
|
const cache = await axios.storage.get(res1.id, {
|
||||||
|
// sample of a request config. Just to the test pass.
|
||||||
|
//@ts-expect-error ignore
|
||||||
|
[isCR]: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(cache.state).toBe('cached');
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user