Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/RasCarlito/axios-cache-adapter

Caching adapter for axios. Store request results in a configurable store to prevent unneeded network requests.
https://github.com/RasCarlito/axios-cache-adapter

Last synced: about 2 months ago
JSON representation

Caching adapter for axios. Store request results in a configurable store to prevent unneeded network requests.

Awesome Lists containing this project

README

        

# :rocket: axios-cache-adapter [![Build Status](https://travis-ci.org/RasCarlito/axios-cache-adapter.svg?branch=master)](https://travis-ci.org/RasCarlito/axios-cache-adapter) [![codecov](https://codecov.io/gh/RasCarlito/axios-cache-adapter/branch/master/graph/badge.svg)](https://codecov.io/gh/RasCarlito/axios-cache-adapter) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)

> Caching adapter for axios. Store request results in a configurable store to prevent unneeded network requests.

Adapted from [superapi-cache](https://github.com/stephanebachelier/superapi-cache)
by [@stephanebachelier](https://github.com/stephanebachelier)

## Install

Using npm

```sh
npm install --save axios-cache-adapter
```

Or bower

```sh
bower install --save axios-cache-adapter
```

Or from a CDN like unpkg.com

```html

```

## Usage

**Important note:** Only `GET` request results are cached by default. Executing a request using any method listed in `exclude.methods` will invalidate the cache for the given URL.

### Instantiate adapter on its own

You can instantiate the `axios-cache-adapter` on its own using the `setupCache()` method and then attach the adapter manually to an instance of `axios`.

```js
// Import dependencies
import axios from 'axios'
import { setupCache } from 'axios-cache-adapter'

// Create `axios-cache-adapter` instance
const cache = setupCache({
maxAge: 15 * 60 * 1000
})

// Create `axios` instance passing the newly created `cache.adapter`
const api = axios.create({
adapter: cache.adapter
})

// Send a GET request to some REST api
api({
url: 'http://some-rest.api/url',
method: 'get'
}).then(async (response) => {
// Do something fantastic with response.data \o/
console.log('Request response:', response)

// Interacting with the store, see `localForage` API.
const length = await cache.store.length()

console.log('Cache store length:', length)
})
```

### Instantiate axios with bound adapter

You can use the `setup()` method to get an instance of `axios` pre-configured with the `axios-cache-adapter`. This will remove `axios` as a direct dependency in your code.

```js
// Import dependencies
import { setup } from 'axios-cache-adapter'

// Create `axios` instance with pre-configured `axios-cache-adapter` attached to it
const api = setup({
// `axios` options
baseURL: 'http://some-rest.api',

// `axios-cache-adapter` options
cache: {
maxAge: 15 * 60 * 1000
}
})

// Send a GET request to some REST api
api.get('/url').then(async (response) => {
// Do something awesome with response.data \o/
console.log('Request response:', response)

// Interacting with the store, see `localForage` API.
const length = await api.cache.length()

console.log('Cache store length:', length)
})
```

### Override instance config with per request options

After setting up `axios-cache-adapter` with a specific cache configuration you can override parts of that configuration on individual requests.

```js
import { setup } from 'axios-cache-adapter'

const api = setup({
baseURL: 'https://httpbin.org',

cache: {
maxAge: 15 * 60 * 1000
}
})

// Use global instance config
api.get('/get').then((response) => {
// Do something awesome with response
})

// Override `maxAge` and cache URLs with query parameters
api.get('/get?with=query', {
cache: {
maxAge: 2 * 60 * 1000, // 2 min instead of 15 min
exclude: { query: false }
}
})
.then((response) => {
// Do something beautiful ;)
})
```

_Note: Not all instance options can be overridden per request, see the API documentation at the end of this readme_

### Cache POST request results

You can allow `axios-cache-adapter` to cache the results of a request using (almost) any HTTP method by modifying the `exclude.methods` list.

```js
import { setup } from 'axios-cache-adapter

const api = setup({
baseURL: 'https://httpbin.org',

cache: {
exclude: {
// Only exclude PUT, PATCH and DELETE methods from cache
methods: ['put', 'patch', 'delete']
}
}
})

api.post('/post').then((response) => {
// POST request has been cached \o/
})
```

**Note:** the request method is not used in the cache store key by default, therefore with the above setup, making a `GET` or `POST` request will respond with the same cache.

### Use localforage as cache store

You can give a `localforage` instance to `axios-cache-adapter` which will be used to store cache data instead of the default [in memory](https://github.com/RasCarlito/axios-cache-adapter/blob/master/src/memory.js) store.

_Note: This only works client-side because `localforage` does not work in Node.js_

```js
import localforage from 'localforage'
import memoryDriver from 'localforage-memoryStorageDriver'
import { setup } from 'axios-cache-adapter'

// `async` wrapper to configure `localforage` and instantiate `axios` with `axios-cache-adapter`
async function configure () {
// Register the custom `memoryDriver` to `localforage`
await localforage.defineDriver(memoryDriver)

// Create `localforage` instance
const forageStore = localforage.createInstance({
// List of drivers used
driver: [
localforage.INDEXEDDB,
localforage.LOCALSTORAGE,
memoryDriver._driver
],
// Prefix all storage keys to prevent conflicts
name: 'my-cache'
})

// Create `axios` instance with pre-configured `axios-cache-adapter` using a `localforage` store
return setup({
// `axios` options
baseURL: 'http://some-rest.api',

// `axios-cache-adapter` options
cache: {
maxAge: 15 * 60 * 1000,
store: forageStore // Pass `localforage` store to `axios-cache-adapter`
}
})
}

configure().then(async (api) => {
const response = await api.get('/url')

// Display something beautiful with `response.data` ;)
})
```

### Use redis as cache store

You can give a `RedisStore` instance to `axios-cache-adapter` which will be used to store cache data instead of the default [in memory](https://github.com/RasCarlito/axios-cache-adapter/blob/master/src/memory.js) store.

_Note: This only works server-side_

```js
const { setup, RedisStore } = require('axios-cache-adapter')
const redis = require('redis')

const client = redis.createClient({
url: 'REDIS_URL',
})
const store = new RedisStore(client)
const api = setup({
// `axios` options
baseURL: 'http://some-rest.api',
// `axios-cache-adapter` options
cache: {
maxAge: 15 * 60 * 1000,
store // Pass `RedisStore` store to `axios-cache-adapter`
}
})

const response = await api.get('/url')
```

#### Use Redis Default Store as Cache Store

You can give a `RedisDefaultStore` instance to `axios-cache-adapter` which will be used to store cache data in Redis using the default commands instead of hash commands.

_Note: This only works server-side_

```js
const { setup, RedisDefaultStore } = require('axios-cache-adapter')
const redis = require('redis')

const client = redis.createClient({
url: 'REDIS_URL',
})
const store = new RedisDefaultStore(client, {
prefix: 'namespace_as_prefix' // optional
})
const api = setup({
// `axios` options
baseURL: 'http://some-rest.api',
// `axios-cache-adapter` options
cache: {
maxAge: 15 * 60 * 1000,
store // Pass `RedisDefaultStore` store to `axios-cache-adapter`
}
})

const response = await api.get('/url')
```

### Check if response is served from network or from cache

When a response is served from cache a custom `response.request` object is created with a `fromCache` boolean.

```js
// Import dependencies
import assert from 'assert'
import { setup } from 'axios-cache-adapter'

// Create `axios` instance with pre-configured `axios-cache-adapter`
const api = setup({
cache: {
maxAge: 15 * 60 * 1000
}
})

// Wrap code in an `async` function
async function exec () {
// First request will be served from network
const response = await api.get('http://some-rest.api/url')

// `response.request` will contain the origin `axios` request object
assert.ok(response.request.fromCache !== true)

// Second request to same endpoint will be served from cache
const anotherResponse = await api.get('http://some-rest.api/url')

// `response.request` will contain `fromCache` boolean
assert.ok(anotherResponse.request.fromCache === true)
}

// Execute our `async` wrapper
exec()
```

### Read stale cache data on network error

You can tell `axios-cache-adapter` to read stale cache data when a network error occurs using the `readOnError` option.

`readOnError` can either be a `Boolean` telling cache adapter to attempt reading stale cache when any network error happens or a `Function` which receives the error and request objects and then returns a `Boolean`.

By default `axios-cache-adapter` clears stale cache data automatically, this would conflict with activating the `readOnError` option, so the `clearOnStale` option should be set to `false`.

```js
import { setup } from 'axios-cache-adapter'

const api = setup({
cache: {
// Attempt reading stale cache data when response status is either 4xx or 5xx
readOnError: (error, request) => {
return error.response.status >= 400 && error.response.status < 600
},
// Deactivate `clearOnStale` option so that we can actually read stale cache data
clearOnStale: false
}
})

// Make a first successful request which will store the response in cache
api.get('https://httpbin.org/get').then(response => {
// Response will not come from cache
assert.ok(response.request.fromCache !== true)
})

// Let's say that the stored data has become stale (default 15min max age has passed)
// and we make the same request but it results in an internal server error (status=500)
api.get('https://httpbin.org/get').then(response => {
// Response is served from cache
assert.ok(response.request.fromCache === true)
// We can check that it actually served stale cache data
assert.ok(response.request.stale === true)
}).catch(err => {
// Will not execute this because stale cache data was returned
// If the attempt at reading stale cache fails, the network error will be thrown and this method executed
})
```

_Note: Passing a function to `readOnError` is a smarter thing to do as you get to choose when a stale cache read should be attempted instead of doing it on all kind of errors_

### Invalidate cache entries

Using the default `invalidation` method, a cache entry will be invalidated if a request is made using one of the methods listed in `exclude.methods`.

```js
async function defaultInvalidate (config, request) {
const method = request.method.toLowerCase()

if (config.exclude.methods.includes(method)) {
await config.store.removeItem(config.uuid)
}
}
```

You can customize how `axios-cache-adapter` invalidates stored cache entries by providing a custom `invalidate` function.

```js
import { setup } from 'axios-cache-adapter'

// Create cached axios instance with custom invalidate method
const api = setup({
cache: {
// Invalidate only when a specific option is passed through config
invalidate: async (config, request) => {
if (request.clearCacheEntry) {
await config.store.removeItem(config.uuid)
}
}
}
})

// Make a request that will get stored into cache
api.get('https://httpbin.org/get').then(response => {
assert.ok(response.request.fromCache !== true)
})

// Wait some time

// Make another request to same end point but force cache invalidation
api.get('https://httpbin.org/get', { clearCacheEntry: true }).then(response => {
// Response should not come from cache
assert.ok(response.request.fromCache !== true)
})
```

### Use response headers to automatically set `maxAge`

When you set the `readHeaders` option to `true`, the adapter will try to read `cache-control` or `expires` headers to automatically set the `maxAge` option for the given request.

```js
import assert from 'assert'
import { setup } from 'axios-cache-adapter'

const api = setup({
cache: {
// Tell adapter to attempt using response headers
readHeaders: true,
// For this example to work we disable query exclusion
exclude: { query: false }
}
})

// Make a request which will respond with header `cache-control: max-age=60`
api.get('https://httpbin.org/cache/60').then(response => {
// Cached `response` will expire one minute later
})

// Make a request which responds with header `cache-control: no-cache`
api.get('https://httpbin.org/response-headers?cache-control=no-cache').then(response => {
// Response will not come from cache
assert.ok(response.request.fromCache !== true)

// Check that query was excluded from cache
assert.ok(response.request.excludedFromCache === true)
})
```

_Note: For the `cache-control` header, only the `max-age`, `no-cache` and `no-store` values are interpreted._

## API

### setupCache(options)

Create a cache adapter instance. Takes an `options` object to configure how the cached requests will be handled,
where they will be stored, etc.

#### Options

```js
// Options passed to `setupCache()`.
{
// {Number} Maximum time for storing each request in milliseconds,
// defaults to 15 minutes when using `setup()`.
maxAge: 0,
// {Number} Maximum number of cached request (last in, first out queue system),
// defaults to `false` for no limit. *Cannot be overridden per request*
limit: false,
// {Object} An instance of localforage, defaults to a custom in memory store.
// *Cannot be overridden per request*
store: new MemoryStore(),
// {String|Function} Generate a unique cache key for the request.
// Will use request url and serialized params by default.
key: req => req.url + serializeQuery(req.params),
// {Function} Invalidate stored cache. By default will remove cache when
// making a request with method not `GET`, `POST`, `PUT`, `PATCH` or `DELETE` query.
invalidate: async (cfg, req) => {
const method = req.method.toLowerCase()
if (method !== 'get') {
await cfg.store.removeItem(cfg.uuid)
}
},
// {Object} Define which kind of requests should be excluded from cache.
exclude: {
// {Array} List of regular expressions to match against request URLs.
paths: [],
// {Boolean} Exclude requests with query parameters.
query: true,
// {Function} Method which returns a `Boolean` to determine if request
// should be excluded from cache.
filter: null,
// {Array} HTTP methods which will be excluded from cache.
// Defaults to `['post', 'patch', 'put', 'delete']`
// Any methods listed will also trigger cache invalidation while using the default `config.invalidate` method.
//
// Note: the HEAD method is always excluded (hard coded).
// the OPTIONS method is ignored by this library as it is automatically handled by browsers/clients to resolve cross-site request permissions
methods: ['post', 'patch', 'put', 'delete']
},
// {Boolean} Clear cached item when it is stale.
clearOnStale: true,
// {Boolean} Clear all cache when a cache write error occurs
// (prevents size quota problems in `localStorage`).
clearOnError: true,
// {Function|Boolean} Determine if stale cache should be read when a network error occurs.
readOnError: false,
// {Boolean} Determine if response headers should be read to set `maxAge` automatically.
// Will try to parse `cache-control` or `expires` headers.
readHeaders: false,
// {Boolean} Ignore cache, will force to interpret cache reads as a `cache-miss`.
// Useful to bypass cache for a given request.
ignoreCache: false,
// {Function|Boolean} Print out debug log to console.
debug: false
}
```

#### Returns

`setupCache()` returns an object containing the configured `adapter`, the cache `store` and the `config` that is applied to this instance.

### setup(options)

Create an `axios` instance pre-configured with the cache adapter. Takes an `options` object to configure the cache and
axios at the same time.

#### Options

```js
{
cache: {
// Options passed to the `setupCache()` method
}

// Options passed to `axios.create()` method
}
```

All the other parameters will be passed directly to the [`axios.create`](https://github.com/mzabriskie/axios#creating-an-instance) method.

#### Returns

`setup()` returns an instance of `axios` pre-configured with the cache adapter.
The cache `store` is conveniently attached to the `axios` instance as `instance.cache` for easy access.

### RedisStore(client, [, hashKey])

RedisStore allow you to cache requests on server using [redis](https://redis.io/).
Create a `RedisStore` instance. Takes `client` (`RedisClient`) and optional `hashKey` (name of hashSet to be used in redis).

#### client

```js
// Using redis client https://github.com/NodeRedis/node_redis
// We have tested it with node_redis v.2.8.0 but it's supposed to work smoothly with the comming releases.
const redis = require("redis")
const client = redis.createClient()
```

#### Returns

`new RedisStore()` returns an instance of `RedisStore` to be passed to setupCache() as `store` in config object.

### Per request options

Using the same object definition as the `setup` method you can override cache options for individual requests.

```js
api.get('https://httpbin.org/get', {
cache: {
// Options override
}
})
```

All options except `limit` and `store` can be overridden per request.

Also the following keys are used internally and therefore should not be set in the options: `adapter`, `uuid`, `acceptStale`.

## Building

```sh
npm run build
```

Webpack is used to build [umd](https://github.com/umdjs/umd) versions of the library that are placed in the `dist` folder.

* `cache.js`
* `cache.min.js`
* `cache.node.js`
* `cache.node.min.js`

A different version of `axios-cache-adapter` is generated for node and the browser due to how Webpack 4 uses a `target` to change how the UMD wrapper is generated using `global` or `window`. If you are using the library in node or in your front-end code while using a module bundler (Webpack, rollup, etc) the correct version will be picked up automatically thanks to the `"main"` and `"browser"` fields in the `package.json`.

`axios-cache-adapter` is developped in ES6+ and uses async/await syntax. It is transpiled to ES5 using `babel` with `preset-env`.

## Testing

Tests are executed using [karma](https://github.com/karma-runner/karma).

To launch a single run tests using ChromeHeadless:

```sh
npm test
```

To launch tests in watch mode in Chrome for easier debugging with devtools:

```sh
npm run watch
```

## Browser vs Node.js

`axios-cache-adapter` was designed to run in the browser. It does work in nodejs using the [in memory store](https://github.com/RasCarlito/axios-cache-adapter/blob/master/src/memory.js). But storing data in memory is not the greatests idea ever.

You can give a `store` to override the in memory store but it has to comply with the [`localForage`](https://github.com/localForage/localForage) API and `localForage` does not work in nodejs for very good reasons that are better explained in [this issue](https://github.com/localForage/localForage/issues/57).

The better choice if you want to use `axios-cache-adapter` server-side is to use a redis server with a `RedisStore` instance as explained above in the API section.

## License

MIT © [Carl Ogren](https://github.com/RasCarlito)