Skip to content

Instantly share code, notes, and snippets.

@dhigginbotham
Last active April 14, 2017 21:00
Show Gist options
  • Save dhigginbotham/c69266cbc47e865aad435766985e0777 to your computer and use it in GitHub Desktop.
Save dhigginbotham/c69266cbc47e865aad435766985e0777 to your computer and use it in GitHub Desktop.
Volatile cache
const log = require('debug')('bot:app:shared:cacheable:');
const ms = require('ms');
/**
* Volatile state/cache machine
* @param { object } settings - Settings for cache, includes
*/
function cacheable(settings = {}) {
const { key = 'cacheable', ttl = '3h', staleMaths = 0.65 } = settings;
if (!(settings.hasOwnProperty('expires'))) settings.expires = 0;
/**
* flush the cache, resets back to an empty object or array, depending
* on how you interact with it
*/
function flush() {
settings.cache = Array.isArray(settings.cache) ? [] : {};
return true;
}
/**
* determine cache size either by `Object.keys` or `Array.length`
* @return { number }
*/
function size() {
if (!(settings.hasOwnProperty('cache'))) flush();
return Array.isArray(settings.cache)
? settings.cache.length
: Object.keys(settings.cache).length;
}
/**
* reset's stale/ttl from ts provided with additional offset
* @param now - Defaults to `Date.now()`, however you can provide
* an future date, or a past date to do some funky stuff
*/
function reset(ts = Date.now()) {
settings.ts = ts;
settings.expires = settings.ts + ms(ttl);
settings.stale = settings.ts + (ms(ttl) * staleMaths);
return true;
}
/**
* determine if cache state is expired
* @return { boolean } - value if cache is expired or not
*/
function expired() {
const now = Date.now();
let isExpired = false;
const cacheSize = size();
if (now >= settings.expires || !cacheSize) isExpired = true;
return isExpired;
}
/**
* determine if the cache state is stale
* @return { boolean } - value if cache is stale or not
* @param now { number } - defaults to now, but i can
* see maybe wanting to have control over that adhoc
*/
function stale(now = Date.now()) {
return !expired() && now >= settings.stale;
}
/**
* determines full cache state, if expired, we'll clean up
* @return { boolean } - whether cache is expired or not
*/
function state() {
const now = Date.now();
const isExpired = expired();
const cacheSize = size();
const isStale = stale();
if (isExpired && cacheSize) flush();
if (isExpired) reset();
log('%s:isExpired:%s:cacheSize:%d:isStale:%s:expiresInSecs:%d:staleInSecs:%d',
key, isExpired, cacheSize, isStale,
`${Math.round((settings.expires - now) / 1000)}`,
`${Math.round((settings.stale - now) / 1000)}`);
return isExpired;
}
return { expired, flush, size, stale, state, reset };
}
module.exports = { cacheable };
const store = {
key: 'expensiveCacheExample',
cache: {},
ttl: '1h'
};
const expired = cacheable(store).state; // pulls state out to manage state and check if expired
const { stale, reset, flush } = cacheable(store); // other helpful tings
// example usage for expensive or long running functions
function doSomethingExpensive(fn) {
const isStale = stale();
if (!expired() && !isStale) return fn(null, store.cache);
if (isStale) fn(null, store.cache);
return funcToExpensiveThings((err, resp) => {
if (err) return fn(new Error(err), null);
store.cache = resp;
if (isStale) return reset();
return fn(null, resp);
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment