├── .gitignore ├── LICENSE ├── README.md ├── example.js ├── lib ├── caching.js └── stores │ ├── memory.js │ └── redis.js ├── package.json └── test └── test-caching.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 Mathias Pettersson, mape@mape.me 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-caching 2 | 3 | Makes working with caching easier. 4 | 5 | ## Installation 6 | 7 | Via [npm](http://github.com/isaacs/npm): 8 | 9 | $ npm install caching 10 | 11 | ## Pseudo code example 12 | var Caching = require('caching'); 13 | var cache = new Caching('redis'); /* use 'memory' or 'redis' */ 14 | 15 | var ttl = 60 * 1000; // 1minute; 16 | cache('twitter-users', ttl, function(passalong) { 17 | getMostActiveTwitterUser(function(err, userName) { 18 | fetchTwitterFollowers(userName, passalong); // passalong replaces function(err, userList) {} 19 | }) 20 | }, function(err, userList) { 21 | console.log(userList); 22 | }); 23 | 24 | ## Code example 25 | var Caching = require('caching'); 26 | var cache = new Caching('redis'); /* use 'memory' or 'redis' */ 27 | 28 | setInterval(function() { 29 | cache('key', 10000 /*ttl in ms*/, function(passalong) { 30 | // This will only run once, all following requests will use cached data. 31 | setTimeout(function() { 32 | passalong(null, 'Cached result'); 33 | }, 1000); 34 | }, function(err, results) { 35 | // This callback will be reused each call 36 | console.log(results); 37 | }); 38 | }, 100); 39 | 40 | 41 | ## Built in stores 42 | * Memory 43 | * Redis 44 | 45 | ## Api 46 | 47 | cache(key, ttl, runIfNothingInCache, useReturnedCachedResults); 48 | 49 | ### arguments[0] 50 | Key, `'myKey'` 51 | ### arguments[1] 52 | Time To Live in ms, `60*30*1000` 53 | ### arguments[2] 54 | Callback that will run if results aren't already in cache store. 55 | 56 | function(passalong) { 57 | setTimeout(function() { 58 | passalong(null, 'mape', 'frontend developer', 'sweden'); 59 | }, 1000); 60 | } 61 | 62 | ### arguments[3] 63 | Callback that is called every time the method runs. 64 | 65 | function(err, name, position, location) { 66 | console.log(name, position, location); 67 | } 68 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var Caching = require('./lib/caching'); 2 | var cache = new Caching('memory'); /* use 'memory' or 'redis' */ 3 | 4 | var key = 'a-key-'+Date.now(); 5 | var ttl = 30*1000; 6 | setInterval(function() { 7 | cache(key, ttl, function(passalong) { 8 | console.log('This closure runs when nothing is in cache.'); 9 | setTimeout(function() { 10 | passalong(null, 'cached result with '+ttl+'ms ttl'); 11 | }, 1000); 12 | }, function(err, results) { 13 | // This callback will be reused each call 14 | console.log(new Date().toString().match(/..:..:../)+' - Results:', results); 15 | }); 16 | }, 100); 17 | 18 | // If you want to clear the cache manually you can use: 19 | cache.remove(key); -------------------------------------------------------------------------------- /lib/caching.js: -------------------------------------------------------------------------------- 1 | module.exports = function Caching(store) { 2 | store = store || 'memory'; 3 | 4 | if (typeof store == 'string') { 5 | try { 6 | store = require('./stores/'+store.toLowerCase().trim())(arguments[1]); 7 | } catch(e) { 8 | throw new Error('There is no bundled caching store named "'+store+'"'); 9 | } 10 | } 11 | 12 | var queues = {}; 13 | 14 | var cacher = function(key, ttl, work, done) { 15 | store.get(key, function(err, args) { 16 | if (!err && args) { 17 | done.apply(null, args); 18 | } else if (queues[key]) { 19 | queues[key].push(done); 20 | } else { 21 | queues[key] = [done]; 22 | work(function(){ 23 | var args = Array.prototype.slice.call(arguments, 0); 24 | store.set(key, ttl, args); 25 | queues[key].forEach(function(done){ 26 | done.apply(null, args); 27 | }); 28 | delete queues[key]; 29 | }); 30 | } 31 | }); 32 | }; 33 | cacher.remove = store.remove.bind(store); 34 | cacher.store = store; 35 | 36 | return cacher; 37 | }; -------------------------------------------------------------------------------- /lib/stores/memory.js: -------------------------------------------------------------------------------- 1 | function MemoryStore() { 2 | if (!(this instanceof MemoryStore)) { 3 | return new MemoryStore; 4 | } 5 | 6 | this.cache = {}; 7 | } 8 | 9 | MemoryStore.prototype.get = function(key, callback){ 10 | var self = this; 11 | process.nextTick(function() { 12 | callback(null, self.cache[key] || null); 13 | }); 14 | }; 15 | 16 | MemoryStore.prototype.set = function(key, ttl, result){ 17 | this.cache[key] = result; 18 | if (ttl) { 19 | var self = this; 20 | setTimeout(function(){ 21 | delete self.cache[key]; 22 | }, ttl); 23 | } 24 | }; 25 | 26 | MemoryStore.prototype.remove = function(pattern){ 27 | if (~pattern.indexOf('*')) { 28 | var self = this; 29 | pattern = new RegExp(pattern.replace(/\*/g, '.*'), 'g'); 30 | Object.keys(this.cache).forEach(function(key) { 31 | if (pattern.test(key)) { 32 | delete self.cache[key]; 33 | } 34 | }); 35 | } else { 36 | delete this.cache[pattern]; 37 | } 38 | }; 39 | 40 | module.exports = MemoryStore; 41 | -------------------------------------------------------------------------------- /lib/stores/redis.js: -------------------------------------------------------------------------------- 1 | function RedisStore(opts) { 2 | if (!(this instanceof RedisStore)) { 3 | return new RedisStore(opts); 4 | } 5 | 6 | opts = opts || {}; 7 | this.client = require('redis').createClient(opts.port, opts.host, opts); 8 | } 9 | 10 | RedisStore.prototype.get = function(key, callback) { 11 | this.client.get(key, function(err, result) { 12 | callback(err, JSON.parse(result)); 13 | }); 14 | }; 15 | 16 | RedisStore.prototype.set = function(key, ttl, result) { 17 | if (ttl) { 18 | this.client.setex(key, Math.ceil(ttl/1000), JSON.stringify(result)); 19 | } else { 20 | this.client.set(key, JSON.stringify(result)); 21 | } 22 | }; 23 | 24 | RedisStore.prototype.remove = function(pattern) { 25 | if (~pattern.indexOf('*')) { 26 | var self = this; 27 | this.client.keys(pattern, function(err, keys) { 28 | if (keys.length) { 29 | self.client.del(keys); 30 | } 31 | }); 32 | } else { 33 | this.client.del(pattern); 34 | } 35 | }; 36 | 37 | module.exports = RedisStore; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "caching", 3 | "description": "Easier caching in node.js", 4 | "version": "0.1.4", 5 | "author": "Mathias Pettersson ", 6 | "engines": [ 7 | "node" 8 | ], 9 | "directories": { 10 | "lib": "./lib" 11 | }, 12 | "main": "./lib/caching", 13 | "repositories": [ 14 | { 15 | "type": "git", 16 | "url": "http://github.com/mape/node-caching" 17 | } 18 | ], 19 | "dependencies": { 20 | "hiredis" : ">=0.1.12", 21 | "redis": ">=0.6.7" 22 | }, 23 | "devDependencies": { 24 | "expresso": "~0.8.1" 25 | }, 26 | "scripts": { 27 | "test": "expresso" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/test-caching.js: -------------------------------------------------------------------------------- 1 | var Caching = require('../') 2 | , memoryCache = new Caching('memory') 3 | , assert = require('assert'); 4 | 5 | exports['MemoryStore'] = function(beforeExit) { 6 | var wroteCache = false 7 | , lastResults 8 | , callbacksCalled = 0 9 | , key = 'hello memory '+Math.random() 10 | , ttl = 500; // 1s 11 | 12 | function store(next) { 13 | callbacksCalled++; 14 | wroteCache = true; 15 | setTimeout(function() { 16 | next(null, Date.now()); 17 | }, 200); 18 | } 19 | 20 | // Feel the cache 21 | memoryCache(key, ttl, store, function(err, results) { 22 | callbacksCalled++; 23 | assert.ifError(err); 24 | assert.equal(typeof results, 'number'); 25 | assert.ok(wroteCache); 26 | lastResults = results; 27 | wroteCache = false; 28 | }); 29 | 30 | // Try again 31 | memoryCache(key, ttl, store, function(err, results) { 32 | callbacksCalled++; 33 | assert.ifError(err); 34 | assert.equal(typeof results, 'number'); 35 | assert.equal(results, lastResults); 36 | assert.ok(!wroteCache); 37 | lastResults = results; 38 | wroteCache = false; 39 | }); 40 | 41 | beforeExit(function() { 42 | assert.equal(callbacksCalled, 3); 43 | }); 44 | }; 45 | 46 | exports['MemoryStore expiration'] = function(beforeExit) { 47 | var wroteCache = false 48 | , lastResults 49 | , callbacksCalled = 0 50 | , key = 'hello memory '+Math.random() 51 | , ttl = 500; // .5s 52 | 53 | function store(next) { 54 | callbacksCalled++; 55 | wroteCache = true; 56 | setTimeout(function() { 57 | next(null, Date.now()); 58 | }, 200); 59 | } 60 | 61 | // Feel the cache 62 | memoryCache(key, ttl, store, function(err, results) { 63 | callbacksCalled++; 64 | assert.ifError(err); 65 | assert.equal(typeof results, 'number'); 66 | assert.ok(wroteCache); 67 | lastResults = results; 68 | wroteCache = false; 69 | }); 70 | 71 | // Wait until the cache has expired 72 | setTimeout(function() { 73 | memoryCache(key, ttl, store, function(err, results) { 74 | callbacksCalled++; 75 | assert.ifError(err); 76 | assert.equal(typeof results, 'number'); 77 | assert.notEqual(results, lastResults); 78 | assert.ok(wroteCache); 79 | lastResults = results; 80 | wroteCache = false; 81 | }); 82 | }, ttl*2); 83 | 84 | beforeExit(function() { 85 | assert.equal(callbacksCalled, 4); 86 | }); 87 | }; 88 | 89 | 90 | exports['MemoryStore removal'] = function(beforeExit) { 91 | var wroteCache = false 92 | , callbacksCalled = 0 93 | , key = 'hello rem memory '+Math.random() 94 | , ttl = 500; // .5s 95 | 96 | function store(next) { 97 | callbacksCalled++; 98 | wroteCache = true; 99 | setTimeout(function() { 100 | next(null, Date.now()); 101 | wroteCache = false; 102 | }, 200); 103 | } 104 | 105 | // Feel the cache 106 | memoryCache(key, ttl, store, function setBeforeRemoval(err, results) { 107 | callbacksCalled++; 108 | assert.ifError(err); 109 | assert.equal(typeof results, 'number'); 110 | assert.ok(wroteCache); 111 | 112 | // Remove it manually 113 | memoryCache.remove(key); 114 | 115 | // Try again 116 | memoryCache(key, ttl, store, function getAfterRemoval(err, results) { 117 | callbacksCalled++; 118 | assert.ifError(err); 119 | assert.equal(typeof results, 'number'); 120 | assert.ok(wroteCache); 121 | }); 122 | }); 123 | 124 | beforeExit(function() { 125 | assert.equal(callbacksCalled, 4); 126 | }); 127 | }; 128 | 129 | 130 | exports['MemoryStore removal pattern'] = function(beforeExit) { 131 | var wroteCache = false 132 | , callbacksCalled = 0 133 | , key = 'hello rem memory '+Math.random() 134 | , ttl = 500; // .5s 135 | 136 | function store(next) { 137 | callbacksCalled++; 138 | wroteCache = true; 139 | setTimeout(function() { 140 | next(null, Date.now()); 141 | wroteCache = false; 142 | }, 200); 143 | } 144 | 145 | // Feel the cache 146 | memoryCache(key, ttl, store, function setBeforeRemoval(err, results) { 147 | callbacksCalled++; 148 | assert.ifError(err); 149 | assert.equal(typeof results, 'number'); 150 | assert.ok(wroteCache); 151 | 152 | // Remove it manually (using a pattern) 153 | memoryCache.remove('hello rem*'); 154 | 155 | // Try again 156 | memoryCache(key, ttl, store, function getAfterRemoval(err, results) { 157 | callbacksCalled++; 158 | assert.ifError(err); 159 | assert.equal(typeof results, 'number'); 160 | assert.ok(wroteCache); 161 | }); 162 | }); 163 | 164 | beforeExit(function() { 165 | assert.equal(callbacksCalled, 4); 166 | }); 167 | }; 168 | 169 | 170 | exports['RedisStore'] = function(beforeExit) { 171 | var redisCache = new Caching('redis') 172 | , wroteCache = false 173 | , callbacksCalled = 0 174 | , key = 'hello redis '+Math.random() 175 | , ttl = 500; // 1s 176 | 177 | function store(next) { 178 | callbacksCalled++; 179 | wroteCache = true; 180 | setTimeout(function() { 181 | next(null, Date.now()); 182 | }, 200); 183 | } 184 | 185 | // Feel the cache 186 | redisCache(key, ttl, store, function(err, results) { 187 | callbacksCalled++; 188 | assert.ifError(err); 189 | assert.equal(typeof results, 'number'); 190 | assert.ok(wroteCache); 191 | wroteCache = false; 192 | }); 193 | 194 | // Try again 195 | redisCache(key, ttl, store, function(err, results) { 196 | callbacksCalled++; 197 | assert.ifError(err); 198 | assert.equal(typeof results, 'number'); 199 | assert.ok(!wroteCache); 200 | redisCache.store.client.end(); 201 | }); 202 | 203 | beforeExit(function() { 204 | assert.equal(callbacksCalled, 3); 205 | }); 206 | }; 207 | 208 | 209 | exports['RedisStore expiration'] = function(beforeExit) { 210 | var redisCache = new Caching('redis') 211 | , wroteCache = false 212 | , callbacksCalled = 0 213 | , key = 'hello redis '+Math.random() 214 | , ttl = 1000; // 1s (the least possible on Redis since it only takes integer seconds) 215 | 216 | function store(next) { 217 | callbacksCalled++; 218 | wroteCache = true; 219 | setTimeout(function() { 220 | next(null, Date.now()); 221 | }, 200); 222 | } 223 | 224 | // Feel the cache 225 | redisCache(key, ttl, store, function(err, results) { 226 | callbacksCalled++; 227 | assert.ifError(err); 228 | assert.equal(typeof results, 'number'); 229 | assert.ok(wroteCache); 230 | wroteCache = false; 231 | }); 232 | 233 | // Wait until the cache has expired 234 | var t = Date.now(); 235 | setTimeout(function() { 236 | redisCache(key, ttl, store, function(err, results) { 237 | callbacksCalled++; 238 | assert.ifError(err); 239 | assert.equal(typeof results, 'number'); 240 | assert.ok(wroteCache); 241 | redisCache.store.client.end(); 242 | }); 243 | }, ttl*2); 244 | 245 | beforeExit(function() { 246 | assert.equal(callbacksCalled, 4); 247 | }); 248 | }; 249 | 250 | 251 | exports['RedisStore removal'] = function(beforeExit) { 252 | var redisCache = new Caching('redis') 253 | , wroteCache = false 254 | , callbacksCalled = 0 255 | , key = 'hello rem redis '+Math.random() 256 | , ttl = 500; // .5s 257 | 258 | function store(next) { 259 | callbacksCalled++; 260 | wroteCache = true; 261 | setTimeout(function() { 262 | next(null, Date.now()); 263 | }, 200); 264 | } 265 | 266 | // Feel the cache 267 | redisCache(key, ttl, store, function setBeforeRemoval(err, results) { 268 | callbacksCalled++; 269 | assert.ifError(err); 270 | assert.equal(typeof results, 'number'); 271 | assert.ok(wroteCache); 272 | wroteCache = false; 273 | 274 | // Remove it manually 275 | redisCache.remove(key); 276 | 277 | // Try again (wait a little bit because the patters requires two redis commands) 278 | setTimeout(function() { 279 | redisCache(key, ttl, store, function getAfterRemoval(err, results) { 280 | callbacksCalled++; 281 | assert.ifError(err); 282 | assert.equal(typeof results, 'number'); 283 | assert.ok(wroteCache); 284 | redisCache.store.client.end(); 285 | }); 286 | }, 50); 287 | }); 288 | 289 | beforeExit(function() { 290 | assert.equal(callbacksCalled, 4); 291 | }); 292 | }; 293 | 294 | 295 | exports['RedisStore removal pattern'] = function(beforeExit) { 296 | var redisCache = new Caching('redis') 297 | , wroteCache = false 298 | , callbacksCalled = 0 299 | , key = 'hello rem redis '+Math.random() 300 | , ttl = 500; // .5s 301 | 302 | function store(next) { 303 | callbacksCalled++; 304 | wroteCache = true; 305 | setTimeout(function() { 306 | next(null, Date.now()); 307 | }, 200); 308 | } 309 | 310 | // Feel the cache 311 | redisCache(key, ttl, store, function setBeforeRemoval(err, results) { 312 | callbacksCalled++; 313 | assert.ifError(err); 314 | assert.equal(typeof results, 'number'); 315 | assert.ok(wroteCache); 316 | wroteCache = false; 317 | 318 | // Remove it manually (using a pattern) 319 | redisCache.remove('hello rem*'); 320 | 321 | // Try again (wait a little bit because the patters requires two redis commands) 322 | setTimeout(function() { 323 | redisCache(key, ttl, store, function getAfterRemoval(err, results) { 324 | callbacksCalled++ 325 | assert.ifError(err); 326 | assert.equal(typeof results, 'number') 327 | assert.ok(wroteCache) 328 | redisCache.store.client.end() 329 | }); 330 | }, 50) 331 | }); 332 | 333 | beforeExit(function() { 334 | assert.equal(callbacksCalled, 4); 335 | }); 336 | }; 337 | --------------------------------------------------------------------------------