diff --git a/.changeset/little-roses-doubt.md b/.changeset/little-roses-doubt.md new file mode 100644 index 0000000000..36b77a74d9 --- /dev/null +++ b/.changeset/little-roses-doubt.md @@ -0,0 +1,5 @@ +--- +'@lion/ajax': patch +--- + +Allow getCacheIdentifier to be asynchronous diff --git a/docs/fundamentals/tools/ajax/overview.md b/docs/fundamentals/tools/ajax/overview.md index ea9b8aa7c9..9fb4ff121a 100644 --- a/docs/fundamentals/tools/ajax/overview.md +++ b/docs/fundamentals/tools/ajax/overview.md @@ -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) { diff --git a/packages/ajax/src/interceptors/cacheInterceptors.js b/packages/ajax/src/interceptors/cacheInterceptors.js index 53b1f3bdc3..54a789c383 100644 --- a/packages/ajax/src/interceptors/cacheInterceptors.js +++ b/packages/ajax/src/interceptors/cacheInterceptors.js @@ -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} 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} getCacheId used to invalidate cache if identifier is changed * @param {CacheOptions} globalCacheOptions * @returns {{cacheRequestInterceptor: RequestInterceptor, cacheResponseInterceptor: ResponseInterceptor}} */ diff --git a/packages/ajax/test/interceptors/cacheInterceptors.test.js b/packages/ajax/test/interceptors/cacheInterceptors.test.js index 70cd5bd19f..97cdf8903e 100644 --- a/packages/ajax/test/interceptors/cacheInterceptors.test.js +++ b/packages/ajax/test/interceptors/cacheInterceptors.test.js @@ -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} 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(); diff --git a/packages/ajax/types/types.ts b/packages/ajax/types/types.ts index d4bc531525..228ccef51c 100644 --- a/packages/ajax/types/types.ts +++ b/packages/ajax/types/types.ts @@ -47,7 +47,7 @@ export interface CacheOptions { } export interface CacheOptionsWithIdentifier extends CacheOptions { - getCacheIdentifier?: () => string; + getCacheIdentifier?: () => string|Promise; } export interface ValidatedCacheOptions extends CacheOptions {