lion/packages/ajax/README.md
Yevgeniy Valeyev bbffd7105f feat: port caching feature to fetch proposal
Co-authored-by: Yevgeniy Valeyev <yevgeniy.valeyev@ing.com>
2021-02-11 12:28:05 +01:00

245 lines
7.1 KiB
Markdown

[//]: # 'AUTO INSERT HEADER PREPUBLISH'
# Ajax
`ajax` is a small wrapper around `fetch` which:
- Allows globally registering request and response interceptors
- Throws on 4xx and 5xx status codes
- Prevents network request if a request interceptor returns a response
- Supports a JSON request which automatically encodes/decodes body request and response payload as JSON
- Adds accept-language header to requests based on application language
- Adds XSRF header to request if the cookie is present
## How to use
### Installation
```sh
npm i --save @lion/ajax
```
### Relation to fetch
`ajax` delegates all requests to fetch. `ajax.request` and `ajax.requestJson` have the same function signature as `window.fetch`, you can use any online resource to learn more about fetch. [MDN](http://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) is a great start.
### Example requests
#### GET request
```js
import { ajax } from '@lion/ajax';
const response = await ajax.request('/api/users');
const users = await response.json();
```
#### POST request
```js
import { ajax } from '@lion/ajax';
const response = await ajax.request('/api/users', {
method: 'POST',
body: JSON.stringify({ username: 'steve' }),
});
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:
#### GET JSON request
```js
import { ajax } from '@lion/ajax';
const { response, body } = await ajax.requestJson('/api/users');
```
#### POST JSON request
```js
import { ajax } from '@lion/ajax';
const { response, body } = await ajax.requestJson('/api/users', {
method: 'POST',
body: { username: 'steve' },
});
```
### Error handling
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);
}
} else {
// an error happened before receiving a response, ex. an incorrect request or network error
console.error(error);
}
}
```
## Ajax Cache
A caching library that uses `lion-web/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)
### Getting started
Consume the global `ajax` instance and add the interceptors to it, using a cache configuration
which is applied on application level. If a developer wants to add specifics to cache behavior
they have to provide a cache config per action (`get`, `post`, etc.) via `cacheOptions` field of local ajax config,
see examples below.
> **Note**: make sure to add the **interceptors** only **once**. This is usually
> done on app-level
```js
import {
ajax,
cacheRequestInterceptorFactory,
cacheResponseInterceptorFactory,
} from '@lion-web/ajax.js';
const globalCacheOptions = {
useCache: true,
timeToLive: 1000 * 60 * 5, // 5 minutes
};
// Cache is removed each time an identifier changes,
// for instance when a current user is logged out
const getCacheIdentifier = () => getActiveProfile().profileId;
ajax.addRequestInterceptor(cacheRequestInterceptorFactory(getCacheIdentifier, globalCacheOptions));
ajax.addResponseInterceptor(
cacheResponseInterceptorFactory(getCacheIdentifier, globalCacheOptions),
);
const { response, body } = await ajax.requestJson('/my-url');
```
### Ajax cache example
```js
import {
ajax,
cacheRequestInterceptorFactory,
cacheResponseInterceptorFactory,
} from '@lion-web/ajax';
const 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
};
// 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),
// );
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 });
}
}
```
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.
### Ajax cache Options
```js
const cacheOptions = {
// `useCache`: determines wether or not to use the cache
// can be boolean
// default: false
useCache: true,
// `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)}`;
},
};
```
## Considerations
## 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)