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

View File

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

View File

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

View File

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

View File

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

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