cacheable-response
Version:
An HTTP compliant route path middleware for serving cache response with invalidation support.
113 lines (92 loc) • 2.52 kB
JavaScript
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