feat: Allow getCacheIdentifier cache option to be an asynchronous function
This commit is contained in:
parent
df8bf58f03
commit
c5ffe9cffc
5 changed files with 85 additions and 24 deletions
5
.changeset/little-roses-doubt.md
Normal file
5
.changeset/little-roses-doubt.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/ajax': patch
|
||||
---
|
||||
|
||||
Allow getCacheIdentifier to be asynchronous
|
||||
|
|
@ -126,30 +126,31 @@ 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 |
|
||||
| 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. |
|
||||
| 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.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 |
|
||||
| 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.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.maxCacheSize | number | `undefined` | The maxiumum total size in bytes of the cache; when the cache gets larger it is truncated |
|
||||
| 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 |
|
||||
| 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. |
|
||||
| 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. 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 |
|
||||
| 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.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.maxCacheSize | number | `undefined` | The maxiumum total size in bytes of the cache; when the cache gets larger it is truncated |
|
||||
|
||||
## Caching
|
||||
|
||||
```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) {
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export interface CacheOptions {
|
|||
}
|
||||
|
||||
export interface CacheOptionsWithIdentifier extends CacheOptions {
|
||||
getCacheIdentifier?: () => string;
|
||||
getCacheIdentifier?: () => string|Promise<string>;
|
||||
}
|
||||
|
||||
export interface ValidatedCacheOptions extends CacheOptions {
|
||||
|
|
|
|||
Loading…
Reference in a new issue