Storages
Storages are responsible for saving, retrieving and serializing (if needed) cache data. They are completely customizable and you can code your own, or use one published on NPM.
They are meant to be the middleware between the cache interceptor and some sort of database (persistent or not) you may have. Our interceptors will call its methods internally to save and retrieve data, but you can do it manually to work programmatically your own way.
Currently, two storages are included in the library by default:
- Memory Storage accessible with
buildMemoryStorage
(works on Node and Web) - Web Storage API accessible with
buildWebStorage
(works on Web only)
Memory Storage
WARNING
This is the storage chosen by default
Memory storage is the simplest one. It works everywhere and its values are lost upon page reload or when the process is killed.
If you are directly mutating some response property, you probably will face some reference issues because the storage will also get mutated. To avoid that, you can use the clone: true
option to clone the response before saving it or clone: 'double'
to also clone both ways, on set()
and on get()
. Just like #136 and many others.
For long running processes, you can avoid memory leaks by using playing with the cleanupInterval
option. And can reduce memory usage with maxEntries
.
import Axios from 'axios';
import {
setupCache,
buildMemoryStorage
} from 'axios-cache-interceptor';
setupCache(axios, {
// You don't need to to that, as it is the default option.
storage: buildMemoryStorage(
/* cloneData default=*/ false,
/* cleanupInterval default=*/ false,
/* maxEntries default=*/ false
)
});
Options:
cloneData: Use
true
if the data returned byfind()
should be cloned to avoid mutating the original data outside theset()
method. Use'double'
to also clone before saving value in storage usingset()
. Disabled is defaultcleanupInterval: The interval in milliseconds to run a setInterval job of cleaning old entries. If false, the job will not be created. Disabled is default
maxEntries: The maximum number of entries to keep in the storage. Its hard to determine the size of the entries, so a smart FIFO order is used to determine eviction. If false, no check will be done and you may grow up memory usage. Disabled is default
Web Storage API
If you need persistent caching between page refreshes, you can use the buildWebStorage
to get this behavior. It works by connecting our storage API to the browser’s Storage API.
import Axios from 'axios';
import { setupCache, buildWebStorage } from 'axios-cache-interceptor';
setupCache(axios, {
// As localStorage is a public storage, you can add a prefix
// to all keys to avoid collisions with other code.
storage: buildWebStorage(localStorage, 'axios-cache:')
});
import Axios from 'axios';
import { setupCache, buildWebStorage } from 'axios-cache-interceptor';
setupCache(axios, {
// As sessionStorage is a public storage, you can add a prefix
// to all keys to avoid collisions with other code.
storage: buildWebStorage(sessionStorage, 'axios-cache:')
});
import Axios from 'axios';
import { setupCache, buildWebStorage } from 'axios-cache-interceptor';
const myStorage = new Storage();
setupCache(axios, {
storage: buildWebStorage(myStorage)
});
Browser quota
From v0.9.0
onwards, web storage is able to detect and evict older entries if the browser’s quota is reached.
The eviction is done by the following algorithm:
- Just saved an value and got an error. (Probably quota exceeded)
- Evicts all expired keys that cannot enter the
stale
state. - If it fails again, evicts the oldest key with the given prefix.
- Repeat step 2 and 3 until the object can be saved or the storage has been emptied.
- If it still fails, the data is not saved. Probably because the whole key is greater than the quota or other libraries already consumed the whole usable space.
buildStorage()
All integrated storages are wrappers around the buildStorage
function. External libraries use it and if you want to build your own, buildStorage
is the way to go!
The exported buildStorage
function abstracts the storage interface and requires a super simple object to build the storage. It has 3 methods:
set(key: string, value: NotEmptyStorageValue, currentRequest?: CacheRequestConfig): MaybePromise<void>
: Receives the key and the value, and optionally the current request. It should save the value in the storage.remove(key: string, currentRequest?: CacheRequestConfig): MaybePromise<void>
: Receives the key and optionally the current request. It should remove the value from the storage.find(key: string, currentRequest?: CacheRequestConfig) => MaybePromise<StorageValue | undefined>
: Receives the key and optionally the current request. It should return the value from the storage orundefined
if not found.clear() => MaybePromise<void>
: Clears all data from storage. This method isn’t used by the interceptor itself, instead, its here for you to use it programmatically.
Third Party Storages
These are not guaranteed to work with the latest version of the library as neither are maintained by the axios cache interceptor team. But, as we provide a minimal interface for storages, you can use them as a base to also create your own.
- Node Redis v4
- IndexedDb
- Node Cache
- Have another one?
- Open a PR to add it here.
Node Redis storage
The node redis storage implementation is listed here because it shows the only tricky part when implementing a storage with an third party client that allows auto-evicting entries, as show on the PXAT
property.
import { createClient } from 'redis'; // v4
import { buildStorage, canStale } from 'axios-cache-interceptor';
const client = createClient(/* connection config */);
const redisStorage = buildStorage({
find(key) {
return client
.get(`axios-cache-${key}`)
.then((result) => result && (JSON.parse(result) as StorageValue));
},
set(key, value, req) {
return client.set(`axios-cache-${key}`, JSON.stringify(value), {
PXAT:
// We don't want to keep indefinitely values in the storage if
// their request don't finish somehow. Either set its value as
// the TTL or 1 minute.
value.state === 'loading'
? Date.now() +
(req?.cache && typeof req.cache.ttl === 'number'
? req.cache.ttl
: // 1 minute in seconds
60000)
: // When a stale state has a determined value to expire, we can use it.
// Or if the cached value cannot enter in stale state.
(value.state === 'stale' && value.ttl) ||
(value.state === 'cached' && !canStale(value))
?
value.createdAt + value.ttl!
: // otherwise, we can't determine when it should expire, so we keep
// it indefinitely.
undefined
});
},
remove(key) {
return client.del(`axios-cache-${key}`);
}
});
However you can use the buildStorage
function to integrate with ANY storage you want, like localForage
, ioredis
, memcache
and others.
IndexedDB
Here is an example of how to use the idb-keyval
library to create a storage that uses IndexedDB.
import axios from 'axios';
import { buildStorage } from 'axios-cache-interceptor';
import { clear, del, get, set } from 'idb-keyval';
const indexedDbStorage = buildStorage({
async find(key) {
const value = await get(key);
if (!value) {
return;
}
return JSON.parse(value);
},
async set(key, value) {
await set(key, JSON.stringify(value));
},
async remove(key) {
await del(key);
}
});
Node Cache
This example implementation uses node-cache as a storage method. Do note that this library is somewhat old, however it appears to work at the time of writing.
import { buildStorage } from "axios-cache-interceptor";
import NodeCache from "node-cache";
const cache = new NodeCache({ stdTTL: 60 * 60 * 24 * 7 });
const cacheStorage = buildStorage({
find(key) {
return cache.get(key)
}
set(key, value) {
cache.set(key, value);
},
remove(key) {
cache.del(key);
},
});