Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make getCacheIdentifier config property asynchronous #2266

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -159,7 +161,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 @@ -16,6 +16,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 @@ -36,6 +38,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 @@ -48,6 +52,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 @@ -58,8 +72,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 @@ -154,6 +185,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
Loading