feat: Allow getCacheIdentifier cache option to be an asynchronous function

This commit is contained in:
Alex Thirlwall 2024-04-18 15:14:00 +02:00 committed by Thijs Louisse
parent df8bf58f03
commit c5ffe9cffc
5 changed files with 85 additions and 24 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ajax': patch
---
Allow getCacheIdentifier to be asynchronous

View file

@ -126,30 +126,31 @@ Response interceptors can be async and will be awaited.
## Ajax class options ## Ajax class options
| Property | Type | Default Value | Description | | Property | Type | Default Value | Description |
| -------------------------------- | -------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------------------- | -------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| addAcceptLanguage | boolean | `true` | Whether to add the Accept-Language header from the `data-localize-lang` document property | | addAcceptLanguage | boolean | `true` | Whether to add the Accept-Language header from the `data-localize-lang` document property |
| addCaching | boolean | `false` | Whether to add the cache interceptor and start storing responses in the cache, even if `cacheOptions.useCache` is `false` | | addCaching | boolean | `false` | Whether to add the cache interceptor and start storing responses in the cache, even if `cacheOptions.useCache` is `false` |
| xsrfCookieName | string | `"XSRF-TOKEN"` | The name for the Cross Site Request Forgery cookie | | xsrfCookieName | string | `"XSRF-TOKEN"` | The name for the Cross Site Request Forgery cookie |
| xsrfHeaderName | string | `"X-XSRF-TOKEN"` | The name for the Cross Site Request Forgery header | | xsrfHeaderName | string | `"X-XSRF-TOKEN"` | The name for the Cross Site Request Forgery header |
| xsrfTrustedOrigins | string[] | [] | List of trusted origins, the XSRF header will also be added if the origin is in this list. | | xsrfTrustedOrigins | string[] | [] | List of trusted origins, the XSRF header will also be added if the origin is in this list. |
| jsonPrefix | string | `""` | The prefix to add to add to responses for the `.fetchJson` functions | | jsonPrefix | string | `""` | The prefix to add to add to responses for the `.fetchJson` functions |
| cacheOptions.useCache | boolean | `false` | Whether to use the default cache interceptors to cache requests | | cacheOptions.useCache | boolean | `false` | Whether to use the default cache interceptors to cache requests |
| cacheOptions.getCacheIdentifier | function | a function returning the string `_default` | A function to determine the cache that should be used for each request; used to make sure responses for one session are not used in the next | | cacheOptions.getCacheIdentifier | function | a function returning the string `_default`. | A function to determine the cache that should be used for each request; used to make sure responses for one session are not used in the next. Can be async. |
| cacheOptions.methods | string[] | `["get"]` | The HTTP methods to cache reponses for. Any other method will invalidate the cache for this request, see "Invalidating cache", below | | cacheOptions.methods | string[] | `["get"]` | The HTTP methods to cache reponses for. Any other method will invalidate the cache for this request, see "Invalidating cache", below |
| cacheOptions.maxAge | number | `360000` | The time to keep a response in the cache before invalidating it automatically | | cacheOptions.maxAge | number | `360000` | The time to keep a response in the cache before invalidating it automatically |
| cacheOptions.invalidateUrls | string[] | `undefined` | Urls to invalidate each time a method not in `cacheOptions.methods` is encountered, see "Invalidating cache", below | | cacheOptions.invalidateUrls | string[] | `undefined` | Urls to invalidate each time a method not in `cacheOptions.methods` is encountered, see "Invalidating cache", below |
| cacheOptions.invalidateUrlsRegex | regex | `undefined` | Regular expression matching urls to invalidate each time a method not in `cacheOptions.methods` is encountered, see "Invalidating cache", below | | cacheOptions.invalidateUrlsRegex | regex | `undefined` | Regular expression matching urls to invalidate each time a method not in `cacheOptions.methods` is encountered, see "Invalidating cache", below |
| cacheOptions.requestIdFunction | function | a function returning the base url and serialized search parameters | Function to determine what defines a unique URL | | cacheOptions.requestIdFunction | function | a function returning the base url and serialized search parameters | Function to determine what defines a unique URL |
| cacheOptions.contentTypes | string[] | `undefined` | Whitelist of content types that will be stored to or retrieved from the cache | | cacheOptions.contentTypes | string[] | `undefined` | Whitelist of content types that will be stored to or retrieved from the cache |
| cacheOptions.maxResponseSize | number | `undefined` | The maximum response size in bytes that will be stored to or retrieved from the cache | | cacheOptions.maxResponseSize | number | `undefined` | The maximum response size in bytes that will be stored to or retrieved from the cache |
| cacheOptions.maxCacheSize | number | `undefined` | The maxiumum total size in bytes of the cache; when the cache gets larger it is truncated | | cacheOptions.maxCacheSize | number | `undefined` | The maxiumum total size in bytes of the cache; when the cache gets larger it is truncated |
## Caching ## Caching
```js ```js
import { ajax, createCacheInterceptors } from '@lion/ajax'; import { ajax, createCacheInterceptors } from '@lion/ajax';
// Note: getCacheIdentifier can be async
const getCacheIdentifier = () => { const getCacheIdentifier = () => {
let userId = localStorage.getItem('lion-ajax-cache-demo-user-id'); let userId = localStorage.getItem('lion-ajax-cache-demo-user-id');
if (!userId) { if (!userId) {

View file

@ -59,14 +59,16 @@ const isResponseSizeSupported = (responseSize, maxResponseSize) => {
/** /**
* Request interceptor to return relevant cached requests * Request interceptor to return relevant cached requests
* @param {function(): string} getCacheId used to invalidate cache if identifier is changed * @param {function(): string|Promise<string>} getCacheId used to invalidate cache if identifier is changed
* @param {CacheOptions} globalCacheOptions * @param {CacheOptions} globalCacheOptions
* @returns {RequestInterceptor} * @returns {RequestInterceptor}
*/ */
const createCacheRequestInterceptor = const createCacheRequestInterceptor =
(getCacheId, globalCacheOptions) => /** @param {CacheRequest} request */ async request => { (getCacheId, globalCacheOptions) => /** @param {CacheRequest} request */ async request => {
validateCacheOptions(request.cacheOptions); validateCacheOptions(request.cacheOptions);
const cacheSessionId = getCacheId(); const getCacheIdResult = getCacheId();
const isPromise = typeof getCacheIdResult !== 'string' && 'then' in getCacheIdResult;
const cacheSessionId = isPromise ? await getCacheIdResult : getCacheIdResult;
resetCacheSession(cacheSessionId); // cacheSessionId is used to bind the cache to the current session resetCacheSession(cacheSessionId); // cacheSessionId is used to bind the cache to the current session
const cacheOptions = extendCacheOptions({ const cacheOptions = extendCacheOptions({
@ -165,7 +167,7 @@ const createCacheResponseInterceptor = globalCacheOptions => async responseParam
/** /**
* Response interceptor to cache relevant requests * Response interceptor to cache relevant requests
* @param {function(): string} getCacheId used to invalidate cache if identifier is changed * @param {function(): string|Promise<string>} getCacheId used to invalidate cache if identifier is changed
* @param {CacheOptions} globalCacheOptions * @param {CacheOptions} globalCacheOptions
* @returns {{cacheRequestInterceptor: RequestInterceptor, cacheResponseInterceptor: ResponseInterceptor}} * @returns {{cacheRequestInterceptor: RequestInterceptor, cacheResponseInterceptor: ResponseInterceptor}}
*/ */

View file

@ -21,6 +21,8 @@ let ajax;
/** /**
* @typedef {import('../../types/types.js').CacheOptions} CacheOptions * @typedef {import('../../types/types.js').CacheOptions} CacheOptions
* @typedef {import('../../types/types.js').RequestIdFunction} RequestIdFunction * @typedef {import('../../types/types.js').RequestIdFunction} RequestIdFunction
* @typedef {import('../../types/types.js').RequestInterceptor} RequestInterceptor
* @typedef {import('../../types/types.js').ResponseInterceptor} ResponseInterceptor
*/ */
describe('cache interceptors', () => { describe('cache interceptors', () => {
@ -41,6 +43,8 @@ describe('cache interceptors', () => {
/** @type {Response} */ /** @type {Response} */
let mockResponse; let mockResponse;
const getCacheIdentifier = () => String(cacheId); const getCacheIdentifier = () => String(cacheId);
const getCacheIdentifierAsync = () => Promise.resolve(String(cacheId));
/** @type {sinon.SinonSpy} */ /** @type {sinon.SinonSpy} */
let ajaxRequestSpy; let ajaxRequestSpy;
@ -53,6 +57,16 @@ describe('cache interceptors', () => {
return cacheId; return cacheId;
}; };
/**
* @param {Ajax} ajaxInstance
* @param {RequestInterceptor} cacheRequestInterceptor
* @param {ResponseInterceptor} cacheResponseInterceptor
*/
const assignInterceptors = (ajaxInstance, cacheRequestInterceptor, cacheResponseInterceptor) => {
ajaxInstance._requestInterceptors.push(cacheRequestInterceptor);
ajaxInstance._responseInterceptors.push(cacheResponseInterceptor);
};
/** /**
* @param {Ajax} ajaxInstance * @param {Ajax} ajaxInstance
* @param {CacheOptions} options * @param {CacheOptions} options
@ -63,8 +77,25 @@ describe('cache interceptors', () => {
options, options,
); );
ajaxInstance._requestInterceptors.push(cacheRequestInterceptor); assignInterceptors(ajaxInstance, cacheRequestInterceptor, cacheResponseInterceptor);
ajaxInstance._responseInterceptors.push(cacheResponseInterceptor); };
/**
* @param {Ajax} ajaxInstance
* @param {CacheOptions} options
* @param {() => string|Promise<string>} customGetCacheIdentifier
*/
const addCacheInterceptorsWithCustomGetCacheIdentifier = (
ajaxInstance,
options,
customGetCacheIdentifier,
) => {
const { cacheRequestInterceptor, cacheResponseInterceptor } = createCacheInterceptors(
customGetCacheIdentifier,
options,
);
assignInterceptors(ajaxInstance, cacheRequestInterceptor, cacheResponseInterceptor);
}; };
beforeEach(() => { beforeEach(() => {
@ -159,6 +190,28 @@ describe('cache interceptors', () => {
cacheId = cacheSessionId; cacheId = cacheSessionId;
}); });
it('validates an async cache identifier function', async () => {
const cacheSessionId = cacheId;
// @ts-ignore needed for test
cacheId = '';
addCacheInterceptorsWithCustomGetCacheIdentifier(
ajax,
{ useCache: true },
getCacheIdentifierAsync,
);
await ajax
.fetch('/test')
.then(() => expect.fail('fetch should not resolve here'))
.catch(
/** @param {Error} err */ err => {
expect(err.message).to.equal('Invalid cache identifier');
},
)
.finally(() => {});
cacheId = cacheSessionId;
});
it("throws when using methods other than `['get']`", () => { it("throws when using methods other than `['get']`", () => {
newCacheId(); newCacheId();

View file

@ -47,7 +47,7 @@ export interface CacheOptions {
} }
export interface CacheOptionsWithIdentifier extends CacheOptions { export interface CacheOptionsWithIdentifier extends CacheOptions {
getCacheIdentifier?: () => string; getCacheIdentifier?: () => string|Promise<string>;
} }
export interface ValidatedCacheOptions extends CacheOptions { export interface ValidatedCacheOptions extends CacheOptions {