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));
|
||||
|
||||
// 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
|
||||
emptyOrStale: if (cache.state === 'empty' || cache.state === 'stale') {
|
||||
@ -51,7 +51,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
||||
* started executing.
|
||||
*/
|
||||
if (axios.waiting[key]) {
|
||||
cache = (await axios.storage.get(key)) as
|
||||
cache = (await axios.storage.get(key, config)) as
|
||||
| CachedStorageValue
|
||||
| LoadingStorageValue;
|
||||
|
||||
@ -83,17 +83,21 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
||||
*/
|
||||
axios.waiting[key]?.catch(() => undefined);
|
||||
|
||||
await axios.storage.set(key, {
|
||||
state: 'loading',
|
||||
previous: cache.state,
|
||||
await axios.storage.set(
|
||||
key,
|
||||
{
|
||||
state: 'loading',
|
||||
previous: cache.state,
|
||||
|
||||
// Eslint complains a lot :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
data: cache.data as any,
|
||||
// Eslint complains a lot :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
data: cache.data as any,
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
createdAt: cache.createdAt as any
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
createdAt: cache.createdAt as any
|
||||
},
|
||||
config
|
||||
);
|
||||
|
||||
if (cache.state === 'stale') {
|
||||
updateStaleRequest(cache, config as ConfigWithCache<unknown>);
|
||||
@ -126,7 +130,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
||||
// Just in case, the deferred doesn't exists.
|
||||
/* istanbul ignore if 'really hard to test' */
|
||||
if (!deferred) {
|
||||
await axios.storage.remove(key);
|
||||
await axios.storage.remove(key, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@ -19,9 +19,9 @@ export function defaultResponseInterceptor(
|
||||
*
|
||||
* 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
|
||||
await axios.storage.remove(responseId);
|
||||
await axios.storage.remove(responseId, config);
|
||||
// Reject the deferred if present
|
||||
axios.waiting[responseId]?.reject(null);
|
||||
delete axios.waiting[responseId];
|
||||
@ -59,8 +59,9 @@ export function defaultResponseInterceptor(
|
||||
|
||||
// Request interceptor merges defaults with per request configuration
|
||||
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 the request interceptor had a problem
|
||||
@ -86,7 +87,7 @@ export function defaultResponseInterceptor(
|
||||
!cache.data &&
|
||||
!(await testCachePredicate(response, cacheConfig.cachePredicate))
|
||||
) {
|
||||
await rejectResponse(id);
|
||||
await rejectResponse(id, config);
|
||||
|
||||
if (__ACI_DEV__) {
|
||||
axios.debug?.({
|
||||
@ -125,17 +126,13 @@ export function defaultResponseInterceptor(
|
||||
|
||||
// Cache should not be used
|
||||
if (expirationTime === 'dont cache') {
|
||||
await rejectResponse(id);
|
||||
await rejectResponse(id, config);
|
||||
|
||||
if (__ACI_DEV__) {
|
||||
axios.debug?.({
|
||||
id,
|
||||
msg: `Cache header interpreted as 'dont cache'`,
|
||||
data: {
|
||||
cache,
|
||||
response,
|
||||
expirationTime
|
||||
}
|
||||
data: { cache, response, expirationTime }
|
||||
});
|
||||
}
|
||||
|
||||
@ -191,7 +188,7 @@ export function defaultResponseInterceptor(
|
||||
}
|
||||
|
||||
// Define this key as cache on the storage
|
||||
await axios.storage.set(id, newCache);
|
||||
await axios.storage.set(id, newCache, config);
|
||||
|
||||
if (__ACI_DEV__) {
|
||||
axios.debug?.({
|
||||
@ -206,7 +203,7 @@ export function defaultResponseInterceptor(
|
||||
};
|
||||
|
||||
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.
|
||||
if (!config?.cache || !config.id) {
|
||||
@ -220,7 +217,7 @@ export function defaultResponseInterceptor(
|
||||
throw error;
|
||||
}
|
||||
|
||||
const cache = await axios.storage.get(config.id);
|
||||
const cache = await axios.storage.get(config.id, config);
|
||||
const cacheConfig = config.cache;
|
||||
|
||||
if (
|
||||
@ -228,7 +225,7 @@ export function defaultResponseInterceptor(
|
||||
cache.state !== 'loading' ||
|
||||
cache.previous !== 'stale'
|
||||
) {
|
||||
await rejectResponse(config.id);
|
||||
await rejectResponse(config.id, config);
|
||||
|
||||
if (__ACI_DEV__) {
|
||||
axios.debug?.({
|
||||
@ -267,11 +264,15 @@ export function defaultResponseInterceptor(
|
||||
delete axios.waiting[config.id];
|
||||
|
||||
// re-mark the cache as stale
|
||||
await axios.storage.set(config.id, {
|
||||
state: 'stale',
|
||||
createdAt: Date.now(),
|
||||
data: cache.data
|
||||
});
|
||||
await axios.storage.set(
|
||||
config.id,
|
||||
{
|
||||
state: 'stale',
|
||||
createdAt: Date.now(),
|
||||
data: cache.data
|
||||
},
|
||||
config
|
||||
);
|
||||
|
||||
if (__ACI_DEV__) {
|
||||
axios.debug?.({
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { CacheRequestConfig } from '../cache/axios';
|
||||
import { Header } from '../header/headers';
|
||||
import type { MaybePromise } from '../util/types';
|
||||
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
|
||||
* 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,
|
||||
set,
|
||||
remove,
|
||||
get: async (key) => {
|
||||
const value = await find(key);
|
||||
get: async (key, config) => {
|
||||
const value = await find(key, config);
|
||||
|
||||
if (!value) {
|
||||
return { state: 'empty' };
|
||||
@ -83,11 +90,11 @@ export function buildStorage({ set, find, remove }: BuildStorage): AxiosStorage
|
||||
data: value.data
|
||||
};
|
||||
|
||||
await set(key, stale);
|
||||
await set(key, stale, config);
|
||||
return stale;
|
||||
}
|
||||
|
||||
await remove(key);
|
||||
await remove(key, config);
|
||||
return { state: 'empty' };
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { AxiosResponseHeaders } from 'axios';
|
||||
import type { CacheRequestConfig } from '../cache/axios';
|
||||
import type { MaybePromise } from '../util/types';
|
||||
|
||||
export type CachedResponse = {
|
||||
@ -78,13 +79,34 @@ export type AxiosStorage = {
|
||||
/**
|
||||
* 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]!;
|
||||
|
||||
if (updater === 'delete') {
|
||||
await storage.remove(cacheKey);
|
||||
await storage.remove(cacheKey, data.config);
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = await storage.get(cacheKey);
|
||||
const value = await storage.get(cacheKey, data.config);
|
||||
|
||||
if (value.state === 'loading') {
|
||||
continue;
|
||||
@ -26,12 +26,12 @@ export async function updateCache<T, D>(
|
||||
const newValue = await updater(value, data);
|
||||
|
||||
if (newValue === 'delete') {
|
||||
await storage.remove(cacheKey);
|
||||
await storage.remove(cacheKey, data.config);
|
||||
continue;
|
||||
}
|
||||
|
||||
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