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

@ -127,7 +127,7 @@ Response interceptors can be async and will be awaited.
## Ajax class options
| Property | Type | Default Value | Description |
| -------------------------------- | -------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| -------------------------------- | -------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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` |
| xsrfCookieName | string | `"XSRF-TOKEN"` | The name for the Cross Site Request Forgery cookie |
@ -135,7 +135,7 @@ Response interceptors can be async and will be awaited.
| 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 |
| 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.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 |
@ -150,6 +150,7 @@ Response interceptors can be async and will be awaited.
```js
import { ajax, createCacheInterceptors } from '@lion/ajax';
// Note: getCacheIdentifier can be async
const getCacheIdentifier = () => {
let userId = localStorage.getItem('lion-ajax-cache-demo-user-id');
if (!userId) {

View file

@ -59,14 +59,16 @@ const isResponseSizeSupported = (responseSize, maxResponseSize) => {
/**
* 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
* @returns {RequestInterceptor}
*/
const createCacheRequestInterceptor =
(getCacheId, globalCacheOptions) => /** @param {CacheRequest} request */ async request => {
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
const cacheOptions = extendCacheOptions({
@ -165,7 +167,7 @@ const createCacheResponseInterceptor = globalCacheOptions => async responseParam
/**
* 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
* @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').RequestIdFunction} RequestIdFunction
* @typedef {import('../../types/types.js').RequestInterceptor} RequestInterceptor
* @typedef {import('../../types/types.js').ResponseInterceptor} ResponseInterceptor
*/
describe('cache interceptors', () => {
@ -41,6 +43,8 @@ describe('cache interceptors', () => {
/** @type {Response} */
let mockResponse;
const getCacheIdentifier = () => String(cacheId);
const getCacheIdentifierAsync = () => Promise.resolve(String(cacheId));
/** @type {sinon.SinonSpy} */
let ajaxRequestSpy;
@ -53,6 +57,16 @@ describe('cache interceptors', () => {
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 {CacheOptions} options
@ -63,8 +77,25 @@ describe('cache interceptors', () => {
options,
);
ajaxInstance._requestInterceptors.push(cacheRequestInterceptor);
ajaxInstance._responseInterceptors.push(cacheResponseInterceptor);
assignInterceptors(ajaxInstance, cacheRequestInterceptor, 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(() => {
@ -159,6 +190,28 @@ describe('cache interceptors', () => {
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']`", () => {
newCacheId();

View file

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