Open cnhuye opened 3 years ago
Hello @cnhuye
I don't think that is possible, well at least not cached automatically by the adapter. We need at least one response else we can't know what to cache.
It would be possible to push data to the cache store manually.
One solution would be to put a promise queue in front of the cache-adapter and debounce duplicate requests from there.
I've got a solution for this if you're interested. I wrote a custom debounce that works like this:
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { MD5 } from 'object-hash';
const hash = MD5;
export type CustomGet = {
<T = any, R = AxiosResponse<T>>(
url: string,
config?: CustomHttpRequestConfig,
): Promise<R>;
};
type CustomHttpRequestConfig = Partial<AxiosRequestConfig> & {
debounce?: boolean;
ttl?: number;
};
type Modify<T, R> = Omit<T, keyof R> & R;
export type CustomHttpInstance = Modify<
AxiosInstance,
{
get: CustomGet;
}
>;
export const debounce = (axios: AxiosInstance): CustomHttpInstance => {
const _cache: { [key: string]: Promise<any> } = {};
const cache = (
axiosMethod: CustomGet,
url: string,
config: CustomHttpRequestConfig = {
debounce: true,
ttl: 2000,
},
) => {
const cachekey: string = hash(url);
if (config.debounce === false) {
return axiosMethod(url, config);
}
if (!_cache[cachekey]) {
_cache[cachekey] = axiosMethod(url, config);
}
setTimeout(() => {
delete _cache[cachekey];
}, config.ttl);
return _cache[cachekey];
};
const get: CustomGet = (url, config) => cache(axios.get, url, config);
return {
...axios,
get,
} as CustomHttpInstance;
};
This will expose a client that can take extra debounce
and ttl
options in the GET
request method. For our needs, we specifically only wanted the functionality for GET
requests, but you can see how easy it would be to extend this for other HTTP methods here.
@citypaul Hi,
One gotcha I found doing the debouncing was that returning just the cached value might cause issues if you are using any axios transforms or interceptors.
The data gets cached prior to being processed by the transforms (and/or interceptors, I forget exactly). It looks like your method above is just returning the cached data.
The implementation we are using waits for the axios response and then calls the axios.get with the next request in the Queue. Assuming the first request is cached that will bypass the network request while still getting the response processed by the interceptors/transforms.
We also implemented a dedicated queue cache in case the native request did not have a cache configured for it.
A simple workaround (or even something can be eventually added) is to have an "inflight request" map in front of the cache to store the unresolved promises. So when the promise resolves, All the then
s get the data, and it's safe to consider the request is done, so it can be deleted from the map.
const inflightMap = new Map();
const http = axios.create({
...
});
// ...
if(!inflightMap.has('/users') {
const promise = http.get('/users');
inflightMap.set('/users', promise);
} else {
await inflightMap.get('/users');
await inflightMap.get('/users');
await inflightMap.get('/users');
await inflightMap.get('/users');
inflightMap.delete('./users')
}
This is what I did and works!
Ignore this, go below to the correction
import axios from 'axios' import md5 from 'md5' import { setupCache, serializeQuery } from 'axios-cache-adapter' const { adapter: axiosCacheAdapter, cache } = setupCache({ debug: process.env.NODE_ENV !== 'production', exclude: { query: false }, maxAge: 15 * 60 * 1_000 }) // ------------------------------------------------------- Magic starts here ------------------------------------------------------- const runningRequests = {} const noDuplicateRequestsAdapter = async request => { const requestUrl = `${request.baseURL ? request.baseURL : ''}${request.url}` let requestKey = requestUrl + serializeQuery(request) if (request.data) requestKey = requestKey + md5(request.data) // If there is an exactly equal running request // wait for running request finishes an returns that response if (runningRequests[requestKey]) return await runningRequests[requestKey] // Otherwise // Process the request and add it to running requests runningRequests[requestKey] = axiosCacheAdapter(request) const response = await runningRequests[requestKey] // Delete finished request delete runningRequests[requestKey] return response } const axiosInstance = axios.create({ // Add the adapter and see how magic happens adapter: noDuplicateRequestsAdapter, baseURL: process.env.VUE_APP_API }) axiosInstance.cache = cache // ------------------------------------------------------- Magic ends here -------------------------------------------------------
This is an improvement and correction for my previous answer at 2021-07-14, because I didn't consider an error response and I never delete that errored request from runningRequests
Check the correction:
import axios from 'axios'
import md5 from 'md5'
import { setupCache, serializeQuery } from 'axios-cache-adapter'
const { adapter: axiosCacheAdapter, cache } = setupCache({
debug: process.env.NODE_ENV !== 'production',
exclude: {
query: false
},
maxAge: 15 * 60 * 1_000
})
// ------------------------------------------------------- Magic starts here -------------------------------------------------------
const runningRequests = {}
const noDuplicateRequestsAdapter = request => {
const requestUrl = `${request.baseURL ? request.baseURL : ''}${request.url}`
let requestKey = requestUrl + serializeQuery(request)
if (request.data) requestKey = requestKey + md5(request.data)
// Add the request to runningRequests
if (!runningRequests[requestKey]) runningRequests[requestKey] = axiosCacheAdapter(request)
// Return the response promise
return runningRequests[requestKey].finally(() => {
// Finally, delete the request from the runningRequests whether there's error or not
delete runningRequests[requestKey]
})
}
const axiosInstance = axios.create({
// Add the adapter and see how magic happens
adapter: noDuplicateRequestsAdapter,
baseURL: process.env.VUE_APP_API
})
axiosInstance.cache = cache
// ------------------------------------------------------- Magic ends here -------------------------------------------------------
I was having the same problem. I tried to fix it (and some other things), but I ended up modifying so many things in this package that it felt like it would be more worthwhile to create another one. So i created axios-cache-interceptor
to solve this and other problems I had.
We are already using it in production and it has enough unit tests, so far so good. If you want to take a look: https://github.com/ArthurFiorette/axios-cache-interceptor
I'm sorry for posting another project here, but maybe it will help more people
In my situation,
4 requests run in same time, none of them use cache, I have 4 xhr requests.
Does axios-cache-adapter support caching request before the first request finished, so I can have only 1 real request ?