├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── custom.md
│ └── feature_request.md
└── workflows
│ └── tests.yaml
├── .gitignore
├── .npmrc
├── .travis.yml
├── LICENSE
├── README.md
├── demos
├── basic.js
├── memory-store.js
└── redis-store.js
├── index.js
├── logo.svg
├── package.json
├── test
├── get-keys.test.js
└── smoke.test.js
└── utils.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | custom: https://www.paypal.me/kyberneees
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Custom issue template
3 | about: Describe this issue template's purpose here.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Have you contributed with at least a little donation to support the development of this project?**
11 | - Paypal: https://www.paypal.me/kyberneees
12 | - [TRON](https://www.binance.com/en/buy-TRON) Wallet: `TJ5Bbf9v4kpptnRsePXYDvnYcYrS5Tyxus`
13 |
14 | **Is your feature request related to a problem? Please describe.**
15 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
16 |
17 | **Describe the solution you'd like**
18 | A clear and concise description of what you want to happen.
19 |
20 | **Describe alternatives you've considered**
21 | A clear and concise description of any alternative solutions or features you've considered.
22 |
23 | **Additional context**
24 | Add any other context or screenshots about the feature request here.
25 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: tests
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | testing:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v1
9 | - name: Setup Environment (Using NodeJS 16.x)
10 | uses: actions/setup-node@v1
11 | with:
12 | node-version: 16.x
13 |
14 | - name: Install dependencies
15 | run: npm install
16 |
17 | - name: Linting
18 | run: npx standard
19 |
20 | - name: Run tests
21 | run: npm run test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 |
2 | language: node_js
3 | node_js:
4 | - "10"
5 | - "12"
6 | - "14"
7 |
8 | script:
9 | - npx standard
10 | - npm run test
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Rolando Santamaria Maso
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # http-cache-middleware
2 | [](https://www.npmjs.com/package/http-cache-middleware)
3 | [](https://www.npmjs.com/package/http-cache-middleware)
4 | [](https://www.npmjs.com/package/http-cache-middleware)
5 | [](https://www.npmjs.com/package/http-cache-middleware)
6 | [](https://github.com/jkyberneees/http-cache-middleware)
7 |
8 |
9 |
10 | High performance connect-like HTTP cache middleware for Node.js. So your latency can decrease to single digit milliseconds 🚀
11 |
12 | > Uses `cache-manager` as caching layer, so multiple
13 | storage engines are supported, i.e: Memory, Redis, ... https://www.npmjs.com/package/cache-manager
14 |
15 | ## Install
16 | ```js
17 | npm i http-cache-middleware
18 | ```
19 |
20 | ## Usage
21 | ```js
22 | const middleware = require('http-cache-middleware')()
23 | const service = require('restana')()
24 | service.use(middleware)
25 |
26 | service.get('/cache-on-get', (req, res) => {
27 | setTimeout(() => {
28 | // keep response in cache for 1 minute if not expired before
29 | res.setHeader('x-cache-timeout', '1 minute')
30 | res.send('this supposed to be a cacheable response')
31 | }, 50)
32 | })
33 |
34 | service.delete('/cache', (req, res) => {
35 | // ... the logic here changes the cache state
36 |
37 | // expire the cache keys using pattern
38 | res.setHeader('x-cache-expire', '*/cache-on-get')
39 | res.end()
40 | })
41 |
42 | service.start(3000)
43 | ```
44 | ## Redis cache
45 | ```js
46 | // redis setup
47 | const CacheManager = require('cache-manager')
48 | const redisStore = require('cache-manager-ioredis')
49 | const redisCache = CacheManager.caching({
50 | store: redisStore,
51 | db: 0,
52 | host: 'localhost',
53 | port: 6379,
54 | ttl: 30
55 | })
56 |
57 | // middleware instance
58 | const middleware = require('http-cache-middleware')({
59 | stores: [redisCache]
60 | })
61 | ```
62 |
63 | ## Why cache?
64 | > Because caching is the last mile for low latency distributed systems!
65 |
66 | Enabling proper caching strategies will drastically reduce the latency of your system, as it reduces network round-trips, database calls and CPU processing.
67 | For our services, we are talking here about improvements in response times from `X ms` to `~2ms`, as an example.
68 |
69 | ### Enabling cache for service endpoints
70 | Enabling a response to be cached just requires the
71 | `x-cache-timeout` header to be set:
72 | ```js
73 | res.setHeader('x-cache-timeout', '1 hour')
74 | ```
75 | > 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!
76 |
77 | Example on service using `restana`:
78 | ```js
79 | service.get('/numbers', (req, res) => {
80 | res.setHeader('x-cache-timeout', '1 hour')
81 |
82 | res.send([
83 | 1, 2, 3
84 | ])
85 | })
86 | ```
87 |
88 | ### Caching on the browser side (304 status codes)
89 | > From version `1.2.x` you can also use the HTTP compatible `Cache-Control` header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
90 | When using the Cache-Control header, you can omit the custom `x-cache-timeout` header as the timeout can be passed using the `max-age` directive.
91 |
92 | #### Direct usage:
93 | ```js
94 | res.setHeader('cache-control', 'private, no-cache, max-age=300')
95 | res.setHeader('etag', 'cvbonrw6g00')
96 |
97 | res.end('5 minutes cacheable content here....')
98 | ```
99 |
100 | #### Indirect usage:
101 | When using:
102 | ```js
103 | res.setHeader('x-cache-timeout', '5 minutes')
104 | ```
105 | The middleware will now transparently generate default `Cache-Control` and `ETag` headers as described below:
106 | ```js
107 | res.setHeader('cache-control', 'private, no-cache, max-age=300')
108 | res.setHeader('etag', 'ao8onrw6gbt') // random ETag value
109 | ```
110 | This will enable browser clients to keep a copy of the cache on their side, but still being forced to validate
111 | the cache state on the server before using the cached response, therefore supporting gateway based cache invalidation.
112 |
113 | > NOTE: In order to fetch the generated `Cache-Control` and `ETag` headers, there have to be at least one cache hit.
114 |
115 | ### Invalidating caches
116 | 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:
117 | ```js
118 | res.setHeader('x-cache-expire', '*/numbers')
119 | ```
120 | > Here we use the [`matcher`](`https://www.npmjs.com/package/matcher`) package for matching patterns evaluation.
121 |
122 | Example on service using `restana`:
123 | ```js
124 | service.patch('/numbers', (req, res) => {
125 | // ...
126 |
127 | res.setHeader('x-cache-expire', '*/numbers')
128 | res.send(200)
129 | })
130 | ```
131 |
132 | #### Invalidating multiple patterns
133 | Sometimes is required to expire cache entries using multiple patterns, that is also possible using the `,` separator:
134 | ```js
135 | res.setHeader('x-cache-expire', '*/pattern1,*/pattern2')
136 | ```
137 |
138 | #### Direclty invalidating caches from stores
139 | ```js
140 | const stores = [redisCache]
141 | const middleware = require('http-cache-middleware')({
142 | stores
143 | })
144 |
145 | const { deleteKeys } = require('http-cache-middleware/utils')
146 | deleteKeys(stores, '*/pattern1,*/pattern2')
147 | ```
148 |
149 | ### Custom cache keys
150 | Cache keys are generated using: `req.method + req.url`, however, for indexing/segmenting requirements it makes sense to allow cache keys extensions.
151 |
152 | To accomplish this, we simply recommend using middlewares to extend the keys before caching checks happen:
153 | ```js
154 | service.use((req, res, next) => {
155 | req.cacheAppendKey = (req) => req.user.id // here cache key will be: req.method + req.url + req.user.id
156 | return next()
157 | })
158 | ```
159 | > In this example we also distinguish cache entries by `user.id`, commonly used for authorization reasons.
160 |
161 | In case full control of the `cache-key` value is preferred, just populate the `req.cacheKey` property with a `string` value. In this case, the req.method + req.url prefix is discarded:
162 | ```js
163 | service.use((req, res, next) => {
164 | req.cacheKey = 'CUSTOM-CACHE-KEY'
165 | return next()
166 | })
167 | ```
168 |
169 | ### Disable cache for custom endpoints
170 | You can also disable cache checks for certain requests programmatically:
171 | ```js
172 | service.use((req, res, next) => {
173 | req.cacheDisabled = true
174 | return next()
175 | })
176 | ```
177 |
178 | ## Want to contribute?
179 | This is your repo ;)
180 |
181 | > Note: We aim to be 100% code coverage, please consider it on your pull requests.
182 |
183 | ## Related projects
184 | - fast-gateway (https://www.npmjs.com/package/fast-gateway)
185 |
186 | ## Sponsors
187 | - (INACTIVE) Kindly sponsored by [ShareNow](https://www.share-now.com/), a company that promotes innovation!
188 |
189 | ## Support / Donate 💚
190 | You can support the maintenance of this project:
191 | - Paypal: https://www.paypal.me/kyberneees
192 | - [TRON](https://www.binance.com/en/buy-TRON) Wallet: `TJ5Bbf9v4kpptnRsePXYDvnYcYrS5Tyxus`
--------------------------------------------------------------------------------
/demos/basic.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const middleware = require('./../index')()
4 | const service = require('restana')()
5 | service.use(middleware)
6 |
7 | service.get('/cache-on-get', (req, res) => {
8 | setTimeout(() => {
9 | // keep response in cache for 1 minute if not expired before
10 | res.setHeader('x-cache-timeout', '1 minute')
11 | res.send('this supposed to be a cacheable response')
12 | }, 50)
13 | })
14 |
15 | service.get('/cache-control', (req, res) => {
16 | setTimeout(() => {
17 | // keep response in cache for 1 minute if not expired before
18 | res.setHeader('cache-control', 'private, no-cache, max-age=60')
19 | res.send('this supposed to be a cacheable response')
20 | }, 50)
21 | })
22 |
23 | service.delete('/cache', (req, res) => {
24 | // ... the logic here changes the cache state
25 |
26 | // expire the cache keys using pattern
27 | res.setHeader('x-cache-expire', '*/cache-*')
28 | res.end()
29 | })
30 |
31 | service.start(3000)
32 |
--------------------------------------------------------------------------------
/demos/memory-store.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const CacheManager = require('cache-manager')
4 | const middleware = require('./../index')({
5 | stores: [CacheManager.caching({ store: 'memory', max: 2000, ttl: 30 })]
6 | })
7 | const service = require('restana')()
8 | service.use(middleware)
9 | // ...
10 |
11 | service.start(3000)
12 |
--------------------------------------------------------------------------------
/demos/redis-store.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const CacheManager = require('cache-manager')
4 | const redisStore = require('cache-manager-ioredis')
5 | const redisCache = CacheManager.caching({
6 | store: redisStore,
7 | db: 0,
8 | host: 'localhost',
9 | port: 6379,
10 | ttl: 30
11 | })
12 | const middleware = require('../index')({
13 | stores: [redisCache]
14 | })
15 |
16 | const service = require('restana')()
17 | service.use(middleware)
18 |
19 | service.get('/cache', (req, res) => {
20 | setTimeout(() => {
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 | res.setHeader('x-cache-expire', '*/cache-*')
28 | res.end()
29 | })
30 |
31 | service.start(3000)
32 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const CacheManager = require('cache-manager')
4 | const iu = require('middleware-if-unless')()
5 | const { parse: cacheControl } = require('@tusbar/cache-control')
6 | const ms = require('ms')
7 | const onEnd = require('on-http-end')
8 | const { get, deleteKeys, DATA_POSTFIX } = require('./utils')
9 |
10 | const X_CACHE_EXPIRE = 'x-cache-expire'
11 | const X_CACHE_TIMEOUT = 'x-cache-timeout'
12 | const X_CACHE_HIT = 'x-cache-hit'
13 | const CACHE_ETAG = 'etag'
14 | const CACHE_CONTROL = 'cache-control'
15 | const CACHE_IF_NONE_MATCH = 'if-none-match'
16 |
17 | const middleware = (opts) => {
18 | opts = Object.assign({
19 | stores: [CacheManager.caching({ store: 'memory', max: 1000, ttl: 30 })]
20 | }, opts)
21 | const mcache = CacheManager.multiCaching(opts.stores)
22 |
23 | return iu(async (req, res, next) => {
24 | try {
25 | if (req.cacheDisabled) return next()
26 |
27 | if (typeof req.cacheKey !== 'string') {
28 | let { url, cacheAppendKey = req => '' } = req
29 | cacheAppendKey = await cacheAppendKey(req)
30 |
31 | const key = req.method + url + cacheAppendKey
32 | // ref cache key on req object
33 | req.cacheKey = key
34 | }
35 |
36 | // try to retrieve cached response metadata
37 | const metadata = await get(mcache, req.cacheKey)
38 |
39 | if (metadata) {
40 | // respond from cache if there is a hit
41 | const { status, headers, encoding } = JSON.parse(metadata)
42 |
43 | // pre-checking If-None-Match header
44 | if (req.headers[CACHE_IF_NONE_MATCH] && req.headers[CACHE_IF_NONE_MATCH] === headers[CACHE_ETAG]) {
45 | res.setHeader('content-length', '0')
46 | res.statusCode = 304
47 | res.end()
48 |
49 | return
50 | } else {
51 | // try to retrieve cached response data
52 | const payload = await get(mcache, req.cacheKey + DATA_POSTFIX)
53 | if (payload) {
54 | let { data } = JSON.parse(payload)
55 | if (typeof data === 'object' && data.type === 'Buffer') {
56 | data = Buffer.from(data.data)
57 | }
58 | headers[X_CACHE_HIT] = '1'
59 |
60 | // set cached response headers
61 | Object.keys(headers).forEach(header => res.setHeader(header, headers[header]))
62 |
63 | // send cached payload
64 | req.cacheHit = true
65 | res.statusCode = status
66 | res.end(data, encoding)
67 |
68 | return
69 | }
70 | }
71 | }
72 |
73 | onEnd(res, async (payload) => {
74 | if (payload.status === 304) return
75 |
76 | if (payload.headers[X_CACHE_EXPIRE]) {
77 | // support service level expiration
78 | const keysPattern = payload.headers[X_CACHE_EXPIRE].replace(/\s/g, '')
79 | const patterns = keysPattern.split(',')
80 | // delete keys on all cache tiers
81 | deleteKeys(opts.stores, patterns)
82 | } else if (payload.headers[X_CACHE_TIMEOUT] || payload.headers[CACHE_CONTROL]) {
83 | // extract cache ttl
84 | let ttl = 0
85 | if (payload.headers[CACHE_CONTROL]) {
86 | ttl = cacheControl(payload.headers[CACHE_CONTROL]).maxAge
87 | }
88 | if (!ttl) {
89 | if (payload.headers[X_CACHE_TIMEOUT]) {
90 | ttl = Math.max(ms(payload.headers[X_CACHE_TIMEOUT]), 1000) / 1000 // min value: 1 second
91 | } else {
92 | return // no TTL found, we don't cache
93 | }
94 | }
95 |
96 | // setting cache-control header if absent
97 | if (!payload.headers[CACHE_CONTROL]) {
98 | payload.headers[CACHE_CONTROL] = `private, no-cache, max-age=${ttl}`
99 | }
100 | // setting ETag if absent
101 | if (!payload.headers[CACHE_ETAG]) {
102 | payload.headers[CACHE_ETAG] = Math.random().toString(36).substring(2, 16)
103 | }
104 |
105 | // cache response data
106 | await mcache.set(req.cacheKey + DATA_POSTFIX, JSON.stringify({ data: payload.data }), { ttl })
107 | delete payload.data
108 | // cache response metadata
109 | await mcache.set(req.cacheKey, JSON.stringify(payload), { ttl })
110 | }
111 | })
112 |
113 | return next()
114 | } catch (err) {
115 | return next(err)
116 | }
117 | })
118 | }
119 |
120 | module.exports = middleware
121 | module.exports.deleteKeys = deleteKeys
122 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "http-cache-middleware",
3 | "version": "1.4.1",
4 | "description": "HTTP Cache Middleware",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "nyc mocha test/*.test.js --exit"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/jkyberneees/http-cache-middleware.git"
12 | },
13 | "keywords": [
14 | "http",
15 | "cache",
16 | "middleware"
17 | ],
18 | "files": [
19 | "index.js",
20 | "utils.js",
21 | "README.md",
22 | "LICENSE"
23 | ],
24 | "author": "Rolando Santamaria Maso ",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/jkyberneees/http-cache-middleware/issues"
28 | },
29 | "homepage": "https://github.com/jkyberneees/http-cache-middleware#readme",
30 | "devDependencies": {
31 | "cache-manager-ioredis": "^2.1.0",
32 | "chai": "^4.3.6",
33 | "got": "^11.8.5",
34 | "mocha": "^10.0.0",
35 | "nyc": "^15.1.0",
36 | "restana": "^4.9.6"
37 | },
38 | "dependencies": {
39 | "@tusbar/cache-control": "^0.6.1",
40 | "cache-manager": "^4.1.0",
41 | "matcher": "^4.0.0",
42 | "middleware-if-unless": "^1.3.0",
43 | "ms": "^2.1.3",
44 | "on-http-end": "^1.0.2"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/test/get-keys.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* global describe, it */
4 |
5 | const { getKeys } = require('./../utils')
6 | const expect = require('chai').expect
7 |
8 | describe('get-keys', () => {
9 | const pattern = '*/numbers'
10 | const redisCache = {
11 | store: { name: 'redis' },
12 | keys: (pattern, callback) => {
13 | if (!pattern) throw new Error('pattern is missing')
14 | return callback(null, ['GET/numbers'])
15 | }
16 | }
17 |
18 | const memoryCache = {
19 | store: { name: 'memory' },
20 | keys: (callback) => {
21 | return callback(null, ['GET/numbers'])
22 | }
23 | }
24 |
25 | it('should retrieve redis keys', function (done) {
26 | getKeys(redisCache, pattern).then(keys => {
27 | expect(keys.includes('GET/numbers')).to.equal(true)
28 | done()
29 | })
30 | })
31 |
32 | it('should skip pattern', function (done) {
33 | getKeys(redisCache, 'GET/numbers').then(keys => {
34 | expect(keys.includes('GET/numbers')).to.equal(true)
35 | done()
36 | })
37 | })
38 |
39 | it('should retrieve memory keys', function (done) {
40 | getKeys(memoryCache, pattern).then(keys => {
41 | expect(keys.includes('GET/numbers')).to.equal(true)
42 | done()
43 | })
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/test/smoke.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* global describe, it */
4 | const got = require('got')
5 | const expect = require('chai').expect
6 |
7 | describe('cache middleware', () => {
8 | const server = require('restana')()
9 |
10 | it('init', async () => {
11 | const middleware = require('./../index')()
12 | server.use((req, res, next) => {
13 | if (req.url === '/cache-disabled') {
14 | req.cacheDisabled = true
15 | }
16 |
17 | next()
18 | })
19 | server.use((req, res, next) => {
20 | if (req.url === '/custom-cache-key') {
21 | req.cacheKey = 'my-custom-cache-key'
22 | }
23 |
24 | next()
25 | })
26 |
27 | server.use(middleware)
28 |
29 | server.get('/health', (req, res) => {
30 | res.send()
31 | })
32 |
33 | server.get('/cache-disabled', (req, res) => {
34 | setTimeout(() => {
35 | res.setHeader('x-cache-timeout', '1 minute')
36 | res.send('hello')
37 | }, 50)
38 | })
39 |
40 | server.get('/custom-cache-key', (req, res) => {
41 | res.send(req.cacheKey)
42 | })
43 |
44 | server.get('/cache', (req, res) => {
45 | setTimeout(() => {
46 | res.setHeader('x-cache-timeout', '1 minute')
47 | res.send('hello')
48 | }, 50)
49 | })
50 |
51 | server.get('/cache-buffer', (req, res) => {
52 | setTimeout(() => {
53 | res.setHeader('x-cache-timeout', '1 minute')
54 | res.setHeader('etag', '1')
55 | res.setHeader('cache-control', 'no-cache')
56 | res.send(Buffer.from('world'))
57 | }, 50)
58 | })
59 |
60 | server.get('/cache-no-maxage', (req, res) => {
61 | res.setHeader('etag', '1')
62 | res.setHeader('cache-control', 'no-cache')
63 | res.send('!maxage')
64 | })
65 |
66 | server.get('/cache-control', (req, res) => {
67 | res.setHeader('cache-control', 'max-age=60')
68 | res.setHeader('etag', '1')
69 | res.send('cache')
70 | })
71 |
72 | server.delete('/cache', (req, res) => {
73 | res.setHeader('x-cache-expire', '*/cache')
74 | res.end()
75 | })
76 |
77 | server.delete('/cache2', (req, res) => {
78 | res.setHeader('x-cache-expire', '*/cache*')
79 | res.end()
80 | })
81 | })
82 |
83 | it('start', async () => {
84 | return server.start(3000)
85 | })
86 |
87 | it('no-cache', async () => {
88 | const res = await got('http://localhost:3000/health')
89 | expect(res.headers['x-cache-hit']).to.equal(undefined)
90 | })
91 |
92 | it('cache disabled', async () => {
93 | await got('http://localhost:3000/cache-disabled')
94 | const res = await got('http://localhost:3000/cache-disabled')
95 | expect(res.headers['x-cache-hit']).to.equal(undefined)
96 | })
97 |
98 | it('custom cache key', async () => {
99 | const res = await got('http://localhost:3000/custom-cache-key')
100 | expect(res.body).to.equal('my-custom-cache-key')
101 | })
102 |
103 | it('create cache', async () => {
104 | const res = await got('http://localhost:3000/cache')
105 | expect(res.body).to.equal('hello')
106 | expect(res.headers['x-cache-hit']).to.equal(undefined)
107 | })
108 |
109 | it('cache hit', async () => {
110 | const res = await got('http://localhost:3000/cache')
111 | expect(res.body).to.equal('hello')
112 | expect(res.headers['x-cache-hit']).to.equal('1')
113 | })
114 |
115 | it('cache expire', async () => {
116 | await got.delete('http://localhost:3000/cache')
117 | })
118 |
119 | it('re-create cache', async () => {
120 | const res = await got('http://localhost:3000/cache')
121 | expect(res.body).to.equal('hello')
122 | expect(res.headers['x-cache-hit']).to.equal(undefined)
123 | })
124 |
125 | it('cache expire using wildcard', async () => {
126 | await got.delete('http://localhost:3000/cache2')
127 | })
128 |
129 | it('re-create cache', async () => {
130 | const res = await got('http://localhost:3000/cache')
131 | expect(res.body).to.equal('hello')
132 | expect(res.headers['x-cache-hit']).to.equal(undefined)
133 | })
134 |
135 | it('create cache (buffer)', async () => {
136 | const res = await got('http://localhost:3000/cache-buffer')
137 | expect(res.body).to.equal('world')
138 | expect(res.headers['x-cache-hit']).to.equal(undefined)
139 | })
140 |
141 | it('no TTL detected', async () => {
142 | await got('http://localhost:3000/cache-no-maxage')
143 | const res = await got('http://localhost:3000/cache-no-maxage')
144 | expect(res.body).to.equal('!maxage')
145 | expect(res.headers['x-cache-hit']).to.equal(undefined)
146 | })
147 |
148 | it('cache hit (buffer)', async () => {
149 | const res = await got('http://localhost:3000/cache-buffer')
150 | expect(res.body).to.equal('world')
151 | expect(res.headers['x-cache-hit']).to.equal('1')
152 | expect(res.headers['cache-control']).to.equal('no-cache')
153 | expect(res.headers.etag).to.equal('1')
154 | })
155 |
156 | it('cache hit (buffer) - Etag', async () => {
157 | const res = await got('http://localhost:3000/cache-buffer')
158 | expect(res.body).to.equal('world')
159 | expect(res.headers['x-cache-hit']).to.equal('1')
160 | expect(res.headers.etag).to.equal('1')
161 | })
162 |
163 | it('cache hit (buffer) - If-None-Match', async () => {
164 | const res = await got('http://localhost:3000/cache-buffer', {
165 | headers: {
166 | 'If-None-Match': '1'
167 | }
168 | })
169 | expect(res.statusCode).to.equal(304)
170 | })
171 |
172 | it('cache create - cache-control', async () => {
173 | const res = await got('http://localhost:3000/cache-control')
174 | expect(res.body).to.equal('cache')
175 | })
176 |
177 | it('cache hit - cache-control', async () => {
178 | const res = await got('http://localhost:3000/cache-control')
179 | expect(res.body).to.equal('cache')
180 | expect(res.headers['x-cache-hit']).to.equal('1')
181 | })
182 |
183 | it('close', async () => {
184 | return server.close()
185 | })
186 | })
187 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const matcher = require('matcher')
4 |
5 | const DATA_POSTFIX = '-d'
6 |
7 | const getKeys = (cache, pattern) => new Promise((resolve) => {
8 | if (pattern.indexOf('*') > -1) {
9 | const args = [pattern, (_, res) => resolve(matcher(res, [pattern]))]
10 | if (cache.store.name !== 'redis') {
11 | args.shift()
12 | }
13 |
14 | cache.keys.apply(cache, args)
15 | } else resolve([pattern])
16 | })
17 |
18 | const get = (cache, key) => cache.getAndPassUp(key)
19 |
20 | const deleteKeys = (stores, patterns) => {
21 | patterns = patterns.map(pattern =>
22 | pattern.endsWith('*') ? pattern : [pattern, pattern + DATA_POSTFIX]
23 | ).flat()
24 |
25 | patterns.forEach(pattern => stores.forEach(store => getKeys(store, pattern).then(keys => keys.length > 0 ? store.del(keys) : null)))
26 | }
27 |
28 | module.exports = {
29 | get,
30 | deleteKeys,
31 | getKeys,
32 | DATA_POSTFIX
33 | }
34 |
--------------------------------------------------------------------------------