A generic cache/refreshing module for api clients. out-of-band-cache
can be
useful in caching your network requests, which can allow you avoid making
needlessly repeated network requests to the same endpoint. You can also avoid
hitting rate limits by limiting your requests to those which are unique.
npm install --save out-of-band-cache
You can instantiate the cache with 3 configurable options: maxAge
,
maxStaleness
, and fsCachePath
.
maxAge
: The duration, in milliseconds, before a cached item expiresmaxStaleness
: (Optional) The duration, in milliseconds, in which expired cache items are still served. Defaults to 0, meaning never serve data past expiration. Can also be set toInfinity
, meaning always give at least a cached version.maxMemoryItems
: (Optional) Discards the least recently used items from memory when storing items past this maximum.fsCachePath
: (Optional) file path to create a file-system based cacheshouldCache
: (Optional) a function to determine whether or not you will cache a found item
const Cache = require('out-of-band-cache');
const cache = new Cache({
maxAge: 10 * 60 * 1000, // cache items for 10 minutes
maxStaleness: 60 * 60 * 1000 // Still serve expired cache items for 1 hour
});
If want to use a file system cache, simply pass the desired pathname as fsCachePath
.
Here we create a file system cache in the current directory with the name .cache
:
const Cache = require('out-of-band-cache');
const path = require('path');
const cache = new Cache({
maxAge: 10 * 60 * 1000,
maxStaleness: 60 * 60 * 1000,
fsCachePath: path.join(__dirname, '.cache')
});
This is driven through cache.get
and takes 3 parameters:
key
: The cache keyopts
: Options for this particular read. You can overridemaxAge
or potentially skip the cache entirelygetter
: anasync
function that defines how to fetch the data for the givenkey
.- The data returned is assumed to be valid when passed to
JSON.stringify
.
- The data returned is assumed to be valid when passed to
The results will be returned to you wrapped in an object with 2 properties:
value
: The result for the givenkey
(i.e. the value that was cached or retrieved from yourgetter
)fromCache
: A boolean that indicates whether the value was retrieved from the cache orfalse
if thegetter
was used. This can be useful when debugging to know where the values you are using came from.
Very basic usage can be as follows:
async function getter(key) {
return 'Here is your ' + key;
}
await cache.get('data', {}, getter); // { value: 'Here is your data', fromCache: false }
await cache.get('data', {}, getter); // { value: 'Here is your data', fromCache: true }
In this example, getter
is the definition on what it means to get fresh
data the first time. out-of-band-cache
will store this result, either in
memory on disk, so that future get
calls with the same key, in this case
data
, will be immediately returned, rather then performing a potentially
expensive or time-consuming operation. This is how you can avoid needlessly
hitting the same API url via network request over and over.
If you get something that you would want to keep around for a while you can
pass a different maxAge
:
await cache.get('data', { maxAge: 120 * 60 * 1000 }, getter);
In some cases, you may want to modify how your cache
remembers data.
You can do this in two ways. The first is to skip the cache entirely, going
directly to getter
for your data. This can be done via the skipCache
option.
It is important to note that this condition is evaluated before the getter
.
await cache.get('data', { skipCache: true }, getter);
The other method is conditionally choose not to remember certain items based on
their value. This can be done via the shouldCache
option. For example: here we
are only remembering values that === 'data'
. It is important to note that this
condition is evaluated after the getter
.
await cache.get('data', { shouldCache: value => value === 'data' }, getter);
The getter
function that you pass to get
is used to retrieve fresh data
for the cache. When it is invoked, it will be sent both the key
that needs
to be refreshed, but also the previous value of that key in the cache. This
can be useful when you know that values for particular keys never change and
are expensive to calculate.
async function reusingGetter(key, previousValue) {
// If the key never changes and we already have a value, return it.
if (key === 'this-thing' && previousValue) return previousValue;
// Do some expensive operation
return `${key} is expensive to calculate`;
}
await cache.get('this-thing', {}, reusingGetter);
// -> { value: 'this-thing is expensive to calculate', fromCache: false }
await cache.get('this-thing', { maxAge: -1, maxStaleness: -1 }, reusingGetter);
// -> { value: 'this-thing is expensive to calculate', fromCache: false }
If you want to wipe out the existing cache, you can simply use cache.reset
.
This will remove all items from all of the caches associated with your
client
, so both the in-memory and file-system caches will be wiped.
// fetch fresh data
await cache.get('data', {}, getter);
// get it again, returning the cached value
await cache.get('data', {}, getter);
// clear the cache, forgetting what "data" is
await cache.reset();
// Will get fresh data again
await cache.get('data', {}, getter);
The example provided outlines a barebones API client that
takes advantage of out-of-band-cache
. You can run the example by cloning
this repo and running npm run example
. The script should take about 4
seconds to run:
$ npm run example
Fetched {"value":{"code":123465,"planet":"Druidia"},"fromCache":false} in 2005ms
Fetched {"value":{"code":123465,"planet":"Druidia"},"fromCache":true} in 0ms
Fetched {"value":{"code":123465,"planet":"Druidia"},"fromCache":true} in 0ms
Fetched {"value":{"code":123465,"planet":"Druidia"},"fromCache":true} in 0ms
Fetched {"value":{"code":123465,"planet":"Druidia"},"fromCache":false} in 2001ms
Let's say you want to add functionality to persist your cache in places other
than just in memory and on disk. You can provide the fallback
option to
provide additional persistence methods to your cache.
const Cache = require('out-of-band-cache');
const OtherCache = require('./path/to/my/my-custom-cache');
const fallback = [ new OtherCache(optionsToContructIt) ];
const cache = new Cache({
maxAge: 10 * 60 * 1000,
maxStaleness: 60 * 60 * 1000,
fallback
});
Your new OtherCache
will then be used after the built-in caches. In the
above case a fallback for the memory
cache: any key not found in the memory
cache will then be looked up in OtherCache
.
If you instead want to entirely replace the built-in array of caches, you can provide a caches
array to override it. In this example, we replicate the default implementation by overriding with the Memory
and File
cache implementations that come with out-of-band-cache
:
const path = require('path');
const { Memory, File } = require('out-of-band-cache');
const caches = [
new Memory(),
new File({
path: path.join(__dirname, '.cache')
})
];
const cache = new Cache({
maxAge: 10 * 60 * 1000,
maxStaleness: 60 * 60 * 1000,
caches
});
The out-of-band-cache
library comes with three cache implementations:
Memory
: A simple in-memory cache that stores values in an object mapFile
: A file-based cache that stores values in a directory on disk. It takes a config object with apath
property that specifies the directory to use.LRU
: An in-memory cache that stores a maximum number of items, evicting the least recently used items when the cache is full. It takes a config object with amaxItems
property that specifies the maximum number of items to store andmaxAge
.
A custom cache must, at a minimum, match the following spec to integrate
properly with out-of-band-cache
:
class MyCustomCache {
/**
* Initializes the cache. This may need to set up connections, create files, or
* something else that is needed before any other operations occur.
*
* @returns {Promise<void>} a Promise which resolves when initialization has completed.
*/
async init() { }
/**
* Tries to retrieve a cache item from the existing cache
*
* @param {String} key - The cache key
* @returns {Promise<JSONSerializable>} a Promise which resolves if an item was found
* @throws {Error} an error that is thrown is the item cannot be found
*/
async get(key) { }
/**
* Stores a cache item
*
* @param {String} key - The cache key
* @param {JSONSerializable} value - The JSON-serializable value to store
* @returns {Promise<void>} a Promise which resolves once storage completes,
* or fails if there is an error writing the value into the cache.
*/
async set(key, value) { }
/**
* Clears the cache entirely
*
* @returns {Promise<void>} a Promise which resolves once all cache files are
* deleted or fails if there was an error.
*/
async reset() { }
}
If you want to ensure that your cache works properly, we have included a test
suite that you can run directly on your cache. NB, you will need to have
mocha
and assume
already installed to use this test.
const cacheTest = require('out-of-band-cache/test/cache');
const otherCache = require('./path/to/my/my-custom-cache');
describe('MyCustomCache', function () {
it('is correctly consumed by out-of-band-cache', function () {
const options = {
beforeEach: function () {
// any setup that needs to be done before a test
},
afterEach: function () {
// any teardown that needs to be done after a test
},
constructor: otherCache,
builder: {
// any options that are needed to construct an otherCache via
// new contructor(builder)
}
}
// run the mocha suite
cacheTest(options)();
});
})