├── .babelrc ├── .gitignore ├── .npmignore ├── .release.json ├── .travis.yml ├── Makefile ├── README.md ├── lib └── index.js ├── package.json └── test ├── index.js └── mocha.opts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | node 5 | tmp -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | tmp 4 | test 5 | lib -------------------------------------------------------------------------------- /.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "non-interactive": true, 3 | "dry-run": false, 4 | "verbose": false, 5 | "force": false, 6 | "pkgFiles": ["package.json"], 7 | "increment": "patch", 8 | "commitMessage": "[release] %s", 9 | "tagName": "%s", 10 | "tagAnnotation": "Release %s", 11 | "buildCommand": false, 12 | "distRepo": false, 13 | "distStageDir": ".stage", 14 | "distBase": "dist", 15 | "distFiles": ["**/*"], 16 | "publish": true, 17 | "publishPath": "." 18 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "10" 5 | - "8" 6 | - "6" 7 | before_install: 8 | - "npm install npm -g" 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BABEL = ./node_modules/.bin/babel 2 | 3 | all: node 4 | 5 | node: lib 6 | @mkdir -p node/ 7 | $(BABEL) lib -d node 8 | 9 | clean: 10 | rm -rf node/ 11 | 12 | test: 13 | @./node_modules/.bin/mocha \ 14 | --require babel-core/register \ 15 | --reporter spec \ 16 | --recursive \ 17 | test 18 | 19 | .PHONY: all clean test node -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cacheman 2 | 3 | [![Build Status](https://travis-ci.org/cayasso/cacheman.png?branch=master)](https://travis-ci.org/cayasso/cacheman) 4 | [![NPM version](https://badge.fury.io/js/cacheman.png)](http://badge.fury.io/js/cacheman) 5 | 6 | Small and efficient cache provider for Node.JS with In-memory, File, Redis and MongoDB engines. 7 | 8 | ## Installation 9 | 10 | ``` bash 11 | $ npm install cacheman 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```javascript 17 | var Cacheman = require('cacheman'); 18 | var cache = new Cacheman(); 19 | 20 | // or 21 | var cache = new Cacheman('todo'); 22 | 23 | // or 24 | var cache = new Cacheman({ ttl: 90 }); 25 | 26 | // or 27 | var cache = new Cacheman('todo', { ttl: 90 }); 28 | 29 | // set the value 30 | cache.set('my key', { foo: 'bar' }, function (error) { 31 | 32 | if (error) throw error; 33 | 34 | // get the value 35 | cache.get('my key', function (error, value) { 36 | 37 | if (error) throw error; 38 | 39 | console.log(value); //-> {foo:"bar"} 40 | 41 | // delete entry 42 | cache.del('my key', function (error){ 43 | 44 | if (error) throw error; 45 | 46 | console.log('value deleted'); 47 | }); 48 | 49 | }); 50 | }); 51 | ``` 52 | 53 | ## API 54 | 55 | ### Cacheman([name, [options]]) 56 | 57 | Create `cacheman` instance. It accepts an `name`(optional) and `options`(optional). `options` can contain `ttl` to set the default "Time To Live" in seconds (default: `60s`), `delimiter` to change the delimiter used for array keys (default: `':'`), `Promise` can set a Promise library to use for promises, `engine` that could be "memory", "in file", "redis" or "mongo" (default: `memory`), and the corresponding engine options that can be passed like `port`, `host`, etc. 58 | 59 | You can also pass an already initialized client `engine` as valid engine so you can re-use among multiple cacheman instances. 60 | 61 | By default `cacheman` uses the `cacheman-memory` engine. 62 | 63 | ```javascript 64 | var options = { 65 | ttl: 90, 66 | engine: 'redis', 67 | port: 9999, 68 | host: '127.0.0.1' 69 | }; 70 | 71 | var cache = new Cacheman('todo', options); 72 | ``` 73 | 74 | Reuse engine in multiple cache instances: 75 | 76 | ```javascript 77 | var Cacheman = require('cacheman'); 78 | var EngineMongo = require('cacheman-mongo'); 79 | var engine = new Engine(); 80 | 81 | // Share same engine: 82 | var todoCache = new Cacheman('todo', { engine: engine }); 83 | var blogCache = new Cacheman('blog', { engine: engine }); 84 | ``` 85 | 86 | ### cache.set(key, value, [ttl, [fn]]) 87 | 88 | Stores or updates a value. 89 | 90 | ```javascript 91 | cache.set('foo', { a: 'bar' }, function (err, value) { 92 | if (err) throw err; 93 | console.log(value); //-> {a:'bar'} 94 | }); 95 | 96 | cache.set('foo', { a: 'bar' }) 97 | .then(function (value) { 98 | console.log(value); //-> {a:'bar'} 99 | }); 100 | ``` 101 | 102 | Or add a TTL(Time To Live) in seconds like this: 103 | 104 | ```javascript 105 | // key will expire in 60 seconds 106 | cache.set('foo', { a: 'bar' }, 60, function (err, value) { 107 | if (err) throw err; 108 | console.log(value); //-> {a:'bar'} 109 | }); 110 | ``` 111 | 112 | You can also use human readable values for `ttl` like: `1s`, `1m`, etc. Check out the [ms](https://github.com/guille/ms.js) project for additional information on supported formats. 113 | 114 | ```javascript 115 | // key will expire in 45 seconds 116 | cache.set('foo', { a: 'bar' }, '45s', function (err, value) { 117 | if (err) throw err; 118 | console.log(value); //-> {a:'bar'} 119 | }); 120 | ``` 121 | 122 | The key may also be an array. It will be joined into a string using the `delimiter` option. 123 | ```javascript 124 | // equivalent to setting 'foo:bar' 125 | cache.set(['foo', 'bar'], { a: 'baz' }, function (err, value) { 126 | if (err) throw err; 127 | console.log(value); //-> {a:'baz'} 128 | }); 129 | ``` 130 | 131 | ### cache.get(key, fn) 132 | 133 | Retrieves a value for a given key, if there is no value for the given key a null value will be returned. 134 | 135 | ```javascript 136 | cache.get('foo', function (err, value) { 137 | if (err) throw err; 138 | console.log(value); 139 | }); 140 | 141 | cache.get('foo') 142 | .then(function (value) { 143 | console.log(value); 144 | }); 145 | ``` 146 | 147 | ### cache.del(key, [fn]) 148 | 149 | Deletes a key out of the cache. 150 | 151 | ```javascript 152 | cache.del('foo', function (err) { 153 | if (err) throw err; 154 | // foo was deleted 155 | }); 156 | 157 | cache.del('foo') 158 | .then(function () { 159 | // foo was deleted 160 | }); 161 | ``` 162 | 163 | ### cache.clear([fn]) 164 | 165 | Clear the cache entirely, throwing away all values. 166 | 167 | ```javascript 168 | cache.clear(function (err) { 169 | if (err) throw err; 170 | // cache is now clear 171 | }); 172 | 173 | cache.clear() 174 | .then(function () { 175 | // cache is now clear 176 | }); 177 | ``` 178 | 179 | ### cache.cache(key, data, ttl, [fn]) 180 | 181 | Cache shortcut method that support middleware. This method will first call `get` 182 | and if the key is not found in cache it will call `set` to save the value in cache. 183 | 184 | ```javascript 185 | cache.cache('foo', { a: 'bar' }, '45s', function (err) { 186 | if (err) throw err; 187 | console.log(value); //-> {a:'bar'} 188 | }); 189 | 190 | cache.cache('foo', { a: 'bar' }, '45s') 191 | .then(function () { 192 | console.log(value); //-> {a:'bar'} 193 | }); 194 | ``` 195 | 196 | ### cache.use(fn) 197 | 198 | This method allow to add middlewares that will be executed when the `cache` method 199 | is called, meaning that you can intercept the function right after the `get` and `set` methods. 200 | 201 | For example we can add a middleware that will force ttl of 10 seconds on all values to cache: 202 | 203 | ```javascript 204 | function expireInMiddleware (expireIn) { 205 | return function (key, data, ttl, next) { 206 | next(null, data, expire); 207 | } 208 | }; 209 | 210 | cache.use(expireInMiddleware('10s')); 211 | 212 | cache.cache('foo', { a: 'bar' }, '45s', function (err) { 213 | if (err) throw err; 214 | console.log(value); //-> {a:'bar'} 215 | }); 216 | ``` 217 | 218 | Or we can add a middleware to overwrite the value: 219 | 220 | ```javascript 221 | function overwriteMiddleware (val) { 222 | return function (key, data, ttl, next) { 223 | next(null, val, expire); 224 | } 225 | }; 226 | 227 | cache.use(overwriteMiddleware({ a: 'foo' })); 228 | 229 | cache.cache('foo', { a: 'bar' }, '45s', function (err, data) { 230 | if (err) throw err; 231 | console.log(data); //-> {a:'foo'} 232 | }); 233 | ``` 234 | 235 | You can also pass errors as first argument to stop the cache execution: 236 | 237 | ```javascript 238 | function overwriteMiddleware () { 239 | return function (key, data, ttl, next) { 240 | next(new Error('There was an error')); 241 | } 242 | }; 243 | 244 | cache.use(overwriteMiddleware()); 245 | 246 | cache.cache('foo', { a: 'bar' }, '45s', function (err) { 247 | if (err) throw err; // Will throw the error 248 | }); 249 | ``` 250 | 251 | ### cache.wrap(key, work, [ttl, [fn]]) 252 | 253 | Wraps a function in cache. The first time the function is run, its results are 254 | stored in cache so subsequent calls retrieve from cache instead of calling the function. 255 | 256 | The `work` function can call a node style callback argument or return a value including a promise. 257 | 258 | The `work` function and `ttl` can also be passed in the opposite order. This is primarily to make promise returning calls cleaner. 259 | 260 | ```javascript 261 | function work(callback) { 262 | callback(null, { a: 'foo' }); 263 | } 264 | 265 | cache.wrap('foo', work, '45s', function (err, data) { 266 | console.log(data); //-> {a: 'foo'} 267 | }); 268 | 269 | cache.wrap('foo', work, '45s') 270 | .then(function (data) { 271 | console.log(data); //-> {a: 'foo'} 272 | }); 273 | 274 | cache.wrap('foo', '45s', 275 | function() { 276 | return Promise.resolve({ a: 'foo' }); 277 | }) 278 | .then(function (data) { 279 | console.log(data); //-> {a: 'foo'} 280 | }); 281 | 282 | cache.wrap('foo', '45s', 283 | function() { 284 | return { a: 'foo' }; 285 | }) 286 | .then(function (data) { 287 | console.log(data); //-> {a: 'foo'} 288 | }); 289 | ``` 290 | 291 | ## Run tests 292 | 293 | ``` bash 294 | $ make test 295 | ``` 296 | 297 | ## Supported engines 298 | 299 | * [cacheman-memory](https://github.com/cayasso/cacheman-memory) 300 | * [cacheman-redis](https://github.com/cayasso/cacheman-redis) 301 | * [cacheman-mongo](https://github.com/cayasso/cacheman-mongo) 302 | * [cacheman-file](https://github.com/taronfoxworth/cacheman-file) 303 | 304 | ## Credits 305 | 306 | This library was inspired by the [hilmi](https://github.com/eknkc/hilmi) project. 307 | 308 | ## License 309 | 310 | (The MIT License) 311 | 312 | Copyright (c) 2013 Jonathan Brumley <cayasso@gmail.com> 313 | 314 | Permission is hereby granted, free of charge, to any person obtaining 315 | a copy of this software and associated documentation files (the 316 | 'Software'), to deal in the Software without restriction, including 317 | without limitation the rights to use, copy, modify, merge, publish, 318 | distribute, sublicense, and/or sell copies of the Software, and to 319 | permit persons to whom the Software is furnished to do so, subject to 320 | the following conditions: 321 | 322 | The above copyright notice and this permission notice shall be 323 | included in all copies or substantial portions of the Software. 324 | 325 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 326 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 327 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 328 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 329 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 330 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 331 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 332 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | import ms from 'ms'; 8 | 9 | /** 10 | * Module constants. 11 | */ 12 | 13 | const engines = ['memory', 'redis', 'mongo', 'file']; 14 | 15 | /** 16 | * Cacheman base error class. 17 | * 18 | * @constructor 19 | * @param {String} message 20 | * @api private 21 | */ 22 | 23 | class CachemanError extends Error { 24 | constructor(message) { 25 | super(message); 26 | this.name = this.constructor.name; 27 | this.message = message; 28 | Error.captureStackTrace(this, this.constructor); 29 | } 30 | } 31 | 32 | /** 33 | * Helper to allow all async methods to support both callbacks and promises 34 | */ 35 | 36 | function maybePromised(_this, callback, wrapped) { 37 | if ('function' === typeof callback) { 38 | // Call wrapped with unmodified callback 39 | wrapped(callback); 40 | 41 | // Return `this` to keep the same behaviour Cacheman had before promises were added 42 | return _this; 43 | } else { 44 | let _Promise = _this.options.Promise; 45 | 46 | if ('function' !== typeof _Promise) { 47 | throw new CachemanError('Promises not available: Please polyfill native Promise before creating a Cacheman object, pass a Promise library as a Cacheman option, or use the callback interface') 48 | } 49 | 50 | if (_Promise.fromCallback) { 51 | // Bluebird's fromCallback, this is faster than new Promise 52 | return _Promise.fromCallback(wrapped) 53 | } 54 | 55 | // Standard new Promise based wrapper for native Promises 56 | return new _Promise(function(resolve, reject) { 57 | wrapped(function(err, value) { 58 | if (err) { 59 | reject(err); 60 | } else { 61 | resolve(value); 62 | } 63 | }); 64 | }); 65 | } 66 | } 67 | 68 | /** 69 | * Cacheman constructor. 70 | * 71 | * @param {String} name 72 | * @param {Object} options 73 | * @api public 74 | */ 75 | 76 | export default class Cacheman { 77 | 78 | /** 79 | * Class constructor method. 80 | * 81 | * @param {String} name 82 | * @param {Object} [options] 83 | * @return {Cacheman} this 84 | * @api public 85 | */ 86 | 87 | constructor(name, options = {}) { 88 | if (name && 'object' === typeof name) { 89 | options = name; 90 | name = null; 91 | } 92 | 93 | const _Promise = options.Promise || (function() { 94 | try { 95 | return Promise; 96 | } catch (e) {} 97 | })(); 98 | 99 | let { 100 | prefix = 'cacheman', 101 | engine = 'memory', 102 | delimiter = ':', 103 | ttl = 60 104 | } = options; 105 | 106 | if ('string' === typeof ttl) { 107 | ttl = Math.round(ms(ttl)/1000); 108 | } 109 | 110 | prefix = [prefix, name || 'cache', ''].join(delimiter); 111 | this.options = { ...options, Promise: _Promise, delimiter, prefix, ttl, count: 1000 }; 112 | this._prefix = prefix; 113 | this._ttl = ttl; 114 | this._fns = []; 115 | this.engine(engine); 116 | } 117 | 118 | /** 119 | * Set get engine. 120 | * 121 | * @param {String} engine 122 | * @param {Object} options 123 | * @return {Cacheman} this 124 | * @api public 125 | */ 126 | 127 | engine(engine, options) { 128 | 129 | if (!arguments.length) return this._engine; 130 | 131 | const type = typeof engine; 132 | 133 | if (! /string|function|object/.test(type)) { 134 | throw new CachemanError('Invalid engine format, engine must be a String, Function or a valid engine instance'); 135 | } 136 | 137 | if ('string' === type) { 138 | 139 | let Engine; 140 | 141 | if (~Cacheman.engines.indexOf(engine)) { 142 | engine = `cacheman-${engine}`; 143 | } 144 | 145 | try { 146 | Engine = require(engine); 147 | } catch(e) { 148 | if (e.code === 'MODULE_NOT_FOUND') { 149 | throw new CachemanError(`Missing required npm module ${engine}`); 150 | } else { 151 | throw e; 152 | } 153 | } 154 | 155 | this._engine = new Engine(options || this.options, this); 156 | 157 | } else if ('object' === type) { 158 | ['get', 'set', 'del', 'clear'].forEach(key => { 159 | if ('function' !== typeof engine[key]) { 160 | throw new CachemanError('Invalid engine format, must be a valid engine instance'); 161 | } 162 | }) 163 | 164 | this._engine = engine; 165 | 166 | } else { 167 | this._engine = engine(options || this.options, this); 168 | } 169 | 170 | return this; 171 | } 172 | 173 | /** 174 | * Wrap key with prefix. 175 | * 176 | * @param {String} key 177 | * @return {String} 178 | * @api private 179 | */ 180 | 181 | key(key) { 182 | if ( Array.isArray(key) ) { 183 | key = key.join(this.options.delimiter); 184 | } 185 | return (this.options.engine === 'redis') ? key : this._prefix + key; 186 | } 187 | 188 | /** 189 | * Sets up namespace middleware. 190 | * 191 | * @return {Cacheman} this 192 | * @api public 193 | */ 194 | 195 | use(fn) { 196 | this._fns.push(fn); 197 | return this; 198 | } 199 | 200 | /** 201 | * Executes the cache middleware. 202 | * 203 | * @param {String} key 204 | * @param {Mixed} data 205 | * @param {Number} ttl 206 | * @param {Function} fn 207 | * @api private 208 | */ 209 | 210 | run(key, data, ttl, fn) { 211 | const fns = this._fns.slice(0); 212 | if (!fns.length) return fn(null); 213 | 214 | const go = i => { 215 | fns[i](key, data, ttl, (err, _data, _ttl, _force) => { 216 | // upon error, short-circuit 217 | if (err) return fn(err); 218 | 219 | // if no middleware left, summon callback 220 | if (!fns[i + 1]) return fn(null, _data, _ttl, _force); 221 | 222 | // go on to next 223 | go(i + 1); 224 | }); 225 | } 226 | 227 | go(0); 228 | } 229 | 230 | /** 231 | * Set an entry. 232 | * 233 | * @param {String} key 234 | * @param {Mixed} data 235 | * @param {Number} ttl 236 | * @param {Function} [fn] 237 | * @return {Cacheman} this 238 | * @api public 239 | */ 240 | 241 | cache(key, data, ttl, fn) { 242 | 243 | if ('function' === typeof ttl) { 244 | fn = ttl; 245 | ttl = null; 246 | } 247 | 248 | return maybePromised(this, fn, (fn) => { 249 | 250 | this.get(key, (err, res) => { 251 | 252 | this.run(key, res, ttl, (_err, _data, _ttl, _force) => { 253 | 254 | if (err || _err) return fn(err || _err); 255 | 256 | let force = false; 257 | 258 | if ('undefined' !== typeof _data) { 259 | force = true; 260 | data = _data; 261 | } 262 | 263 | if ('undefined' !== typeof _ttl) { 264 | force = true; 265 | ttl = _ttl; 266 | } 267 | 268 | if ('undefined' === typeof res || force) { 269 | return this.set(key, data, ttl, fn); 270 | } 271 | 272 | fn(null, res); 273 | 274 | }); 275 | 276 | }); 277 | 278 | }); 279 | } 280 | 281 | /** 282 | * Get an entry. 283 | * 284 | * @param {String} key 285 | * @param {Function} [fn] 286 | * @return {Cacheman} this 287 | * @api public 288 | */ 289 | 290 | get(key, fn) { 291 | return maybePromised(this, fn, (fn) => 292 | this._engine.get(this.key(key), fn)); 293 | } 294 | 295 | /** 296 | * Set an entry. 297 | * 298 | * @param {String} key 299 | * @param {Mixed} data 300 | * @param {Number} ttl 301 | * @param {Function} [fn] 302 | * @return {Cacheman} this 303 | * @api public 304 | */ 305 | 306 | set(key, data, ttl, fn) { 307 | 308 | if ('function' === typeof ttl) { 309 | fn = ttl; 310 | ttl = null; 311 | } 312 | 313 | if ('string' === typeof ttl) { 314 | ttl = Math.round(ms(ttl)/1000); 315 | } 316 | 317 | return maybePromised(this, fn, (fn) => { 318 | if ('string' !== typeof key && !Array.isArray(key)) { 319 | return process.nextTick(() => { 320 | fn(new CachemanError('Invalid key, key must be a string or array.')); 321 | }); 322 | } 323 | 324 | if ('undefined' === typeof data) { 325 | return process.nextTick(fn); 326 | } 327 | 328 | return this._engine.set(this.key(key), data, ttl || this._ttl, fn); 329 | }); 330 | } 331 | 332 | /** 333 | * Delete an entry. 334 | * 335 | * @param {String} key 336 | * @param {Function} [fn] 337 | * @return {Cacheman} this 338 | * @api public 339 | */ 340 | 341 | del(key, fn) { 342 | 343 | if ('function' === typeof key) { 344 | fn = key; 345 | key = ''; 346 | } 347 | 348 | return maybePromised(this, fn, (fn) => 349 | this._engine.del(this.key(key), fn)); 350 | } 351 | 352 | /** 353 | * Clear all entries. 354 | * 355 | * @param {String} key 356 | * @param {Function} [fn] 357 | * @return {Cacheman} this 358 | * @api public 359 | */ 360 | 361 | clear(fn) { 362 | return maybePromised(this, fn, (fn) => 363 | this._engine.clear(fn)); 364 | } 365 | 366 | /** 367 | * Wraps a function in cache. I.e., the first time the function is run, 368 | * its results are stored in cache so subsequent calls retrieve from cache 369 | * instead of calling the function. 370 | * 371 | * @param {String} key 372 | * @param {Function} work 373 | * @param {Number} ttl 374 | * @param {Function} [fn] 375 | * @api public 376 | */ 377 | 378 | wrap(key, work, ttl, fn) { 379 | 380 | // Allow work and ttl to be passed in the oposite order to make promises nicer 381 | if ('function' !== typeof work && 'function' === typeof ttl) { 382 | [ttl, work] = [work, ttl]; 383 | } 384 | 385 | if ('function' === typeof ttl) { 386 | fn = ttl; 387 | ttl = null; 388 | } 389 | 390 | return maybePromised(this, fn, (fn) => { 391 | 392 | this.get(key, (err, res) => { 393 | if (err || res) return fn(err, res); 394 | 395 | let next = (err, data) => { 396 | if (err) return fn(err); 397 | this.set(key, data, ttl, err => { 398 | fn(err, data); 399 | }); 400 | 401 | // Don't allow callbacks to be called twice 402 | next = () => { 403 | process.nextTick(() => { 404 | throw new CachemanError('callback called twice'); 405 | }); 406 | }; 407 | } 408 | 409 | if ( work.length >= 1 ) { 410 | const result = work((err, data) => next(err, data)); 411 | if ('undefined' !== typeof result) { 412 | process.nextTick(() => { 413 | throw new CachemanError('return value cannot be used when callback argument is used'); 414 | }); 415 | } 416 | } else { 417 | try { 418 | const result = work(); 419 | if ('object' === typeof result && 'function' === typeof result.then) { 420 | result 421 | .then((value) => next(null, value)) 422 | .then(null, (err) => next(err)); 423 | } else { 424 | next(null, result); 425 | } 426 | } catch (err) { 427 | next(err); 428 | } 429 | } 430 | }); 431 | 432 | }); 433 | } 434 | } 435 | 436 | Cacheman.engines = engines; 437 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cacheman", 3 | "version": "2.2.1", 4 | "description": "Small and efficient cache provider for Node.JS with In-memory, Redis and MongoDB engines", 5 | "author": "Jonathan Brumley ", 6 | "main": "./node/index", 7 | "scripts": { 8 | "test": "make test", 9 | "prepublish": "make" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/cayasso/cacheman.git" 14 | }, 15 | "keywords": [ 16 | "cache", 17 | "file", 18 | "redis", 19 | "memory", 20 | "mongodb", 21 | "caching", 22 | "mongo", 23 | "store", 24 | "ttl", 25 | "middleware", 26 | "bucket" 27 | ], 28 | "license": "MIT", 29 | "devDependencies": { 30 | "babel-cli": "^6.4.5", 31 | "babel-core": "^6.4.5", 32 | "babel-plugin-add-module-exports": "^0.1.2", 33 | "babel-preset-es2015": "^6.3.13", 34 | "babel-preset-stage-2": "^6.3.13", 35 | "bluebird": "^3.1.5", 36 | "mocha": "^2.4.4", 37 | "pre-commit": "^1.1.2" 38 | }, 39 | "dependencies": { 40 | "cacheman-memory": "^1.1.0", 41 | "ms": "^0.7.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | import assert from 'assert'; 8 | import Bluebird from 'bluebird'; 9 | import Cacheman from '../lib/index'; 10 | 11 | Bluebird.noConflict(); 12 | 13 | let cache; 14 | let n = 0; 15 | 16 | function testKey() { 17 | return 'test' + (++n); 18 | } 19 | 20 | describe('cacheman', function () { 21 | 22 | beforeEach(function(done){ 23 | cache = new Cacheman('testing'); 24 | done(); 25 | }); 26 | 27 | afterEach(function(done){ 28 | cache.clear(done); 29 | }); 30 | 31 | it('should return a proper CommonJS module, not an ES6-only one', function() { 32 | var mod = require('../node/index'); 33 | assert.equal(typeof(mod), 'function'); 34 | }); 35 | 36 | it('should have main methods', function () { 37 | assert.ok(cache.set); 38 | assert.ok(cache.get); 39 | assert.ok(cache.del); 40 | assert.ok(cache.clear); 41 | assert.ok(cache.cache); 42 | }); 43 | 44 | it('should set correct prefix', function () { 45 | let c1 = new Cacheman(); 46 | let c2 = new Cacheman('foo'); 47 | let c3 = new Cacheman('foo', { prefix: 'myprefix' }); 48 | assert.equal(c1._prefix, 'cacheman:cache:'); 49 | assert.equal(c2._prefix, 'cacheman:foo:'); 50 | assert.equal(c3._prefix, 'myprefix:foo:'); 51 | }); 52 | 53 | it('should have a default ttl', function () { 54 | assert.equal(cache._ttl, 60); 55 | assert.equal(cache.options.ttl, 60); 56 | }) 57 | 58 | it('should not allow invalid keys', function (done) { 59 | let msg = 'Invalid key, key must be a string or array.'; 60 | cache.set(1, {}, function (err) { 61 | assert.equal(err.message, msg); 62 | cache.set(null, {}, function (err) { 63 | assert.equal(err.message, msg); 64 | cache.set(undefined, {}, function (err) { 65 | assert.equal(err.message, msg); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | }); 71 | 72 | it('should store items', function (done) { 73 | let key = testKey(); 74 | cache.set(key, { a: 1 }, function (err) { 75 | if (err) return done(err); 76 | cache.get(key, function (err, data) { 77 | if (err) return done(err); 78 | assert.equal(data.a, 1); 79 | done(); 80 | }); 81 | }); 82 | }); 83 | 84 | it('should store zero', function (done) { 85 | let key = testKey(); 86 | cache.set(key, 0, function (err) { 87 | if (err) return done(err); 88 | cache.get(key, function (err, data) { 89 | if (err) return done(err); 90 | assert.strictEqual(data, 0); 91 | done(); 92 | }); 93 | }); 94 | }); 95 | 96 | it('should store false', function (done) { 97 | let key = testKey(); 98 | cache.set(key, false, function (err) { 99 | if (err) return done(err); 100 | cache.get(key, function (err, data) { 101 | if (err) return done(err); 102 | assert.strictEqual(data, false); 103 | done(); 104 | }); 105 | }); 106 | }); 107 | 108 | it('should store null', function (done) { 109 | let key = testKey(); 110 | cache.set(key, null, function (err) { 111 | if (err) return done(err); 112 | cache.get(key, function (err, data) { 113 | if (err) return done(err); 114 | assert.strictEqual(data, null); 115 | done(); 116 | }); 117 | }); 118 | }); 119 | 120 | it('should delete items', function (done) { 121 | let key = testKey() 122 | , value = Date.now(); 123 | cache.set(key, value, function (err) { 124 | if (err) return done(err); 125 | cache.get(key, function (err, data) { 126 | if (err) return done(err); 127 | assert.equal(data, value); 128 | cache.del(key, function (err) { 129 | if (err) return done(err); 130 | cache.get(key, function (err, data) { 131 | if (err) return done(err); 132 | assert.equal(data, null); 133 | done(); 134 | }); 135 | }); 136 | }); 137 | }); 138 | }); 139 | 140 | it('should clear items', function (done) { 141 | let key = testKey() 142 | , value = Date.now(); 143 | cache.set(key, value, function (err) { 144 | if (err) return done(err); 145 | cache.get(key, function (err, data) { 146 | if (err) return done(err); 147 | assert.equal(data, value); 148 | cache.clear(function (err) { 149 | if (err) return done(err); 150 | cache.get(key, function (err, data) { 151 | if (err) return done(err); 152 | assert.equal(data, null); 153 | done(); 154 | }); 155 | }); 156 | }); 157 | }); 158 | }); 159 | 160 | it('should cache items', function (done) { 161 | let key = testKey() 162 | , value = Date.now(); 163 | cache.cache(key, value, 10, function (err, data) { 164 | assert.equal(data, value); 165 | done(); 166 | }); 167 | }); 168 | 169 | it('should allow middleware when using `cache` method', function (done) { 170 | 171 | this.timeout(0); 172 | let key = testKey() 173 | , value = Date.now(); 174 | 175 | function middleware() { 176 | return function(key, data, ttl, next){ 177 | next(); 178 | }; 179 | } 180 | 181 | cache.use(middleware()); 182 | cache.cache(key, value, 1, function (err, data) { 183 | assert.strictEqual(data, value); 184 | done(); 185 | }); 186 | }); 187 | 188 | it('should allow middleware to overwrite caching values', function (done) { 189 | let key = testKey() 190 | , value = Date.now(); 191 | 192 | function middleware() { 193 | return function(key, data, ttl, next){ 194 | next(null, 'data', 1); 195 | }; 196 | } 197 | 198 | cache.use(middleware()); 199 | cache.cache(key, value, 1, function (err, data) { 200 | assert.strictEqual(data, 'data'); 201 | done(); 202 | }); 203 | }); 204 | 205 | it('should allow middleware to accept errors', function (done) { 206 | 207 | let key = testKey() 208 | , value = Date.now() 209 | , error = new Error('not'); 210 | 211 | function middleware() { 212 | return function(key, data, ttl, next){ 213 | next(error); 214 | }; 215 | } 216 | 217 | cache.use(middleware()); 218 | 219 | cache.cache(key, value, 1, function (err, data) { 220 | if (1 === arguments.length && err) { 221 | assert.strictEqual(err, error); 222 | done(); 223 | } 224 | }); 225 | }); 226 | 227 | it('should cache zero', function (done) { 228 | let key = testKey(); 229 | cache.cache(key, 0, function (err, data) { 230 | assert.strictEqual(data, 0); 231 | done(); 232 | }); 233 | }); 234 | 235 | it('should cache false', function (done) { 236 | let key = testKey(); 237 | cache.cache(key, false, function (err, data) { 238 | assert.strictEqual(data, false); 239 | done(); 240 | }); 241 | }); 242 | 243 | it('should cache null', function (done) { 244 | let key = testKey(); 245 | cache.cache(key, null, 10, function (err, data) { 246 | assert.strictEqual(data, null); 247 | done(); 248 | }); 249 | }); 250 | 251 | it('should allow array keys', function (done) { 252 | cache.set(['a', 'b'], 'array keyed', function (err) { 253 | if (err) return done(err); 254 | cache.get('a:b', function (err, data) { 255 | if (err) return done(err); 256 | assert.equal(data, 'array keyed'); 257 | done(); 258 | }); 259 | }); 260 | }); 261 | 262 | it('should allow the delimiter to be customized', function (done) { 263 | let c = new Cacheman({ delimiter: '-' }); 264 | c.set(['a', 'b'], 'array keyed', function (err) { 265 | if (err) return done(err); 266 | c.get('a-b', function (err, data) { 267 | if (err) return done(err); 268 | assert.equal(data, 'array keyed'); 269 | done(); 270 | }); 271 | }); 272 | }); 273 | 274 | it('should accept a valid engine', function (done) { 275 | function engine(bucket, options) { 276 | let store = {}; 277 | return { 278 | set: function (key, data, ttl, fn) { store[key] = data; fn(null, data); }, 279 | get: function (key, fn) { fn(null, store[key]); }, 280 | del: function (key) { delete store[key]; }, 281 | clear: function () { store = {}; } 282 | }; 283 | } 284 | 285 | let c = new Cacheman('test', { engine: engine }); 286 | c.set('test1', { a: 1 }, function (err) { 287 | if (err) return done(err); 288 | c.get('test1', function () { 289 | done(); 290 | }); 291 | }); 292 | }); 293 | 294 | it('should throw error on missing engine', function () { 295 | assert.throws(function() { 296 | new Cacheman(null, { engine: 'missing' }); 297 | }, 298 | Error 299 | ); 300 | }); 301 | 302 | it('should allow passing ttl in human readable format minutes', function (done) { 303 | let key = testKey(); 304 | cache.set(key, 'human way', '1m', function (err, data) { 305 | cache.get(key, function (err, data) { 306 | assert.strictEqual(data, 'human way'); 307 | done(); 308 | }); 309 | }); 310 | }); 311 | 312 | it('should allow passing ttl in human readable format seconds', function (done) { 313 | let key = testKey(); 314 | cache.set(key, 'human way again', '1s', function (err, data) { 315 | setTimeout(function () { 316 | cache.get(key, function (err, data) { 317 | assert.equal(data, null); 318 | done(); 319 | }); 320 | }, 1100); 321 | }); 322 | }); 323 | 324 | it('should expire key', function (done) { 325 | this.timeout(0); 326 | let key = testKey(); 327 | cache.set(key, { a: 1 }, 1, function (err) { 328 | if (err) return done(err); 329 | setTimeout(function () { 330 | cache.get(key, function (err, data) { 331 | if (err) return done(err); 332 | assert.equal(data, null); 333 | done(); 334 | }); 335 | }, 1100); 336 | }); 337 | }); 338 | 339 | it('should support a custom default ttl', function (done) { 340 | let c = new Cacheman('test', { ttl: 2000 }); 341 | let key = testKey(); 342 | c.set(key, 'default human way', function (err) { 343 | if (err) return done(err); 344 | setTimeout(function () { 345 | c.get(key, function (err, data) { 346 | assert.equal(data, 'default human way'); 347 | done(); 348 | }); 349 | }, 1100); 350 | }); 351 | }); 352 | 353 | it('should support a custom default ttl in human readable seconds', function (done) { 354 | let c = new Cacheman('test', { ttl: '2s' }); 355 | let key = testKey(); 356 | c.set(key, 'default human way', function (err) { 357 | if (err) return done(err); 358 | setTimeout(function () { 359 | c.get(key, function (err, data) { 360 | assert.equal(data, 'default human way'); 361 | done(); 362 | }); 363 | }, 1100); 364 | }); 365 | }); 366 | 367 | it('should wrap a function in cache', function (done) { 368 | this.timeout(0); 369 | let key = testKey(); 370 | cache.wrap(key, function (callback) { 371 | callback(null, {a: 1}) 372 | }, 1100, function (err, data) { 373 | if (err) return done(err); 374 | assert.equal(data.a, 1); 375 | done(); 376 | }); 377 | }); 378 | 379 | it('should not change wrapped function result type', function(done) { 380 | let key = testKey(); 381 | var cache = new Cacheman('testing'); 382 | cache.wrap(key, function (callback) { 383 | callback(null, {a: 1}) 384 | }, 1100, function (err, data) { 385 | if (err) return done(err); 386 | assert.equal(typeof(data), 'object'); 387 | done(); 388 | }); 389 | }); 390 | 391 | // Make sure to test the .skip using 0.12 and 0.10 392 | (global.Promise ? it : it.skip.bind(it))('should support native Promises', function (done) { 393 | let key = testKey(); 394 | cache.set(key, 'test value', function (err) { 395 | if (err) return done(err); 396 | let p = cache.get(key); 397 | assert.equal(typeof(p), 'object'); 398 | assert.notEqual(typeof(p._engine), 'function'); 399 | assert.equal(typeof(p.then), 'function'); 400 | p 401 | .then(function(data) { 402 | assert.equal(data, 'test value'); 403 | done(); 404 | }) 405 | .catch(function(err) { 406 | done(err); 407 | }); 408 | }); 409 | }); 410 | 411 | it('should return a Bluebird promise', function (done) { 412 | let c = new Cacheman('testing', {Promise: Bluebird}) 413 | , key = testKey(); 414 | c.set(key, 'test value', function (err) { 415 | if (err) return done(err); 416 | let p = c.get(key); 417 | assert.equal(typeof(p), 'object'); 418 | assert.notEqual(typeof(p._engine), 'function'); 419 | assert.equal(typeof(p.then), 'function'); 420 | p 421 | .then(function(data) { 422 | assert.equal(data, 'test value'); 423 | done(); 424 | }) 425 | .catch(function(err) { 426 | done(err); 427 | }); 428 | }); 429 | }); 430 | 431 | it('should return a Promise from set', function (done) { 432 | let c = new Cacheman('testing', {Promise: Bluebird}) 433 | , key = testKey() 434 | , p = c.set(key, 'test value'); 435 | assert.equal(typeof(p), 'object'); 436 | assert.notEqual(typeof(p._engine), 'function'); 437 | assert.equal(typeof(p.then), 'function'); 438 | p 439 | .then(function(data) { 440 | assert.equal(data, 'test value'); 441 | 442 | c.get(key, function (err, data) { 443 | if (err) return done(err); 444 | assert.equal(data, 'test value'); 445 | done(); 446 | }); 447 | }) 448 | .catch(function(err) { 449 | done(err); 450 | }); 451 | }); 452 | 453 | it('should return a Promise from cache', function (done) { 454 | let c = new Cacheman('testing', {Promise: Bluebird}) 455 | , key = testKey() 456 | , p = c.cache(key, 'test value'); 457 | assert.equal(typeof(p), 'object'); 458 | assert.notEqual(typeof(p._engine), 'function'); 459 | assert.equal(typeof(p.then), 'function'); 460 | p 461 | .then(function(data) { 462 | assert.equal(data, 'test value'); 463 | 464 | c.get(key, function (err, data) { 465 | if (err) return done(err); 466 | assert.equal(data, 'test value'); 467 | done(); 468 | }); 469 | }) 470 | .catch(function(err) { 471 | done(err); 472 | }); 473 | }); 474 | 475 | it('should return a Promise from del', function (done) { 476 | let c = new Cacheman('testing', {Promise: Bluebird}) 477 | , key = testKey(); 478 | c.set(key, 'test value', function (err) { 479 | if (err) return done(err); 480 | let p = c.del(key); 481 | assert.equal(typeof(p), 'object'); 482 | assert.notEqual(typeof(p._engine), 'function'); 483 | assert.equal(typeof(p.then), 'function'); 484 | p 485 | .then(function() { 486 | c.get(key, function (err, data) { 487 | if (err) return done(err); 488 | assert.equal(data, null); 489 | done(); 490 | }); 491 | }) 492 | .catch(function(err) { 493 | done(err); 494 | }); 495 | }); 496 | }); 497 | 498 | it('should return a Promise from clear', function (done) { 499 | let c = new Cacheman('testing', {Promise: Bluebird}) 500 | , key = testKey(); 501 | c.set(key, 'test value', function (err) { 502 | if (err) return done(err); 503 | let p = c.clear(); 504 | assert.equal(typeof(p), 'object'); 505 | assert.notEqual(typeof(p._engine), 'function'); 506 | assert.equal(typeof(p.then), 'function'); 507 | p 508 | .then(function() { 509 | c.get(key, function (err, data) { 510 | if (err) return done(err); 511 | assert.equal(data, null); 512 | done(); 513 | }); 514 | }) 515 | .catch(function(err) { 516 | done(err); 517 | }); 518 | }); 519 | }); 520 | 521 | it('should return a Promise from wrap', function (done) { 522 | this.timeout(0); 523 | let c = new Cacheman('testing', {Promise: Bluebird}) 524 | , key = testKey() 525 | , p = c.wrap(key, function (callback) { 526 | callback(null, 'test value') 527 | }, 1100); 528 | assert.equal(typeof(p), 'object'); 529 | assert.notEqual(typeof(p._engine), 'function'); 530 | assert.equal(typeof(p.then), 'function'); 531 | p 532 | .then(function(data) { 533 | assert.equal(data, 'test value'); 534 | done(); 535 | }) 536 | .catch(function(err) { 537 | done(err); 538 | }); 539 | }); 540 | 541 | it('should accept a promise returned by a wrapped function', function (done) { 542 | this.timeout(5); 543 | let key = testKey(); 544 | cache.wrap(key, function () { 545 | return Bluebird.resolve('test value') 546 | }, 1100, function (err, data) { 547 | if (err) return done(err); 548 | assert.equal(data, 'test value'); 549 | cache.get(key, function (err, data) { 550 | if (err) return done(err); 551 | assert.equal(data, 'test value'); 552 | done(); 553 | }); 554 | }); 555 | }); 556 | 557 | it('should accept values returned by a wrapped function', function (done) { 558 | this.timeout(5); 559 | let key = testKey(); 560 | cache.wrap(key, function () { 561 | return 'test value' 562 | }, 1100, function (err, data) { 563 | if (err) return done(err); 564 | assert.equal(data, 'test value'); 565 | cache.get(key, function (err, data) { 566 | if (err) return done(err); 567 | assert.equal(data, 'test value'); 568 | done(); 569 | }); 570 | }); 571 | }); 572 | 573 | it('should accept ttl and wraped function in inverted order', function (done) { 574 | this.timeout(5); 575 | let key = testKey(); 576 | cache.wrap(key, 1100, function (callback) { 577 | callback(null, 'test value') 578 | }, function (err, data) { 579 | if (err) return done(err); 580 | assert.equal(data, 'test value'); 581 | cache.get(key, function (err, data) { 582 | if (err) return done(err); 583 | assert.equal(data, 'test value'); 584 | done(); 585 | }); 586 | }); 587 | }); 588 | 589 | }); 590 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-core/register --------------------------------------------------------------------------------