lion/packages/ajax
2021-02-15 18:07:33 +01:00
..
docs feat: port caching feature to fetch proposal 2021-02-11 12:28:05 +01:00
src feat: port caching feature to fetch proposal 2021-02-11 12:28:05 +01:00
test chore: use async/await in cache interceptor tests 2021-02-15 13:55:49 +01:00
types feat: port caching feature to fetch proposal 2021-02-11 12:28:05 +01:00
CHANGELOG.md chore: make interceptors async, add changeset breaking change 2021-02-11 12:04:14 +01:00
custom-elements.json docs: add custom elements manifest 2021-02-15 18:07:33 +01:00
index.js feat: port caching feature to fetch proposal 2021-02-11 12:28:05 +01:00
package.json docs: add custom elements manifest 2021-02-15 18:07:33 +01:00
README.md feat: port caching feature to fetch proposal 2021-02-11 12:28:05 +01:00

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

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 is a great start.

Example requests

GET request

import { ajax } from '@lion/ajax';

const response = await ajax.request('/api/users');
const users = await response.json();

POST request

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

import { ajax } from '@lion/ajax';

const { response, body } = await ajax.requestJson('/api/users');

POST JSON request

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:

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

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

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

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

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. It also has a section for polyfilling AbortController