import Cache from './Cache.js'; import PendingRequestStore from './PendingRequestStore.js'; /** * @typedef {import('../types/types.js').CacheRequest} CacheRequest * @typedef {import('../types/types.js').CacheOptions} CacheOptions * @typedef {import('../types/types.js').ValidatedCacheOptions} ValidatedCacheOptions */ /** * The id for the cache session * @type {string | undefined} */ let cacheSessionId; /** * The ajax cache * @type {Cache} */ export const ajaxCache = new Cache(); /** * The pending request store * @type {PendingRequestStore} */ export const pendingRequestStore = new PendingRequestStore(); /** * Checks whether the given cacheSessionId matches the currently active id. * * @param {string|undefined} cacheId The cache id to check */ export const isCurrentSessionId = cacheId => cacheId === cacheSessionId; /** * Sets the current cache session ID. * * @param {string} id The id that will be tied to the current session */ export const setCacheSessionId = id => { cacheSessionId = id; }; /** * Resets the cache session when the cacheId changes. * * There can be only 1 active session at all times. * @param {string} cacheId The cache id that is tied to the current session */ export const resetCacheSession = cacheId => { if (!cacheId) { throw new Error('Invalid cache identifier'); } if (!isCurrentSessionId(cacheId)) { setCacheSessionId(cacheId); ajaxCache.reset(); pendingRequestStore.reset(); } }; /** * Stringify URL search params * @param {*} params query string parameters object * @returns {string} of querystring parameters WITHOUT `?` or empty string '' */ const stringifySearchParams = (params = {}) => typeof params === 'object' && params !== null ? new URLSearchParams(params).toString() : ''; /** * Returns request key string, which uniquely identifies a Request * @param {Partial} request Request object * @param {function} serializeSearchParams Function to serialize URL search params * @returns {string} requestId to uniquely identify a request */ const DEFAULT_GET_REQUEST_ID = ( { url = '', params }, serializeSearchParams = stringifySearchParams, ) => { const serializedParams = serializeSearchParams(params); return serializedParams ? `${url}?${serializedParams}` : url; }; /** * Defaults to 1 hour */ const DEFAULT_MAX_AGE = 1000 * 60 * 60; /** * @param {CacheOptions} options Cache options * @returns {ValidatedCacheOptions} */ export const extendCacheOptions = ({ useCache = false, methods = ['get'], maxAge = DEFAULT_MAX_AGE, requestIdFunction = DEFAULT_GET_REQUEST_ID, invalidateUrls, invalidateUrlsRegex, contentTypes, maxResponseSize, maxCacheSize, }) => ({ useCache, methods, maxAge, requestIdFunction, invalidateUrls, invalidateUrlsRegex, contentTypes, maxResponseSize, maxCacheSize, }); /** * @param {CacheOptions} options Cache options */ export const validateCacheOptions = ({ useCache, methods, maxAge, requestIdFunction, invalidateUrls, invalidateUrlsRegex, contentTypes, maxResponseSize, maxCacheSize, } = {}) => { if (useCache !== undefined && typeof useCache !== 'boolean') { throw new Error('Property `useCache` must be a `boolean`'); } if (methods !== undefined && JSON.stringify(methods) !== JSON.stringify(['get'])) { throw new Error('Cache can only be utilized with `GET` method'); } if (maxAge !== undefined && !Number.isFinite(maxAge)) { throw new Error('Property `maxAge` must be a finite `number`'); } if (invalidateUrls !== undefined && !Array.isArray(invalidateUrls)) { throw new Error('Property `invalidateUrls` must be an `Array` or `undefined`'); } if (invalidateUrlsRegex !== undefined && !(invalidateUrlsRegex instanceof RegExp)) { throw new Error('Property `invalidateUrlsRegex` must be a `RegExp` or `undefined`'); } if (requestIdFunction !== undefined && typeof requestIdFunction !== 'function') { throw new Error('Property `requestIdFunction` must be a `function`'); } if (contentTypes !== undefined && !Array.isArray(contentTypes)) { throw new Error('Property `contentTypes` must be an `Array` or `undefined`'); } if (maxResponseSize !== undefined && !Number.isFinite(maxResponseSize)) { throw new Error('Property `maxResponseSize` must be a finite `number`'); } if (maxCacheSize !== undefined && !Number.isFinite(maxCacheSize)) { throw new Error('Property `maxCacheSize` must be a finite `number`'); } }; /** * Invalidates matching requestIds in the cache and pendingRequestStore * * There are two kinds of invalidate rules: * invalidateUrls (array of URL like strings) * invalidateUrlsRegex (RegExp) * If a non-GET method is fired, by default it only invalidates its own endpoint. * Invalidating /api/users cache by doing a PATCH, will not invalidate /api/accounts cache. * However, in the case of users and accounts, they may be very interconnected, * so perhaps you do want to invalidate /api/accounts when invalidating /api/users. * If it's NOT one of the config.methods, invalidate caches * * @param {string} requestId * @param {CacheOptions} cacheOptions */ export const invalidateMatchingCache = (requestId, { invalidateUrls, invalidateUrlsRegex }) => { // invalidate this request ajaxCache.delete(requestId); pendingRequestStore.resolve(requestId); // also invalidate caches matching to invalidateUrls if (Array.isArray(invalidateUrls)) { invalidateUrls.forEach(url => { ajaxCache.delete(url); pendingRequestStore.resolve(url); }); } // also invalidate caches matching to invalidateUrlsRegex if (invalidateUrlsRegex) { ajaxCache.deleteMatching(invalidateUrlsRegex); pendingRequestStore.resolveMatching(invalidateUrlsRegex); } };