UNPKG

cacheable-response

Version:

An HTTP compliant route path middleware for serving cache response with invalidation support.

113 lines (92 loc) 2.52 kB
'use strict' const createCompress = require('compress-brotli') const memoize = require('@keyvhq/memoize') const Keyv = require('@keyvhq/core') const assert = require('assert') const { createKey, isFunction, setHeaders, size } = require('./util') const cacheableResponse = ({ logger = () => {}, bypassQueryParameter = 'force', cache = new Keyv({ namespace: 'ssr' }), compress: enableCompression = false, get: rawGet, key: getKey = createKey(bypassQueryParameter), send, staleTtl: rawStaleTtl = 3600000, ttl: rawTtl = 86400000, ...compressOpts } = {}) => { assert(rawGet, '.get required') assert(send, '.send required') const staleTtl = isFunction(rawStaleTtl) ? rawStaleTtl : ({ staleTtl = rawStaleTtl } = {}) => staleTtl const ttl = isFunction(rawTtl) ? rawTtl : ({ ttl = rawTtl } = {}) => ttl const { serialize, compress, decompress } = createCompress({ enable: enableCompression, ...compressOpts }) const getEtag = input => require('etag')(serialize(input)) const get = opts => Promise.resolve(rawGet(opts)).then(result => { if (typeof result !== 'object') return result result.etag = getEtag(result) return result }) const memoGet = memoize(get, cache, { key: getKey, objectMode: true, staleTtl, ttl, value: compress }) const fn = async opts => { const { req, res } = opts const [raw, { forceExpiration, hasValue, key, isExpired, isStale }] = await memoGet(opts) if (res.writableEnded) return const result = (await decompress(raw)) || {} const isHit = !forceExpiration && !isExpired && hasValue const { createdAt = Date.now(), data = null, etag = getEtag(result), staleTtl = memoGet.staleTtl(result), ttl = memoGet.ttl(result), ...props } = result const ifNoneMatch = req.headers['if-none-match'] const isModified = etag !== ifNoneMatch logger({ key, isHit, isExpired, isStale, result: size(result) === 0, etag, ifNoneMatch, isModified }) setHeaders({ createdAt, etag, forceExpiration, hasValue, isHit, isStale, res, staleTtl, ttl }) if (!forceExpiration && !isModified) { res.statusCode = 304 res.end() return } return send({ data, res, req, ...props }) } fn.getEtag = getEtag return fn } module.exports = cacheableResponse module.exports.setHeaders = setHeaders