Skip to content

Commit

Permalink
feat: Allow getCacheIdentifier cache option to be an asynchronous fun…
Browse files Browse the repository at this point in the history
…ction
  • Loading branch information
ing-athirlwall authored and tlouisse committed May 7, 2024
1 parent df8bf58 commit c5ffe9c
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-roses-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lion/ajax': patch
---

Allow getCacheIdentifier to be asynchronous
37 changes: 19 additions & 18 deletions docs/fundamentals/tools/ajax/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 5 additions & 3 deletions packages/ajax/src/interceptors/cacheInterceptors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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}}
*/
Expand Down
57 changes: 55 additions & 2 deletions packages/ajax/test/interceptors/cacheInterceptors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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;

Expand All @@ -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
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion packages/ajax/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface CacheOptions {
}

export interface CacheOptionsWithIdentifier extends CacheOptions {
getCacheIdentifier?: () => string;
getCacheIdentifier?: () => string|Promise<string>;
}

export interface ValidatedCacheOptions extends CacheOptions {
Expand Down

0 comments on commit c5ffe9c

Please sign in to comment.