mirror of
https://github.com/arthurfiorette/axios-cache-interceptor.git
synced 2025-12-08 17:36:16 +00:00
feat: override cache option
This commit is contained in:
parent
d87307ae93
commit
268fccb935
9
src/cache/cache.ts
vendored
9
src/cache/cache.ts
vendored
@ -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
4
src/cache/create.ts
vendored
@ -92,7 +92,9 @@ export function setupCache(
|
||||
|
||||
interpretHeader: options.interpretHeader ?? true,
|
||||
|
||||
staleIfError: options.staleIfError ?? true
|
||||
staleIfError: options.staleIfError ?? true,
|
||||
|
||||
override: false
|
||||
};
|
||||
|
||||
// Apply interceptors
|
||||
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -153,7 +153,7 @@ export function defaultResponseInterceptor(
|
||||
axios.debug?.({
|
||||
id,
|
||||
msg: 'Useful response configuration found',
|
||||
data: { cacheConfig, ttl, cacheResponse: data }
|
||||
data: { cacheConfig, cacheResponse: data }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user