Skip to content

Commit b3f3cd3

Browse files
kellmarieKellie Selinka <> and Adam Stankiewicz
andauthored
feat: Add usage of the axios cache adapter as a means to have client caching (#119)
Co-authored-by: Kellie Selinka <> and Adam Stankiewicz <astankiewicz@edx.org>
1 parent 09e3edd commit b3f3cd3

7 files changed

Lines changed: 393 additions & 16 deletions

File tree

docs/how_tos/caching.rst

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
############################
2+
How to: Caching API Requests
3+
############################
4+
5+
.. contents:: Table of Contents
6+
7+
Introduction
8+
************
9+
10+
Often, a web application needs to make the same repeated API calls, where the data returned is mostly static (i.e., does not change often). In such situations, caching is a viable solution to prevent unnecessary and potentially expensive operations, resulting in increased performance and a better user experience.
11+
12+
When considering caching options, there's a few approaches to consider:
13+
14+
Server caching
15+
==============
16+
17+
Server caching helps to limit the cost of an API request on the server and its dependent systems. When a client makes a request to the API, the server will check for a local copy of the requested resource. If the local resource exists, the server will respond with it; otherwise, the request is processed normally (e.g., performing database lookups). With this approach, the cost savings to the server may be significant, utilizing less resources.
18+
19+
Client caching
20+
==============
21+
22+
Client caching helps to limit the cost of an API request incurred by the user. Rather than relying on a API implementing server caching, commonly referenced data may be cached locally within the client (i.e., browser). Similar to server caching, on the first request of an API, the request will occur as usual but the response data will be stored in the client.
23+
24+
However, on subsequent requests (within a given timeframe), the client will check if a copy of the requested resource exists. If it does, it will read from the client cache rather than making a network request to the API. This has the benefit of freeing up resources on the server as it no longer needs to handle repeat queries within a given timeframe.
25+
26+
Typically, client caching stores data for a given time (e.g., 5 minutes) since it was last requested. This allows the client cache to be dynamic in the sense that data will be eventually consistent and unneeded local data can be cleared once it is no longer relevant.
27+
28+
Hybrid caching
29+
==============
30+
31+
Both server and client caching can be combined to provide best of both worlds. Regardless of how an API request is made, utilizing a hybrid approach will ensure data is read from a local cache first, whether that be on the server or the client.
32+
33+
How do I use client caching with ``@edx/frontend-platform?``
34+
************************************************************
35+
36+
The ``@edx/frontend-platform`` package supports an Axios HTTP client that uses client caching under the hood through the use of `axios-cache-adapter <https://www.npmjs.com/package/axios-cache-adapter>`_.
37+
38+
``axios-cache-adapter`` is configured to use `localForage <https://www.npmjs.com/package/localforage>`_ as its store; ``localForage`` is a package that provides a similar API to browser's localStorage, except is asynchronous (non-blocking). ``localForage`` is configured to prefer using IndexedDB, falling back to localStorage if browser support for IndexedDB is lacking. If all else fails, an in-memory store is used instead. IndexedDB is more performant and may contain a broader range (and volume) of data in comparison to localStorage (see `browser support <https://caniuse.com/indexeddb>`_ for IndexedDB).
39+
40+
When importing an HTTP client from ``@edx/frontend-platform``, you may specify options for how you wish to configure the HTTP client::
41+
42+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform';
43+
44+
// ``cachedHttpClient`` is configured to use client caching under the hood.
45+
const cachedHttpClient = getAuthenticatedHttpClient({ useCache: true });
46+
47+
The examples below demonstrate how to configure the caching behavior on a per-request basis. For more details about how to configure the caching behavior, you may refer to the `axios-cache-adapter documentation <https://www.npmjs.com/package/axios-cache-adapter>`_.
48+
49+
Modify expiry behavior for a single request
50+
===========================================
51+
52+
By default, API requests using the cached HTTP client will be cached for 5 minutes. However, cache options may be configured on a per-request basis::
53+
54+
cachedHttpClient.get('/courses/', {
55+
cache: {
56+
maxAge: 15 * 60 * 1000, // 15 minutes instead of the default 5.
57+
},
58+
});
59+
60+
Invalidate cache for a single request
61+
=====================================
62+
63+
In certain situations, it may be necessary to invalidate cached data to force a true network request to the server::
64+
65+
cachedHttpClient.get('/courses/', { clearCacheEntry: true });
66+
67+
Check if response is served from network or from cache
68+
======================================================
69+
70+
If there is a need to know whether a response was served from the network (i.e., server) or from the local client cache, you may refer to the ``response.request`` object::
71+
72+
cachedHttpClient.get('/courses/').then((response) => {
73+
console.log(response.request.fromCache); // will be true if served from the client cache
74+
});

package-lock.json

Lines changed: 57 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"@commitlint/config-angular": "8.2.0",
3636
"@edx/frontend-build": "5.3.2",
3737
"@edx/paragon": "7.1.5",
38-
"axios-mock-adapter": "1.17.0",
38+
"axios-mock-adapter": "1.19.0",
3939
"babel-polyfill": "6.26.0",
4040
"codecov": "3.7.2",
4141
"enzyme": "3.10.0",
@@ -53,11 +53,14 @@
5353
"dependencies": {
5454
"@cospired/i18n-iso-languages": "2.1.2",
5555
"axios": "0.18.1",
56+
"axios-cache-adapter": "^2.5.0",
5657
"form-urlencoded": "4.1.4",
5758
"glob": "7.1.6",
5859
"history": "4.10.1",
5960
"i18n-iso-countries": "4.3.1",
6061
"jwt-decode": "2.2.0",
62+
"localforage": "^1.9.0",
63+
"localforage-memoryStorageDriver": "^0.9.2",
6164
"lodash.camelcase": "4.3.0",
6265
"lodash.memoize": "4.1.2",
6366
"lodash.merge": "4.6.2",

src/auth/AxiosJwtAuthService.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import createCsrfTokenProviderInterceptor from './interceptors/createCsrfTokenPr
77
import createProcessAxiosRequestErrorInterceptor from './interceptors/createProcessAxiosRequestErrorInterceptor';
88
import AxiosJwtTokenService from './AxiosJwtTokenService';
99
import AxiosCsrfTokenService from './AxiosCsrfTokenService';
10+
import configureCache from './LocalForageCache';
1011

1112
const optionsPropTypes = {
1213
config: PropTypes.shape({
@@ -44,6 +45,8 @@ class AxiosJwtAuthService {
4445
constructor(options) {
4546
this.authenticatedHttpClient = null;
4647
this.httpClient = null;
48+
this.cachedAuthenticatedHttpClient = null;
49+
this.cachedHttpClient = null;
4750
this.authenticatedUser = null;
4851

4952
ensureDefinedConfig(options, 'AuthService');
@@ -59,24 +62,48 @@ class AxiosJwtAuthService {
5962
this.csrfTokenService = new AxiosCsrfTokenService(this.config.CSRF_TOKEN_API_PATH);
6063
this.authenticatedHttpClient = this.addAuthenticationToHttpClient(axios.create());
6164
this.httpClient = axios.create();
65+
configureCache()
66+
.then((cachedAxiosClient) => {
67+
this.cachedAuthenticatedHttpClient = this.addAuthenticationToHttpClient(cachedAxiosClient);
68+
this.cachedHttpClient = cachedAxiosClient;
69+
})
70+
.catch((e) => {
71+
// fallback to non-cached HTTP clients and log error
72+
this.cachedAuthenticatedHttpClient = this.authenticatedHttpClient;
73+
this.cachedHttpClient = this.httpClient;
74+
logFrontendAuthError(this.loggingService, `configureCache failed with error: ${e.message}`);
75+
});
6276
}
6377

6478
/**
6579
* Gets the authenticated HTTP client for the service. This is an axios instance.
6680
*
81+
* @param {Object} [options] Optional options for how the HTTP client should be configured.
82+
* @param {boolean} [options.useCache] Whether to use front end caching for all requests made
83+
* with the returned client.
84+
*
6785
* @returns {HttpClient} A configured axios http client which can be used for authenticated
6886
* requests.
6987
*/
70-
getAuthenticatedHttpClient() {
88+
getAuthenticatedHttpClient(options = {}) {
89+
if (options.useCache) {
90+
return this.cachedAuthenticatedHttpClient;
91+
}
7192
return this.authenticatedHttpClient;
7293
}
7394

7495
/**
7596
* Gets the unauthenticated HTTP client for the service. This is an axios instance.
7697
*
98+
* @param {Object} [options] Optional options for how the HTTP client should be configured.
99+
* @param {boolean} [options.useCache] Whether to use front end caching for all requests made
100+
* with the returned client.
77101
* @returns {HttpClient} A configured axios http client.
78102
*/
79-
getHttpClient() {
103+
getHttpClient(options = {}) {
104+
if (options.useCache) {
105+
return this.cachedHttpClient;
106+
}
80107
return this.httpClient;
81108
}
82109

0 commit comments

Comments
 (0)