diff --git a/.changeset/eleven-tips-grow.md b/.changeset/eleven-tips-grow.md
new file mode 100644
index 000000000..3518bbc0c
--- /dev/null
+++ b/.changeset/eleven-tips-grow.md
@@ -0,0 +1,5 @@
+---
+'@lion/ajax': minor
+---
+
+Set fromCache property on the Response, for user consumption. Allow setting cacheOptions on the AjaxClient upon instantiation. Create docs/demos.
diff --git a/.storybook/main.js b/.storybook/main.js
index eb21ffd6c..81b7fdd96 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -3,7 +3,7 @@ const path = require('path');
module.exports = {
stories: [
- '../{packages,packages-node}/!(ajax)*/README.md',
+ '../{packages,packages-node}/*/README.md',
'../{packages,packages-node}/*/docs/*.md',
'../{packages,packages-node}/*/docs/!(assets)**/*.md',
'../packages/helpers/*/README.md',
diff --git a/packages/ajax/README.md b/packages/ajax/README.md
index e5cc99163..40705a2cd 100644
--- a/packages/ajax/README.md
+++ b/packages/ajax/README.md
@@ -2,6 +2,36 @@
# Ajax
+```js script
+import { html } from '@lion/core';
+import { renderLitAsNode } from '@lion/helpers';
+import { ajax, AjaxClient, cacheRequestInterceptorFactory, cacheResponseInterceptorFactory } from '@lion/ajax';
+import '@lion/helpers/sb-action-logger';
+
+const getCacheIdentifier = () => {
+ let userId = localStorage.getItem('lion-ajax-cache-demo-user-id');
+ if (!userId) {
+ localStorage.setItem('lion-ajax-cache-demo-user-id', '1');
+ userId = '1';
+ }
+ return userId;
+}
+
+const cacheOptions = {
+ useCache: true,
+ timeToLive: 1000 * 60 * 10, // 10 minutes
+};
+
+ajax.addRequestInterceptor(cacheRequestInterceptorFactory(getCacheIdentifier, cacheOptions));
+ajax.addResponseInterceptor(
+ cacheResponseInterceptorFactory(getCacheIdentifier, cacheOptions),
+);
+
+export default {
+ title: 'Ajax/Ajax',
+};
+```
+
`ajax` is a small wrapper around `fetch` which:
- Allows globally registering request and response interceptors
@@ -27,11 +57,27 @@ npm i --save @lion/ajax
#### GET request
-```js
-import { ajax } from '@lion/ajax';
-
-const response = await ajax.request('/api/users');
-const users = await response.json();
+```js preview-story
+export const getRequest = () => {
+ const actionLogger = renderLitAsNode(html``);
+ const fetchHandler = (name) => {
+ ajax.request(`./packages/ajax/docs/${name}.json`)
+ .then(response => response.json())
+ .then(result => {
+ actionLogger.log(JSON.stringify(result, null, 2));
+ });
+ }
+ return html`
+
+
+
+ ${actionLogger}
+ `;
+}
```
#### POST request
@@ -48,14 +94,33 @@ const newUser = await response.json();
### JSON requests
-We usually deal with JSON requests and responses. With `requestJson` you don't need to specifically stringify the request body or parse the response body:
+We usually deal with JSON requests and responses. With `requestJson` you don't need to specifically stringify the request body or parse the response body.
+
+The result will have the Response object on `.response` property, and the decoded json will be available on `.body`.
#### GET JSON request
-```js
-import { ajax } from '@lion/ajax';
-
-const { response, body } = await ajax.requestJson('/api/users');
+```js preview-story
+export const getJsonRequest = () => {
+ const actionLogger = renderLitAsNode(html``);
+ const fetchHandler = (name) => {
+ ajax.requestJson(`./packages/ajax/docs/${name}.json`)
+ .then(result => {
+ console.log(result.response);
+ actionLogger.log(JSON.stringify(result.body, null, 2));
+ });
+ }
+ return html`
+
+
+
+ ${actionLogger}
+ `;
+}
```
#### POST JSON request
@@ -73,32 +138,54 @@ const { response, body } = await ajax.requestJson('/api/users', {
Different from fetch, `ajax` throws when the server returns a 4xx or 5xx, returning the request and response:
-```js
-import { ajax } from '@lion/ajax';
-
-try {
- const users = await ajax.requestJson('/api/users');
-} catch (error) {
- if (error.response) {
- if (error.response.status === 400) {
- // handle a specific status code, for example 400 bad request
- } else {
- console.error(error);
+```js preview-story
+export const errorHandling = () => {
+ const actionLogger = renderLitAsNode(html``);
+ const fetchHandler = async () => {
+ try {
+ const users = await ajax.requestJson('/api/users');
+ } catch (error) {
+ if (error.response) {
+ if (error.response.status === 400) {
+ // handle a specific status code, for example 400 bad request
+ } else {
+ actionLogger.log(error);
+ }
+ } else {
+ // an error happened before receiving a response, ex. an incorrect request or network error
+ actionLogger.log(error);
+ }
}
- } else {
- // an error happened before receiving a response, ex. an incorrect request or network error
- console.error(error);
}
+ return html`
+
+
+ ${actionLogger}
+ `;
}
```
+## Fetch Polyfill
+
+For IE11 you will need a polyfill for fetch. You should add this on your top level layer, e.g. your application.
+
+[This is the polyfill we recommend](https://github.com/github/fetch). It also has a [section for polyfilling AbortController](https://github.com/github/fetch#aborting-requests)
+
## Ajax Cache
-A caching library that uses `lion-web/ajax` and adds cache interceptors to provide caching for use in
+A caching library that uses `@lion/ajax` and adds cache interceptors to provide caching for use in
frontend `services`.
-> Technical documentation and decisions can be found in
-> [./docs/technical-docs.md](./docs/technical-docs.md)
+The **request interceptor**'s main goal is to determine whether or not to
+**return the cached object**. This is done based on the options that are being
+passed.
+
+The **response interceptor**'s goal is to determine **when to cache** the
+requested response, based on the options that are being passed.
### Getting started
@@ -133,113 +220,247 @@ ajax.addResponseInterceptor(
const { response, body } = await ajax.requestJson('/my-url');
```
-### Ajax cache example
+Alternatively, most often for subclassers, you can extend or import `AjaxClient` yourself, and pass cacheOptions when instantiating the ajax singleton.
```js
-import {
- ajax,
- cacheRequestInterceptorFactory,
- cacheResponseInterceptorFactory,
-} from '@lion-web/ajax';
+import { AjaxClient } from '@lion/ajax';
-const getCacheIdentifier = () => getActiveProfile().profileId;
+export const ajax = new AjaxClient({
+ cacheOptions: {
+ useCache: true,
+ timeToLive: 1000 * 60 * 5, // 5 minutes
+ getCacheIdentifier: () => getActiveProfile().profileId,
+ },
+})
+```
-const globalCacheOptions = {
- useCache: false,
- timeToLive: 50, // default: one hour (the cache instance will be replaced in 1 hour, regardless of this setting)
- methods: ['get'], // default: ['get'] NOTE for now only 'get' is supported
- // requestIdentificationFn: (requestConfig) => { }, // see docs below for more info
- // invalidateUrls: [], see docs below for more info
- // invalidateUrlsRegex: RegExp, // see docs below for more info
-};
+### Ajax cache example
-// pass a function to the interceptorFactory that retrieves a cache identifier
-// ajax.interceptors.request.use(cacheRequestInterceptorFactory(getCacheIdentifier, cacheOptions));
-// ajax.interceptors.response.use(
-// cacheResponseInterceptorFactory(getCacheIdentifier, cacheOptions),
-// );
+> Let's assume that we have a user session, for this demo purposes we already created an identifier function for this and set the cache interceptors.
-class TodoService {
- constructor() {
- this.localAjaxConfig = {
- cacheOptions: {
- invalidateUrls: ['/api/todosbykeyword'], // default: []
- },
- };
- }
-
- /**
- * Returns all todos from cache if not older than 5 minutes
- */
- getTodos() {
- return ajax.requestJson(`/api/todos`, this.localAjaxConfig);
- }
-
- /**
- *
- */
- getTodosByKeyword(keyword) {
- return ajax.requestJson(`/api/todosbykeyword/${keyword}`, this.localAjaxConfig);
- }
-
- /**
- * Creates new todo and invalidates cache.
- * `getTodos` will NOT take the response from cache
- */
- saveTodo(todo) {
- return ajax.requestJson(`/api/todos`, { method: 'POST', body: todo, ...this.localAjaxConfig });
+We can see if a response is served from the cache by checking the `response.fromCache` property,
+which is either undefined for normal requests, or set to true for responses that were served from cache.
+
+```js preview-story
+export const cache = () => {
+ const actionLogger = renderLitAsNode(html``);
+ const fetchHandler = (name) => {
+ ajax.requestJson(`./packages/ajax/docs/${name}.json`)
+ .then(result => {
+ actionLogger.log(`From cache: ${result.response.fromCache || false}`);
+ actionLogger.log(JSON.stringify(result.body, null, 2));
+ });
}
+ return html`
+
+
+
+ ${actionLogger}
+ `;
}
```
-If a value returned by `cacheIdentifier` changes the cache is reset. We avoid situation of accessing old cache and proactively clean it, for instance when a user session is ended.
+You can also change the cache options per request, which is handy if you don't want to remove and re-add the interceptors for a simple configuration change.
-### Ajax cache Options
+In this demo, when we fetch naga, we always pass `useCache: false` so the Response is never a cached one.
-```js
-const cacheOptions = {
- // `useCache`: determines wether or not to use the cache
- // can be boolean
- // default: false
- useCache: true,
+```js preview-story
+export const cacheActionOptions = () => {
+ const actionLogger = renderLitAsNode(html``);
+ const fetchHandler = (name) => {
+ let actionCacheOptions;
+ if (name === 'naga') {
+ actionCacheOptions = {
+ useCache: false,
+ }
+ }
- // `timeToLive`: is the time the cache should be kept in ms
- // default: 0
- // Note: regardless of this setting, the cache instance holding all the caches
- // will be invalidated after one hour
- timeToLive: 1000 * 60 * 5,
-
- // `methods`: an array of methods on which this configuration is applied
- // Note: when `useCache` is `false` this will not be used
- // NOTE: ONLY GET IS SUPPORTED
- // default: ['get']
- methods: ['get'],
-
- // `invalidateUrls`: an array of strings that for each string that partially
- // occurs as key in the cache, will be removed
- // default: []
- // Note: can be invalidated only by non-get request to the same url
- invalidateUrls: ['/api/todosbykeyword'],
-
- // `invalidateUrlsRegex`: a RegExp object to match and delete
- // each matched key in the cache
- // Note: can be invalidated only by non-get request to the same url
- invalidateUrlsRegex: /posts/
-
- // `requestIdentificationFn`: a function to provide a string that should be
- // taken as a key in the cache.
- // This can be used to cache post-requests.
- // default: (requestConfig, searchParamsSerializer) => url + params
- requestIdentificationFn: (request, serializer) => {
- return `${request.url}?${serializer(request.params)}`;
- },
-};
+ ajax.requestJson(`./packages/ajax/docs/${name}.json`, { cacheOptions: actionCacheOptions })
+ .then(result => {
+ actionLogger.log(`From cache: ${result.response.fromCache || false}`);
+ actionLogger.log(JSON.stringify(result.body, null, 2));
+ });
+ }
+ return html`
+
+
+
+ ${actionLogger}
+ `;
+}
```
-## Considerations
+### Invalidating cache
-## Fetch Polyfill
+Invalidating the cache, or cache busting, can be done in multiple ways:
-For IE11 you will need a polyfill for fetch. You should add this on your top level layer, e.g. your application.
+- Going past the `timeToLive` of the cache object
+- Changing cache identifier (e.g. user session or active profile changes)
+- Doing a non GET request to the cached endpoint
+ - Invalidates the cache of that endpoint
+ - Invalidates the cache of all other endpoints matching `invalidatesUrls` and `invalidateUrlsRegex`
-[This is the polyfill we recommend](https://github.com/github/fetch). It also has a [section for polyfilling AbortController](https://github.com/github/fetch#aborting-requests)
+#### Time to live
+
+In this demo we pass a timeToLive of three seconds.
+Try clicking the fetch button and watch fromCache change whenever TTL expires.
+
+After TTL expires, the next request will set the cache again, and for the next 3 seconds you will get cached responses for subsequent requests.
+
+```js preview-story
+export const cacheTimeToLive = () => {
+ const actionLogger = renderLitAsNode(html``);
+ const fetchHandler = () => {
+ ajax.requestJson(`./packages/ajax/docs/pabu.json`, {
+ cacheOptions: {
+ timeToLive: 1000 * 3, // 3 seconds
+ }
+ })
+ .then(result => {
+ actionLogger.log(`From cache: ${result.response.fromCache || false}`);
+ actionLogger.log(JSON.stringify(result.body, null, 2));
+ });
+ }
+ return html`
+
+
+ ${actionLogger}
+ `;
+}
+```
+
+#### Changing cache identifier
+
+For this demo we use localStorage to set a user id to `'1'`.
+
+Now we will allow you to change this identifier to invalidate the cache.
+
+```js preview-story
+export const changeCacheIdentifier = () => {
+ const actionLogger = renderLitAsNode(html``);
+ const fetchHandler = () => {
+ ajax.requestJson(`./packages/ajax/docs/pabu.json`)
+ .then(result => {
+ actionLogger.log(`From cache: ${result.response.fromCache || false}`);
+ actionLogger.log(JSON.stringify(result.body, null, 2));
+ });
+ }
+
+ const changeUserHandler = () => {
+ const currentUser = parseInt(localStorage.getItem('lion-ajax-cache-demo-user-id'), 10);
+ localStorage.setItem('lion-ajax-cache-demo-user-id', `${currentUser + 1}`);
+ }
+
+ return html`
+
+
+
+ ${actionLogger}
+ `;
+}
+```
+
+#### Non-GET request
+
+In this demo we show that by doing a PATCH request, you invalidate the cache of the endpoint for subsequent GET requests.
+
+Try clicking the GET pabu button twice so you see a cached response.
+Then click the PATCH pabu button, followed by another GET, and you will see that this one is not served from cache, because the PATCH invalidated it.
+
+The rationale is that if a user does a non-GET request to an endpoint, it will make the client-side caching of this endpoint outdated.
+This is because non-GET requests usually in some way mutate the state of the database through interacting with this endpoint.
+Therefore, we invalidate the cache, so the user gets the latest state from the database on the next GET request.
+
+> Ignore the browser errors when clicking PATCH buttons, JSON files (our mock database) don't accept PATCH requests.
+
+```js preview-story
+export const nonGETRequest = () => {
+ const actionLogger = renderLitAsNode(html``);
+ const fetchHandler = (name, method) => {
+ ajax.requestJson(`./packages/ajax/docs/${name}.json`, { method })
+ .then(result => {
+ actionLogger.log(`From cache: ${result.response.fromCache || false}`);
+ actionLogger.log(JSON.stringify(result.body, null, 2));
+ });
+ }
+ return html`
+
+
+
+
+
+ ${actionLogger}
+ `;
+}
+```
+
+#### Invalidate Rules
+
+There are two kinds of invalidate rules:
+
+- `invalidateUrls` (array of URL like strings)
+- `invalidateUrlsRegex` (RegExp)
+
+If a non-GET method is fired, by default it only invalidates its own endpoint.
+Invalidating `/api/users` cache by doing a PATCH, will not invalidate `/api/accounts` cache.
+
+However, in the case of users and accounts, they may be very interconnected, so perhaps you do want to invalidate `/api/accounts` when invalidating `/api/users`.
+
+This is what the invalidate rules are for.
+
+In this demo, invalidating the `pabu` endpoint will invalidate `naga`, but not the other way around.
+
+> For invalidateUrls you need the full URL e.g. `://:/` so it's often easier to use invalidateUrlsRegex
+
+```js preview-story
+export const invalidateRules = () => {
+ const actionLogger = renderLitAsNode(html``);
+ const fetchHandler = (name, method) => {
+ const actionCacheOptions = {};
+ if (name === 'pabu') {
+ actionCacheOptions.invalidateUrlsRegex = /\/packages\/ajax\/docs\/naga.json/;
+ }
+
+ ajax.requestJson(`./packages/ajax/docs/${name}.json`, {
+ method,
+ cacheOptions: actionCacheOptions,
+ })
+ .then(result => {
+ actionLogger.log(`From cache: ${result.response.fromCache || false}`);
+ actionLogger.log(JSON.stringify(result.body, null, 2));
+ });
+ }
+ return html`
+
+
+
+
+
+ ${actionLogger}
+ `;
+}
+```
\ No newline at end of file
diff --git a/packages/ajax/docs/cache-technical-docs.md b/packages/ajax/docs/cache-technical-docs.md
deleted file mode 100644
index b6975ef77..000000000
--- a/packages/ajax/docs/cache-technical-docs.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# Ajax Cache
-
-## Technical documentation
-
-The library consists of 2 major parts:
-
-1. A cache class
-2. Request and Response Interceptors
-
-### Cache class
-
-The cache class is responsible for keeping cached data and keeping it valid.
-This class isn't exposed outside, and remains private. Together with this class
-we provide a `getCache(cacheIdentifier)` method that enforces a clean cache when
-the `cacheIdentifier` changes.
-
-> **Note**: the `cacheIdentifier` should be bound to the users session.
-> Advice: Use the sessionToken as cacheIdentifier
-
-Core invalidation rules are:
-
-1. The `LionCache` instance is bound to a `cacheIdentifier`. When the `getCache`
- receives another token, all instances of `LionCache` will be invalidated.
-2. The `LionCache` instance is created with an expiration date **one hour** in
- the future. Each method on the `LionCache` validates that this time hasn't
- passed, and if it does, the cache object in the `LionCache` is cleared.
-
-### Request and Response Interceptors
-
-The interceptors are the core of the logic of when to cache.
-
-To make the cache mechanism work, these interceptors have to be added to an ajax instance (for caching needs).
-
-The **request interceptor**'s main goal is to determine whether or not to
-**return the cached object**. This is done based on the options that are being
-passed to the factory function.
-
-The **response interceptor**'s goal is to determine **when to cache** the
-requested response, based on the options that are being passed in the factory
-function.
-
-Interceptors require `cacheIdentifier` function and `cacheOptions` config.
-The configuration is used by the interceptors to determine what to put in the cache and when to use the cached data.
-
-A cache configuration per action (pre `get` etc) can be placed in ajax configuration in `lionCacheOptions` field, it needed for situations when you want your, for instance, `get` request to have specific cache parameters, like `timeToLive`.
diff --git a/packages/ajax/docs/naga.json b/packages/ajax/docs/naga.json
new file mode 100644
index 000000000..0445fee3f
--- /dev/null
+++ b/packages/ajax/docs/naga.json
@@ -0,0 +1,9 @@
+{
+ "id": "2",
+ "type": "Polar Bear Dog",
+ "name": "Naga",
+ "skin": {
+ "type": "fur",
+ "color": "white"
+ }
+}
diff --git a/packages/ajax/docs/pabu.json b/packages/ajax/docs/pabu.json
new file mode 100644
index 000000000..9c693a143
--- /dev/null
+++ b/packages/ajax/docs/pabu.json
@@ -0,0 +1,9 @@
+{
+ "id": "4",
+ "type": "Fire Ferret",
+ "name": "Pabu",
+ "skin": {
+ "type": "fur",
+ "color": "red"
+ }
+}
diff --git a/packages/ajax/src/AjaxClient.js b/packages/ajax/src/AjaxClient.js
index 0709f359b..37b72a2c8 100644
--- a/packages/ajax/src/AjaxClient.js
+++ b/packages/ajax/src/AjaxClient.js
@@ -1,4 +1,8 @@
/* eslint-disable consistent-return */
+import {
+ cacheRequestInterceptorFactory,
+ cacheResponseInterceptorFactory,
+} from './interceptors-cache.js';
import { acceptLanguageRequestInterceptor, createXSRFRequestInterceptor } from './interceptors.js';
import { AjaxClientFetchError } from './AjaxClientFetchError.js';
@@ -14,11 +18,16 @@ export class AjaxClient {
* @param {Partial} config
*/
constructor(config = {}) {
+ /** @type {Partial} */
this.__config = {
addAcceptLanguage: true,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
jsonPrefix: '',
+ cacheOptions: {
+ getCacheIdentifier: () => '_default',
+ ...config.cacheOptions,
+ },
...config,
};
@@ -36,11 +45,26 @@ export class AjaxClient {
createXSRFRequestInterceptor(this.__config.xsrfCookieName, this.__config.xsrfHeaderName),
);
}
+
+ if (this.__config.cacheOptions && this.__config.cacheOptions.useCache) {
+ this.addRequestInterceptor(
+ cacheRequestInterceptorFactory(
+ this.__config.cacheOptions.getCacheIdentifier,
+ this.__config.cacheOptions,
+ ),
+ );
+ this.addResponseInterceptor(
+ cacheResponseInterceptorFactory(
+ this.__config.cacheOptions.getCacheIdentifier,
+ this.__config.cacheOptions,
+ ),
+ );
+ }
}
/**
* Sets the config for the instance
- * @param {AjaxClientConfig} config configuration for the AjaxClass instance
+ * @param {Partial} config configuration for the AjaxClass instance
*/
set options(config) {
this.__config = config;
diff --git a/packages/ajax/src/interceptors-cache.js b/packages/ajax/src/interceptors-cache.js
index 68eef2a60..5c861b318 100644
--- a/packages/ajax/src/interceptors-cache.js
+++ b/packages/ajax/src/interceptors-cache.js
@@ -204,22 +204,16 @@ export const validateOptions = ({
* @returns {ValidatedCacheOptions}
*/
function composeCacheOptions(validatedInitialCacheOptions, configCacheOptions) {
- /** @type {any} */
- let actionCacheOptions = {};
+ let actionCacheOptions = validatedInitialCacheOptions;
- actionCacheOptions =
- configCacheOptions &&
- validateOptions({
+ if (configCacheOptions) {
+ actionCacheOptions = validateOptions({
...validatedInitialCacheOptions,
...configCacheOptions,
});
+ }
- const cacheOptions = {
- ...validatedInitialCacheOptions,
- ...actionCacheOptions,
- };
-
- return cacheOptions;
+ return actionCacheOptions;
}
/**
@@ -249,7 +243,6 @@ export const cacheRequestInterceptorFactory = (getCacheIdentifier, globalCacheOp
// cacheIdentifier is used to bind the cache to the current session
const currentCache = getCache(getCacheIdentifier());
-
const cacheResponse = currentCache.get(cacheId, cacheOptions.timeToLive);
// don't use cache if the request method is not part of the configs methods
@@ -260,6 +253,7 @@ export const cacheRequestInterceptorFactory = (getCacheIdentifier, globalCacheOp
if (cacheOptions.invalidateUrls) {
cacheOptions.invalidateUrls.forEach(
/** @type {string} */ invalidateUrl => {
+ console.log('invalidaaaating', currentCache._cacheObject);
currentCache.delete(invalidateUrl);
},
);
@@ -277,16 +271,17 @@ export const cacheRequestInterceptorFactory = (getCacheIdentifier, globalCacheOp
if (!cacheRequest.cacheOptions) {
cacheRequest.cacheOptions = { useCache: false };
}
- cacheRequest.cacheOptions.fromCache = true;
const init = /** @type {LionRequestInit} */ ({
status,
statusText,
headers,
- request: cacheRequest,
});
- return /** @type {CacheResponse} */ (new Response(cacheResponse, init));
+ const response = /** @type {CacheResponse} */ (new Response(cacheResponse, init));
+ response.request = cacheRequest;
+ response.fromCache = true;
+ return response;
}
return cacheRequest;
@@ -315,7 +310,7 @@ export const cacheResponseInterceptorFactory = (getCacheIdentifier, globalCacheO
cacheResponse.request?.cacheOptions,
);
- const isAlreadyFromCache = !!cacheOptions.fromCache;
+ const isAlreadyFromCache = !!cacheResponse.fromCache;
// caching all responses with not default `timeToLive`
const isCacheActive = cacheOptions.timeToLive > 0;
@@ -334,8 +329,9 @@ export const cacheResponseInterceptorFactory = (getCacheIdentifier, globalCacheO
searchParamSerializer,
);
+ const responseBody = await cacheResponse.clone().text();
// store the response data in the cache
- getCache(getCacheIdentifier()).set(cacheId, cacheResponse.body);
+ getCache(getCacheIdentifier()).set(cacheId, responseBody);
} else {
// don't store in cache if the request method is not part of the configs methods
return cacheResponse;
diff --git a/packages/ajax/test/AjaxClient.test.js b/packages/ajax/test/AjaxClient.test.js
index 55442c466..8d1330863 100644
--- a/packages/ajax/test/AjaxClient.test.js
+++ b/packages/ajax/test/AjaxClient.test.js
@@ -1,5 +1,5 @@
import { expect } from '@open-wc/testing';
-import { stub } from 'sinon';
+import { stub, useFakeTimers } from 'sinon';
import { AjaxClient, AjaxClientFetchError } from '@lion/ajax';
describe('AjaxClient', () => {
@@ -210,6 +210,61 @@ describe('AjaxClient', () => {
});
});
+ describe('Caching', () => {
+ /** @type {number | undefined} */
+ let cacheId;
+ /** @type {() => string} */
+ let getCacheIdentifier;
+
+ const newCacheId = () => {
+ if (!cacheId) {
+ cacheId = 1;
+ } else {
+ cacheId += 1;
+ }
+ return cacheId;
+ };
+
+ beforeEach(() => {
+ getCacheIdentifier = () => String(cacheId);
+ });
+
+ it('allows configuring cache interceptors on the AjaxClient config', async () => {
+ newCacheId();
+ const customAjax = new AjaxClient({
+ cacheOptions: {
+ useCache: true,
+ timeToLive: 100,
+ getCacheIdentifier,
+ },
+ });
+
+ const clock = useFakeTimers({
+ shouldAdvanceTime: true,
+ });
+
+ // Smoke test 1: verify caching works
+ await customAjax.request('/foo');
+ expect(fetchStub.callCount).to.equal(1);
+ await customAjax.request('/foo');
+ expect(fetchStub.callCount).to.equal(1);
+
+ // Smoke test 2: verify caching is invalidated on non-get method
+ await customAjax.request('/foo', { method: 'POST' });
+ expect(fetchStub.callCount).to.equal(2);
+ await customAjax.request('/foo');
+ expect(fetchStub.callCount).to.equal(3);
+
+ // Smoke test 3: verify caching is invalidated after TTL has passed
+ await customAjax.request('/foo');
+ expect(fetchStub.callCount).to.equal(3);
+ clock.tick(101);
+ await customAjax.request('/foo');
+ expect(fetchStub.callCount).to.equal(4);
+ clock.restore();
+ });
+ });
+
describe('Abort', () => {
it('support aborting requests with AbortController', async () => {
fetchStub.restore();
diff --git a/packages/ajax/types/types.d.ts b/packages/ajax/types/types.d.ts
index 750bc4f83..e12b405d2 100644
--- a/packages/ajax/types/types.d.ts
+++ b/packages/ajax/types/types.d.ts
@@ -14,6 +14,7 @@ export interface AjaxClientConfig {
addAcceptLanguage: boolean;
xsrfCookieName: string | null;
xsrfHeaderName: string | null;
+ cacheOptions: CacheOptionsWithIdentifier;
jsonPrefix: string;
}
@@ -38,17 +39,17 @@ export interface CacheOptions {
invalidateUrls?: string[];
invalidateUrlsRegex?: RegExp;
requestIdentificationFn?: RequestIdentificationFn;
- fromCache?: boolean;
}
-export interface ValidatedCacheOptions {
+export interface CacheOptionsWithIdentifier extends CacheOptions {
+ getCacheIdentifier: () => string;
+}
+
+export interface ValidatedCacheOptions extends CacheOptions {
useCache: boolean;
methods: string[];
timeToLive: number;
- invalidateUrls?: string[];
- invalidateUrlsRegex?: RegExp;
requestIdentificationFn: RequestIdentificationFn;
- fromCache?: boolean;
}
export interface CacheRequestExtension {
@@ -67,6 +68,7 @@ export interface CacheResponseRequest {
export interface CacheResponseExtension {
request: CacheResponseRequest;
data: object | string;
+ fromCache?: boolean;
}
export type CacheRequest = Request & Partial;