Skip to content

Instantly share code, notes, and snippets.

@ahayes91
Last active September 25, 2021 17:13
Show Gist options
  • Save ahayes91/f10c4155e3f9c59808d58946b8f97c50 to your computer and use it in GitHub Desktop.
Save ahayes91/f10c4155e3f9c59808d58946b8f97c50 to your computer and use it in GitHub Desktop.
Reusable cache code for Axios and localForage
import { setupCache } from 'axios-cache-adapter';
import localforage from 'localforage';
import memoryDriver from 'localforage-memoryStorageDriver';
import { NO_CACHE } from 'cache-expiration/src/cacheExpirationValues';
import { getUserCtx } from 'authentication-helper';
// Plenty of other fun stuff in here, but not needed for the purposes of this demonstration...
const FORAGE_STORE_PREFIX = 'HMH';
/**
* CACHE_KEY_SEPARATOR separates the prefix from the rest of the URL made for the request.
* This is to allow us to easily strip the prefix from the key if we need to later.
*/
const CACHE_KEY_SEPARATOR = '::';
/**
* Change KILL_CACHE to TRUE if you want to turn off caching, and deploy this change through to PROD (for investigating issues, etc)
* This would probably be better as some kind of configurable browser-setting or feature flag, rather than a kill switch in the code.
* But it's still good to show this an example of what we have so far.
*/
const KILL_CACHE = false;
/**
* The key for each request item in the cache should start with the current user ID.
* We also allow requests to add an optional prefix _after_ the userId to the key used in the localForage store,
* otherwise use the request URL (including query parameters) is appended to the userId after a `::` separator.
* @returns {string}
*/
const setLocalForageKey = request => {
const { userId } = getUserCtx();
return request.cache && request.cache.keyPrefix
? `${userId}_${request.cache.keyPrefix}${CACHE_KEY_SEPARATOR}${request.url}`
: `${userId}${CACHE_KEY_SEPARATOR}${request.url}`;
};
/**
* Create forageStore and cache values for using in axios client:
* - In the forageStore, we name the store with a prefix, and each request key also includes the current userId to prevent conflicts if users log in/out of the same browser
* - localforage.createInstance with the same name will reuse an existing store if it already exists in the browser
* - All requests made through createAxiosCancelable will be logged in the store with a default maxAge of 0, & this can be overridden by the request itself
* - LocalStorage is the default driver for the store, with a JS in-memory driver for Safari in private mode.
* - localforage.WEBSQL didn't appear to provide any caching with Chrome & axios-cache-adapter
* - localforage.INDEXEDDB didn't support multiple tabs open with different user sessions and also left empty databases behind after clearing.
*/
export const forageStore = localforage.createInstance({
name: FORAGE_STORE_PREFIX,
driver: [
localforage.LOCALSTORAGE,
/* eslint-disable-next-line no-underscore-dangle */
memoryDriver._driver,
],
});
const cache = setupCache({
maxAge: NO_CACHE,
// Allow requests with query parameters (query params will appear in cache keys)
exclude: { query: false },
store: forageStore,
key: setLocalForageKey,
});
/**
* Clears the store named FORAGE_STORE_PREFIX, to avoid potential conflicts between users signing into the same browser session.
*
* This occurs:
* - On login of Ed
* - On logout of Ed
* - On unmount of Ed
*
* A browser refresh, tab close without logout, logout, and new login should clear all FORAGE_STORE_PREFIX named items in the cache.
*/
export const clearLocalForageCache = async function() {
await forageStore.clear();
};
/**
* Used to clear related cache items when other requests are made successfully.
* Should _only_ be used by cache-api-helper package.
* @param {string} keyPrefix string value that should be used to identify cache items that need to be expired
*/
export const clearCacheContainingKey = async function(keyPrefix) {
const allCacheKeys = await forageStore.keys();
const keysToClear = allCacheKeys.filter(cacheKey =>
cacheKey.includes(keyPrefix),
);
if (keysToClear && keysToClear.length > 0) {
await keysToClear.forEach(async keyToClear => {
const result = await forageStore.getItem(keyToClear);
if (result && 'expires' in result) {
result.expires = Date.now(); // immediately expire
await forageStore.setItem(keyToClear, result);
}
});
}
};
/**
* @param {number} min retry delay in ms
* @param {number} max retry delay in ms
* @param {boolean} includeAuth whether to include Authorization in headers (default to true and will fetch current sif)
* @returns {Object} cancelableAxios
* @returns {AxiosInstance} cancelableAxios.client
* @returns {function} cancelableAxios.cancel
* @returns {Promise} cancelableAxios.cancelToken
* @returns {function} cancelableAxios.isCancel
*/
export const createAxiosCancelable = ({
min = 1000,
max = 15000,
retryCondition,
includeAuth = true,
} = {}) => {
const canceler = axios.CancelToken.source();
const auth = includeAuth ? getAuthHeader() : getHeaderWithoutAuth();
/*
This might return something like:
headers: {
Authorization: accessToken,
CorrelationId: correlationId,
...optionalHeaders,
},
*/
// Only use the cache if the user is logged in
if (getUserCtx().userId && !KILL_CACHE) {
auth.adapter = cache.adapter;
}
const client = axios.create(auth);
axiosRetry(client, backoff(min, max, retryCondition));
client.interceptors.response.use(null, errorInterceptor);
/**
* Axios error interceptor,
* that sends the error to the main Error Store.
* It also handles the redirect if a user navigates to Ed when not logged in.
*
*/
return {
client,
cancel: canceler.cancel,
cancelToken: canceler.token,
isCancel: axios.isCancel,
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment