Pass currentRequest on each access to storages (#220) (#226)

* feat: initial working code

* fix: better usage of currentRequest on storages

* feat: added tests
This commit is contained in:
Arthur Fiorette 2022-05-01 10:03:30 -03:00 committed by GitHub
parent 55f9599788
commit 17682ca0b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 78 deletions

View File

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

View File

@ -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?.({

View File

@ -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' };
}
};

View File

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

View File

@ -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);
}
}
}

View File

@ -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();
});

View 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');
});