Skip to content

Commit b050c25

Browse files
authored
Merge pull request #1 from jkyberneees/initial-commit
initial commit
2 parents ea7fd0c + 29498fe commit b050c25

File tree

9 files changed

+3716
-1
lines changed

9 files changed

+3716
-1
lines changed

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
language: node_js
3+
node_js:
4+
- "10"
5+
6+
install: npm install

README.md

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,124 @@
11
# http-cache-middleware
2-
HTTP Cache Middleware
2+
High performance connect-like HTTP cache middleware for Node.js.
3+
4+
> Uses `cache-manager` as caching layer, so multiple
5+
storage engines are supported, i.e: Memory, Redis, ... https://www.npmjs.com/package/cache-manager
6+
7+
## Install
8+
```js
9+
npm i http-cache-middleware
10+
```
11+
12+
## Usage
13+
```js
14+
const middleware = require('http-cache-middleware')()
15+
const service = require('restana')()
16+
service.use(middleware)
17+
18+
service.get('/cache-on-get', (req, res) => {
19+
setTimeout(() => {
20+
// keep response in cache for 1 minute if not expired before
21+
res.setHeader('x-cache-timeout', '1 minute')
22+
res.send('this supposed to be a cacheable response')
23+
}, 50)
24+
})
25+
26+
service.delete('/cache', (req, res) => {
27+
// ... the logic here changes the cache state
28+
29+
// expire the cache keys using pattern
30+
res.setHeader('x-cache-expire', '*/cache-on-get')
31+
res.end()
32+
})
33+
34+
service.start(3000)
35+
```
36+
## Redis cache
37+
```js
38+
// redis setup
39+
const CacheManager = require('cache-manager')
40+
const redisStore = require('cache-manager-ioredis')
41+
const redisCache = CacheManager.caching({
42+
store: redisStore,
43+
db: 0,
44+
host: 'localhost',
45+
port: 6379,
46+
ttl: 30
47+
})
48+
49+
// middleware instance
50+
const middleware = require('http-cache-middleware')({
51+
stores: [redisCache]
52+
})
53+
```
54+
55+
## Why cache?
56+
> Because caching is the last mile for low latency distributed systems!
57+
58+
Enabling proper caching strategies will drastically reduce the latency of your system, as it reduces network round-trips, database calls and CPU processing.
59+
For our services, we are talking here about improvements in response times from `X ms` to `~2ms`, as an example.
60+
61+
### Enabling cache for service endpoints
62+
Enabling a response to be cached just requires the
63+
`x-cache-timeout` header to be set:
64+
```js
65+
res.setHeader('x-cache-timeout', '1 hour')
66+
```
67+
> Here we use the [`ms`](`https://www.npmjs.com/package/ms`) package to convert timeout to seconds. Please note that `millisecond` unit is not supported!
68+
69+
Example on service using `restana`:
70+
```js
71+
service.get('/numbers', (req, res) => {
72+
res.setHeader('x-cache-timeout', '1 hour')
73+
74+
res.send([
75+
1, 2, 3
76+
])
77+
})
78+
```
79+
80+
### Invalidating caches
81+
Services can easily expire cache entries on demand, i.e: when the data state changes. Here we use the `x-cache-expire` header to indicate the cache entries to expire using a matching pattern:
82+
```js
83+
res.setHeader('x-cache-expire', '*/numbers')
84+
```
85+
> Here we use the [`matcher`](`https://www.npmjs.com/package/matcher`) package for matching patterns evaluation.
86+
87+
Example on service using `restana`:
88+
```js
89+
service.patch('/numbers', (req, res) => {
90+
// ...
91+
92+
res.setHeader('x-cache-expire', '*/numbers')
93+
res.send(200)
94+
})
95+
```
96+
97+
### Custom cache keys
98+
Cache keys are generated using: `req.method + req.url`, however, for indexing/segmenting requirements it makes sense to allow cache keys extensions.
99+
100+
For doing this, we simply recommend using middlewares to extend the keys before caching checks happen:
101+
```js
102+
service.use((req, res, next) => {
103+
req.cacheAppendKey = (req) => req.user.id // here cache key will be: req.method + req.url + req.user.id
104+
return next()
105+
})
106+
```
107+
> In this example we also distinguish cache entries by `user.id`, very important for authorization reasons.
108+
109+
### Disable cache for custom endpoints
110+
You can also disable cache checks for certain requests programmatically:
111+
```js
112+
service.use((req, res, next) => {
113+
req.cacheDisabled = true
114+
return next()
115+
})
116+
```
117+
118+
## Want to contribute?
119+
This is your repo ;)
120+
121+
> Note: We aim to be 100% code coverage, please consider it on your pull requests.
122+
123+
## Related projects
124+
- fast-gateway (https://www.npmjs.com/package/fast-gateway)

demos/basic.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const middleware = require('./../index')()
2+
const service = require('restana')()
3+
service.use(middleware)
4+
5+
service.get('/cache-on-get', (req, res) => {
6+
setTimeout(() => {
7+
// keep response in cache for 1 minute if not expired before
8+
res.setHeader('x-cache-timeout', '1 minute')
9+
res.send('this supposed to be a cacheable response')
10+
}, 50)
11+
})
12+
13+
service.delete('/cache', (req, res) => {
14+
// ... the logic here changes the cache state
15+
16+
// expire the cache keys using pattern
17+
res.setHeader('x-cache-expire', '*/cache-on-get')
18+
res.end()
19+
})
20+
21+
service.start(3000)

get-keys.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const matcher = require('matcher')
2+
3+
const getKeys = (cache, pattern) => new Promise((resolve) => {
4+
if (pattern.indexOf('*') > -1) {
5+
const args = [pattern, (_, res) => resolve(matcher(res, [pattern]))]
6+
if (cache.store.name !== 'redis') {
7+
args.shift()
8+
}
9+
10+
cache.keys.apply(cache, args)
11+
} else resolve([pattern])
12+
})
13+
14+
module.exports = getKeys

index.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const CacheManager = require('cache-manager')
2+
const iu = require('middleware-if-unless')()
3+
const ms = require('ms')
4+
const onEnd = require('on-http-end')
5+
const getKeys = require('./get-keys')
6+
7+
const X_CACHE_EXPIRE = 'x-cache-expire'
8+
const X_CACHE_TIMEOUT = 'x-cache-timeout'
9+
const X_CACHE_HIT = 'x-cache-hit'
10+
11+
const middleware = (opts) => async (req, res, next) => {
12+
opts = Object.assign({
13+
stores: [CacheManager.caching({ store: 'memory', max: 1000, ttl: 30 })]
14+
}, opts)
15+
16+
// creating multi-cache instance
17+
const mcache = CacheManager.multiCaching(opts.stores)
18+
19+
if (req.cacheDisabled) return next()
20+
21+
let { url, cacheAppendKey = req => '' } = req
22+
cacheAppendKey = await cacheAppendKey(req)
23+
24+
const key = req.method + url + cacheAppendKey
25+
// ref cache key on req object
26+
req.cacheKey = key
27+
28+
// try to retrieve cached response
29+
const cached = await get(mcache, key)
30+
31+
if (cached) {
32+
// respond from cache if there is a hit
33+
let { status, headers, data } = JSON.parse(cached)
34+
if (typeof data === 'object' && data.type === 'Buffer') {
35+
data = Buffer.from(data.data)
36+
}
37+
headers[X_CACHE_HIT] = '1'
38+
39+
// set cached response headers
40+
Object.keys(headers).forEach(header => res.setHeader(header, headers[header]))
41+
42+
// send cached payload
43+
req.cacheHit = true
44+
res.statusCode = status
45+
res.end(data)
46+
47+
return
48+
}
49+
50+
onEnd(res, (payload) => {
51+
if (payload.headers[X_CACHE_EXPIRE]) {
52+
// support service level expiration
53+
const keysPattern = payload.headers[X_CACHE_EXPIRE]
54+
// delete keys on all cache tiers
55+
opts.stores.forEach(cache => getKeys(cache, keysPattern).then(keys => mcache.del(keys)))
56+
} else if (payload.headers[X_CACHE_TIMEOUT]) {
57+
// we need to cache response
58+
mcache.set(req.cacheKey, JSON.stringify(payload), {
59+
// @NOTE: cache-manager uses seconds as TTL unit
60+
// restrict to min value "1 second"
61+
ttl: Math.max(ms(payload.headers[X_CACHE_TIMEOUT]), 1000) / 1000
62+
})
63+
}
64+
})
65+
66+
return next()
67+
}
68+
69+
const get = (cache, key) => new Promise((resolve) => {
70+
cache.getAndPassUp(key, (_, res) => {
71+
resolve(res)
72+
})
73+
})
74+
75+
module.exports = iu(middleware)

0 commit comments

Comments
 (0)