httplease-cache
Version:
An implementation of HTTP caching as an httplease filter
125 lines (97 loc) • 3.29 kB
JavaScript
;
const {promisify} = require('util');
const NodeCache = require('node-cache');
const defaultCacheOptions = {
stdTTL: 10 * 60, // 10 minutes
checkperiod: 10 // 10 seconds
};
function generateDefaultCacheKey(requestConfig) {
return JSON.stringify({
protocol: requestConfig.httpOptions.protocol,
hostname: requestConfig.httpOptions.hostname,
port: requestConfig.httpOptions.port,
method: requestConfig.httpOptions.method,
path: requestConfig.httpOptions.path,
headers: requestConfig.httpOptions.headers,
body: requestConfig.body
});
}
function safeNumber(number) {
if (!isNaN(number) && isFinite(number)) {
return number;
} else {
return 0;
}
}
function parseCacheControl(cacheControlHeader) {
// max-age=600,no-cache,foo=bar
// ->
// {'max-age': 600, 'no-cache': true, 'foo': 'bar'}
return (cacheControlHeader || '')
.split(',')
.map((item) => item.split('='))
.reduce((d, keyvalue) => {
const key = keyvalue[0];
const value = keyvalue[1];
d[key.trim()] = value !== undefined ? value.trim() : true;
return d;
}, {});
}
function parseCacheData(headers) {
const cacheControl = parseCacheControl(headers['cache-control']);
const d = {
maxAge: safeNumber(parseInt(cacheControl['max-age'], 10)),
noCache: cacheControl['no-cache'],
expires: safeNumber(Date.parse(headers['expires']) / 1000),
date: safeNumber(Date.parse(headers['date']) / 1000),
age: safeNumber(headers['age'])
};
if (d.date === 0) {
d.date = Date.now() / 1000;
}
return d;
}
function getResponseAge(d) {
const now = Date.now() / 1000;
return Math.max(now - d.date, d.age, 0);
}
function getFreshnessLifetime(d) {
if (d.noCache) {
return 0;
}
if (d.maxAge) {
return Math.max(d.maxAge, 0);
}
if (d.expires) {
return Math.max(d.expires - d.date, 0);
}
return 0;
}
function calculateTtl(headers) {
// RFC 2616 Section 13.2
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
const d = parseCacheData(headers);
const responseAge = getResponseAge(d);
const freshnessLifetime = getFreshnessLifetime(d);
return freshnessLifetime - responseAge;
}
function createCacheFilter(opts={}) {
const theCache = opts.theCache || new NodeCache(defaultCacheOptions);
const generateCacheKey = opts.generateCacheKey || generateDefaultCacheKey;
const loadFromCache = promisify(theCache.get.bind(theCache));
const storeToCache = promisify(theCache.set.bind(theCache));
return async function cacheRequest(requestConfig, next) {
const cacheKey = generateCacheKey(requestConfig);
const cachedResponse = await loadFromCache(cacheKey);
if (cachedResponse !== undefined) {
return cachedResponse;
}
const response = await next(requestConfig); // eslint-disable-line callback-return
const ttl = calculateTtl(response.headers);
if (ttl > 0) {
await storeToCache(cacheKey, response, ttl);
}
return response;
};
}
module.exports = createCacheFilter;