@lion/ajax
Advanced tools
Comparing version 0.6.0 to 0.7.0
# Change Log | ||
## 0.7.0 | ||
### Minor Changes | ||
- 2cd7993d: Set fromCache property on the Response, for user consumption. Allow setting cacheOptions on the AjaxClient upon instantiation. Create docs/demos. | ||
## 0.6.0 | ||
@@ -4,0 +10,0 @@ |
@@ -192,3 +192,3 @@ { | ||
"type": { | ||
"text": "{\n addAcceptLanguage: boolean;\n xsrfCookieName: string | null;\n xsrfHeaderName: string | null;\n jsonPrefix: string;\n }" | ||
"text": "Partial<AjaxClientConfig>" | ||
} | ||
@@ -1078,10 +1078,4 @@ }, | ||
"exports": [] | ||
}, | ||
{ | ||
"kind": "javascript-module", | ||
"path": "./docs/cache-technical-docs.md", | ||
"declarations": [], | ||
"exports": [] | ||
} | ||
] | ||
} | ||
} |
{ | ||
"name": "@lion/ajax", | ||
"version": "0.6.0", | ||
"version": "0.7.0", | ||
"description": "Thin wrapper around fetch with support for interceptors.", | ||
@@ -27,3 +27,3 @@ "license": "MIT", | ||
"scripts": { | ||
"custom-elements-manifest": "custom-elements-manifest analyze --exclude 'docs/**/*'", | ||
"custom-elements-manifest": "custom-elements-manifest analyze --litelement --exclude 'docs/**/*'", | ||
"debug": "cd ../../ && npm run debug -- --group ajax", | ||
@@ -45,4 +45,4 @@ "debug:firefox": "cd ../../ && npm run debug:firefox -- --group ajax", | ||
}, | ||
"exports": "./index.js", | ||
"customElementsManifest": "custom-elements.json" | ||
} | ||
"customElementsManifest": "custom-elements.json", | ||
"exports": "./index.js" | ||
} |
441
README.md
@@ -5,2 +5,32 @@ [//]: # 'AUTO INSERT HEADER PREPUBLISH' | ||
```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: | ||
@@ -31,7 +61,23 @@ | ||
```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`<sb-action-logger></sb-action-logger>`); | ||
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` | ||
<style> | ||
sb-action-logger { | ||
--sb-action-logger-max-height: 300px; | ||
} | ||
</style> | ||
<button @click=${() => fetchHandler('pabu')}>Fetch Pabu</button> | ||
<button @click=${() => fetchHandler('naga')}>Fetch Naga</button> | ||
${actionLogger} | ||
`; | ||
} | ||
``` | ||
@@ -53,10 +99,29 @@ | ||
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`<sb-action-logger></sb-action-logger>`); | ||
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` | ||
<style> | ||
sb-action-logger { | ||
--sb-action-logger-max-height: 300px; | ||
} | ||
</style> | ||
<button @click=${() => fetchHandler('pabu')}>Fetch Pabu</button> | ||
<button @click=${() => fetchHandler('naga')}>Fetch Naga</button> | ||
${actionLogger} | ||
`; | ||
} | ||
``` | ||
@@ -79,29 +144,51 @@ | ||
```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`<sb-action-logger></sb-action-logger>`); | ||
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` | ||
<style> | ||
sb-action-logger { | ||
--sb-action-logger-max-height: 300px; | ||
} | ||
</style> | ||
<button @click=${fetchHandler}>Fetch</button> | ||
${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](https://github.com/ing-bank/lion/blob/master/packages/ajax/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 | ||
@@ -140,111 +227,245 @@ | ||
### 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: [] | ||
}, | ||
}; | ||
} | ||
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. | ||
/** | ||
* Returns all todos from cache if not older than 5 minutes | ||
*/ | ||
getTodos() { | ||
return ajax.requestJson(`/api/todos`, this.localAjaxConfig); | ||
```js preview-story | ||
export const cache = () => { | ||
const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`); | ||
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` | ||
<style> | ||
sb-action-logger { | ||
--sb-action-logger-max-height: 300px; | ||
} | ||
</style> | ||
<button @click=${() => fetchHandler('pabu')}>Fetch Pabu</button> | ||
<button @click=${() => fetchHandler('naga')}>Fetch Naga</button> | ||
${actionLogger} | ||
`; | ||
} | ||
``` | ||
/** | ||
* | ||
*/ | ||
getTodosByKeyword(keyword) { | ||
return ajax.requestJson(`/api/todosbykeyword/${keyword}`, this.localAjaxConfig); | ||
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. | ||
In this demo, when we fetch naga, we always pass `useCache: false` so the Response is never a cached one. | ||
```js preview-story | ||
export const cacheActionOptions = () => { | ||
const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`); | ||
const fetchHandler = (name) => { | ||
let actionCacheOptions; | ||
if (name === 'naga') { | ||
actionCacheOptions = { | ||
useCache: false, | ||
} | ||
} | ||
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` | ||
<style> | ||
sb-action-logger { | ||
--sb-action-logger-max-height: 300px; | ||
} | ||
</style> | ||
<button @click=${() => fetchHandler('pabu')}>Fetch Pabu</button> | ||
<button @click=${() => fetchHandler('naga')}>Fetch Naga</button> | ||
${actionLogger} | ||
`; | ||
} | ||
``` | ||
/** | ||
* 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 }); | ||
### Invalidating cache | ||
Invalidating the cache, or cache busting, can be done in multiple ways: | ||
- 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` | ||
#### 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`<sb-action-logger></sb-action-logger>`); | ||
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` | ||
<style> | ||
sb-action-logger { | ||
--sb-action-logger-max-height: 300px; | ||
} | ||
</style> | ||
<button @click=${fetchHandler}>Fetch Pabu</button> | ||
${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. | ||
#### Changing cache identifier | ||
### Ajax cache Options | ||
For this demo we use localStorage to set a user id to `'1'`. | ||
```js | ||
const cacheOptions = { | ||
// `useCache`: determines wether or not to use the cache | ||
// can be boolean | ||
// default: false | ||
useCache: true, | ||
Now we will allow you to change this identifier to invalidate the cache. | ||
// `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, | ||
```js preview-story | ||
export const changeCacheIdentifier = () => { | ||
const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`); | ||
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)); | ||
}); | ||
} | ||
// `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'], | ||
const changeUserHandler = () => { | ||
const currentUser = parseInt(localStorage.getItem('lion-ajax-cache-demo-user-id'), 10); | ||
localStorage.setItem('lion-ajax-cache-demo-user-id', `${currentUser + 1}`); | ||
} | ||
// `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'], | ||
return html` | ||
<style> | ||
sb-action-logger { | ||
--sb-action-logger-max-height: 300px; | ||
} | ||
</style> | ||
<button @click=${fetchHandler}>Fetch Pabu</button> | ||
<button @click=${changeUserHandler}>Change user</button> | ||
${actionLogger} | ||
`; | ||
} | ||
``` | ||
// `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/ | ||
#### Non-GET request | ||
// `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)}`; | ||
}, | ||
}; | ||
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`<sb-action-logger></sb-action-logger>`); | ||
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` | ||
<style> | ||
sb-action-logger { | ||
--sb-action-logger-max-height: 300px; | ||
} | ||
</style> | ||
<button @click=${() => fetchHandler('pabu', 'GET')}>GET Pabu</button> | ||
<button @click=${() => fetchHandler('pabu', 'PATCH')}>PATCH Pabu</button> | ||
<button @click=${() => fetchHandler('naga', 'GET')}>GET Naga</button> | ||
<button @click=${() => fetchHandler('naga', 'PATCH')}>PATCH Naga</button> | ||
${actionLogger} | ||
`; | ||
} | ||
``` | ||
## Considerations | ||
#### Invalidate Rules | ||
## Fetch Polyfill | ||
There are two kinds of invalidate rules: | ||
For IE11 you will need a polyfill for fetch. You should add this on your top level layer, e.g. your application. | ||
- `invalidateUrls` (array of URL like strings) | ||
- `invalidateUrlsRegex` (RegExp) | ||
[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) | ||
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. `<protocol>://<domain>:<port>/<url>` so it's often easier to use invalidateUrlsRegex | ||
```js preview-story | ||
export const invalidateRules = () => { | ||
const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`); | ||
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` | ||
<style> | ||
sb-action-logger { | ||
--sb-action-logger-max-height: 300px; | ||
} | ||
</style> | ||
<button @click=${() => fetchHandler('pabu', 'GET')}>GET Pabu</button> | ||
<button @click=${() => fetchHandler('pabu', 'PATCH')}>PATCH Pabu</button> | ||
<button @click=${() => fetchHandler('naga', 'GET')}>GET Naga</button> | ||
<button @click=${() => fetchHandler('naga', 'PATCH')}>PATCH Naga</button> | ||
${actionLogger} | ||
`; | ||
} | ||
``` |
@@ -11,8 +11,4 @@ /** | ||
constructor(config?: Partial<AjaxClientConfig>); | ||
__config: { | ||
addAcceptLanguage: boolean; | ||
xsrfCookieName: string | null; | ||
xsrfHeaderName: string | null; | ||
jsonPrefix: string; | ||
}; | ||
/** @type {Partial<AjaxClientConfig>} */ | ||
__config: Partial<AjaxClientConfig>; | ||
/** @type {Array.<RequestInterceptor|CachedRequestInterceptor>} */ | ||
@@ -24,6 +20,6 @@ _requestInterceptors: Array<RequestInterceptor | CachedRequestInterceptor>; | ||
* Sets the config for the instance | ||
* @param {AjaxClientConfig} config configuration for the AjaxClass instance | ||
* @param {Partial<AjaxClientConfig>} config configuration for the AjaxClass instance | ||
*/ | ||
set options(arg: import("../types/types.js").AjaxClientConfig); | ||
get options(): import("../types/types.js").AjaxClientConfig; | ||
set options(arg: Partial<import("../types/types.js").AjaxClientConfig>); | ||
get options(): Partial<import("../types/types.js").AjaxClientConfig>; | ||
/** @param {RequestInterceptor} requestInterceptor */ | ||
@@ -30,0 +26,0 @@ addRequestInterceptor(requestInterceptor: RequestInterceptor): void; |
/* eslint-disable consistent-return */ | ||
import { | ||
cacheRequestInterceptorFactory, | ||
cacheResponseInterceptorFactory, | ||
} from './interceptors-cache.js'; | ||
import { acceptLanguageRequestInterceptor, createXSRFRequestInterceptor } from './interceptors.js'; | ||
@@ -17,2 +21,3 @@ import { AjaxClientFetchError } from './AjaxClientFetchError.js'; | ||
constructor(config = {}) { | ||
/** @type {Partial<AjaxClientConfig>} */ | ||
this.__config = { | ||
@@ -23,2 +28,6 @@ addAcceptLanguage: true, | ||
jsonPrefix: '', | ||
cacheOptions: { | ||
getCacheIdentifier: () => '_default', | ||
...config.cacheOptions, | ||
}, | ||
...config, | ||
@@ -41,2 +50,17 @@ }; | ||
} | ||
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, | ||
), | ||
); | ||
} | ||
} | ||
@@ -46,3 +70,3 @@ | ||
* Sets the config for the instance | ||
* @param {AjaxClientConfig} config configuration for the AjaxClass instance | ||
* @param {Partial<AjaxClientConfig>} config configuration for the AjaxClass instance | ||
*/ | ||
@@ -49,0 +73,0 @@ set options(config) { |
@@ -207,18 +207,12 @@ /* eslint-disable consistent-return */ | ||
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; | ||
} | ||
@@ -253,3 +247,2 @@ | ||
const currentCache = getCache(getCacheIdentifier()); | ||
const cacheResponse = currentCache.get(cacheId, cacheOptions.timeToLive); | ||
@@ -282,3 +275,2 @@ | ||
} | ||
cacheRequest.cacheOptions.fromCache = true; | ||
@@ -289,6 +281,8 @@ const init = /** @type {LionRequestInit} */ ({ | ||
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; | ||
} | ||
@@ -322,3 +316,3 @@ | ||
const isAlreadyFromCache = !!cacheOptions.fromCache; | ||
const isAlreadyFromCache = !!cacheResponse.fromCache; | ||
// caching all responses with not default `timeToLive` | ||
@@ -342,4 +336,5 @@ const isCacheActive = cacheOptions.timeToLive > 0; | ||
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 { | ||
@@ -346,0 +341,0 @@ // don't store in cache if the request method is not part of the configs methods |
import { expect } from '@open-wc/testing'; | ||
import { stub } from 'sinon'; | ||
import { stub, useFakeTimers } from 'sinon'; | ||
import { AjaxClient, AjaxClientFetchError } from '@lion/ajax'; | ||
@@ -106,5 +106,3 @@ | ||
it('addRequestInterceptor() adds a function which intercepts the request', async () => { | ||
ajax.addRequestInterceptor(async r => { | ||
return new Request(`${r.url}/intercepted-1`); | ||
}); | ||
ajax.addRequestInterceptor(async r => new Request(`${r.url}/intercepted-1`)); | ||
ajax.addRequestInterceptor(async r => new Request(`${r.url}/intercepted-2`)); | ||
@@ -214,2 +212,57 @@ | ||
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', () => { | ||
@@ -216,0 +269,0 @@ it('support aborting requests with AbortController', async () => { |
@@ -7,3 +7,3 @@ import { expect } from '@open-wc/testing'; | ||
describe('ajax cache', function describeLibCache() { | ||
describe('ajax cache', () => { | ||
/** @type {number | undefined} */ | ||
@@ -10,0 +10,0 @@ let cacheId; |
@@ -17,2 +17,3 @@ /** | ||
xsrfHeaderName: string | null; | ||
cacheOptions: CacheOptionsWithIdentifier; | ||
jsonPrefix: string; | ||
@@ -42,13 +43,13 @@ } | ||
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; | ||
} | ||
@@ -72,2 +73,3 @@ | ||
data: object | string; | ||
fromCache?: boolean; | ||
} | ||
@@ -74,0 +76,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
113595
30
2560
467
1