UNPKG

httplease-cache

Version:

An implementation of HTTP caching as an httplease filter

125 lines (97 loc) 3.29 kB
'use strict'; 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;