kuitos / axios-extensions

🍱 axios extensions lib, including throttle, cache, retry features etc...
MIT License
831 stars 49 forks source link

Failed call disappears from the cache #41

Closed speyou closed 5 years ago

speyou commented 5 years ago

Hi, first I want to say this is an awesome lib! Thanks for sharing. I'm not sure I am using it well in the following scenario: 1- cache all requests 2- forceUpdate all requests in order to have the latest data from server 3- if a request fails because of lost connection, I start a so called "offline mode" and set forceUpdate to false 4- I can use my app offline if I go on all the places I already went to (because they are cached) 5- all the places BUT the first one, the one that failed at step 3. It's either not cached or cached with failure. Even if I had it cached previously, I lost the data.

My question is: is it possible to keep the cache even if the axios call is rejected?

kuitos commented 5 years ago

Sorry for the delay, and thanks for your nice comment! Your question may related with this issue https://github.com/kuitos/axios-extensions/issues/37 Feel free to ask any questions!

speyou commented 5 years ago

No problem for the delay, thanks very much for your answer! I tried to play with a customAdapter on top of yours but I don't find a way to: a) prevent the deletion of the cached result b) make another call to the cache

function customAdapter(adapter) {
  return async config => {
    try {
      // I think cache.del(index) will be called here anyway
      return await adapter(config);
    } catch (e) {
      if (e.request && e.request.status === 0) {
        // how to call cacheAdapterEnhancer again and cache.get(index)?
      }
      throw e;
    }
  };
}

export const http = axios.create({
  // setup default URL
  baseURL: API_URL,
  headers: { "Cache-Control": "no-cache" },
  adapter: cacheAdapterEnhancer(customAdapter(axios.defaults.adapter))
});
kuitos commented 5 years ago

You can make a request interceptor which returns the previous cache while in 'offline mode'. here is minimal example you can refer to:


import { Cache, cacheAdapterEnhancer } from 'axios-extensions'
import buildSortedURL from 'axios-extensions/lib/utils/buildSortedURL'

const cache = new Cache();

export const http = axios.create({
  // setup default URL
  baseURL: API_URL,
  headers: { "Cache-Control": "no-cache" },
  adapter: cacheAdapterEnhancer(axios.defaults.adapter, { defaultCache: cache }))
});

http.interceptors.request.use(config => config, error => {
  if (inOfflineMode) {
    const { url, params, paramsSerializer } = errror.config
    const index = buildSortedURL(url, params, paramsSerializer)
    return cache.get(index)
  }

  return Promise.reject(error)
})
speyou commented 5 years ago

Thanks for your most precious help! Since I'm using default caching + forceUpdate (in order to use cache ONLY in the event of lost connection), I had to resort to a different process. I'm now using two caches: the default one (as you suggested above) and an "offline cache" that will allow the first timeout call to be answered with cache (because the default cache will delete anyway after the timeout).

If I can use some of your time again, can you give your opinion about this? Especially performance wise, is it okay to have 2 caches? (the offline one being longer in time but shorter in size).

// https://github.com/kuitos/axios-extensions
import buildSortedURL from "axios-extensions/lib/utils/buildSortedURL";
import { Cache } from "axios-extensions";
// the defaultCache used by axios
import { cache } from "../plugins/http";
import ConnectionService from "../services/connection.service";

const TWENTY_MINUTES = 1000 * 60 * 20;
const CAPACITY = 20;

export const offlineCache = new Cache({ maxAge: TWENTY_MINUTES, max: CAPACITY });

export function offlineRequestInterceptor(config) {
  // since cache is enabled by default
  // we need to specify forceUpdate
  // in order to always get the latest data from server
  config.forceUpdate = true;

  // in case of offline, we stop updating
  // and return the cached result instead
  if (ConnectionService.isOffline() === true) {
    config.forceUpdate = false;
  }

  return config;
}

export function offlineResponseInterceptor(response) {
  const { method, url, params, paramsSerializer } = response.config;
  // we cache only get methods and  don't cache offline results
  if (method === "get" && ConnectionService.isOffline() === false) {
    // we get the unique key needed for the cache
    const index = buildSortedURL(url, params, paramsSerializer);
    // and we set the offline cache
    offlineCache.set(index, cache.get(index));
  }

  return response;
}

export async function offlineResponseErrorInterceptor(error) {
  // error.request is an instance of XMLHttpRequest
  const status = error.request && error.request.status;
  // be sure it's a couldn't reach server error (status 0)
  // and of course not already offline
  if (status === 0 && ConnectionService.isOffline() === false) {
    // we await the answer in order to be able to use the following lines
    await ConnectionService.checkOfflineStatus();
  }
  // is the app offline?
  if (ConnectionService.isOffline() === true) {
    const { method, url, params, paramsSerializer } = error.config;
    // get the cached version (if any)
    if (method === "get") {
      const index = buildSortedURL(url, params, paramsSerializer);
      // the beauty of it reside in the fact that
      // requests won't fail if cached by regular cache
      // so offline cache will get useful only for the first few timeout-pending requests
      return offlineCache.get(index);
    }
  }

  // if error is not handled yet, we return falty promise
  return Promise.reject(error);
}

And somewhere else:

  httpInstance.interceptors.request.use(offlineRequestInterceptor);
  httpInstance.interceptors.response.use(offlineResponseInterceptor, offlineResponseErrorInterceptor);
kuitos commented 5 years ago

Awesome workaround! From my rough review, the advice is that u could use the native web api to check the offline status, such as navigator.onLine, and remove the offlineResponseInterceptor interceptor.

export function offlineRequestInterceptor(config) {
  // since cache is enabled by default
  // we need to specify forceUpdate
  // in order to always get the latest data from server
  config.forceUpdate = true;

  // in case of offline, we stop updating
  // and return the cached result instead
  if (navigator.onLine === false) {
    config.forceUpdate = false;
    const index = buildSortedURL(url, params, paramsSerializer);
    offlineCache.set(index, cache.get(index));
  } 
  return config;
}

export async function offlineResponseInterceptor(error) {
  // error.request is an instance of XMLHttpRequest
  const status = error.request && error.request.status;

  if (navigator.onLine === false) {
    const { method, url, params, paramsSerializer } = error.config;
    // get the cached version (if any)
    if (method === "get") {
      const index = buildSortedURL(url, params, paramsSerializer);
      // the beauty of it reside in the fact that
      // requests won't fail if cached by regular cache
      // so offline cache will get useful only for the first few timeout-pending requests
      return offlineCache.get(index);
    }
  }

  // if error is not handled yet, we return falty promise
  return Promise.reject(error);
}
kuitos commented 5 years ago

And, if you do not wanna enable the cache by default, you could use the config like below to instead of forceUpdate setting:

adapter: cacheAdapterEnhancer(axios.defaults.adapter, { enabledByDefault: false }))

For more information u could check the api doc https://github.com/kuitos/axios-extensions#cacheadapterenhancer

speyou commented 5 years ago

If I don't enabledByDefault I witnessed it won't cache anything. I want it to cache but I don't want it to access this cache unless its offline.

Anyway you answered my doubts and I have a neat feature thanks to you now, we can close. :)