forked from Kikobeats/cacheable-response
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
129 lines (109 loc) · 3.38 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
'use strict'
const debug = require('debug-logfmt')('cacheable-response')
const createCompress = require('compress-brotli')
const normalizeUrl = require('normalize-url')
const { parse } = require('querystring')
const prettyMs = require('pretty-ms')
const assert = require('assert')
const getEtag = require('etag')
const { URL } = require('url')
const Keyv = require('keyv')
const isEmpty = value =>
value === undefined ||
value === null ||
(typeof value === 'object' && Object.keys(value).length === 0) ||
(typeof value === 'string' && value.trim().length === 0)
const getKeyDefault = ({ req }) => {
const url = new URL(req.url, 'http://localhost').toString()
const { origin } = new URL(url)
const baseKey = normalizeUrl(url, {
removeQueryParameters: ['force', /^utm_\w+/i]
})
return baseKey.replace(origin, '').replace('/?', '')
}
const toSeconds = ms => Math.floor(ms / 1000)
const createSetHeaders = ({ revalidate }) => {
return ({ res, createdAt, isHit, ttl, hasForce, etag }) => {
// Specifies the maximum amount of time a resource
// will be considered fresh in seconds
const diff = hasForce ? 0 : createdAt + ttl - Date.now()
const maxAge = toSeconds(diff)
const revalidation = toSeconds(revalidate(ttl))
res.setHeader(
'Cache-Control',
`public, must-revalidate, max-age=${maxAge}, s-maxage=${maxAge}, stale-while-revalidate=${revalidation}, stale-if-error=${revalidation}`
)
res.setHeader('X-Cache-Status', isHit ? 'HIT' : 'MISS')
res.setHeader('X-Cache-Expired-At', prettyMs(diff))
res.setHeader('ETag', etag)
}
}
module.exports = ({
cache = new Keyv({ namespace: 'ssr' }),
compress: enableCompression = false,
getKey = getKeyDefault,
get,
send,
revalidate = ttl => Math.round(ttl * 0.2),
ttl: defaultTtl = 7200000,
...compressOpts
} = {}) => {
assert(get, '.get required')
assert(send, '.send required')
const setHeaders = createSetHeaders({
revalidate: typeof revalidate === 'function' ? revalidate : () => revalidate
})
const { serialize, compress, decompress } = createCompress({
enable: enableCompression,
...compressOpts
})
return async opts => {
const { req, res } = opts
const hasForce = Boolean(
req.query ? req.query.force : parse(req.url.split('?')[1]).force
)
const key = getKey(opts)
const cachedResult = await decompress(await cache.get(key))
const isHit = !hasForce && cachedResult !== undefined
const result = isHit ? cachedResult : await get(opts)
if (isEmpty(result)) return
const {
etag: cachedEtag,
ttl = defaultTtl,
createdAt = Date.now(),
data,
...props
} = result
const etag = cachedEtag || getEtag(serialize(data))
const ifNoneMatch = req.headers['if-none-match']
const isModified = etag !== ifNoneMatch
debug({
key,
isHit,
cachedResult: !isEmpty(cachedResult),
result: !isEmpty(result),
etag,
ifNoneMatch
})
setHeaders({
etag,
res,
createdAt,
isHit,
ttl,
hasForce
})
if (!isHit) {
const payload = { etag, createdAt, ttl, data, ...props }
const value = await compress(payload)
await cache.set(key, value, ttl)
}
if (!isModified) {
res.statusCode = 304
res.end()
return
}
return send({ data, res, req, ...props })
}
}
module.exports.getKey = getKeyDefault