-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathindex.js
124 lines (102 loc) · 3.39 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
'use strict'
const fp = require('fastify-plugin')
const Keyv = require('keyv')
const crypto = require('crypto')
const {EventEmitter} = require('events')
const CACHEABLE_METHODS = ['GET']
const X_RESPONSE_CACHE = 'x-response-cache'
const X_RESPONSE_CACHE_HIT = 'hit'
const X_RESPONSE_CACHE_MISS = 'miss'
function isCacheableRequest(req) {
return CACHEABLE_METHODS.includes(req.raw.method)
}
function buildCacheKey(req, {headers}) {
const {url, headers: requestHeaders} = req.raw
const additionalCondition = headers.reduce((acc, header) => {
return `${acc}__${header}:${requestHeaders[header] || ''}`
}, '')
const data = `${url}__${additionalCondition}`
const encrytedData = crypto.createHash('md5').update(data)
const key = encrytedData.digest('hex')
return key
}
function createOnRequestHandler({ttl, additionalCondition: {headers}}) {
return async function handler(req, res) {
if (!isCacheableRequest(req)) {
return
}
const cache = this.responseCache
const cacheNotifier = this.responseCacheNotifier
const key = buildCacheKey(req, {headers})
const requestKey = `${key}__requested`
const isRequestExisted = await cache.get(requestKey)
async function waitForCacheFulfilled(key) {
return new Promise((resolve) => {
cache.get(key).then((cachedString) => {
if (cachedString) {
resolve(cachedString)
}
})
const handler = async () => {
const cachedString = await cache.get(key)
resolve(cachedString)
}
cacheNotifier.once(key, handler)
setTimeout(() => cacheNotifier.removeListener(key, handler), ttl)
setTimeout(() => resolve(), ttl)
})
}
if (isRequestExisted) {
const cachedString = await waitForCacheFulfilled(key)
if (cachedString) {
const cached = JSON.parse(cachedString)
res.header(X_RESPONSE_CACHE, X_RESPONSE_CACHE_HIT)
return res.code(cached.statusCode).header('Content-Type', cached.contentType).send(cached.payload)
} else {
res.header(X_RESPONSE_CACHE, X_RESPONSE_CACHE_MISS)
}
} else {
await cache.set(requestKey, 'cached', ttl)
res.header(X_RESPONSE_CACHE, X_RESPONSE_CACHE_MISS)
}
}
}
function createOnSendHandler({ttl, additionalCondition: {headers}}) {
return async function handler(req, res, payload) {
if (!isCacheableRequest(req)) {
return
}
const cache = this.responseCache
const cacheNotifier = this.responseCacheNotifier
const key = buildCacheKey(req, {headers})
await cache.set(
key,
JSON.stringify({
statusCode: res.statusCode,
contentType: res.getHeader('Content-Type'),
payload,
}),
ttl,
)
cacheNotifier.emit(key)
}
}
const responseCachingPlugin = (
instance,
{ttl = 1000, additionalCondition = {}},
next,
) => {
const headers = additionalCondition.headers || []
const opts = {ttl, additionalCondition: {headers}}
const responseCache = new Keyv()
const responseCacheNotifier = new EventEmitter()
instance.decorate('responseCache', responseCache)
instance.decorate('responseCacheNotifier', responseCacheNotifier)
instance.addHook('onRequest', createOnRequestHandler(opts))
instance.addHook('onSend', createOnSendHandler(opts))
return next()
}
module.exports = fp(responseCachingPlugin, {
fastify: '3.x',
name: 'fastify-response-caching',
})