├── .gitignore ├── test ├── mocha.opts ├── setup.js └── specs.js ├── index.js ├── .travis.yml ├── HISTORY.md ├── package.json ├── README.md └── lib └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/setup 2 | --reporter spec 3 | --ui bdd -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Export lib 3 | */ 4 | 5 | module.exports = require('./lib/'); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | #set node version to latest stable 2 | language: node_js 3 | node_js: 4 | - "0.10" 5 | services: 6 | - redis-server -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | global.chai = require('chai'); 2 | global.should = chai.should(); 3 | global.expect = require('chai').expect(); 4 | global.redis = require("redis").createClient(6379, 'localhost'); 5 | global.cache = require('../lib'); 6 | 7 | global.PAYLOAD_PREFIX = 'payload_'; 8 | global.HEADER_PREFIX = 'header_'; -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 0.2.1 / 2014-02-01 2 | ================== 3 | 4 | * added; callback to `after` 5 | * tests 6 | 7 | 0.2.0 / 2014-01-29 8 | ================== 9 | 10 | * added; cache headers as well 11 | * changed; store payload and headers under different keys 12 | 13 | 14 | 0.1.0 / 2014-01-28 15 | ================== 16 | 17 | * fixed; use raw methods instead of JSON.parse -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restify-cache", 3 | "version": "0.3.1", 4 | "description": "Restify cache using Redis", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/mocha/bin/mocha test/**/*.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/gergelyke/restify-cache.git" 12 | }, 13 | "keywords": [ 14 | "redis", 15 | "restify", 16 | "cache" 17 | ], 18 | "author": "Gergely Nemeth", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/gergelyke/restify-cache/issues" 22 | }, 23 | "homepage": "https://github.com/gergelyke/restify-cache", 24 | "dependencies": { 25 | "redis": "~0.10.0", 26 | "hiredis": "~0.1.16" 27 | }, 28 | "devDependencies": { 29 | "mocha": "~1.17.1", 30 | "chai": "~1.9.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Restify-cache 2 | ------------- 3 | Based on the query string, it will store/retrieve JSON responses from Redis. 4 | This is a Work In Progress, not ready for production (headers are not cached as of now). 5 | 6 | [![Build Status](https://travis-ci.org/gergelyke/restify-cache.png?branch=feature/test)](https://travis-ci.org/gergelyke/restify-cache) 7 | [![NPM](https://nodei.co/npm/restify-cache.png)](https://nodei.co/npm/restify-cache/) 8 | 9 | 10 | ### Usage ### 11 | 12 | ``` 13 | var cache = require('restify-cache'); 14 | cache.config({ 15 | redisPort: 6379, //default: '6379' 16 | redisHost: 'localhost', //default: 'localhost' 17 | redisOptions: {}, //optional 18 | ttl: 60 * 60 //default: 60 * 60; in seconds 19 | }); 20 | ``` 21 | 22 | The first middleware after auth (if there is any) should be the cache's before. 23 | 24 | ``` 25 | server.use(cache.before); 26 | ``` 27 | 28 | You have to subscribe for the server's after event as well. 29 | 30 | __WARNING! In your route handlers, you always have to call `next()`!__ 31 | 32 | ``` 33 | server.on('after', cache.after); 34 | ``` 35 | 36 | ### Cache Control ### 37 | Use of Restify's [res.cache()](http://mcavage.me/node-restify/#Response-API) method will control the [EXPIRE](http://redis.io/commands/expire) time in Redis. The absence of a response cache will use the **cache.config.ttl** value identified above. 38 | 39 | Indicates that the response should be cached for 600 seconds. 40 | ``` 41 | res.cache('public', 600); 42 | ``` 43 | 44 | A maxAge value of 0 will engage Redis, but set the expire seconds to 0 (essentially expiring immediately). 45 | ``` 46 | res.cache('public', 0); 47 | ``` 48 | 49 | ### Additional Headers ### 50 | A header is added to each response: 51 | 52 | * __X-Cache: HIT__ - the response was served from cache 53 | * __X-Cache: MISS__ - the response generation fell through to the endpoint 54 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var redis = require("redis"), 2 | client, 3 | config; 4 | 5 | var PAYLOAD_PREFIX = 'payload_', 6 | HEADER_PREFIX = 'header_'; 7 | /* 8 | * Sets the things 9 | * */ 10 | exports.config = function (cfg) { 11 | config = cfg || {}; 12 | config.redisPort = config.redisPort || 6379; 13 | config.redisHost = config.redisHost || 'localhost'; 14 | config.ttl = config.ttl || 60 * 60; //1 hour 15 | client = redis.createClient(config.redisPort, config.redisHost, config.redisOptions); 16 | }; 17 | 18 | /* 19 | * Checks if we have the response in Redis 20 | * */ 21 | exports.before = function (req, res, next) { 22 | var url; 23 | // if config wasn't called, lets set it now. 24 | if (!client) { 25 | exports.config(); 26 | } 27 | url = req.url; 28 | client.get(PAYLOAD_PREFIX + url, function(err, payload) { 29 | if (err) { 30 | return next(err); 31 | } 32 | client.get(HEADER_PREFIX + url, function(err, headers) { 33 | var parsedHeaders, 34 | headerItem; 35 | 36 | if (err) { 37 | return next(err); 38 | } 39 | if (payload && headers) { 40 | parsedHeaders = JSON.parse(headers); 41 | for (headerItem in parsedHeaders) { 42 | res.header(headerItem, parsedHeaders[headerItem]); 43 | } 44 | 45 | res.header('X-Cache', 'HIT'); 46 | res.writeHead(200); 47 | res.end(payload); 48 | } else { 49 | res.header('X-Cache', 'MISS'); 50 | next(); 51 | } 52 | }); 53 | }); 54 | }; 55 | 56 | /* 57 | * Put the response into Redis 58 | * */ 59 | exports.after = function(req, res, route, error, cb) { 60 | if (error) { 61 | if (cb) { 62 | return cb(error); 63 | } 64 | 65 | return; 66 | } 67 | 68 | // if config wasn't called, lets set it now. 69 | if (!client) { 70 | exports.config(); 71 | } 72 | // save the headers 73 | 74 | client.set(HEADER_PREFIX + req.url, JSON.stringify(res.headers()), function(err ){ 75 | client.expire(HEADER_PREFIX + req.url, determineCacheTTL(res)); 76 | 77 | // save the payload 78 | client.set(PAYLOAD_PREFIX + req.url, res._data, function(err ){ 79 | client.expire(PAYLOAD_PREFIX + req.url, determineCacheTTL(res), cb); 80 | }); 81 | 82 | }); 83 | 84 | 85 | 86 | }; 87 | 88 | function determineCacheTTL(res) { 89 | var cacheControl = res.getHeader('cache-control'); 90 | 91 | if (cacheControl) { 92 | var maxAgeMatch = /max-age=(\d+)/.exec(cacheControl); 93 | 94 | if (maxAgeMatch) { 95 | return maxAgeMatch[1]; 96 | } 97 | } 98 | 99 | return config.ttl; 100 | } -------------------------------------------------------------------------------- /test/specs.js: -------------------------------------------------------------------------------- 1 | describe('Redis-cache', function () { 2 | 3 | var testUrl = '/testing-the-redis-cache'; 4 | 5 | describe("errors do not write cache", function() { 6 | it("do not throw if callback is not provided", function (done) { 7 | var ex = new Error('expected'); 8 | 9 | var fn = function(){ 10 | cache.after({url: testUrl}, null, null, ex) 11 | }; 12 | 13 | chai.expect(fn).to.not.throw(); 14 | 15 | done(); 16 | }); 17 | 18 | it("do not write cache in after", function (done) { 19 | var ex = new Error('expected'); 20 | cache.after({url: testUrl}, null, null, ex, function (err) { 21 | 22 | ex.should.be.equal(err); 23 | 24 | redis.get(HEADER_PREFIX + testUrl, function (err, data) { 25 | if (err) { 26 | return done(err); 27 | } 28 | 29 | (data === null).should.equal(true); 30 | 31 | return done(); 32 | }); 33 | }); 34 | }); 35 | }); 36 | 37 | describe('saves', function () { 38 | 39 | 40 | afterEach(function (done) { 41 | redis.del(HEADER_PREFIX + testUrl, function (err) { 42 | if (err) { 43 | return done(err); 44 | } 45 | redis.del(PAYLOAD_PREFIX + testUrl, function (err) { 46 | done(err); 47 | }); 48 | }); 49 | }); 50 | 51 | describe("cache-control not specified", function () { 52 | it('responseheaders', function (done) { 53 | cache.after({ 54 | url: testUrl 55 | }, { 56 | headers: function () { 57 | return { 58 | 'Accept': 'application/xml' 59 | }; 60 | }, 61 | header: function(key, value) { 62 | key.should.equal('X-Cache'); 63 | value.should.equal('MISS'); 64 | }, 65 | getHeader: function () { 66 | return undefined; 67 | } 68 | }, null, null, function (err) { 69 | if (err) { 70 | return done(err); 71 | } 72 | redis.get(HEADER_PREFIX + testUrl, function (err, data) { 73 | if (err) { 74 | return done(err); 75 | } 76 | if (!data) { 77 | return done('No data!') 78 | } 79 | data.should.equal('{"Accept":"application/xml"}'); 80 | 81 | redis.ttl(HEADER_PREFIX + testUrl, function (err, data) { 82 | if (err) { 83 | return done(err); 84 | } 85 | if (!data) { 86 | return done('No data!') 87 | } 88 | data.should.equal(3600); 89 | 90 | done(); 91 | }); 92 | }); 93 | }) 94 | }); 95 | 96 | it('payload', function (done) { 97 | cache.after({ 98 | url: testUrl 99 | }, { 100 | headers: function () { 101 | return {}; 102 | }, 103 | header: function(key, value) { 104 | key.should.equal('X-Cache'); 105 | value.should.equal('MISS'); 106 | }, 107 | getHeader: function () { 108 | return undefined; 109 | }, 110 | _data: JSON.stringify({ 111 | 'testing': 1, 112 | 'expect': 'works' 113 | }) 114 | }, null, null, function (err) { 115 | if (err) { 116 | return done(err); 117 | } 118 | redis.get(PAYLOAD_PREFIX + testUrl, function (err, data) { 119 | if (err) { 120 | return done(err); 121 | } 122 | if (!data) { 123 | return done('No data!') 124 | } 125 | data.should.equal('{\"testing\":1,\"expect\":\"works\"}'); 126 | 127 | redis.ttl(PAYLOAD_PREFIX + testUrl, function (err, data) { 128 | if (err) { 129 | return done(err); 130 | } 131 | if (!data) { 132 | return done('No data!') 133 | } 134 | data.should.equal(3600); 135 | done(); 136 | }); 137 | }); 138 | }); 139 | }); 140 | }); 141 | 142 | describe("cache-control specified", function () { 143 | it('responseheaders', function (done) { 144 | cache.after({ 145 | url: testUrl 146 | }, { 147 | headers: function () { 148 | return { 149 | 'Accept': 'application/xml', 150 | 'Cache-Control': 'public, max-age=54321' 151 | }; 152 | }, 153 | header: function(key, value) { 154 | key.should.equal('X-Cache'); 155 | value.should.equal('MISS'); 156 | }, 157 | getHeader: function () { 158 | return 'public, max-age=54321'; 159 | } 160 | }, null, null, function (err) { 161 | if (err) { 162 | return done(err); 163 | } 164 | redis.get(HEADER_PREFIX + testUrl, function (err, data) { 165 | if (err) { 166 | return done(err); 167 | } 168 | if (!data) { 169 | return done('No data!') 170 | } 171 | data.should.equal('{"Accept":"application/xml","Cache-Control":"public, max-age=54321"}'); 172 | 173 | redis.ttl(HEADER_PREFIX + testUrl, function (err, data) { 174 | if (err) { 175 | return done(err); 176 | } 177 | if (!data) { 178 | return done('No data!') 179 | } 180 | data.should.equal(54321); 181 | }); 182 | done(); 183 | }); 184 | }) 185 | }); 186 | 187 | it('payload', function (done) { 188 | cache.after({ 189 | url: testUrl 190 | }, { 191 | headers: function () { 192 | return {}; 193 | }, 194 | header: function(key, value) { 195 | key.should.equal('X-Cache'); 196 | value.should.equal('MISS'); 197 | }, 198 | getHeader: function () { 199 | return 'public, max-age=1'; 200 | }, 201 | _data: JSON.stringify({ 202 | 'testing': 1, 203 | 'expect': 'works' 204 | }) 205 | }, null, null, function (err) { 206 | if (err) { 207 | return done(err); 208 | } 209 | redis.get(PAYLOAD_PREFIX + testUrl, function (err, data) { 210 | if (err) { 211 | return done(err); 212 | } 213 | if (!data) { 214 | return done('No data!') 215 | } 216 | data.should.equal('{\"testing\":1,\"expect\":\"works\"}'); 217 | 218 | redis.ttl(PAYLOAD_PREFIX + testUrl, function (err, data) { 219 | if (err) { 220 | return done(err); 221 | } 222 | if (!data) { 223 | return done('No data!') 224 | } 225 | data.should.equal(1); 226 | }); 227 | 228 | done(); 229 | }); 230 | }); 231 | }); 232 | }); 233 | }); 234 | describe('retrieves', function () { 235 | 236 | beforeEach(function (done) { 237 | cache.after({ 238 | url: testUrl 239 | }, { 240 | headers: function () { 241 | return { 242 | 'Accept': 'application/xml' 243 | }; 244 | }, 245 | header: function() { 246 | }, 247 | getHeader: function() { 248 | return undefined; 249 | }, 250 | _data: JSON.stringify({ 251 | 'testing': 1, 252 | 'expect': 'works' 253 | }) 254 | }, null, null, done); 255 | }); 256 | 257 | afterEach(function (done) { 258 | redis.del(HEADER_PREFIX + testUrl, function (err) { 259 | if (err) { 260 | return done(err); 261 | } 262 | redis.del(PAYLOAD_PREFIX + testUrl, function (err) { 263 | done(err); 264 | }); 265 | }); 266 | }); 267 | 268 | it('a response', function (done) { 269 | var headers = {}; 270 | cache.before({ 271 | url: testUrl 272 | }, { 273 | header: function(key, value) { 274 | headers[key] = value; 275 | }, 276 | end: function (data) { 277 | data.should.equal('{\"testing\":1,\"expect\":\"works\"}'); 278 | 279 | headers['X-Cache'].should.equal('HIT'); 280 | headers['Accept'].should.equal('application/xml'); 281 | done(); 282 | }, 283 | writeHead: function() {} 284 | }, function(){ 285 | 286 | }); 287 | }); 288 | }); 289 | 290 | }); --------------------------------------------------------------------------------