feat: override cache option

This commit is contained in:
arthurfiorette 2022-06-05 11:26:00 -03:00
parent d87307ae93
commit 268fccb935
No known key found for this signature in database
GPG Key ID: 9D190CD53C53C555
5 changed files with 156 additions and 10 deletions

9
src/cache/cache.ts vendored
View File

@ -109,6 +109,15 @@ export type CacheProperties<R = unknown, D = unknown> = {
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-if-error
*/
staleIfError: StaleIfErrorPredicate<R, D>;
/**
* This options makes the interceptors ignore the available cache and always make a new
* request. But, different from `cache: false`, this will not delete the current cache
* and will update the cache when the request is successful.
*
* @default false
*/
override: boolean;
};
export interface CacheInstance {

4
src/cache/create.ts vendored
View File

@ -92,7 +92,9 @@ export function setupCache(
interpretHeader: options.interpretHeader ?? true,
staleIfError: options.staleIfError ?? true
staleIfError: options.staleIfError ?? true,
override: false
};
// Apply interceptors

View File

@ -20,7 +20,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
if (config.cache === false) {
if (__ACI_DEV__) {
axios.debug?.({
msg: 'Ignoring cache because config.cache is false',
msg: 'Ignoring cache because config.cache === false',
data: config
});
}
@ -43,15 +43,20 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
// Assumes that the storage handled staled responses
let cache = await axios.storage.get(key, config);
const overrideCache = config.cache.override;
// Not cached, continue the request, and mark it as fetching
emptyOrStale: if (cache.state === 'empty' || cache.state === 'stale') {
ignoreAndRequest: if (
cache.state === 'empty' ||
cache.state === 'stale' ||
overrideCache
) {
/**
* This checks for simultaneous access to a new key. The js event loop jumps on the
* first await statement, so the second (asynchronous call) request may have already
* started executing.
*/
if (axios.waiting[key]) {
if (axios.waiting[key] && !overrideCache) {
cache = (await axios.storage.get(key, config)) as
| CachedStorageValue
| LoadingStorageValue;
@ -71,7 +76,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
});
}
break emptyOrStale;
break ignoreAndRequest;
}
}
@ -88,14 +93,24 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
key,
{
state: 'loading',
previous: cache.state,
previous: overrideCache
? // Simply determine if the request is stale or not
// based if it had previous data or not
cache.data
? 'stale'
: 'empty'
: // Typescript doesn't know that cache.state here can only be 'empty' or 'stale'
(cache.state as 'stale'),
// 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
// If the cache is empty and asked to override it, use the current timestamp
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
createdAt:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
overrideCache && !cache.createdAt ? Date.now() : (cache.createdAt as any)
},
config
);
@ -116,7 +131,11 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
if (__ACI_DEV__) {
axios.debug?.({
id: key,
msg: 'Sending request, waiting for response'
msg: 'Sending request, waiting for response',
data: {
overrideCache,
state: cache.state
}
});
}

View File

@ -153,7 +153,7 @@ export function defaultResponseInterceptor(
axios.debug?.({
id,
msg: 'Useful response configuration found',
data: { cacheConfig, ttl, cacheResponse: data }
data: { cacheConfig, cacheResponse: data }
});
}

View File

@ -1,3 +1,5 @@
import { setTimeout } from 'timers/promises';
import type { LoadingStorageValue } from '../../src';
import type { CacheRequestConfig } from '../../src/cache/axios';
import { mockAxios } from '../mocks/axios';
import { sleep } from '../utils';
@ -187,4 +189,118 @@ describe('test request interceptor', () => {
expect(newState).not.toBe('empty');
expect(axios.waiting[ID]).toBeUndefined();
});
it('tests cache.override = true with previous cache', async () => {
const axios = mockAxios();
// First normal request to populate cache
const { id, ...initialResponse } = await axios.get('url');
expect(initialResponse.cached).toBe(false);
// Ensure cache was populated
const c1 = await axios.storage.get(id);
expect(c1.state).toBe('cached');
// Make a request with cache.override = true
const promise = axios.get('url', {
id,
cache: { override: true },
// Simple adapter that resolves after the deferred is completed.
adapter: async (config: CacheRequestConfig) => {
await setTimeout(150);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const response = await axios.defaults.adapter!(config);
// Changes the response to be different from `true` (default)
response.data = 'overridden response';
return response;
}
});
// These two setTimeouts is to ensure this code is executed after
// the request interceptor, but before the response interceptor.
// Leading to test the intermediate loading state.
{
await setTimeout(50);
const c2 = (await axios.storage.get(id)) as LoadingStorageValue;
expect(c2.state).toBe('loading');
expect(c2.previous).toBe('stale');
expect(c2.data).toBe(c1.data);
expect(c2.createdAt).toBe(c1.createdAt);
}
// Waits for the promise completion
const newResponse = await promise;
// This step is after the cache was updated with the new response.
{
const c3 = await axios.storage.get(id);
expect(newResponse.cached).toBe(false);
expect(c3.state).toBe('cached');
expect(c3.data).not.toBe(c1.data); // `'overridden response'`, not `true`
expect(c3.createdAt).not.toBe(c1.createdAt);
}
});
it('tests cache.override = true without previous cache', async () => {
const id = 'CUSTOM_RANDOM_ID';
const axios = mockAxios();
const c1 = await axios.storage.get(id);
expect(c1.state).toBe('empty');
// Make a request with cache.override = true
const promise = axios.get('url', {
id,
cache: { override: true },
// Simple adapter that resolves after the deferred is completed.
adapter: async (config: CacheRequestConfig) => {
await setTimeout(150);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return axios.defaults.adapter!(config);
}
});
// These two setTimeouts is to ensure this code is executed after
// the request interceptor, but before the response interceptor.
// Leading to test the intermediate loading state.
{
await setTimeout(50);
const c2 = (await axios.storage.get(id)) as LoadingStorageValue;
expect(c2.state).toBe('loading');
expect(c2.previous).toBe('empty');
expect(c2.data).toBeUndefined();
expect(c2.createdAt).not.toBe(c1.createdAt);
}
// Waits for the promise completion
const newResponse = await promise;
// This step is after the cache was updated with the new response.
{
const c3 = await axios.storage.get(id);
expect(newResponse.cached).toBe(false);
expect(c3.state).toBe('cached');
expect(c3.data).not.toBeUndefined();
expect(c3.createdAt).not.toBe(c1.createdAt);
}
});
});