Last active
September 25, 2021 17:13
-
-
Save ahayes91/f10c4155e3f9c59808d58946b8f97c50 to your computer and use it in GitHub Desktop.
Reusable cache code for Axios and localForage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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