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
|
## 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) {
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue