├── .gitignore ├── .jscsrc ├── .travis.yml ├── .zuul.yml ├── README.md ├── TODO.md ├── index.js ├── package.json └── test ├── cache.test.js ├── core.js └── errors.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "node-style-guide", 3 | "requireCapitalizedComments": null, 4 | "requireSpacesInAnonymousFunctionExpression": { 5 | "beforeOpeningCurlyBrace": true, 6 | "beforeOpeningRoundBrace": true 7 | }, 8 | "disallowSpacesInNamedFunctionExpression": { 9 | "beforeOpeningRoundBrace": true 10 | }, 11 | "excludeFiles": ["node_modules/**"], 12 | "disallowSpacesInFunction": null 13 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - iojs-v2 10 | - iojs-v1 11 | - '0.12' 12 | - '0.10' 13 | before_install: 14 | - npm i -g npm@^2.0.0 15 | before_script: 16 | - npm prune 17 | - 'curl -Lo travis_after_all.py https://git.io/vLSON' 18 | after_success: 19 | - python travis_after_all.py 20 | - export $(cat .to_export_back) 21 | - npm run semantic-release 22 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: tape 2 | browsers: 3 | - name: chrome 4 | version: latest 5 | - name: safari 6 | version: latest 7 | - name: firefox 8 | version: latest -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autocache 2 | 3 | [![Travis Status](https://travis-ci.org/remy/autocache.svg?branch=master)](https://travis-ci.org/remy/autocache) 4 | 5 | **TL;DR memorisation back by a persistent store.** 6 | 7 | Instead of caching single keys and values, autocache allows you to define a setter and when a request for that key is placed, it will run the setter live, caching the result and returning it. 8 | 9 | Importantly, the autocache can, *and should* be used with a persistent store so long as the adapter implements the [storage api](#storage-api). 10 | 11 | Note that by default, the cache is stored in memory (which kinda isn't the point), so when you restart, the cache will be lost. 12 | 13 | ## Usage 14 | 15 | Autocache can be used either on the server or on the client (again, recommended with a persistent adapter). 16 | 17 | General usage: 18 | 19 | - Define a storage proceedure against a `key` 20 | - Get the key value 21 | - Clear/invalidate values 22 | 23 | Note that autocache is a singleton, so we only need to set the store once. 24 | 25 | ```js 26 | var redisAC = require('autocache-redis'); 27 | var cache = require('autocache')({ store: redisAC }); 28 | 29 | cache.define('testStatus', function (done) { 30 | // call an imaginary test status API 31 | http.request('/test-status').then(function (result) { 32 | done(null, result.status); 33 | }).catch(function (error) { 34 | done(error); 35 | }); 36 | }); 37 | ``` 38 | 39 | ...now in another script elsewhere: 40 | 41 | ```js 42 | var cache = require('autocache'); 43 | 44 | app.get('/status', function (req, res) { 45 | cache.get('testStatus', function (error, status) { 46 | if (error) { 47 | return res.send(error); 48 | } 49 | 50 | res.render(status === 'pass' ? 'test-passing' : 'test-fail'); 51 | }); 52 | }); 53 | 54 | // every 10 minutes, clear the cache 55 | // note: we could also do this using the object notation and the TTL property 56 | setInterval(function () { 57 | cache.clear('testStatus'); 58 | }, 10 * 60 * 1000); 59 | ``` 60 | 61 | ## Important notes 62 | 63 | - If an update is taking a long time, this does not block `.get` requests. The gets will be served with the old value until the update has completed and commited a new value. 64 | - Calls to `.clear` will queue (and thus block) gets until the clear is complete, after which the `.get` requests are automatically flushed. 65 | 66 | ## Adapters 67 | 68 | Current adapters: 69 | 70 | * [Redis](https://www.npmjs.com/package/autocache-redis) 71 | * [localStorage](https://www.npmjs.com/package/autocache-localstorage) 72 | 73 | Please do contribute your own adapters - missing: mongodb, memcache, sqlite..? 74 | 75 | ## Methods 76 | 77 | ### cache.define(string, function) 78 | 79 | For a particular `string` key, set a function that will return a cached value. 80 | 81 | Note that the `function` can be synchronous or asynchronous. If your code accepts a `done` function, you can pass the value you wish to cache to the `done` function argument (as seen in the usage example above). 82 | 83 | ### cache.define(options) 84 | 85 | As above, but with extended options: 86 | 87 | ```js 88 | { 89 | name: "string", 90 | update: function () {}, 91 | ttl: 1000, // time to live (ms) 92 | ttr: 1000, // time to refresh (ms) 93 | } 94 | ``` 95 | 96 | TTL will auto expire (and `clear`) the entry based on the `ttl` milliseconds since the last time it was *accessed*. 97 | 98 | Note that if `ttr` is present, `ttl` will be ignored. 99 | 100 | ### cache.get(string, function) 101 | 102 | If a cached value is available for `string` it will call your `function` with an error first, then the result. 103 | 104 | If there is no cached value, autocache will run the definition, cache the value and then call your `function`. 105 | 106 | If multiple calls are made to `get` under the same `string` value, and the value hasn't been cached yet, the calls will queue up until a cached value has been returned, after which all the queued `function`s will be called. 107 | 108 | ### cache.get(string, [fn args], function) 109 | 110 | Cache getting also supports arguments to your definition function. This is *only* supported on async definitions. 111 | 112 | For example: 113 | 114 | ```js 115 | cache.define('location', function (name, done) { 116 | xhr.get('/location-lookup/' + name).then(function (result) { 117 | done(null, result); 118 | }).catch(function (error) { 119 | done(error); 120 | }); 121 | }); 122 | 123 | // this will miss the cache, and run the definition 124 | cache.get('location', 'brighton', function (error, data) { 125 | // does something with data 126 | }); 127 | 128 | // this will ALSO miss the cache 129 | cache.get('location', 'berlin', function (error, data) { 130 | // does something with data 131 | }); 132 | 133 | // this will hit the cache 134 | cache.get('location', 'berlin', function (error, data) { 135 | // does something with data 136 | }); 137 | ``` 138 | 139 | In the above example, once the cache is called with the argument `brighton`, the name and argument are now the unique key to the cache. 140 | 141 | ### cache.update(string, function) 142 | 143 | Calls the definition for the `string`, caches it internally, and calls your `function` with and error and the result. 144 | 145 | ### cache.clear([string]) 146 | 147 | Clear all (with no arguments) or a single cached entry. 148 | 149 | ### cache.destroy([string]) 150 | 151 | Destroy the all definitions (with no arguments) or a single definition entry. 152 | 153 | ### cache.configure({ store: adapter }) 154 | 155 | Set and store the storage adapter for persistent storage. See notes on [adapters](#apaters). 156 | 157 | ### cache.reset() 158 | 159 | Clear all of the internal state of the cache, except for the storage adapter. 160 | 161 | ## Storage API 162 | 163 | If you want to write your own adapter for persistent storage you must implement the following functions: 164 | 165 | ```text 166 | get(key, callback) // get single 167 | set(key, value, callback) // set single 168 | destroy([key], callback) // delete single 169 | clear(callback) // delete all 170 | dock(autocache) // passes copy of active cache 171 | toString() // returns a string representation of your store (for debugging) 172 | ``` 173 | 174 | See the [adapters](https://github.com/remy/autocache/tree/master/adapters) for examples of code. 175 | 176 | Notes: 177 | 178 | 1. Callbacks must pass an error first object, then the value. The value should be `undefined` if not found. 179 | 2. Callbacks are expected to be asynchronous (but are acceptable as synchronous). 180 | 3. `clear` should only clear objects created by the cache (which can be identified by a prefix). 181 | 4. Calling the adapter function should accept the `autocache` as an argument, example below. 182 | 5. Autocache will handle converting user objects to and from JSON, so the adapter will always be storing a string. 183 | 6. `dock` is called with the autocache instance passed in, if the store is already connected, you should call `autocache.emit('connect')` immediately. 184 | 185 | **Important** once your adapter has been attached, it should emit a `connect` event: 186 | 187 | ```js 188 | // this tells autocache that we're reading to start caching 189 | autocache.emit('connect'); 190 | ``` 191 | 192 | ### Automatically setting the autocache store 193 | 194 | When the adapter is required, the user must be able to pass the autocache object into your adapter. This call will set the autocache's store to your adapter. 195 | 196 | Below is the code from the `localStorage` adapter. It returns the store if called, but also checks if the autocache was passed in, and if it was, calls the `configure` function to assign the store as itself: 197 | 198 | ```js 199 | function LocalStore(autocache) { 200 | if (autocache) { 201 | autocache.configure({ store: new LocalStore() }); 202 | return LocalStore; 203 | } 204 | } 205 | ``` 206 | 207 | ## TODO 208 | 209 | - Test prefix support 210 | 211 | ## License 212 | 213 | [MIT](http://rem.mit-license.org) -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - Validate store API when linked 4 | - Add `.keys(s)` function to get all the keys matching `s`, also supporting `string:*` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Cache = (function () { 2 | var noop = function () {}; 3 | 4 | var active = true; 5 | 6 | // only use require if we're in a node-like environment 7 | var debugOn = false; 8 | var debug = typeof exports !== 'undefined' ? 9 | require('debug')('autocache') : 10 | function (log) { 11 | if (console && console.log && cache.debug) { 12 | console.log(log); 13 | } 14 | }; 15 | 16 | var connected = false; 17 | var queue = []; 18 | var useQueue = false; 19 | 20 | function MemoryStore() { 21 | this.data = {}; 22 | debug('Using MemoryStore'); 23 | if (!debug) { 24 | console.warn('Using internal MemoryStore - this will not persist'); 25 | } 26 | } 27 | 28 | MemoryStore.prototype = { 29 | toString: function () { 30 | return 'MemoryStore(#' + Object.keys(this.data).length + ')'; 31 | }, 32 | get: function (key, callback) { 33 | callback(null, this.data[key]); 34 | }, 35 | set: function (key, value, callback) { 36 | this.data[key] = value; 37 | if (callback) { 38 | callback(null, value); 39 | } 40 | }, 41 | destroy: function (key, callback) { 42 | var found = this.data[key] !== undefined; 43 | delete this.data[key]; 44 | if (callback) { 45 | callback(null, found); 46 | } 47 | }, 48 | clear: function (callback) { 49 | this.data = {}; 50 | if (callback) { 51 | callback(null, true); 52 | } 53 | }, 54 | dock: function (cache) { 55 | cache.emit('connect'); 56 | }, 57 | }; 58 | 59 | var settings = { 60 | // store: new MemoryStore(), 61 | definitions: {}, 62 | queue: {}, // not the same as the queued calls 63 | }; 64 | 65 | function generateKey() { 66 | var args = [].slice.call(arguments); 67 | var key = args.shift(); 68 | if (typeof args.slice(-1).pop() === 'function') { 69 | args.pop(); // drop the callback 70 | } 71 | 72 | if (!args.length) { 73 | return key; // FIXME this is hiding a bug in .clear(key); 74 | } 75 | 76 | return key + ':' + JSON.stringify(args); 77 | } 78 | 79 | function queueJob(method) { 80 | var methodArgs = ''; 81 | var args = [].slice.call(arguments, 1); 82 | if (args.length) { 83 | methodArgs = generateKey.apply(this, args); 84 | } 85 | var sig = method + '(' + methodArgs + ')'; 86 | 87 | debug('queued: ' + sig); 88 | return queue.push({ 89 | method: method, 90 | context: this, 91 | arguments: args, 92 | sig: sig, 93 | }); 94 | } 95 | 96 | function stub(method, fn) { 97 | return function stubWrapper() { 98 | if (!connected) { 99 | var args = [].slice.call(arguments); 100 | args.unshift(method); 101 | return queueJob.apply(this, args); 102 | } 103 | fn.apply(this, arguments); 104 | }; 105 | } 106 | 107 | function flush() { 108 | debug('flushing queued calls'); 109 | queue.forEach(function (job) { 110 | debug('flush %s: %s', job.method, job.sig); 111 | cache[job.method].apply(job.context, job.arguments); 112 | }); 113 | queue = []; 114 | } 115 | 116 | function reset() { 117 | debug('reset'); 118 | Object.keys(settings.definitions).forEach(function (key) { 119 | clearTTL(key); 120 | if (settings.definitions[key].ttr) { 121 | clearInterval(settings.definitions[key].ttr); 122 | } 123 | }); 124 | settings.definitions = {}; 125 | settings.queue = {}; 126 | return cache; 127 | } 128 | 129 | function cache(options) { 130 | if (options === undefined) { 131 | options = {}; 132 | } 133 | 134 | if (!settings.store) { 135 | reset(); 136 | } 137 | 138 | if (options.store !== undefined) { 139 | connected = false; 140 | debug('assigned caching store: ' + options.store.toString()); 141 | settings.store = options.store; 142 | } 143 | 144 | if (!settings.store) { 145 | settings.store = new MemoryStore(); 146 | } 147 | 148 | // try to dock 149 | if (settings.store.dock) { 150 | settings.store.dock(cache); 151 | } 152 | 153 | return cache; 154 | } 155 | 156 | function define(key, callback) { 157 | var options = {}; 158 | if (!callback && typeof key !== 'string') { 159 | // expect object with options 160 | options = key; 161 | callback = options.update; 162 | key = options.name; 163 | } else { 164 | options.update = callback; 165 | options.name = key; 166 | } 167 | 168 | if (!key || !callback) { 169 | throw new Error('.define requires a name and callback'); 170 | } 171 | 172 | if (settings.definitions[key] && settings.definitions[key].timer) { 173 | clearInterval(settings.definitions[key].timer); 174 | } 175 | 176 | settings.definitions[key] = options; 177 | 178 | if (options.ttr) { 179 | settings.definitions[key].timer = setInterval(function () { 180 | debug('%s: TTR fired - updating', key); 181 | cache.update(key); 182 | }, options.ttr); 183 | } 184 | 185 | } 186 | 187 | function update(key) { 188 | var args = [].slice.apply(arguments); 189 | 190 | if (typeof args.slice(-1).pop() !== 'function') { 191 | args.push(noop); 192 | } 193 | 194 | var callback = args[args.length - 1]; 195 | var storeKey = generateKey.apply(this, args); 196 | 197 | if (!settings.definitions[key]) { 198 | return callback(new Error('No definition found in update for ' + key)); 199 | } 200 | 201 | function done(error, result) { 202 | if (error) { 203 | debug('%s: update errored, ignoring store:', storeKey, error); 204 | } else { 205 | debug('%s: updated & stored', storeKey); 206 | } 207 | 208 | var defs = settings.definitions[key]; 209 | 210 | if (!error && defs && defs.ttl) { 211 | defs.ttlTimer = setTimeout(function () { 212 | debug('%s: TTL expired', storeKey); 213 | cache.clear(storeKey); 214 | }, defs.ttl); 215 | } 216 | 217 | callback(error, result); 218 | if (settings.queue[storeKey] && settings.queue[storeKey].length) { 219 | settings.queue[storeKey].forEach(function (callback) { 220 | callback(error, result); 221 | }); 222 | } 223 | delete settings.queue[storeKey]; 224 | } 225 | 226 | try { 227 | var fn = settings.definitions[key].update; 228 | if (fn.length) { 229 | fn.apply(this, args.slice(1, -1).concat(function (error, result) { 230 | if (error) { 231 | // don't store if there's an error 232 | return done(error); 233 | } 234 | 235 | settings.store.set( 236 | storeKey, 237 | JSON.stringify(result), 238 | function (error) { 239 | done(error, result); 240 | } 241 | ); 242 | })); 243 | } else { 244 | var result = fn(); 245 | settings.store.set( 246 | storeKey, 247 | JSON.stringify(result), 248 | function (error) { 249 | done(error, result); 250 | } 251 | ); 252 | } 253 | } catch (e) { 254 | debug('%s: exception in user code', key); 255 | done(e); 256 | } 257 | } 258 | 259 | function get(key) { 260 | var args; 261 | if (useQueue) { 262 | args = [].slice.call(arguments); 263 | args.unshift('get'); 264 | return queueJob.apply(this, args); 265 | } 266 | 267 | args = [].slice.apply(arguments); 268 | 269 | 270 | if (typeof args.slice(-1).pop() !== 'function') { 271 | args.push(noop); 272 | } 273 | 274 | var callback = args[args.length - 1]; 275 | var storeKey = generateKey.apply(this, args); // jshint ignore:line 276 | 277 | debug('-> get: %s', storeKey); 278 | 279 | var handler = function (error, result) { 280 | if (error) { 281 | return callback(error); 282 | } 283 | 284 | if (!error && result === undefined) { 285 | debug('<- %s: get miss', storeKey); 286 | 287 | if (!settings.definitions[key]) { 288 | return callback(new Error('No definition found in get for ' + key)); 289 | } 290 | 291 | // if there's a queue waiting for this data, hold up, 292 | // else go get it 293 | if (settings.queue[storeKey] !== undefined) { 294 | return settings.queue[storeKey].push(callback); 295 | } else { 296 | settings.queue[storeKey] = []; 297 | // call update with 298 | return update.apply(this, args); 299 | } 300 | } 301 | 302 | debug('<- %s: get hit', storeKey); 303 | 304 | // reset the TTL if there is one 305 | startTTL(storeKey); 306 | 307 | try { 308 | return callback(null, JSON.parse(result)); 309 | } catch (error) { 310 | return callback(error); 311 | } 312 | }; 313 | 314 | if (active) { 315 | settings.store.get(storeKey, handler); 316 | } else { 317 | debug('cache disabled, skipping cache check'); 318 | handler(null); 319 | } 320 | } 321 | 322 | function clearTTL(key) { 323 | if (settings.definitions[key] && settings.definitions[key].ttlTimer) { 324 | debug('%s: TTL cleared', key); 325 | clearTimeout(settings.definitions[key].ttlTimer); 326 | delete settings.definitions[key].ttlTimer; 327 | } 328 | } 329 | 330 | function startTTL(key) { 331 | clearTTL(key); 332 | var root = key.split(':').shift(); 333 | if (settings.definitions[root] && settings.definitions[root].ttl) { 334 | debug('%s: TTL set (in ' + settings.definitions[root].ttl + 'ms)', key); 335 | if (!settings.definitions[key]) { 336 | settings.definitions[key] = {}; 337 | } 338 | settings.definitions[key].ttlTimer = setTimeout(function () { 339 | debug('%s: TTL expired', key); 340 | cache.clear(key); 341 | }, settings.definitions[root].ttl); 342 | } 343 | } 344 | 345 | function clear(key, callback) { 346 | if (useQueue) { 347 | var args = [].slice.call(arguments); 348 | args.unshift('clear'); 349 | return queueJob.apply(this, args); 350 | } 351 | 352 | debug('queuing upcoming gets'); 353 | 354 | if (typeof key === 'function') { 355 | callback = key; 356 | key = null; 357 | } 358 | 359 | useQueue = true; 360 | var wrapped = function () { 361 | useQueue = false; 362 | flush(); 363 | if (callback) { 364 | callback.apply(this, arguments); 365 | } 366 | }; 367 | 368 | if (!key) { 369 | debug('clearing all'); 370 | Object.keys(settings.definitions).forEach(clearTTL); 371 | settings.store.clear(wrapped); 372 | } else { 373 | debug('clearing one: %s', key); 374 | clearTTL(key); 375 | settings.store.destroy(key, wrapped); 376 | } 377 | } 378 | 379 | function destroy(key, callback) { 380 | if (typeof key === 'function') { 381 | callback = key; 382 | key = null; 383 | } else if (!callback) { 384 | callback = noop; 385 | } 386 | 387 | var keys = []; 388 | 389 | if (!key) { 390 | // destory all 391 | debug('destroying all'); 392 | keys = Object.keys(settings.definitions); 393 | } else { 394 | debug('destroying one: %s', key); 395 | keys = [key]; 396 | } 397 | 398 | keys.map(function (key) { 399 | clearTTL(key); 400 | 401 | if (settings.definitions[key].timer) { 402 | clearInterval(settings.definitions[key].timer); 403 | } 404 | settings.definitions[key] = {}; 405 | }); 406 | 407 | callback(null); 408 | } 409 | 410 | function emit(event) { 411 | // allow for typos 412 | if (event === 'connect' || event === 'connected') { 413 | connected = true; 414 | debug('connected - flushing queue'); 415 | flush(); 416 | } else if (event === 'disconnect') { 417 | connected = false; 418 | console.log('autocache has lost it\'s persistent connection'); 419 | } 420 | } 421 | 422 | 423 | if (Object.defineProperty) { 424 | Object.defineProperty(cache, 'debug', { 425 | get: function () { 426 | return debugOn; 427 | }, 428 | set: function (value) { 429 | debugOn = value; 430 | if (debugOn) { 431 | cache.settings = settings; 432 | } else { 433 | delete cache.settings; 434 | } 435 | }, 436 | }); 437 | 438 | Object.defineProperty(cache, 'active', { 439 | get: function () { 440 | return active; 441 | }, 442 | set: function (value) { 443 | active = value; 444 | }, 445 | }); 446 | } else { 447 | cache.debug = false; 448 | } 449 | 450 | cache.emit = emit; 451 | cache.configure = cache; // circular 452 | cache.clear = stub('clear', clear); 453 | cache.define = stub('define', define); 454 | cache.destroy = stub('destroy', destroy); 455 | cache.get = stub('get', get); 456 | cache.reset = reset; 457 | cache.update = stub('update', update); 458 | 459 | if (typeof process !== 'undefined') { 460 | if (process.env.NODE_ENV === 'test') { 461 | // expose settings when debugging 462 | cache.debug = true; 463 | } 464 | } 465 | 466 | return cache; 467 | })(); 468 | 469 | if (typeof exports !== 'undefined') { 470 | module.exports = Cache; 471 | module.exports.version = require('./package').version || 'development'; 472 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autocache", 3 | "description": "Caching system that automatically populates itself", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "for FILE in test/*.test.js; do echo $FILE; NODE_ENV=test tape $FILE | tap-spec; if [ $? -ne 0 ]; then exit 1; fi; done", 7 | "zuul": "zuul --local 8080 --ui tape -- test/cache.test.js node_modules/autocache-localstorage/test/localstorage.test.js", 8 | "test-solo": "tape test/cache.test.js | tap-bail | tap-spec", 9 | "watch": "nodemon -x 'node test/*.test.js | tap-spec'", 10 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 11 | }, 12 | "keywords": [ 13 | "cache" 14 | ], 15 | "author": "Remy Sharp", 16 | "license": "MIT", 17 | "repository": { 18 | "type": "git", 19 | "url": "http://github.com/remy/autocache.git" 20 | }, 21 | "dependencies": { 22 | "debug": "^2.1.1" 23 | }, 24 | "devDependencies": { 25 | "async": "^1.4.2", 26 | "autocache-localstorage": "^1.2.0", 27 | "autocache-redis": "^1.3.0", 28 | "nodemon": "^1.3.7", 29 | "redis": "^0.12.1", 30 | "semantic-release": "^4.0.3", 31 | "tap-bail": "0.0.0", 32 | "tap-spec": "^2.2.1", 33 | "tape": "^3.5.0", 34 | "semantic-release": "^4.0.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/cache.test.js: -------------------------------------------------------------------------------- 1 | var cache = require('../')(); 2 | require('./core')(cache); -------------------------------------------------------------------------------- /test/core.js: -------------------------------------------------------------------------------- 1 | module.exports = runtests; 2 | 3 | var test = require('tape'); 4 | var async = require('async'); 5 | 6 | function runtests(cache, done) { 7 | cache.debug = true; 8 | 9 | test = beforeEach(test, function (t) { 10 | cache.reset().clear(function () { 11 | t.end(); 12 | }); 13 | }); 14 | 15 | test('sync cache', function (t) { 16 | t.plan(3); 17 | 18 | cache.reset().clear(); 19 | 20 | var n = 20; 21 | 22 | cache.define('number', function () { 23 | return n++; 24 | }); 25 | 26 | cache.get('number', function (error, result) { 27 | t.ok(result === 20, 'should return 20'); 28 | }); 29 | 30 | cache.get('number', function (error, result) { 31 | t.ok(error === null, 'should not error'); 32 | t.ok(result === 20, 'should return 20'); 33 | }); 34 | }); 35 | 36 | test('clearing values', function (t) { 37 | t.plan(3); 38 | 39 | var n = 20; 40 | 41 | cache.define('number', function () { 42 | return n++; 43 | }); 44 | 45 | t.test('inital tests', function (t) { 46 | t.plan(2); 47 | 48 | cache.get('number', function (error, result) { 49 | t.equal(result, 20, 'inital value is correct'); 50 | }); 51 | 52 | cache.get('number', function (error, result) { 53 | t.equal(result, 20, 'cached value has not changed'); 54 | }); 55 | }); 56 | 57 | t.test('clear', function (t) { 58 | t.plan(3); 59 | cache.clear('number', function (error, res) { 60 | t.equal(res, true, 'value was found and cleared'); 61 | 62 | cache.get('number', function (error, result) { 63 | t.ok(!error, 'cleared value and re-collects'); 64 | t.equal(result, 21, 'supports closures, value now: ' + result); 65 | }); 66 | }); 67 | }); 68 | 69 | t.test('destroy', function (t) { 70 | t.plan(3); 71 | cache.destroy('number', function () { 72 | cache.get('number', function (error, result) { 73 | t.ok(error === null, 'no error on cached result'); 74 | t.ok(result === 21, 'number exists after definition is deleted: ' + result); 75 | }); 76 | }); 77 | 78 | cache.define('name', function () { 79 | return 'remy'; 80 | }); 81 | 82 | cache.destroy('name', function () { 83 | cache.get('name', function (error, data) { 84 | t.ok(error instanceof Error, 'destroyed definition'); 85 | t.end(); 86 | }); 87 | }); 88 | }); 89 | }); 90 | 91 | test('async cache', function (t) { 92 | t.plan(3); 93 | 94 | cache.reset().clear(); 95 | 96 | var n = 20; 97 | 98 | cache.define('number', function (done) { 99 | done(null, n); 100 | n++; 101 | }); 102 | 103 | cache.get('number', function (error, result) { 104 | t.ok(result === 20, 'should return 20'); 105 | }); 106 | 107 | cache.get('number', function (error) { 108 | t.ok(error === null, 'should not error'); 109 | }); 110 | 111 | 112 | setTimeout(function () { 113 | cache.clear('number', function () { 114 | cache.get('number', function (error, result) { 115 | t.equal(result, 21, 'should support closures'); 116 | }); 117 | }); 118 | }, 100); 119 | 120 | }); 121 | 122 | test('singleton cache', function (t) { 123 | t.plan(2); 124 | cache.reset(); 125 | var cache1 = cache(); 126 | var cache2 = cache(); 127 | 128 | var n = 20; 129 | 130 | cache1.define('number', function () { 131 | return n++; 132 | }); 133 | 134 | cache1.get('number', function (error, result) { 135 | t.ok(result === 20, 'cache1 should return 20'); 136 | }); 137 | 138 | cache2.get('number', function (error, result) { 139 | t.ok(result === 20, 'cache2 should also return 20'); 140 | }); 141 | }); 142 | 143 | test('ttr', function (t) { 144 | t.plan(2); 145 | 146 | async.waterfall([ 147 | function (done) { 148 | cache.reset().clear(function () { 149 | var n = 19; 150 | cache.define({ 151 | name: 'number', 152 | update: function () { 153 | n++; 154 | return n; 155 | }, 156 | ttr: 500, 157 | }); 158 | done(); 159 | }); 160 | }, 161 | function (done) { 162 | cache.get('number', function (error, result) { 163 | t.ok(result === 20, 'result was ' + result); 164 | done(); 165 | }); 166 | }, 167 | function (done) { 168 | setTimeout(function () { 169 | cache.get('number', function (error, result) { 170 | t.equal(result, 21, 'result was ' + result + ' after auto refresh'); 171 | // hack: redefine to clear the ttr 172 | cache.define('number', function () {}); 173 | done(); 174 | // t.end(); 175 | }); 176 | }, 750); 177 | 178 | }, 179 | ]); 180 | }); 181 | 182 | test('ttr still accessible, but state', { skip: true }, function (t) { 183 | t.plan(12); 184 | 185 | var n = 0; 186 | var timeout = null; 187 | cache.define({ 188 | name: 'number', 189 | update: function (done) { 190 | n++; 191 | timeout = setTimeout(function () { 192 | t.pass('definition set'); 193 | done(null, n); 194 | }, 500); 195 | }, 196 | ttr: 1000, 197 | }); 198 | 199 | function check(expect, n, done) { 200 | setTimeout(function () { 201 | cache.get('number', function (error, result) { 202 | t.equal(result, expect, 'expecting ' + expect + ', result for ' + 203 | n + 'ms was ' + result); 204 | done(); 205 | }); 206 | }, n); 207 | 208 | // if (expect === 2) { 209 | // setTimeout(function () { 210 | // cache.reset(); 211 | // }, n + 100); 212 | // } 213 | } 214 | 215 | cache.get('number', function (error, result) { 216 | t.equal(result, 1, 'initial expecting 1, result was ' + result); 217 | 218 | async.waterfall([ 219 | check.bind(null, 1, 800), 220 | check.bind(null, 1, 0), 221 | check.bind(null, 1, 0), 222 | check.bind(null, 1, 0), 223 | check.bind(null, 1, 0), 224 | check.bind(null, 1, 0), 225 | check.bind(null, 1, 0), 226 | check.bind(null, 2, 700), 227 | function (done) { 228 | setTimeout(function () { 229 | cache.destroy('number'); 230 | clearTimeout(timeout); 231 | cache.clear('number', done); 232 | }, 900); 233 | }, 234 | ]); 235 | 236 | // setTimeout(function () { 237 | // cache.destroy('number'); 238 | // }, 800); 239 | }); 240 | }); 241 | 242 | test('ttl', function (t) { 243 | t.plan(11); 244 | 245 | /** 246 | * plan: test time to live automatically resets timeout 247 | * 248 | * 1. create "number" definition (ttl: 500) returns ++19 249 | * 2. get number (20) 250 | * 3. get number again inside timeout (20) 251 | * 4. clear number (value found: true) 252 | * 5. get number (21) 253 | */ 254 | 255 | var n = 19; 256 | cache.define({ 257 | name: 'number', 258 | update: function () { 259 | t.pass('definition called'); 260 | n++; 261 | return n; 262 | }, 263 | ttl: 500, 264 | }); 265 | 266 | async.waterfall([ 267 | function (done) { 268 | cache.get('number', function (error, result) { 269 | t.equal(result, 20, 'initial result was ' + result); 270 | done(); 271 | }); 272 | }, 273 | function (done) { 274 | cache.get('number', function (error, result) { 275 | t.equal(result, 20, 'initial result was ' + result); 276 | done(); 277 | }); 278 | }, 279 | function (done) { 280 | cache.clear('number', function (error, found) { 281 | t.equal(found, true, 'value found in cache'); 282 | done(); 283 | }); 284 | }, 285 | function (done) { 286 | // get again to re-cache 287 | cache.get('number', function (error, result) { 288 | t.equal(result, 21, 'hot cache result was ' + result); 289 | done(); 290 | }); 291 | }, 292 | function (done) { 293 | // should reset the timer on the TTL 294 | setTimeout(function () { 295 | // get again to re-cache 296 | cache.get('number', function (error, result) { 297 | t.equal(result, 21, 'expected to still be hot after @ 400ms: ' + result); 298 | }); 299 | }, 400); 300 | 301 | setTimeout(function () { 302 | cache.get('number', function (error, result) { 303 | t.equal(result, 21, 'value should still be hot @ 600ms: ' + result); 304 | done(); 305 | }); 306 | }, 600); 307 | }, 308 | function (done) { 309 | // in 600ms it should have fully expired 310 | setTimeout(function () { 311 | cache.clear('number', function (error, found) { 312 | t.equal(found, false, 'value correctly missing from cache'); 313 | 314 | cache.get('number', function (error, result) { 315 | t.equal(result, 22, 'result was ' + result + ' after expired'); 316 | // hack: redefine to clear the ttr 317 | cache.define('number', function () {}); 318 | done(); 319 | }); 320 | }); 321 | }, 600 + 500 + 100); 322 | }, 323 | ]); 324 | 325 | 326 | }); 327 | 328 | test('function signatures', function (t) { 329 | t.plan(11); 330 | 331 | var ppl = { 332 | remy: 'brighton', 333 | andrew: 'winchester', 334 | mark: 'oxford', 335 | }; 336 | 337 | var unqiueCalls = {}; 338 | 339 | cache.define({ 340 | name: 'location', 341 | update: function (person, done) { 342 | if (!unqiueCalls[person]) { 343 | // expects to be called twice 344 | t.ok(true, 'definition called for "' + person + '"'); 345 | unqiueCalls[person] = true; 346 | } else { 347 | t.fail('definition called too many times'); 348 | } 349 | done(null, ppl[person]); 350 | }, 351 | ttl: 500, 352 | }); 353 | 354 | async.waterfall([ 355 | function (done) { 356 | cache.get('location', 'remy', function (error, result) { 357 | t.ok(result === 'brighton', 'cold call for "remy"'); 358 | done(); 359 | }); 360 | }, 361 | function (done) { 362 | cache.get('location', 'remy', function (error, result) { 363 | t.ok(result === 'brighton', 'cached call for "remy"'); 364 | done(); 365 | }); 366 | }, 367 | function (done) { 368 | cache.get('location', 'mark', function (error, result) { 369 | t.ok(result === 'oxford', 'different arg for "mark"'); 370 | done(); 371 | }); 372 | }, 373 | 374 | function (done) { 375 | cache.clear(function () { 376 | // reset the definition call 377 | delete unqiueCalls.remy; 378 | 379 | t.pass('clearing cache for "remy"'); 380 | 381 | cache.get('location', 'remy', function (error, result) { 382 | t.ok(result === 'brighton', 'cold call for "remy"'); 383 | done(); 384 | }); 385 | }); 386 | }, 387 | 388 | function (done) { 389 | cache.clear('remy', function (err, cleared) { 390 | t.equal(cleared, false, 'cleared individual state, expecting cache miss'); 391 | 392 | // reset the definition call 393 | delete unqiueCalls.remy; 394 | 395 | cache.get('location', 'remy', function (error, result) { 396 | t.ok(result === 'brighton', 'cold call for "remy"'); 397 | done(); 398 | }); 399 | }); 400 | }, 401 | 402 | function (done) { 403 | cache.clear('location', function (error, found) { 404 | t.ok(found === false, 'cache entry is empty'); 405 | done(); 406 | }); 407 | }, 408 | ]); 409 | }); 410 | 411 | test('error in setting does not clear cache', function (t) { 412 | t.plan(3); 413 | cache.reset().clear(); 414 | 415 | var n = 0; 416 | 417 | cache.define({ 418 | name: 'number', 419 | update: function (done) { 420 | if (n > 0) { 421 | return done(new Error('fail')); 422 | } 423 | 424 | n++; 425 | done(null, n); 426 | }, 427 | }); 428 | 429 | cache.get('number', function (error, number) { 430 | cache.update('number', function (error) { 431 | t.equal(error.message, 'fail'); 432 | }); 433 | 434 | t.ok(number === 1, 'TEST: 1st call, number is ' + number); 435 | 436 | cache.get('number', function (error, number) { 437 | t.ok(number === 1, 'TEST: 2nd call, number is ' + number); 438 | t.end(); 439 | }); 440 | }); 441 | }); 442 | 443 | test('disabling cache', function (t) { 444 | t.plan(4); 445 | 446 | var n = 0; 447 | cache.define('number', function () { 448 | n++; 449 | return n; 450 | }); 451 | 452 | cache.active = false; 453 | 454 | async.series([ 455 | function (done) { 456 | cache.get('number', function (error, result) { 457 | t.equal(result, 1, 'should increment'); 458 | }); 459 | cache.get('number', function (error, result) { 460 | t.equal(result, 2, 'should increment'); 461 | }); 462 | cache.get('number', function (error, result) { 463 | t.equal(result, 3, 'should increment'); 464 | }); 465 | cache.get('number', function (error, result) { 466 | t.equal(result, 4, 'should increment'); 467 | done(); 468 | }); 469 | }, 470 | function (done) { 471 | cache.active = true; 472 | done(); 473 | }, 474 | ]); 475 | }); 476 | 477 | test('ttl with primed keyed store', function (t) { 478 | // prime the store 479 | t.plan(4); 480 | cache.settings.store.set('test-ttl:["foo"]', 10, function (error) { 481 | t.equal(error, null, 'cached primed'); 482 | 483 | cache.define({ 484 | name: 'test-ttl', 485 | update: function (value, done) { 486 | t.ok(true, 'update was called'); 487 | done(null, 20); 488 | }, 489 | ttl: 1000, // auto drop this cache 490 | }); 491 | 492 | // initial hit should read primed value 493 | cache.get('test-ttl', 'foo', function (error, data) { 494 | if (error) { 495 | t.fail(error.message); 496 | } 497 | t.equal(data, 10, 'primed value was correct'); 498 | }); 499 | 500 | setTimeout(function () { 501 | cache.get('test-ttl', 'foo', function (error, data) { 502 | if (error) { 503 | t.fail(error.message); 504 | } 505 | t.equal(data, 20, 'primed value expired'); 506 | }); 507 | }, 2000); 508 | }); 509 | }); 510 | 511 | if (done) { 512 | test('final tests', function (t) { 513 | done(t); 514 | }); 515 | } 516 | } 517 | 518 | function beforeEach(test, handler) { 519 | function tapish(name, opts, listener) { 520 | if (typeof opts === 'function') { 521 | listener = opts; 522 | opts = {}; 523 | } 524 | test(name, opts, function (assert) { 525 | var _end = assert.end; 526 | assert.end = function () { 527 | assert.end = _end; 528 | listener(assert); 529 | } 530 | 531 | handler(assert); 532 | }); 533 | } 534 | 535 | tapish.only = test.only; 536 | 537 | return tapish; 538 | } 539 | 540 | function afterEach(test, handler) { 541 | function tapish(name, listener) { 542 | test(name, function (assert) { 543 | var _end = assert.end; 544 | assert.end = function () { 545 | assert.end = _end; 546 | handler(assert); 547 | }; 548 | 549 | listener(assert); 550 | }); 551 | } 552 | 553 | return tapish; 554 | } -------------------------------------------------------------------------------- /test/errors.test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var cache = require('..'); 3 | 4 | // NOTE: errors must be last, as the internal memory store has been lost 5 | test('errors', function (t) { 6 | t.plan(2); 7 | 8 | t.test('throwing', function (t) { 9 | t.plan(1); 10 | cache.configure({ store: { 11 | toString: function () { 12 | return 'ErrorStore'; 13 | }, 14 | get: function (key, callback) { 15 | callback(new Error('failed')); 16 | }, 17 | set: function () {}, 18 | destroy: function () {}, 19 | clear: function () {}, 20 | dock: function () {}, 21 | }, }); 22 | 23 | cache.emit('connect'); 24 | 25 | cache.define('number', function () { 26 | return 20; 27 | }); 28 | 29 | cache.get('number', function (error) { 30 | t.ok(error instanceof Error, 'error returned from get'); 31 | }); 32 | }); 33 | 34 | t.test('missing', function (t) { 35 | t.plan(3); 36 | var cache2 = cache({ store: false }); 37 | 38 | cache2.get('missing', function (error) { 39 | t.ok(error.message.indexOf('No definition found') === 0, 'error returned from missing definition'); 40 | }); 41 | 42 | cache2.update('missing', function (error) { 43 | t.ok(error.message.indexOf('No definition found') === 0, 'error returned from missing definition'); 44 | }); 45 | 46 | cache2.define('erroring', function (done) { 47 | callunknownFunction(); // jshint ignore:line 48 | done(20); 49 | }); 50 | 51 | cache2.get('erroring', function (error) { 52 | t.ok(error.message.indexOf('callunknownFunction') !== -1, 'captured error from definition'); 53 | }); 54 | }); 55 | }); --------------------------------------------------------------------------------