├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── assets │ └── img │ │ └── console-logger.png ├── debugging.md ├── events.md ├── optimizations.md └── timeIntervals.md ├── examples ├── populate.js ├── server.js └── two_caches.js ├── lib ├── Cache.js ├── CacheClient.js ├── StoreFacade.js ├── decorators │ ├── BaseDecorator.js │ ├── EventDecorator.js │ ├── ExpiresDecorator.js │ ├── MarshallDecorator.js │ ├── OnlySetChangedDecorator.js │ ├── PopulateDecorator.js │ ├── PopulateInDecorator.js │ └── PromiseDecorator.js ├── index.js └── util.js ├── package.json └── test ├── Cache-test.js ├── CacheClient-test.js ├── StoreFacade-test.js ├── _all.js ├── decorators ├── BaseDecorator-test.js ├── EventDecorator-test.js ├── ExpiresDecorator-test.js ├── MarshallDecorator-test.js ├── OnlySetChangedDecorator-test.js ├── PopulateDecorator-test.js ├── PopulateInDecorator-test.js └── PromiseDecorator-test.js ├── helpers └── wrap.js ├── index-test.js ├── integration └── events-test.js └── util-test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true 5 | }, 6 | "rules": { 7 | "indent": [2, 2], 8 | "brace-style": [2, "1tbs"], 9 | "comma-style": [2, "last"], 10 | "curly": [2, "multi-line"], 11 | "quotes": [2, "single"], 12 | "object-curly-spacing": [2, "never"], 13 | "strict": [2, "never"], 14 | "no-debugger": 2, 15 | "no-console": 2, 16 | "handle-callback-err": 2, 17 | "camelcase": 0, 18 | "no-underscore-dangle": 0, 19 | "no-mixed-requires": 0, 20 | "no-use-before-define": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.* 2 | !.eslintrc 3 | !.gitignore 4 | !.npmignore 5 | !.travis.yml 6 | /*.log 7 | /node_modules 8 | /npm-shrinkwrap.json 9 | /coverage 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | - 4.1 6 | 7 | branches: 8 | only: 9 | - master 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased][unreleased] 6 | 7 | ## [6.1.0] - 2016-02-01 8 | #### Added 9 | - Support for Promises in `get`, `set`, `del` and `populate` ([#5](https://github.com/dowjones/distribucache/issues/5)) 10 | 11 | ## [6.0.1] - 2015-07-17 12 | #### Added 13 | - `namespace` as the second argument of the `CacheClient` `create` event. 14 | 15 | ## [6.0.0] - 2015-07-17 16 | #### Added 17 | - Robust events, to be used for debugging and collecting stats 18 | (see the [Events docs](/docs/events.md) for more info). 19 | - ESLint validation on `npm test`. Also runs via `npm run lint`. 20 | 21 | #### Removed 22 | - (!) Event propagation from `Cache` to `CacheClient`. The only remaining 23 | propagated event is the `error` event. If you're interested in capturing 24 | events from all caches of a single client, you may subscribe to the `create` 25 | event of the `CacheClient` and then listen to specific events emitted by the 26 | created cache. 27 | 28 | ## [5.1.0] - 2015-07-04 29 | #### Added 30 | - Namespace in unhandled error messages. 31 | 32 | ## [5.0.0] - 2015-06-01 33 | #### Removed 34 | - (!) `Cache#set()` no longer returns the provided value. 35 | 36 | #### Added 37 | - Support `config.optimizeForBuffers` to skip marshalling / unmarshalling, 38 | as it is redundant when caching buffers. 39 | 40 | #### Changed 41 | - Improved README and package.json metadata. 42 | 43 | ## [4.0.0] - 2015-05-06 44 | #### Added 45 | - (!) Major refactoring of the store API for extensibility. 46 | - Support `config.populateInAttempts` to control the maximum number of attempts. 47 | 48 | #### Changed 49 | - Release the lease regardless of populate errors. 50 | - Clean up long populate stack traces. 51 | 52 | ## [3.1.0] - 2015-04-24 53 | #### Added 54 | - Support `config.timeoutPopulateIn` and increase the lease-expire timeout 55 | to `timeoutPopulateIn + 1 sec`. 56 | 57 | ## [3.0.3] - 2015-03-23 58 | #### Changed 59 | - Refactored timer strategy. 60 | 61 | ## [3.0.2] - 2015-03-23 62 | #### Changed 63 | - Returned (lost in datastore transition) cache namespace. 64 | - Marshall undefined value as `null`. 65 | 66 | ## [3.0.1] - 2015-03-19 67 | #### Changed 68 | - Log possible errors while attempting to delete an expired value. 69 | 70 | ## [3.0.0] - 2015-03-18 71 | #### Added 72 | - (!) Distribucache is now **datastore independent**! There are two backing stores available: 73 | [Redis](https://github.com/dowjones/distribucache-redis-store) and 74 | [Memory](https://github.com/dowjones/distribucache-memory-store). 75 | 76 | ## [2.6.2] - 2015-02-20 77 | #### Changed 78 | - Ensure that the hash is only set after the value is set. Otherwise, it was possible 79 | for the hash to be update and the value not updated, causing stale values to appear 80 | for longer than desired. 81 | 82 | ## [2.6.1] - 2014-12-18 83 | #### Changed 84 | - Proxy Redis errors to the CacheClient. 85 | 86 | ## [2.6.0] - 2014-12-12 87 | #### Changed 88 | - Log uncaught error to `stderr` instead of throwing. Alternatively you may 89 | subscribe to the `error` event on the client. 90 | 91 | ## [2.5.0] - 2014-12-11 92 | #### Added 93 | - Support events for the CacheClient: `get`, `set`, `del`, `stale` and `error`. 94 | 95 | ## [2.3.1] - 2014-12-04 96 | #### Changed 97 | - Fixed Redis connection mode issue. 98 | 99 | ## [2.3.0] - 2014-12-04 100 | #### Added 101 | - Support environments with no "configure" (e.g., Amazon elasticache). This is done 102 | by attempting to configure, and if that fails `console.warn` a message to the client 103 | stating that manual configuration is required. 104 | 105 | ## [2.2.0] - 2014-12-04 106 | #### Added 107 | - (Improved performance) The populate method that is called when a value is stale will 108 | now be run through a time-released lock (lease). This ensures that only one client 109 | is involved in updating on a stale at any given time, instead of all 110 | clients attacking the datastore when their values are stale. 111 | 112 | ## [2.1.2] - 2014-12-04 113 | #### Changed 114 | - Further simplify the marshaller. 115 | 116 | ## [2.1.1] - 2014-12-04 117 | #### Changed 118 | - Refactoring of the marshaller. 119 | 120 | ## [2.1.0] - 2014-12-04 121 | #### Added 122 | - Added built-in marshalling of values. 123 | 124 | ## [2.0.0] - 2014-12-03 125 | #### Changed 126 | - (!) Enforce a `namespace` in the `cacheClient.create` API. 127 | - Improved README by including the `optimizeForSmallValues` explanation. 128 | 129 | ## [1.0.0] - 2014-12-02 130 | #### Added 131 | - Initial release of a Redis-backed automatically-repopulating cache. 132 | 133 | 134 | [unreleased]: https://github.com/dowjones/distribucache/compare/v6.0.1...HEAD 135 | [6.0.1]: https://github.com/dowjones/distribucache/compare/v6.0.0...v6.0.1 136 | [6.0.0]: https://github.com/dowjones/distribucache/compare/v5.1.0...v6.0.0 137 | [5.1.0]: https://github.com/dowjones/distribucache/compare/v5.0.0...v5.1.0 138 | [5.0.0]: https://github.com/dowjones/distribucache/compare/v4.0.0...v5.0.0 139 | [4.0.0]: https://github.com/dowjones/distribucache/compare/v3.1.0...v4.0.0 140 | [3.1.0]: https://github.com/dowjones/distribucache/compare/v3.0.3...v3.1.0 141 | [3.0.3]: https://github.com/dowjones/distribucache/compare/v3.0.2...v3.0.3 142 | [3.0.2]: https://github.com/dowjones/distribucache/compare/v3.0.1...v3.0.2 143 | [3.0.1]: https://github.com/dowjones/distribucache/compare/v3.0.0...v3.0.1 144 | [3.0.0]: https://github.com/dowjones/distribucache/compare/v2.6.2...v3.0.0 145 | [2.6.2]: https://github.com/dowjones/distribucache/compare/v2.6.1...v2.6.2 146 | [2.6.1]: https://github.com/dowjones/distribucache/compare/v2.6.0...v2.6.1 147 | [2.6.0]: https://github.com/dowjones/distribucache/compare/v2.5.0...v2.6.0 148 | [2.5.0]: https://github.com/dowjones/distribucache/compare/v2.3.1...v2.5.0 149 | [2.3.1]: https://github.com/dowjones/distribucache/compare/v2.3.0...v2.3.1 150 | [2.3.0]: https://github.com/dowjones/distribucache/compare/v2.2.0...v2.3.0 151 | [2.2.0]: https://github.com/dowjones/distribucache/compare/v2.1.2...v2.2.0 152 | [2.1.2]: https://github.com/dowjones/distribucache/compare/v2.1.1...v2.1.2 153 | [2.1.1]: https://github.com/dowjones/distribucache/compare/v2.1.0...v2.1.1 154 | [2.1.0]: https://github.com/dowjones/distribucache/compare/v2.0.0...v2.1.0 155 | [2.0.0]: https://github.com/dowjones/distribucache/compare/v1.0.0...v2.0.0 156 | [1.0.0]: https://github.com/dowjones/distribucache/compare/5bc09c79ac8652c8a07407e803d5ddc74ebe761c...v1.0.0 157 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dow Jones & Company 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Distribucache [](http://travis-ci.org/dowjones/distribucache) [](http://badge.fury.io/js/distribucache) 2 | 3 | Datastore-independent automatically-repopulating cache. This cache does everything in 4 | its power to shield the caller from the delays of the downstream services. It has a unique 5 | feature, where the cache will populate itself on a certain interval, and will 6 | stop doing so when the values that were being refreshed have not been used. 7 | 8 | There are multiple available **datastores**, including: 9 | - [Redis](https://github.com/dowjones/distribucache-redis-store) 10 | - [Memory](https://github.com/dowjones/distribucache-memory-store) 11 | 12 | The cache can be used in various ways, ranging from the simplest get / set, to 13 | complex scenarios with watermarks for staleness and final expiration. 14 | 15 | 16 | ## Usage 17 | 18 | ### Basic 19 | 20 | There are many different ways to use the cache. Features are added to the cache, 21 | based on the configuration that you use. Below is an example of the simplest cache: 22 | 23 | ```javascript 24 | var distribucache = require('distribucache'), 25 | // create a Redis store (to keep track of the Redis connections) 26 | // generally performed once in the lifetime of the app 27 | redisStore = require('distribucache-redis-store'), 28 | cacheClient = distribucache.createClient(redisStore({ 29 | host: 'localhost', 30 | port: 6379 31 | })), 32 | // create a new cache 33 | // performed every time a new cache configuration is needed 34 | cache = cacheClient.create('nsp'); 35 | 36 | cache.get('k1', function (err, value) { 37 | if (err) throw err; 38 | console.log('got value:', value); 39 | }); 40 | 41 | cache.set('k1', 'v1', function (err) { 42 | if (err) throw err; 43 | console.log('set value'); 44 | }); 45 | 46 | cache.del('k1', function (err) { 47 | if (err) throw err; 48 | console.log('deleted k1'); 49 | }); 50 | ``` 51 | 52 | **Promises:** if a callback is not provided as the last argument, a 53 | [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 54 | will be returned from the `get`, `set` and `del` methods. 55 | 56 | **Note:** the `value` from the `get` will be `null` if 57 | the value is not in the cache. 58 | 59 | ### Configuration 60 | 61 | The cache can be configured in two places: (a) when creating a cache-client, 62 | and (b) when creating a cache. As you expect, the configuration in the 63 | cache overrides the configuration of the cache-client: 64 | 65 | ```javascript 66 | var cacheClient = distribucache.createClient(store, { 67 | expiresIn: '2 sec' // setting globally 68 | }); 69 | 70 | // overrides the globally set `expiresIn` 71 | var articleCache = cacheClient.create('articles', { 72 | expiresIn: '1 min' 73 | }); 74 | 75 | // uses the global `expiresIn` 76 | var pageCache = cacheClient.create('pages'); 77 | ``` 78 | 79 | 80 | ### Populating 81 | 82 | A common pattern is to call the `get` first, and if the item is not 83 | in the cache, call `set`. For this common pattern, you can provide 84 | a `populate` function when creating the cache. On a `get`, if the 85 | cache is empty your `populate` function will be called to populate the 86 | cache, and then the flow will continue to the `get` callback. This ensures 87 | that the `get` always returns a value, either from the cache or from 88 | the downstream service. 89 | 90 | ```javascript 91 | var cache = cacheClient.create('nsp', { 92 | populate: function (key, cb) { 93 | setTimeout(function () { 94 | cb(null, 42); 95 | }, 100); 96 | } 97 | }); 98 | 99 | cache.get('k1', function (err, value) { 100 | console.log(value); // 42 101 | }); 102 | ``` 103 | 104 | **Promises:** the `populate` function may return a 105 | [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 106 | if you choose. 107 | 108 | 109 | ### Expiration / Staleness 110 | 111 | When an `expiresIn` is set, a get request will return `null` 112 | after the time expires. After this, the value will be dropped 113 | from the datastore. When the `populate` function is set, 114 | instead of returning `null` the `populate` method will be called. 115 | 116 | The `expiresIn` may be set in milliseconds or in the 117 | [human-readable](/docs/timeIntervals.md) format. 118 | 119 | ```javascript 120 | var cache = cacheClient.create('nsp', { 121 | expiresIn: 2000 // 2 seconds 122 | }); 123 | ``` 124 | 125 | A `staleIn` can also be set. It acts as a low-water-mark. When a value 126 | is stale it is still returned as is to the caller. Two additional things happen: 127 | (a) the `stale` event is called (with `key` as the argument) and (b) the `populate` 128 | is called in the background if it is provided; allowing the next `get` call to 129 | get a fresh value, without incurring the delay of accessing a downstream service. 130 | 131 | ```javascript 132 | var cache = cacheClient.create('nsp', { 133 | staleIn: 1000 // 1 second 134 | }); 135 | ``` 136 | 137 | 138 | ### Timer-based Background Population 139 | 140 | The more complex, yet most powerful feature of the cache is its ability 141 | to update its keys on a specific interval. To do this set the `populateIn` 142 | config. You must also set a `pausePopulateIn` to make sure the cache 143 | is not re-populated forever needlessly. 144 | 145 | The cache will use the `pausePopulateIn` to check whether the key has 146 | been used during that interval. The cache does this by tracking the 147 | access time of keys. For example, if you want the cache to stop populating when the 148 | key hasn't been used for a minute, set `pausePopulateIn` to `1000 * 60` ms. 149 | 150 | ```javascript 151 | var cache = cacheClient.create('nsp', { 152 | populateIn: 1000 // 1 second 153 | pausePopulateIn: 1000 * 60 // 1 minute 154 | }); 155 | ``` 156 | 157 | *Note:* this feature will work even with disruptions to the service, as the burden 158 | of determining which keys need to be re-populated is on the store (e.g., in the Redis store this 159 | is done using a combination of keyspace events and expiring keys). 160 | 161 | 162 | ### API 163 | 164 | #### Distribucache 165 | 166 | - `createClient(store, config)` 167 | 168 | Possible `config` values below. 169 | ``` 170 | {String} [config.namespace] 171 | {Boolean} [config.optimizeForSmallValues] defaults to false 172 | {Boolean} [config.optimizeForBuffers] defaults to false 173 | {String} [config.expiresIn] in ms 174 | {String} [config.staleIn] in ms 175 | {Function} [config.populate] 176 | {Number} [config.populateIn] in ms, defaults to 30sec 177 | {Number} [config.populateInAttempts] defaults to 5 178 | {Number} [config.pausePopulateIn] in ms, defaults to 30sec 179 | {Number} [config.timeoutPopulateIn] in ms 180 | {Number} [config.leaseExpiresIn] in ms 181 | {Number} [config.accessedAtThrottle] in ms, defaults to 1000 182 | ``` 183 | 184 | **Notes:** 185 | - The values above are allowed for the config and are 186 | also available to the `CacheClient#create` 187 | - See the [Optimizations docs](/docs/optimizations.md) for values 188 | that begin with `optimizeFor` 189 | 190 | #### CacheClient 191 | 192 | - `create(namespace, config)` 193 | - `namespace` is a `String` that will identify the particular cache. 194 | It is good practice to add a version to the namespace in order to 195 | make sure that when you change the interface, you will not get 196 | older cached objects (with a possibly different signature). 197 | For example: `create('articles:v1')`. 198 | - `config` is an `Object`. See the global config above for all 199 | of the possible values. 200 | 201 | ### More 202 | 203 | - [Events](/docs/events.md) 204 | - [Optimizations](/docs/optimizations.md) 205 | - [Human-readable Time Intervals](/docs/timeIntervals.md) 206 | - [Debugging](/docs/debugging.md) 207 | 208 | ## License 209 | 210 | [MIT](/LICENSE) 211 | -------------------------------------------------------------------------------- /docs/assets/img/console-logger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dowjones/distribucache/ad79caee277771a6e49ee4a98c4983480cd89945/docs/assets/img/console-logger.png -------------------------------------------------------------------------------- /docs/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | To output all events emitted by the library to the stdout / stderr, 4 | use [Distribucache Console Logger](https://github.com/dowjones/distribucache-console-logger). 5 | 6 | Here's what the output looks like: 7 | 8 |  9 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | ### CacheClient-emitted Events 4 | 5 | Name | Arguments | Emitted 6 | -----|-----------|-------- 7 | `create` | `cache, namespace` | when the `CacheClient#create` method is called 8 | `error` | `err, namespace` | on errors 9 | 10 | The `CacheClient` `error` event is propagated from the `Cache`s created by the client. 11 | 12 | 13 | ### Cache-emitted Events 14 | 15 | The events below are emitted by the `Cache` created by 16 | the `create` method of the `CacheClient`. 17 | 18 | Name | Arguments | Emitted | Followed by 19 | -----|-----------|---------|------------- 20 | `get:before` | `key`| before the datastore is called to get a value | none 21 | `get:stale` | `key` | when an element exceeds its `staleIn` time | `populate` 22 | `get:expire` | `key` | when an element exceeds its `expireIn` time | `del` 23 | `get:hit` | `key` | when an element exceeds its `expireIn` time | none 24 | `get:miss` | `key` | when an element is not in the cache | `populate` 25 | `get:after` | `key, elapsedTimeInMs` | after the datastore returns a value or an error | none 26 | `get:error` | `err, key` | when a datastore returns an error | none 27 | | | | | 28 | | | | | 29 | `set:before` | `key, value` | before the datastore is called to set a value | none 30 | `set:identical` | `key, value` | when a value that is being set is identical to the one in the datastore. | none 31 | `set:after` | `key, value, elapsedTimeInMs` | after the datastore is done setting a value | none 32 | `set:error` | `err, key, value` | when a datastore returns an error | none 33 | | | | | 34 | | | | | 35 | `del:before` | `key` | before the datastore is called to delete a value | none 36 | `del:after` | `key, elapsedTimeInMs` | after the datastore is done deleting an element | none 37 | `del:error` | `err, key` | when a datastore returns an error | none 38 | | | | | 39 | | | | | 40 | `populate:before` | `key` | before distribucache attempts to populate a value | `set` 41 | `populate:after` | `key, elapsedTimeInMs` | after distribucahce populates a value or returns an error | none 42 | `populate:error` | `err, key` | when a datastore returns an error | none 43 | | | | | 44 | | | | | 45 | `populateIn:before` | `key` | before distribucache attempts to populate a value (on the `populateIn` interval) | `populate` 46 | `populateIn:pause` | `key` | when the cache hasn't been re-populated within the `pausePopulateIn` time | none 47 | `populateIn:maxAttempts` | `key` | when the cache reached the maximum number of attempts to populate (as configured by `populateInAttempts`) | none 48 | `populateIn:after` | `key, elapsedTimeInMs` | after distribucache is done setting a value or returns an error | none 49 | `populateIn:error` | `err, key` | when a datastore returns an error | none 50 | 51 | 52 | One use-case for the events is debugging. See the [Debugging docs](/docs/debugging.md) 53 | for more info. 54 | -------------------------------------------------------------------------------- /docs/optimizations.md: -------------------------------------------------------------------------------- 1 | # Optimizations 2 | 3 | ### Stored Value Size Optimization 4 | 5 | The default assumption for this cache is that the values stored will be large. 6 | Thus, unnecessarily storing a value identical to the one that is already in 7 | the cache should be avoided, even at some cost. 8 | 9 | When a value is set into the cache, an md5 hash of the value is stored along 10 | with it. On subsequent `set` calls, first the hash is retrieved from the cache, 11 | and if it is identical to the hash of the new value, the new value is not 12 | sent to the cache. Thus, for the price of an additional call to the 13 | datastore and a few extra CPU cycles for the md5 checksum the cache makes 14 | sure that the large value does not get (un)marshalled and transmitted to 15 | the datastore. 16 | 17 | If the values that you intend to store are small (say, < 0.1 KB; the hash itself is 16 bytes), 18 | it may not make sense to have the extra call. Thus, you may want to disable 19 | this feature in that case. To do so, set the `optimizeForSmallValues` 20 | config parameter to `true`: 21 | 22 | ```javascript 23 | var cache = cacheClient.create('nsp', { 24 | optimizeForSmallValues: true 25 | }); 26 | ``` 27 | 28 | 29 | ### Optimization For Caching Buffers 30 | 31 | By default the library is configured to store objects. Distribucache marshalls 32 | the objects to a string (via `JSON.stringify`) and stores it in a datastore. 33 | When retrieving an object from a store distribucache will unmarshall the string 34 | (via `JSON.parse`) and return the object to the client. This works well for objects, 35 | but is not optimal for storing `Buffer`s, as said marshalling has memory and CPU overhead. 36 | To minimize that overhead distribucache has an `optimizeForBuffers` configuration option. 37 | 38 | With the `distribucache-redis-store` for example, the full path an object takes may be: 39 | `Buffer (Redis) -> String (Redis) -> Object (Distribucache) -> String (App) -> Buffer (Http)` 40 | 41 | When `optimizeForBuffers: true` is enabled, on the other hand, the path will be: 42 | `Buffer (Redis) -> [same] Buffer (Distribucache) -> [same] Buffer (App) -> [same] Buffer (Http)`, 43 | thereby avoiding taking up the extra memory by the `String` / `Object` representations 44 | and also avoiding the CPU overhead of converting and garbage-collecting. 45 | 46 | ```javascript 47 | var cache = cacheClient.create('nsp', { 48 | optimizeForBuffers: true 49 | }); 50 | ``` 51 | 52 | Once set, `cache.get()` will return a `Buffer` as the value. 53 | -------------------------------------------------------------------------------- /docs/timeIntervals.md: -------------------------------------------------------------------------------- 1 | # Human-readable Time Intervals 2 | 3 | The time intervals in this library can be provided as a `number` 4 | in milliseconds **or** as a human-readable time interval. 5 | 6 | Below are a few examples: 7 | 8 | - `1 ms` 9 | - `5 days` 10 | - `3 minutes` 11 | - `10 hours` 12 | - `30 seconds` 13 | 14 | There are also a few supported abbreviations (either can be used): 15 | 16 | - `ms` 17 | - `sec` -> `second` 18 | - `min` -> `minute` 19 | -------------------------------------------------------------------------------- /examples/populate.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | var distribucache = require('../lib'), 4 | //memoryStore = require('distribucache-memory-store'), 5 | //store = memoryStore(), 6 | redisStore = require('distribucache-redis-store'), 7 | logEvents = require('distribucache-console-logger'), 8 | store = redisStore({namespace: 'ex', host: 'localhost', port: 6379}), 9 | cacheClient = distribucache.createClient(store), 10 | cache; 11 | 12 | cache = cacheClient.create('randomness', { 13 | staleIn: '10 sec', 14 | populateIn: '5 sec', 15 | pausePopulateIn: '1 min', 16 | populate: function (key, cb) { 17 | setTimeout(function () { 18 | var value = Math.round(Math.random() * 1000); 19 | console.log('[client] populating with:', value); 20 | cb(null, value); 21 | }, 250); 22 | } 23 | }); 24 | 25 | logEvents(cache); 26 | 27 | function doIt() { 28 | var t = Date.now(); 29 | cache.get('k8', function (err, value) { 30 | if (err) return console.error('[client] ', err); 31 | console.log('[client] got `%j` (type: %s) in %dms', 32 | value, typeof value, Date.now() - t); 33 | }); 34 | } 35 | 36 | setInterval(doIt, 2000); 37 | doIt(); 38 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | var http = require('http'), 4 | dcache = require('../lib'), 5 | dstore = require('distribucache-redis-store'), 6 | //dstore = require('distribucache-memory-store'), 7 | cacheClient = dcache.createClient(dstore()), 8 | server, cache; 9 | 10 | cache = cacheClient.create('page', { 11 | staleIn: '5 sec', 12 | populate: generatePage, 13 | optimizeForBuffers: true 14 | }); 15 | 16 | server = http.createServer(function (req, res) { 17 | cache.get('home', function (err, page) { 18 | if (err) { 19 | res.writeHead(500); 20 | res.end(err.message); 21 | return; 22 | } 23 | 24 | res.writeHead(200); 25 | res.end(page); 26 | }); 27 | }); 28 | 29 | function generatePage(pageId, cb) { 30 | console.log('[page] generating...'); 31 | 32 | setTimeout(function () { 33 | cb(null, new Buffer('
Hello world!
')); 34 | }, 300); 35 | } 36 | 37 | server.listen(2000, function () { 38 | console.log('on :2000'); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/two_caches.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | var distribucache = require('../lib'), 4 | memoryStore = require('distribucache-memory-store'), 5 | store = memoryStore(), 6 | //redisStore = require('distribucache-redis-store'), 7 | //store = redisStore({namespace: 'ex', host: 'localhost', port: 6379}), 8 | cacheClient = distribucache.createClient(store), 9 | numCache, strCache; 10 | 11 | numCache = cacheClient.create('num:v1', { 12 | populateIn: '10 sec', 13 | pausePopulateIn: '1 min', 14 | populate: function (key, cb) { 15 | var num = Math.floor(Math.random() * 10); 16 | console.log('[populate] NUM', num); 17 | cb(null, num); 18 | } 19 | }); 20 | 21 | strCache = cacheClient.create('str:v1', { 22 | populateIn: '3 sec', 23 | pausePopulateIn: '1 min', 24 | populate: function (key, cb) { 25 | var alphanum = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 26 | word = ''; 27 | for (var i = 0; i < 5; i++) { 28 | word += alphanum.charAt(Math.floor( 29 | Math.random() * alphanum.length)); 30 | } 31 | console.log('[populate] STRING', word); 32 | cb(null, word); 33 | } 34 | }); 35 | 36 | function main() { 37 | numCache.get('k', function (err, v) { 38 | if (err) return console.error('[client]', err); 39 | if (typeof v !== 'number') throw new Error('not number'); 40 | console.log('[client]', v); 41 | }); 42 | 43 | strCache.get('k', function (err, v) { 44 | if (err) return console.error('[client]', err); 45 | if (typeof v !== 'string') throw new Error('not string'); 46 | console.log('[client]', v); 47 | }); 48 | } 49 | 50 | setInterval(main, 2000); 51 | main(); 52 | -------------------------------------------------------------------------------- /lib/Cache.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('eventemitter2').EventEmitter2, 2 | inherits = require('util').inherits; 3 | 4 | module.exports = Cache; 5 | 6 | /** 7 | * Basic Store-backed cache 8 | */ 9 | 10 | function Cache(store) { 11 | EventEmitter.call(this); 12 | this._store = store; 13 | } 14 | 15 | inherits(Cache, EventEmitter); 16 | 17 | /** 18 | * Get key from cache 19 | * 20 | * @param {String} key 21 | * @param {Function} cb 22 | */ 23 | 24 | Cache.prototype.get = function (key, cb) { 25 | this._store.getValue(key, cb); 26 | }; 27 | 28 | /** 29 | * Set a new object into the cache 30 | * 31 | * @param {String} key 32 | * @param {Object} value 33 | * @param {Function} cb 34 | */ 35 | 36 | Cache.prototype.set = function (key, value, cb) { 37 | this._store.setValue(key, value, cb); 38 | }; 39 | 40 | /** 41 | * Remove key from cache 42 | * 43 | * @param {String} key 44 | * @param {Function} cb 45 | */ 46 | 47 | Cache.prototype.del = function (key, cb) { 48 | this._store.del(key, cb); 49 | }; 50 | 51 | /** 52 | * Get primary store 53 | * 54 | * @protected 55 | * @returns {Store} 56 | */ 57 | 58 | Cache.prototype._getStore = function () { 59 | return this._store; 60 | }; 61 | -------------------------------------------------------------------------------- /lib/CacheClient.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('eventemitter2').EventEmitter2, 2 | inherits = require('util').inherits, 3 | assert = require('assert'), 4 | assign = require('lodash/object/assign'), 5 | requireDir = require('require-directory'), 6 | StoreFacade = require('./StoreFacade'), 7 | Cache = require('./Cache'), 8 | deco = requireDir(module, './decorators'), 9 | util = require('./util'); 10 | 11 | module.exports = CacheClient; 12 | 13 | /** 14 | * Create a new cache client, 15 | * decorated with the desired features, 16 | * based on the provided config. 17 | * 18 | * @constructor 19 | * @param {Store} store 20 | * @param {Object} [config] 21 | * 22 | * @param {Boolean} [config.isPreconfigured] defaults to false 23 | * 24 | * @param {String} [config.host] defaults to 'localhost' 25 | * @param {Number} [config.port] defaults to 6379 26 | * @param {String} [config.password] 27 | * @param {String} [config.namespace] 28 | * 29 | * @param {Boolean} [config.optimizeForSmallValues] defaults to false 30 | * @param {Boolean} [config.optimizeForBuffers] defaults to false 31 | * 32 | * @param {String} [config.expiresIn] in ms 33 | * @param {String} [config.staleIn] in ms 34 | * 35 | * @param {Function} [config.populate] 36 | * 37 | * @param {Number} [config.populateIn] in ms, defaults to 30sec 38 | * @param {Number} [config.populateInAttempts] defaults to 5 39 | * @param {Number} [config.pausePopulateIn] in ms 40 | * @param {Number} [config.leaseExpiresIn] in ms 41 | * @param {Number} [config.timeoutPopulateIn] in ms 42 | * @param {Number} [config.accessedAtThrottle] in ms 43 | * @param {Number} [config.namespace] 44 | */ 45 | 46 | function CacheClient(store, config) { 47 | EventEmitter.call(this); 48 | 49 | this._store = store; 50 | this._config = config || {}; 51 | 52 | util.propagateEvent(this._store, this, 'error'); 53 | this.on('error', unhandledErrorListener); 54 | } 55 | 56 | inherits(CacheClient, EventEmitter); 57 | 58 | /** 59 | * Create a new Cache 60 | * 61 | * @param {String} namespace 62 | * @param {Object} config @see CacheClient constructor 63 | */ 64 | 65 | CacheClient.prototype.create = function (namespace, config) { 66 | var store, cache; 67 | 68 | assert(namespace, 'namespace must be set'); 69 | namespace = util.createNamespace(this._config.namespace, namespace); 70 | config = assign({}, this._config, config); 71 | 72 | store = new StoreFacade(this._store, namespace); 73 | cache = new Cache(store); 74 | 75 | if (!config.optimizeForSmallValues) { 76 | cache = new deco.OnlySetChangedDecorator(cache); 77 | } 78 | 79 | if (!config.optimizeForBuffers) { 80 | cache = new deco.MarshallDecorator(cache); 81 | } 82 | 83 | if (config.expiresIn || config.staleIn) { 84 | cache = new deco.ExpiresDecorator(cache, { 85 | expiresIn: config.expiresIn, 86 | staleIn: config.staleIn 87 | }); 88 | } 89 | 90 | cache = new deco.EventDecorator(cache); 91 | 92 | if (config.populate) { 93 | cache = new deco.PopulateDecorator(cache, { 94 | populate: config.populate, 95 | timeoutPopulateIn: config.timeoutPopulateIn, 96 | leaseExpiresIn: config.leaseExpiresIn 97 | }); 98 | 99 | if (config.populateIn) { 100 | cache = new deco.PopulateInDecorator(cache, { 101 | populateIn: config.populateIn, 102 | populateInAttempts: config.populateInAttempts, 103 | pausePopulateIn: config.pausePopulateIn, 104 | accessedAtThrottle: config.accessedAtThrottle 105 | }); 106 | } 107 | } 108 | 109 | cache = new deco.PromiseDecorator(cache); 110 | 111 | util.propagateEvents(cache, this, ['error'], namespace); 112 | this.emit('create', cache, namespace); 113 | 114 | return cache; 115 | }; 116 | 117 | /** 118 | * Wrap EventEmitter#on in order to remove the 119 | * default unhandled-error listener. 120 | * 121 | * @param {String} type 122 | * @param {Function} listener 123 | */ 124 | 125 | CacheClient.prototype.on = function (type, listener) { 126 | if (type === 'error' && listener !== unhandledErrorListener) { 127 | this.removeListener(type, unhandledErrorListener); 128 | } 129 | EventEmitter.prototype.on.call(this, type, listener); 130 | }; 131 | 132 | /*eslint-disable no-console */ 133 | function unhandledErrorListener(err, namespace) { 134 | console.error('[distribucache] (%s) error from an unhandled ' + 135 | '`error` event:\n', namespace, err && err.stack || err); 136 | } 137 | /*eslint-enable */ 138 | -------------------------------------------------------------------------------- /lib/StoreFacade.js: -------------------------------------------------------------------------------- 1 | module.exports = StoreFacade; 2 | 3 | function StoreFacade(store, namespace) { 4 | this._store = store; 5 | this._namespace = namespace; 6 | } 7 | 8 | StoreFacade.prototype.createLease = function (ttl) { 9 | var lease = this._store.createLease(ttl), 10 | self = this; 11 | return function (key, cb) { 12 | lease(self._toStoreKey(key), cb); 13 | }; 14 | }; 15 | 16 | StoreFacade.prototype.createTimer = function () { 17 | return this._store.createTimer(this._namespace); 18 | }; 19 | 20 | StoreFacade.prototype.on = function (eventName, cb) { 21 | this._store.on(eventName, cb); 22 | }; 23 | 24 | StoreFacade.prototype.del = function (key, cb) { 25 | this._store.del(this._toStoreKey(key), cb); 26 | }; 27 | 28 | StoreFacade.prototype.expire = function (key, ttlInMs, cb) { 29 | this._store.expire(this._toStoreKey(key), ttlInMs, cb); 30 | }; 31 | 32 | StoreFacade.prototype.getAccessedAt = function (key, cb) { 33 | this._store.getProp(this._toStoreKey(key), 'accessedAt', toNumber(cb)); 34 | }; 35 | 36 | StoreFacade.prototype.getCreatedAt = function (key, cb) { 37 | this._store.getProp(this._toStoreKey(key), 'createdAt', toNumber(cb)); 38 | }; 39 | 40 | StoreFacade.prototype.getHash = function (key, cb) { 41 | this._store.getProp(this._toStoreKey(key), 'hash', toString(cb)); 42 | }; 43 | 44 | StoreFacade.prototype.getValue = function (key, cb) { 45 | this._store.getProp(this._toStoreKey(key), 'value', cb); 46 | }; 47 | 48 | StoreFacade.prototype.setAccessedAt = function (key, date, cb) { 49 | this._store.setProp(this._toStoreKey(key), 'accessedAt', date, cb); 50 | }; 51 | 52 | StoreFacade.prototype.setCreatedAt = function (key, date, cb) { 53 | this._store.setProp(this._toStoreKey(key), 'createdAt', date, cb); 54 | }; 55 | 56 | StoreFacade.prototype.setHash = function (key, hash, cb) { 57 | this._store.setProp(this._toStoreKey(key), 'hash', hash, cb); 58 | }; 59 | 60 | StoreFacade.prototype.setValue = function (key, value, cb) { 61 | this._store.setProp(this._toStoreKey(key), 'value', value, cb); 62 | }; 63 | 64 | StoreFacade.prototype.resetPopulateInErrorCount = function (key, cb) { 65 | this._store.delProp(this._toStoreKey(key), 'populateInErrorCount', cb); 66 | }; 67 | 68 | StoreFacade.prototype.incrementPopulateInErrorCount = function (key, cb) { 69 | this._store.incrPropBy(this._toStoreKey(key), 'populateInErrorCount', 1, cb); 70 | }; 71 | 72 | /** 73 | * Get namespaced key that 74 | * will be used when accessing 75 | * the store. 76 | * 77 | * @protected 78 | * @returns {String} 79 | */ 80 | 81 | StoreFacade.prototype._toStoreKey = function (key) { 82 | return this._namespace + ':' + key; 83 | }; 84 | 85 | function toNumber(cb) { 86 | return function (err, value) { 87 | if (err) return cb(err); 88 | if (typeof value === 'number') return cb(null, value); 89 | cb(null, value && parseInt(value, 10)); 90 | }; 91 | } 92 | 93 | function toString(cb) { 94 | return function (err, value) { 95 | if (err) return cb(err); 96 | if (!value || typeof value === 'string') return cb(null, value); 97 | return cb(null, value.toString()); 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /lib/decorators/BaseDecorator.js: -------------------------------------------------------------------------------- 1 | var joi = require('joi'), 2 | intervalToMs = require('../util').intervalToMs, 3 | TIME_SUFFIX_RE = /.*(?:In)$/; 4 | 5 | module.exports = BaseDecorator; 6 | 7 | /** 8 | * The root of the Decorator tree 9 | * @constructor 10 | */ 11 | 12 | function BaseDecorator(cache, config, configSchema) { 13 | this._cache = cache; 14 | 15 | if (config) { 16 | this._configSchema = configSchema; 17 | this._config = this._humanTimeIntervalToMs(config); 18 | this._config = this._validateConfig(configSchema, this._config); 19 | } 20 | 21 | // substitute cb, for cases 22 | // when you don't need a callback, but 23 | // don't want to loose the error. 24 | this._emitError = function (err) { 25 | if (err) this.emit('error', err); 26 | }.bind(this); 27 | } 28 | 29 | [ 30 | // BaseCache 31 | 'get', 'set', 'del', '_getStore', 32 | 33 | // EventEmitter 34 | 'addListener', 'on', 'once', 35 | 'removeListener', 'removeAllListeners', 36 | 'setMaxListeners', 'listeners', 'emit', 37 | 38 | // EventEmitter2 39 | 'onAny', 'offAny' 40 | ].forEach(function (methodName) { 41 | BaseDecorator.prototype[methodName] = function () { 42 | return this._cache[methodName].apply(this._cache, arguments); 43 | }; 44 | }); 45 | 46 | /** 47 | * Validate the cache via the provided schema. 48 | * Throw if invalid. Return the value with defaults 49 | * if valid. 50 | * 51 | * @private 52 | * @param {Object} schema 53 | * @param {Object} config 54 | */ 55 | 56 | BaseDecorator.prototype._validateConfig = function (schema, config) { 57 | var validation = joi.validate(config, schema); 58 | if (validation.error) throw validation.error; 59 | return validation.value; 60 | }; 61 | 62 | /** 63 | * Convert all configs with a possible human time interval 64 | * to a millisecond interval. 65 | * 66 | * @param {Object} config 67 | * @reurn {Object} config 68 | */ 69 | 70 | BaseDecorator.prototype._humanTimeIntervalToMs = function (config) { 71 | Object.keys(config).forEach(function (key) { 72 | var value = config[key]; 73 | if (!TIME_SUFFIX_RE.test(key) || !value) return; 74 | config[key] = intervalToMs(value); 75 | }); 76 | return config; 77 | }; 78 | -------------------------------------------------------------------------------- /lib/decorators/EventDecorator.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits, 2 | BaseDecorator = require('./BaseDecorator'), 3 | eventWrap = require('../util').eventWrap; 4 | 5 | module.exports = EventDecorator; 6 | 7 | /** 8 | * Decorator which is responsible 9 | * for wrapping the cache functions 10 | * in :before / :after events. 11 | * 12 | * @param {Cache} cache 13 | * @param {Object} [config] 14 | */ 15 | 16 | function EventDecorator(cache) { 17 | BaseDecorator.call(this, cache); 18 | } 19 | 20 | inherits(EventDecorator, BaseDecorator); 21 | 22 | /** 23 | * Get and wrap with emit 24 | * 25 | * @param {String} key 26 | * @param {String} value 27 | * @param {Function} cb 28 | */ 29 | 30 | EventDecorator.prototype.get = function (key, cb) { 31 | var gotValue = function (err, value) { 32 | if (err) return cb(err); 33 | if (value === null) this.emit('get:miss', key); 34 | else this.emit('get:hit', key); 35 | cb(null, value); 36 | }; 37 | 38 | gotValue = eventWrap(this, 'get', [key], gotValue); 39 | 40 | this._cache.get(key, gotValue); 41 | }; 42 | 43 | /** 44 | * Set and wrap with emit 45 | * 46 | * @param {String} key 47 | * @param {String} value 48 | * @param {Function} cb 49 | */ 50 | 51 | EventDecorator.prototype.set = function (key, value, cb) { 52 | cb = eventWrap(this, 'set', [key, value], cb); 53 | this._cache.set(key, value, cb); 54 | }; 55 | 56 | /** 57 | * Del and wrap with emit 58 | * 59 | * @param {String} key 60 | * @param {String} value 61 | * @param {Function} cb 62 | */ 63 | 64 | EventDecorator.prototype.del = function (key, cb) { 65 | cb = eventWrap(this, 'del', [key], cb); 66 | this._cache.del(key, cb); 67 | }; 68 | -------------------------------------------------------------------------------- /lib/decorators/ExpiresDecorator.js: -------------------------------------------------------------------------------- 1 | var joi = require('joi'), 2 | async = require('async'), 3 | inherits = require('util').inherits, 4 | eventWrap = require('../util').eventWrap, 5 | BaseDecorator = require('./BaseDecorator'); 6 | 7 | module.exports = ExpiresDecorator; 8 | 9 | /** 10 | * Expire Redis-backed cache 11 | * 12 | * @param {Cache} cache 13 | * @param {Object} [config] 14 | * @param {String} [config.expiresIn] in ms 15 | * @param {String} [config.staleIn] in ms 16 | */ 17 | 18 | function ExpiresDecorator(cache, config) { 19 | BaseDecorator.call(this, cache, config, joi.object().keys({ 20 | expiresIn: joi.number().integer().min(0).default(Infinity), 21 | staleIn: joi.number().integer().min(0).default(Infinity) 22 | })); 23 | this.on('set:after', this.setCreatedAt.bind(this)); 24 | this._store = this._getStore(); 25 | } 26 | 27 | inherits(ExpiresDecorator, BaseDecorator); 28 | 29 | /** 30 | * Set the createdAt timestamp for a given key. 31 | * 32 | * @param {String} key 33 | */ 34 | 35 | ExpiresDecorator.prototype.setCreatedAt = function (key) { 36 | this._store.setCreatedAt(key, Date.now(), this._emitError); 37 | }; 38 | 39 | /** 40 | * Get a value from the cache. 41 | * 42 | * If the returned value has expired, 43 | * null will be returned, and the item 44 | * will be deleted from the db. 45 | * 46 | * @param {String} key 47 | * @param {Function} cb 48 | */ 49 | 50 | ExpiresDecorator.prototype.get = function (key, cb) { 51 | var cache = this._cache, 52 | self = this; 53 | 54 | async.waterfall([ 55 | this._store.getCreatedAt.bind(this._store, key), 56 | 57 | function maybeGet(createdAt, icb) { 58 | var isExpired, isStale; 59 | 60 | if (createdAt === null) { 61 | return icb(null, null); 62 | } 63 | 64 | isExpired = createdAt + self._config.expiresIn < Date.now(); 65 | isStale = createdAt + self._config.staleIn < Date.now(); 66 | 67 | if (isExpired) { 68 | self.emit('get:expire', key); 69 | self.del(key, eventWrap(self, 'del', [key], self._emitError)); 70 | return icb(null, null); 71 | } 72 | 73 | if (isStale) { 74 | self.emit('get:stale', key); 75 | } 76 | 77 | cache.get(key, icb); 78 | } 79 | ], cb); 80 | }; 81 | -------------------------------------------------------------------------------- /lib/decorators/MarshallDecorator.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits, 2 | MarshallingError = require('common-errors').helpers 3 | .generateClass('MarshallingError'), 4 | BaseDecorator = require('./BaseDecorator'); 5 | 6 | module.exports = MarshallDecorator; 7 | 8 | /** 9 | * Decorator which takes care of marshalling 10 | * the data to / from the cache. 11 | * 12 | * @param {Cache} cache 13 | * @param {Object} [config] 14 | */ 15 | 16 | function MarshallDecorator(cache) { 17 | BaseDecorator.call(this, cache); 18 | } 19 | 20 | inherits(MarshallDecorator, BaseDecorator); 21 | 22 | /** 23 | * Unmarshall the data and get it 24 | * 25 | * @param {String} key 26 | * @param {String} value 27 | * @param {Function} cb 28 | */ 29 | 30 | MarshallDecorator.prototype.get = function (key, cb) { 31 | this._cache.get(key, unmarshallWrapper(cb)); 32 | }; 33 | 34 | /** 35 | * Marshall the value and set it. 36 | * 37 | * @param {String} key 38 | * @param {String} value 39 | * @param {Function} cb 40 | */ 41 | 42 | MarshallDecorator.prototype.set = function (key, value, cb) { 43 | this._cache.set(key, marshall(value), cb); 44 | }; 45 | 46 | /** 47 | * Marshall the value to a string 48 | * 49 | * @param {Object} value 50 | * @return {String} 51 | */ 52 | 53 | function marshall(value) { 54 | if (typeof value === 'undefined') value = null; 55 | return JSON.stringify(value); 56 | } 57 | 58 | /** 59 | * Unmarshall a previously marshalled value 60 | * back to its original type / state. 61 | * 62 | * @param {String} value - marshalled 63 | * @return {Object} 64 | */ 65 | 66 | function unmarshall(value) { 67 | var err; 68 | try { 69 | value = JSON.parse(value); 70 | } catch (e) { 71 | value = (typeof value === 'string') ? value.substr(0, 50) : ''; 72 | err = new MarshallingError('Failed to marshall value: ' + value, e); 73 | } 74 | return {error: err, value: value}; 75 | } 76 | 77 | /** 78 | * Helper for unmarshalling values 79 | * provided to a callback. 80 | * 81 | * @param {Object} cb 82 | * @return {Function} (err, marshalledValue) 83 | */ 84 | 85 | function unmarshallWrapper (cb) { 86 | return function (err, value) { 87 | if (err) return cb(err); 88 | var m = unmarshall(value); 89 | cb(m.error, m.value); 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /lib/decorators/OnlySetChangedDecorator.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | inherits = require('util').inherits, 3 | BaseDecorator = require('./BaseDecorator'), 4 | createHash = require('../util').createHash; 5 | 6 | module.exports = OnlySetChangedDecorator; 7 | 8 | /** 9 | * Decorator which only actually update the value 10 | * if the value has changed. 11 | * 12 | * @param {Cache} cache 13 | * @param {Object} [config] 14 | */ 15 | 16 | function OnlySetChangedDecorator(cache) { 17 | BaseDecorator.call(this, cache); 18 | } 19 | 20 | inherits(OnlySetChangedDecorator, BaseDecorator); 21 | 22 | /** 23 | * 24 | * @param {String} key 25 | * @param {String} value 26 | * @param {Function} cb 27 | */ 28 | 29 | OnlySetChangedDecorator.prototype.set = function (key, value, cb) { 30 | var cache = this._cache, 31 | store = cache._getStore(), 32 | self = this; 33 | 34 | async.waterfall([ 35 | store.getHash.bind(store, key), 36 | 37 | function maybeSet(existingHash, icb) { 38 | var newHash = createHash(value); 39 | 40 | // only emit the `set` event (which happens before the set) 41 | // don't set the actual value 42 | if (newHash === existingHash) { 43 | self.emit('set:identical', key); 44 | return icb(null); 45 | } 46 | 47 | function setHash(err) { 48 | if (err) return icb(err); 49 | store.setHash(key, newHash, self._emitError); 50 | icb(null); 51 | } 52 | 53 | cache.set(key, value, setHash); 54 | } 55 | ], cb); 56 | }; 57 | -------------------------------------------------------------------------------- /lib/decorators/PopulateDecorator.js: -------------------------------------------------------------------------------- 1 | var joi = require('joi'), 2 | async = require('async'), 3 | inherits = require('util').inherits, 4 | isPromise = require('is-promise'), 5 | BaseDecorator = require('./BaseDecorator'), 6 | completeWithin = require('../util').completeWithin, 7 | eventWrap = require('../util').eventWrap; 8 | 9 | 10 | module.exports = PopulateDecorator; 11 | 12 | /** 13 | * Self-populating data-store independent cache 14 | * 15 | * @param {Cache} cache 16 | * @param {Object} config 17 | * @param {Function} config.populate 18 | * @param {Number} [config.leaseExpiresIn] in ms 19 | * @param {Number} [config.timeoutPopulateIn] in ms, defaults to 30sec 20 | */ 21 | 22 | function PopulateDecorator(cache, config) { 23 | BaseDecorator.call(this, cache, config, joi.object().keys({ 24 | populate: joi.func().required(), 25 | timeoutPopulateIn: joi.number().integer().default(1000 * 30), 26 | leaseExpiresIn: joi.number().integer() 27 | })); 28 | this._store = this._getStore(); 29 | this._lease = this._store.createLease( 30 | this._config.leaseExpiresIn || this._config.timeoutPopulateIn + 1000); 31 | this.on('get:stale', this._onStaleEvent.bind(this)); 32 | } 33 | 34 | inherits(PopulateDecorator, BaseDecorator); 35 | 36 | /** 37 | * Get a value from the cache. 38 | * 39 | * @param {String} key 40 | * @param {Function} cb 41 | */ 42 | 43 | PopulateDecorator.prototype.get = function (key, cb) { 44 | var self = this; 45 | async.waterfall([ 46 | this._cache.get.bind(this._cache, key), 47 | function returnOrPopulate(value, cbi) { 48 | if (value !== null) return cbi(null, value); 49 | self.populate(key, cbi); 50 | } 51 | ], cb); 52 | }; 53 | 54 | /** 55 | * Populate a value into the cache 56 | * 57 | * @param {String} key 58 | * @param {Function} cb 59 | */ 60 | 61 | PopulateDecorator.prototype.populate = function (key, cb) { 62 | var self = this, 63 | value; 64 | 65 | cb = eventWrap(this, 'populate', [key], cb); 66 | 67 | async.waterfall([ 68 | function populate(cbi) { 69 | cbi = completeWithin(self._config.timeoutPopulateIn, cbi); 70 | try { 71 | var p = self._config.populate(key, cbi); 72 | if (isPromise(p)) p.catch(cbi).then(cbi.bind(null, null)); 73 | } catch (e) { 74 | e.message = 'populate threw an error; cause: ' + e.message; 75 | cbi(e); 76 | } 77 | }, 78 | 79 | function setValue(populateValue, cbi) { 80 | value = populateValue; 81 | self.set(key, value, cbi); 82 | } 83 | ], function (err) { 84 | if (err) { 85 | err.name = 'PopulateError'; 86 | err.message = 'failed to populate key "' + key + '"; ' + 87 | 'cause: ' + err.message; 88 | return cb(err); 89 | } 90 | cb(null, value); 91 | }); 92 | }; 93 | 94 | /** 95 | * When a trigger is expired, the leasedPopulate 96 | * method is called. When this happens, a lease 97 | * is taken out to run the populate method. 98 | * 99 | * This is done to ensure that only one populate 100 | * method is run for all of the processes (as the 101 | * event will be dispatched to all). 102 | * 103 | * @private 104 | * @param {String} key 105 | */ 106 | 107 | PopulateDecorator.prototype.leasedPopulate = function (key, cb) { 108 | var self = this; 109 | 110 | function critical(error, release) { 111 | if (error) { 112 | if (error.name === 'AlreadyLeasedError') return cb(null); 113 | return cb(error); 114 | } 115 | 116 | self.populate(key, function (err, value) { 117 | release(); 118 | cb(err, value); 119 | }); 120 | } 121 | 122 | this._lease(key, critical); 123 | }; 124 | 125 | /** 126 | * Called on the `stale` event 127 | * 128 | * @private 129 | * @param {String} key 130 | */ 131 | 132 | PopulateDecorator.prototype._onStaleEvent = function (key) { 133 | this.leasedPopulate(key, this._emitError); 134 | }; 135 | -------------------------------------------------------------------------------- /lib/decorators/PopulateInDecorator.js: -------------------------------------------------------------------------------- 1 | var joi = require('joi'), 2 | async = require('async'), 3 | throttle = require('lodash/function/throttle'), 4 | inherits = require('util').inherits, 5 | BaseDecorator = require('./BaseDecorator'), 6 | eventWrap = require('../util').eventWrap; 7 | 8 | module.exports = PopulateInDecorator; 9 | 10 | /** 11 | * Auto-populating Redis-backed cache 12 | * 13 | * @constructor 14 | * @param {Cache} cache 15 | * @param {Object} [config] 16 | * @param {String} [config.populateIn] 17 | * @param {String} [config.populateInAttempts] 18 | * @param {String} [config.pausePopulateIn] 19 | * @param {String} [config.accessedAtThrottle] 20 | */ 21 | 22 | function PopulateInDecorator(cache, config) { 23 | BaseDecorator.call(this, cache, config, joi.object().keys({ 24 | populateIn: joi.number().integer().min(500).required(), 25 | populateInAttempts: joi.number().integer().default(5), 26 | pausePopulateIn: joi.number().integer().required(), 27 | accessedAtThrottle: joi.number().integer().default(1000) 28 | })); 29 | 30 | this._store = this._getStore(); 31 | this._timer = this._store.createTimer(); 32 | this._timer.on('error', this._emitError); 33 | this._timer.on('timeout', this._onTimeout.bind(this)); 34 | 35 | this.on('set:after', this.setTimeout.bind(this)); 36 | this.on('get:after', throttle(this.setAccessedAt.bind(this), 37 | this._config.accessedAtThrottle)); 38 | } 39 | 40 | inherits(PopulateInDecorator, BaseDecorator); 41 | 42 | /** 43 | * Set the accessedAt timestamp, to mark 44 | * the last access timestamp. 45 | * 46 | * @param {String} key 47 | */ 48 | 49 | PopulateInDecorator.prototype.setAccessedAt = function (key) { 50 | this._store.setAccessedAt(key, Date.now(), this._emitError); 51 | }; 52 | 53 | /** 54 | * Set a timeout for a given key. 55 | * 56 | * After the configured `populateIn` time, the `timeout` event 57 | * will be called, executing the `leasedPopulate` method in turn. 58 | * 59 | * @param {String} key 60 | */ 61 | 62 | PopulateInDecorator.prototype.setTimeout = function (key) { 63 | this._timer.setTimeout(key, this._config.populateIn, 64 | this._emitError); 65 | }; 66 | 67 | /** 68 | * Populates the cache 69 | * 70 | * @param {String} key 71 | * @param {Function} cb 72 | */ 73 | 74 | PopulateInDecorator.prototype.leasedPopulate = function (key, cb) { 75 | var self = this; 76 | 77 | cb = eventWrap(this, 'populateIn', [key], cb); 78 | 79 | async.waterfall([ 80 | this._store.getAccessedAt.bind(this._store, key), 81 | 82 | function (accessedAt, cbi) { 83 | if (accessedAt) { 84 | // don't populate if the cache is not used 85 | if (accessedAt + self._config.pausePopulateIn < Date.now()) { 86 | self.emit('populateIn:pause', key); 87 | return cbi(null); 88 | } 89 | } 90 | 91 | function maybeContinuePopulateIn(err) { 92 | if (!err) return self._store.resetPopulateInErrorCount(key, cbi); 93 | 94 | function maybeSetTimeout(incrErr, value) { 95 | if (incrErr) return cbi(incrErr); 96 | if (value < self._config.populateInAttempts) { 97 | self.setTimeout(key); 98 | } else { 99 | self.emit('populateIn:maxAttempts', key); 100 | } 101 | cbi(err); 102 | } 103 | 104 | self._store.incrementPopulateInErrorCount(key, maybeSetTimeout); 105 | } 106 | 107 | self._cache.leasedPopulate(key, maybeContinuePopulateIn); 108 | } 109 | ], cb); 110 | }; 111 | 112 | /** 113 | * Called when a `timeout` is emitted 114 | * 115 | * @private 116 | * @param {String} key 117 | */ 118 | 119 | PopulateInDecorator.prototype._onTimeout = function (key) { 120 | this.leasedPopulate(key, this._emitError); 121 | }; 122 | -------------------------------------------------------------------------------- /lib/decorators/PromiseDecorator.js: -------------------------------------------------------------------------------- 1 | var promisify = require('es6-promisify'), 2 | inherits = require('util').inherits, 3 | BaseDecorator = require('./BaseDecorator'); 4 | 5 | module.exports = PromiseDecorator; 6 | 7 | /** 8 | * If no callbacks are provided to 9 | * get, set or del a Promise will 10 | * be returned. 11 | */ 12 | 13 | function PromiseDecorator(cache) { 14 | BaseDecorator.call(this, cache); 15 | this.get = promisify(this.get.bind(this)); 16 | this.set = promisify(this.set.bind(this)); 17 | this.del = promisify(this.del.bind(this)); 18 | } 19 | 20 | inherits(PromiseDecorator, BaseDecorator); 21 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var CacheClient = require('./CacheClient'); 2 | 3 | exports.createClient = function (store, config) { 4 | return new CacheClient(store, config); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | once = require('lodash/function/once'), 3 | TimeoutError = require('common-errors').helpers 4 | .generateClass('TimeoutError'), 5 | HUMAN_IVAL_RE = /([\d.]+)\s?(ms|sec|min|hour|day|week|month|year)(?:ond|ute)?s?/, 6 | slice = Array.prototype.slice; 7 | 8 | /** 9 | * Create an md5 hash 10 | * 11 | * @param {String} str input 12 | * @returns {String} hash 13 | */ 14 | 15 | exports.createHash = function (str) { 16 | return crypto 17 | .createHash('md5') 18 | .update(str.toString()) 19 | .digest('hex'); 20 | }; 21 | 22 | /** 23 | * Ensure that the callback 24 | * completes within the provided 25 | * number of milliseconds. 26 | * 27 | * @param {Number} ms 28 | * @param {Function} cb 29 | */ 30 | 31 | exports.completeWithin = function (ms, cb) { 32 | var timeoutId; 33 | 34 | cb = once(cb); 35 | 36 | timeoutId = setTimeout(function () { 37 | cb(new TimeoutError('timed out after ' + ms + 'ms')); 38 | }, ms); 39 | 40 | function wrap() { 41 | clearTimeout(timeoutId); 42 | cb.apply(null, arguments); 43 | } 44 | 45 | return wrap; 46 | }; 47 | 48 | /** 49 | * Convert human-readible time-interval 50 | * to milliseconds. 51 | * 52 | * Examples: 53 | * - '1 ms' 54 | * - '5 days' 55 | * - '3 minutes' 56 | * - '10 hours' 57 | * - '30 seconds' 58 | * 59 | * Available abbreviations: 60 | * - ms 61 | * - sec -> second 62 | * - min -> minute 63 | * 64 | * @param {String} humanTime 65 | * @return {Number} ms 66 | */ 67 | 68 | exports.intervalToMs = function (humanTime) { 69 | var match, ms; 70 | if (typeof humanTime !== 'string') return humanTime; 71 | 72 | match = humanTime.match(HUMAN_IVAL_RE); 73 | if (!match) return humanTime; 74 | 75 | switch (match[2]) { 76 | case 'ms': ms = 1; break; 77 | case 'sec': ms = 1000; break; 78 | case 'min': ms = 1000 * 60; break; 79 | case 'hour': ms = 1000 * 60 * 60; break; 80 | case 'day': ms = 1000 * 60 * 60 * 24; break; 81 | case 'week': ms = 1000 * 60 * 60 * 24 * 7; break; 82 | case 'month': ms = 1000 * 60 * 60 * 24 * 30; break; 83 | case 'year': ms = 1000 * 60 * 60 * 24 * 356; break; 84 | } 85 | 86 | return parseFloat(match[1], 10) * ms; 87 | }; 88 | 89 | /** 90 | * Create a namespace provided 91 | * a list of argument strings. 92 | * 93 | * @param {String} [namespacePart...] 94 | * @return {String} 95 | */ 96 | 97 | exports.createNamespace = function () { 98 | var args = slice.call(arguments); 99 | args = args.filter(function (arg) { 100 | if (arg) return true; 101 | }); 102 | return args.join(':'); 103 | }; 104 | 105 | /** 106 | * Propagate selected events from one 107 | * EventEmitter to another. 108 | * 109 | * @param {EventEmitter} source 110 | * @param {EventEmitter} dest 111 | * @param {Array[String]} eventNames 112 | * @param {String} [sourceName] - added as the last argument 113 | * to the event, in order to be able to uniquely id the source 114 | */ 115 | 116 | exports.propagateEvents = function (source, dest, eventNames, sourceName) { 117 | eventNames.forEach(function (eventName) { 118 | exports.propagateEvent(source, dest, eventName, sourceName); 119 | }); 120 | }; 121 | 122 | /** 123 | * Propagate an event from one 124 | * EventEmitter to another. 125 | * 126 | * @param {EventEmitter} source 127 | * @param {EventEmitter} dest 128 | * @param {Array[String]} eventName 129 | * @param {String} [sourceName] - added as the last argument 130 | * to the event, in order to be able to uniquely id the source 131 | */ 132 | 133 | exports.propagateEvent = function (source, dest, eventName, sourceName) { 134 | source.on(eventName, function () { 135 | var args = slice.call(arguments); 136 | args.unshift(eventName); 137 | if (sourceName) args.push(sourceName); 138 | dest.emit.apply(dest, args); 139 | }); 140 | }; 141 | 142 | /** 143 | * Helper to call a `:before` event before a function 144 | * is called and `:after` right after the callback is 145 | * returned. 146 | * 147 | * @param {EventEmitter} emitt 148 | * @param {String} name - of the event 149 | * @param {Array} eventArgs - arguments passed to the event 150 | * @param {Function} func 151 | */ 152 | 153 | exports.eventWrap = function (emitter, name, eventArgs, func) { 154 | var before = Date.now(); 155 | emitter.emit.apply(emitter, [name + ':before'].concat(eventArgs)); 156 | return function afterWrapper(err) { 157 | var args; 158 | 159 | if (err) { 160 | args = [name + ':error', err].concat(eventArgs); 161 | emitter.emit.apply(emitter, args); 162 | } 163 | 164 | args = [name + ':after'].concat(eventArgs); 165 | args.push(Date.now() - before); 166 | emitter.emit.apply(emitter, args); 167 | 168 | func.apply(emitter, arguments); 169 | }; 170 | }; 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "distribucache", 3 | "version": "6.1.0", 4 | "description": "Store-backed Automatically Populating Cache", 5 | "keywords": [ 6 | "cache", 7 | "distributed", 8 | "distributed-cache" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/dowjones/distribucache.git" 13 | }, 14 | "author": "nemtsov@gmail.com", 15 | "main": "./lib", 16 | "scripts": { 17 | "test": "sh -c 'npm run lint && npm run test-cover && istanbul check-coverage --statements 100 --functions 100 --branches 100 --lines 100'", 18 | "lint": "eslint examples/ lib/ test/", 19 | "test-cover": "istanbul cover _mocha -- -r should --recursive", 20 | "test-watch": "mocha -r should --recursive -w -R min -t 20" 21 | }, 22 | "dependencies": { 23 | "async": "^1.3.0", 24 | "common-errors": "^0.5.1", 25 | "es6-promisify": "^3.0.0", 26 | "eventemitter2": "^0.4.14", 27 | "is-promise": "^2.1.0", 28 | "joi": "^6.5.0", 29 | "lodash": "^3.10.0", 30 | "require-directory": "^2.1.1" 31 | }, 32 | "devDependencies": { 33 | "distribucache-console-logger": "^1.0.2", 34 | "distribucache-memory-store": "^5.0.0", 35 | "distribucache-redis-store": "^6.0.1", 36 | "es6-promise": "^3.0.2", 37 | "eslint": "^0.24.0", 38 | "istanbul": "^0.3.17", 39 | "mocha": "^2.2.5", 40 | "proxyquire": "^1.6.0", 41 | "should": "^7.0.1", 42 | "sinon": "^1.15.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/Cache-test.js: -------------------------------------------------------------------------------- 1 | var Cache = require('./_all').Cache, 2 | stub = require('sinon').stub; 3 | 4 | describe('Cache', function () { 5 | var unit, store; 6 | 7 | beforeEach(function () { 8 | function noop() {} 9 | store = stub({ 10 | getValue: noop, 11 | setValue: noop, 12 | del: noop 13 | }); 14 | unit = new Cache(store); 15 | }); 16 | 17 | it('should get', function(done) { 18 | store.getValue.withArgs('k').yields(null); 19 | unit.get('k', done); 20 | }); 21 | 22 | it('should set', function(done) { 23 | store.setValue.withArgs('k', 'v').yields(null); 24 | unit.set('k', 'v', done); 25 | }); 26 | 27 | it('should del', function(done) { 28 | store.del.withArgs('k').yields(null); 29 | unit.del('k', done); 30 | }); 31 | 32 | // protected 33 | describe('_getStore', function () { 34 | it('should get store', function () { 35 | unit._getStore().getValue.should.be.type('function'); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/CacheClient-test.js: -------------------------------------------------------------------------------- 1 | var proxyquire = require('proxyquire').noCallThru(), 2 | sinon = require('sinon'), 3 | stub = sinon.stub, 4 | spy = sinon.spy; 5 | 6 | describe('CacheClient', function () { 7 | var store, CacheClient, util, unit, deco, c; 8 | 9 | beforeEach(function () { 10 | function noop() {} 11 | 12 | function Cache() {} 13 | Cache.prototype = {}; 14 | Cache.prototype.on = spy(); 15 | 16 | function Store() {} 17 | Store.prototype = {}; 18 | Store.prototype.on = spy(); 19 | 20 | util = { 21 | createNamespace: spy(), 22 | ensureKeyspaceNotifications: spy(), 23 | propagateEvents: spy(), 24 | propagateEvent: spy() 25 | }; 26 | 27 | deco = {}; 28 | deco.OnlySetChangedDecorator = spy(Cache); 29 | deco.MarshallDecorator = spy(Cache); 30 | deco.EventDecorator = spy(Cache); 31 | deco.ExpiresDecorator = spy(Cache); 32 | deco.PopulateDecorator = spy(Cache); 33 | deco.PopulateInDecorator = spy(Cache); 34 | deco.PromiseDecorator = spy(Cache); 35 | 36 | CacheClient = proxyquire('../lib/CacheClient', { 37 | './Cache': Cache, 38 | './util': util, 39 | './datastore/redis': Store, 40 | 'require-directory': function () { 41 | return deco; 42 | } 43 | }); 44 | 45 | store = stub({on: noop}); 46 | }); 47 | 48 | it('should create a preconfigured client', function () { 49 | unit = new CacheClient(store, {isPreconfigured: true}); 50 | util.ensureKeyspaceNotifications.called.should.not.be.ok(); 51 | }); 52 | 53 | describe('create', function () { 54 | beforeEach(function () { 55 | unit = new CacheClient(); 56 | }); 57 | 58 | it('should create default cache', function () { 59 | c = unit.create('n'); 60 | c.should.be.type('object'); 61 | deco.OnlySetChangedDecorator.calledOnce.should.be.ok(); 62 | }); 63 | 64 | it('should create a small-value optimized cache', function () { 65 | c = unit.create('n', {optimizeForSmallValues: true}); 66 | c.should.be.type('object'); 67 | }); 68 | 69 | it('should create a buffer optimized cache', function () { 70 | c = unit.create('n', {optimizeForBuffers: true}); 71 | c.should.be.type('object'); 72 | }); 73 | 74 | it('should use the expire deco on expiresIn', function () { 75 | c = unit.create('n', {expiresIn: 200}); 76 | c.should.be.type('object'); 77 | 78 | deco.OnlySetChangedDecorator.calledOnce.should.be.ok(); 79 | deco.ExpiresDecorator.calledOnce.should.be.ok(); 80 | }); 81 | 82 | it('should use the expire deco on staleIn', function () { 83 | c = unit.create('n', {staleIn: 200}); 84 | c.should.be.type('object'); 85 | 86 | deco.OnlySetChangedDecorator.calledOnce.should.be.ok(); 87 | deco.ExpiresDecorator.calledOnce.should.be.ok(); 88 | }); 89 | 90 | it('should use the populate deco on populate', function () { 91 | c = unit.create('n', {populate: function () {}}); 92 | c.should.be.type('object'); 93 | 94 | deco.OnlySetChangedDecorator.calledOnce.should.be.ok(); 95 | deco.PopulateDecorator.calledOnce.should.be.ok(); 96 | }); 97 | 98 | it('should use the populateIn deco on populateIn & populate', function () { 99 | c = unit.create('n', {populateIn: 200, populate: function () {}}); 100 | c.should.be.type('object'); 101 | 102 | deco.OnlySetChangedDecorator.calledOnce.should.be.ok(); 103 | deco.PopulateDecorator.calledOnce.should.be.ok(); 104 | deco.PopulateInDecorator.calledOnce.should.be.ok(); 105 | }); 106 | 107 | it('should not use populateIn without populate', function () { 108 | c = unit.create('n', {populateIn: 200}); 109 | c.should.be.type('object'); 110 | 111 | deco.OnlySetChangedDecorator.calledOnce.should.be.ok(); 112 | deco.PopulateInDecorator.calledOnce.should.not.be.ok(); 113 | }); 114 | 115 | describe('error event propagation', function () { 116 | it('should retransmit error events by default', function () { 117 | c = unit.create('n'); 118 | util.propagateEvents.calledOnce.should.be.ok(); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('unhandledErrorListener', function () { 124 | var UNHANDLED_RE = /error from an unhandled/; 125 | 126 | beforeEach(function () { 127 | unit = new CacheClient(); 128 | }); 129 | 130 | it('should call the unhandleErrorListener when `error` is unhandled', function (done) { 131 | onStderr(function (chunk) { 132 | chunk.should.match(UNHANDLED_RE); 133 | done(); 134 | }); 135 | unit.emit('error', 'test unhandled'); 136 | }); 137 | 138 | it('should not call the unhandleErrorListener when `error` is handled', function (done) { 139 | var onerr = onStderr(function (chunk) { 140 | if (UNHANDLED_RE.test(chunk)) { 141 | throw new Error('unhandled error listener in use'); 142 | } 143 | }); 144 | unit.on('error', function () { 145 | onerr.reset(); 146 | done(); 147 | }); 148 | unit.emit('error', 'test unhandled'); 149 | }); 150 | }); 151 | }); 152 | 153 | /** 154 | * Note: I don't like this complexity for listening to stderr, but 155 | * I'd rather have the complexity in the test then in the lib. 156 | * 157 | * Please propose a better way, as long as it does not make 158 | * the implementation more complex. 159 | */ 160 | 161 | function onStderr(cb) { 162 | var write = process.stderr.write; 163 | process.stderr.write = function (chunk) { 164 | process.stderr.write = write; 165 | cb(chunk); 166 | }; 167 | return { 168 | reset: function () { 169 | process.stderr.write = write; 170 | } 171 | }; 172 | } 173 | -------------------------------------------------------------------------------- /test/StoreFacade-test.js: -------------------------------------------------------------------------------- 1 | var StoreFacade = require('./_all').StoreFacade, 2 | async = require('async'), 3 | sinon = require('sinon'), 4 | stub = sinon.stub, 5 | spy = sinon.spy; 6 | 7 | describe('StoreFacade', function () { 8 | var unit, store, api; 9 | 10 | beforeEach(function () { 11 | function noop() {} 12 | 13 | api = { 14 | createLease: noop, 15 | createTimer: noop, 16 | expire: noop, 17 | del: noop, 18 | delProp: noop, 19 | getProp: noop, 20 | setProp: noop, 21 | incrPropBy: noop, 22 | on: noop 23 | }; 24 | 25 | store = stub(api); 26 | 27 | unit = new StoreFacade(store, 'n'); 28 | 29 | unit._toStoreKey = spy(unit, '_toStoreKey'); 30 | }); 31 | 32 | it('should use _toStoreKey for simple methods', function (done) { 33 | var simpleMethods = Object.keys(StoreFacade.prototype).filter(function (name) { 34 | return (name !== 'createLease' && name !== 'createTimer' && 35 | name !== 'on' && name[0] !== '_' && name !== 'resetPopulateInErrorCount' && 36 | name !== 'incrementPopulateInErrorCount'); 37 | }); 38 | 39 | function test(methodName, cb) { 40 | var isSetter = (methodName.indexOf('set') === 0), 41 | args = ['k']; 42 | 43 | if (isSetter) args.push('v'); 44 | args.push(check); 45 | 46 | function check(err) { 47 | if (err) return cb(err); 48 | unit._toStoreKey.calledOnce.should.be.ok(); 49 | unit._toStoreKey.reset(); 50 | cb(); 51 | } 52 | 53 | if (/^get/.test(methodName)) store.getProp.yields(null); 54 | else if (/^set/.test(methodName)) store.setProp.yields(null); 55 | else store[methodName].yields(null); 56 | 57 | unit[methodName].apply(unit, args); 58 | } 59 | 60 | async.map(simpleMethods, test, done); 61 | }); 62 | 63 | describe('populateInErrorCount', function () { 64 | it('should be incremented', function (done) { 65 | function check(err) { 66 | store.incrPropBy.calledOnce.should.be.ok(); 67 | done(err); 68 | } 69 | store.incrPropBy.yields(null); 70 | unit.incrementPopulateInErrorCount('k', check); 71 | }); 72 | 73 | it('should be deleted when reset', function (done) { 74 | function check(err) { 75 | store.delProp.calledOnce.should.be.ok(); 76 | done(err); 77 | } 78 | store.delProp.yields(null); 79 | unit.resetPopulateInErrorCount('k', check); 80 | }); 81 | }); 82 | 83 | it('should use _toStoreKey for createLease', function (done) { 84 | var storeLease, namespacedLease; 85 | 86 | storeLease = stub(); 87 | storeLease.yields(null); 88 | store.createLease.returns(storeLease); 89 | 90 | namespacedLease = unit.createLease(); 91 | namespacedLease('k', function () { 92 | unit._toStoreKey.calledOnce.should.be.ok(); 93 | unit._toStoreKey.reset(); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('should proxy on events', function (done) { 99 | store.on.yields('blah'); 100 | unit.on('error', function (val) { 101 | val.should.equal('blah'); 102 | done(); 103 | }); 104 | }); 105 | 106 | it('should proxy createTimer', function () { 107 | unit.createTimer(); 108 | store.createTimer.calledOnce.should.be.ok(); 109 | store.createTimer.lastCall.args[0].should.equal('n'); 110 | }); 111 | 112 | describe('methods that expect numbers back', function () { 113 | it('should yield an error if the store returns an error', function (done) { 114 | store.getProp.yields('e'); 115 | unit.getAccessedAt(null, function (err) { 116 | err.should.equal('e'); 117 | done(); 118 | }); 119 | }); 120 | 121 | it('should yield a number if the store returns a string', function (done) { 122 | store.getProp.yields(null, '77'); 123 | unit.getCreatedAt(null, function (err, val) { 124 | val.should.equal(77); 125 | done(err); 126 | }); 127 | }); 128 | 129 | it('should yield a number if the store returns a number', function (done) { 130 | store.getProp.yields(null, 33); 131 | unit.getAccessedAt(null, function (err, val) { 132 | val.should.equal(33); 133 | done(err); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('methods that expect string back', function () { 139 | it('should yield an error if the store returns an error', function (done) { 140 | store.getProp.yields('e'); 141 | unit.getHash(null, function (err) { 142 | err.should.equal('e'); 143 | done(); 144 | }); 145 | }); 146 | 147 | it('should yield a string if the store returns an object', function (done) { 148 | store.getProp.yields(null, new Buffer('foo')); 149 | unit.getHash(null, function (err, val) { 150 | val.should.eql('foo'); 151 | done(err); 152 | }); 153 | }); 154 | 155 | it('should yield a string if the store returns a string', function (done) { 156 | store.getProp.yields(null, 'a'); 157 | unit.getHash(null, function (err, val) { 158 | val.should.equal('a'); 159 | done(err); 160 | }); 161 | }); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /test/_all.js: -------------------------------------------------------------------------------- 1 | var requireDirectory = require('require-directory'); 2 | 3 | 4 | /** 5 | * This file makes sure all files have been tested. 6 | * Include all newly created source directories here. 7 | */ 8 | 9 | module.exports = requireDirectory(module, '../lib'); 10 | -------------------------------------------------------------------------------- /test/decorators/BaseDecorator-test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-new: 0 */ 2 | 3 | var BaseDecorator = require('../_all').decorators.BaseDecorator, 4 | stub = require('sinon').stub, 5 | joi = require('joi'); 6 | 7 | describe('decorators/BaseDecorator', function () { 8 | var cache; 9 | 10 | beforeEach(function () { 11 | function noop() {} 12 | cache = stub({emit: noop, on: noop}); 13 | }); 14 | 15 | it('should create a basic dec', function () { 16 | new BaseDecorator(cache); 17 | }); 18 | 19 | describe('_emitError', function () { 20 | it('should emit an error on error or do nothing', function () { 21 | var d = new BaseDecorator(cache); 22 | d._emitError(new Error('bad')); 23 | cache.emit.calledOnce.should.be.ok(); 24 | }); 25 | 26 | it('should not emit an error when none is sent', function () { 27 | var d = new BaseDecorator(cache); 28 | d._emitError(null); 29 | cache.emit.calledOnce.should.not.be.ok(); 30 | }); 31 | }); 32 | 33 | it('should create a dec with a config', function () { 34 | new BaseDecorator(cache, {}, joi.object().keys({})); 35 | }); 36 | 37 | it('should throw on invalid config', function () { 38 | (function () { 39 | new BaseDecorator(cache, {boo: 1}, joi.object().keys({})); 40 | }).should.throw(/not allowed/); 41 | }); 42 | 43 | it('should change human intervals in config', function () { 44 | new BaseDecorator(cache, { 45 | aIn: '1 min', 46 | b: 'hello' 47 | }, joi.object().keys({ 48 | aIn: joi.number(), 49 | b: joi.string() 50 | })); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/decorators/EventDecorator-test.js: -------------------------------------------------------------------------------- 1 | var all = require('../_all'), 2 | EventDecorator = all.decorators.EventDecorator, 3 | stub = require('sinon').stub; 4 | 5 | describe('decorators/EventDecorator', function () { 6 | var unit, cache; 7 | 8 | beforeEach(function () { 9 | function noop() {} 10 | cache = stub({ 11 | emit: noop, 12 | get: noop, 13 | set: noop, 14 | del: noop 15 | }); 16 | unit = new EventDecorator(cache); 17 | }); 18 | 19 | describe('get', function () { 20 | it('should emit a get:hit event', function (done) { 21 | function check() { 22 | cache.emit.thirdCall.args[0].should.equal('get:hit'); 23 | done(); 24 | } 25 | 26 | cache.get.yields(null, 'v'); 27 | unit.get('k', check); 28 | }); 29 | 30 | it('should emit a get:hit event on null value', function (done) { 31 | function check() { 32 | cache.emit.thirdCall.args[0].should.equal('get:miss'); 33 | done(); 34 | } 35 | 36 | cache.get.yields(null, null); 37 | unit.get('k', check); 38 | }); 39 | 40 | it('should proxy error from hget', function (done) { 41 | function check(err) { 42 | err.message.should.equal('handled'); 43 | done(); 44 | } 45 | cache.get.yields(new Error('handled')); 46 | unit.get('k', check); 47 | }); 48 | }); 49 | 50 | it('should set', function(done) { 51 | cache.set.withArgs('k', 'v').yields(null); 52 | unit.set('k', 'v', done); 53 | }); 54 | 55 | it('should del', function(done) { 56 | cache.del.withArgs('k').yields(null); 57 | unit.del('k', done); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/decorators/ExpiresDecorator-test.js: -------------------------------------------------------------------------------- 1 | var ExpiresDecorator = require('../_all').decorators.ExpiresDecorator, 2 | stub = require('sinon').stub; 3 | 4 | describe('decorators/ExpiresDecorator', function () { 5 | var unit, cache, store; 6 | 7 | beforeEach(function () { 8 | function noop() {} 9 | cache = stub({ 10 | on: noop, 11 | emit: noop, 12 | _getStore: noop, 13 | get: noop, 14 | del: noop 15 | }); 16 | store = stub({ 17 | setCreatedAt: noop, 18 | getCreatedAt: noop 19 | }); 20 | cache._getStore.returns(store); 21 | unit = new ExpiresDecorator(cache, {}); 22 | }); 23 | 24 | describe('setCreatedAt', function () { 25 | it('should set the createdAt timestamp', function () { 26 | store.setCreatedAt.yields(null); 27 | unit.setCreatedAt('k'); 28 | store.setCreatedAt.calledOnce.should.be.ok(); 29 | }); 30 | }); 31 | 32 | describe('get', function () { 33 | it('should yield null if createdAt is null (not in cache)', function (done) { 34 | store.getCreatedAt.yields(null, null); 35 | unit.get('k', done); 36 | }); 37 | 38 | it('should look at createdAt first and if not expired call get', function (done) { 39 | store.getCreatedAt.yields(null, '0'); 40 | cache.get.yields(null); 41 | unit.get('k', done); 42 | }); 43 | 44 | it('should yield null if expired and call del', function (done) { 45 | store.getCreatedAt.yields(null, '0'); 46 | unit = new ExpiresDecorator(cache, {expiresIn: 0}); 47 | unit.get('k', function () { 48 | cache.del.called.should.be.ok(); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('should call get if stale emit stale', function (done) { 54 | store.getCreatedAt.yields(null, '0'); 55 | cache.get.yields(null); 56 | unit = new ExpiresDecorator(cache, {staleIn: 0}); 57 | unit.get('k', function () { 58 | cache.emit.calledOnce.should.be.ok(); 59 | cache.emit.firstCall.args[0].should.equal('get:stale'); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/decorators/MarshallDecorator-test.js: -------------------------------------------------------------------------------- 1 | var MarshallDecorator = require('../_all') 2 | .decorators.MarshallDecorator, 3 | stub = require('sinon').stub; 4 | 5 | describe('decorators/MarshallDecorator', function () { 6 | var unit, cache; 7 | 8 | beforeEach(function () { 9 | function noop() {} 10 | cache = stub({get: noop, set: noop}); 11 | unit = new MarshallDecorator(cache); 12 | }); 13 | 14 | describe('get', function () { 15 | it('should get unmarshalled data', function (done) { 16 | function check(err, value) { 17 | if (err) return done(err); 18 | value.should.eql({a: 42, b: true}); 19 | done(); 20 | } 21 | cache.get.yields(null, '{"a":42,"b":true}'); 22 | unit.get('k', check); 23 | }); 24 | 25 | it('should return an error if the value is not a string', function (done) { 26 | function check(err) { 27 | err.name.should.equal('MarshallingError'); 28 | done(); 29 | } 30 | cache.get.yields(null, [1, 2, 3]); 31 | unit.get('k', check); 32 | }); 33 | 34 | it('should return marshalling errors in cb', function (done) { 35 | function check(err) { 36 | err.name.should.equal('MarshallingError'); 37 | err.message.should.match(/Failed to marshall/); 38 | err.message.should.match(/{"a":/); // part of value 39 | done(); 40 | } 41 | cache.get.yields(null, '{"a":42,'); 42 | unit.get('k', check); 43 | }); 44 | 45 | it('should proxy other errors', function (done) { 46 | function check(err) { 47 | err.message.should.equal('bad'); 48 | done(); 49 | } 50 | cache.get.yields(new Error('bad')); 51 | unit.get('k', check); 52 | }); 53 | }); 54 | 55 | describe('set', function () { 56 | it('should marshall an object to a string before setting', function (done) { 57 | function check(err) { 58 | if (err) return done(err); 59 | cache.set.lastCall.args[1] 60 | .should.equal('{"a":42,"b":true}'); 61 | done(); 62 | } 63 | cache.set.yields(null, ''); 64 | unit.set('k', {a: 42, b: true}, check); 65 | }); 66 | 67 | it('should marshall `undefined` as `null`', function (done) { 68 | function check(err) { 69 | if (err) return done(err); 70 | cache.set.lastCall.args[1].should.equal('null'); 71 | done(); 72 | } 73 | cache.set.yields(null, ''); 74 | unit.set('k', undefined, check); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/decorators/OnlySetChangedDecorator-test.js: -------------------------------------------------------------------------------- 1 | var all = require('../_all'), 2 | util = all.util, 3 | OnlySetChangedDecorator = all.decorators.OnlySetChangedDecorator, 4 | stub = require('sinon').stub; 5 | 6 | describe('decorators/OnlySetChangedDecorator', function () { 7 | var unit, cache, store; 8 | 9 | beforeEach(function () { 10 | function noop() {} 11 | cache = stub({ 12 | on: noop, 13 | emit: noop, 14 | _getStore: noop, 15 | get: noop, 16 | del: noop, 17 | set: noop 18 | }); 19 | store = stub({ 20 | getHash: noop, 21 | setHash: noop 22 | }); 23 | cache._getStore.returns(store); 24 | unit = new OnlySetChangedDecorator(cache, {}); 25 | }); 26 | 27 | describe('set', function () { 28 | it('should call set when the hash does not match', function (done) { 29 | store.getHash.yields(null, 'h'); 30 | cache.set.yields(null); 31 | unit.set('k', 'v', function () { 32 | process.nextTick(function () { 33 | store.getHash.calledOnce.should.be.ok(); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | it('should not call set but emit set:identical when hash matches (same val)', function (done) { 40 | store.getHash.yields(null, util.createHash('v')); 41 | cache.set.yields(null); 42 | unit.set('k', 'v', function () { 43 | process.nextTick(function () { 44 | cache.emit.firstCall.args[0].should.equal('set:identical'); 45 | done(); 46 | }); 47 | }); 48 | }); 49 | 50 | it('should not set hash if set had an error', function (done) { 51 | store.getHash.yields(null, 'h'); 52 | cache.set.yields(new Error('bad')); 53 | unit.set('k', 'v', function () { 54 | process.nextTick(function () { 55 | store.setHash.calledOnce.should.not.be.ok(); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/decorators/PopulateDecorator-test.js: -------------------------------------------------------------------------------- 1 | var proxyquire = require('proxyquire').noCallThru(), 2 | stub = require('sinon').stub, 3 | Promise = require('es6-promise').Promise; 4 | 5 | describe('decorators/PopulateDecorator', function () { 6 | var PopulateDecorator, unit, cache, 7 | store, lease, populate; 8 | 9 | beforeEach(function () { 10 | var modulePath; 11 | 12 | function noop() {} 13 | cache = stub({ 14 | on: noop, 15 | emit: noop, 16 | _getStore: noop, 17 | get: noop, 18 | del: noop, 19 | set: noop 20 | }); 21 | 22 | store = stub({ 23 | createLease: noop 24 | }); 25 | lease = stub(); 26 | store.createLease.returns(lease); 27 | cache._getStore.returns(store); 28 | 29 | populate = stub(); 30 | 31 | modulePath = '../../lib/decorators/PopulateDecorator'; 32 | PopulateDecorator = proxyquire(modulePath, { 33 | }); 34 | 35 | unit = new PopulateDecorator(cache, { 36 | populate: populate 37 | }); 38 | }); 39 | 40 | it('should call leasedPopulate on stale event', function (done) { 41 | var onStaleEvent = cache.on.lastCall.args[1]; 42 | unit.leasedPopulate = function (k, cb) { 43 | k.should.equal('a'); 44 | cb.should.be.type('function'); 45 | done(); 46 | }; 47 | onStaleEvent('a'); 48 | }); 49 | 50 | describe('get', function () { 51 | it('should return a cached value if in cache', function (done) { 52 | cache.get.yields(null, 'v'); 53 | unit.get('k', function (err, val) { 54 | if (err) return done(err); 55 | val.should.equal('v'); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should proxy through populate if not in cache', function (done) { 61 | cache.get.yields(null, null); 62 | populate.yields(null, 'pv'); 63 | cache.set.yields(null); 64 | unit.get('k', function (err, val) { 65 | if (err) return done(err); 66 | val.should.equal('pv'); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | describe('populate', function () { 73 | it('should return an error', function (done) { 74 | unit = new PopulateDecorator(cache, { 75 | populate: function () { 76 | throw new Error('bad'); 77 | } 78 | }); 79 | unit.populate('k', function (err) { 80 | err.name.should.equal('PopulateError'); 81 | err.message.should.match(/populate threw/); 82 | done(); 83 | }); 84 | }); 85 | 86 | describe('with promises', function () { 87 | it('should return an error when rejected', function (done) { 88 | unit = new PopulateDecorator(cache, { 89 | populate: function () { 90 | return new Promise(function (resolve, reject) { 91 | reject(new Error('promise rejected')); 92 | }); 93 | } 94 | }); 95 | unit.populate('k', function (err) { 96 | err.name.should.equal('PopulateError'); 97 | err.message.should.match(/promise rejected/); 98 | done(); 99 | }); 100 | }); 101 | 102 | it('should return an error when thrown', function (done) { 103 | unit = new PopulateDecorator(cache, { 104 | populate: function () { 105 | return new Promise(function () { 106 | throw new Error('promise thrown'); 107 | }); 108 | } 109 | }); 110 | unit.populate('k', function (err) { 111 | err.name.should.equal('PopulateError'); 112 | err.message.should.match(/promise thrown/); 113 | done(); 114 | }); 115 | }); 116 | 117 | it('should return a value when resolved', function (done) { 118 | unit = new PopulateDecorator(cache, { 119 | populate: function () { 120 | return Promise.resolve('rv'); 121 | } 122 | }); 123 | cache.set.yields(null); 124 | unit.populate('k', function (err, value) { 125 | if (err) return done(err); 126 | value.should.equal('rv'); 127 | done(); 128 | }); 129 | }); 130 | }); 131 | }); 132 | 133 | describe('leasedPopulate', function () { 134 | beforeEach(function () { 135 | unit.populate = stub(); 136 | }); 137 | 138 | it('should acquire lock lease, populate, and unlock', function (done) { 139 | var unlock = stub(); 140 | function check(err, val) { 141 | if (err) return done(err); 142 | val.should.equal('v'); 143 | unlock.calledOnce.should.be.ok(); 144 | done(); 145 | } 146 | lease.yields(null, unlock); 147 | unit.populate.yields(null, 'v'); 148 | unit.leasedPopulate('k', check); 149 | }); 150 | 151 | it('should not return an error or populate if locked', function (done) { 152 | var err = new Error(); 153 | err.name = 'AlreadyLeasedError'; 154 | lease.yields(err); 155 | unit.leasedPopulate('k', done); 156 | }); 157 | 158 | it('should return an error if lock returned one', function (done) { 159 | function check(err) { 160 | err.message.should.match(/good/); 161 | done(); 162 | } 163 | lease.yields(new Error('good')); 164 | unit.leasedPopulate('k', check); 165 | }); 166 | 167 | it('should proxy populate error and unlock', function (done) { 168 | var unlock = stub(); 169 | function check(err) { 170 | err.name.should.equal('Error'); 171 | err.message.should.match('bad'); 172 | unlock.calledOnce.should.be.ok(); 173 | done(); 174 | } 175 | lease.yields(null, unlock); 176 | unit.populate.yields(new Error('bad')); 177 | unit.leasedPopulate('k', check); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /test/decorators/PopulateInDecorator-test.js: -------------------------------------------------------------------------------- 1 | var proxyquire = require('proxyquire').noCallThru(), 2 | stub = require('sinon').stub; 3 | 4 | describe('decorators/PopulateInDecorator', function () { 5 | var PopulateInDecorator, unit, cache, store, timer; 6 | 7 | beforeEach(function () { 8 | var modulePath; 9 | 10 | function noop() {} 11 | cache = stub({ 12 | on: noop, 13 | emit: noop, 14 | leasedPopulate: noop, 15 | _getStore: noop, 16 | get: noop, 17 | del: noop, 18 | set: noop 19 | }); 20 | 21 | timer = stub({ 22 | on: noop, 23 | setTimeout: noop 24 | }); 25 | 26 | store = stub({ 27 | on: noop, 28 | getAccessedAt: noop, 29 | setAccessedAt: noop, 30 | createTimer: noop, 31 | resetPopulateInErrorCount: noop, 32 | incrementPopulateInErrorCount: noop 33 | }); 34 | store.createTimer.returns(timer); 35 | 36 | cache._getStore.returns(store); 37 | 38 | modulePath = '../../lib/decorators/PopulateInDecorator'; 39 | PopulateInDecorator = proxyquire(modulePath, { 40 | }); 41 | 42 | unit = new PopulateInDecorator(cache, { 43 | populateIn: 1000, 44 | pausePopulateIn: 2000 45 | }); 46 | }); 47 | 48 | it('should set accessedAt', function () { 49 | store.setAccessedAt.yields(null); 50 | unit.setAccessedAt('k'); 51 | store.setAccessedAt.calledOnce.should.be.ok(); 52 | }); 53 | 54 | it('should set a trigger', function () { 55 | timer.setTimeout.yields(null); 56 | unit.setTimeout('k'); 57 | timer.setTimeout.calledOnce.should.be.ok(); 58 | }); 59 | 60 | it('should call leasedPopulate on `timeout`', function (done) { 61 | var _onTimeout = timer.on.lastCall.args[1]; 62 | unit.leasedPopulate = function (k, cb) { 63 | k.should.equal('k'); 64 | cb.should.be.type('function'); 65 | done(); 66 | }; 67 | _onTimeout('k'); 68 | }); 69 | 70 | describe('leasedPopulate', function () { 71 | it('should populate if no accessedAt', function (done) { 72 | store.getAccessedAt.yields(null, null); 73 | cache.leasedPopulate.yields(null); 74 | store.resetPopulateInErrorCount.yields(null); 75 | unit.leasedPopulate('k', done); 76 | }); 77 | 78 | it('should populate if accessedAt and not paused', function (done) { 79 | store.getAccessedAt.yields(null, Infinity); 80 | cache.leasedPopulate.yields(null); 81 | store.resetPopulateInErrorCount.yields(null); 82 | unit.leasedPopulate('k', done); 83 | }); 84 | 85 | it('should not populate if accessedAt but paused', function (done) { 86 | store.getAccessedAt.yields(null, 1); 87 | unit.leasedPopulate('k', done); 88 | }); 89 | 90 | it('should continue to populate despite an error if within attempts', function (done) { 91 | function check(err) { 92 | err.message.should.equal('bad'); 93 | timer.setTimeout.calledOnce.should.be.ok(); 94 | done(); 95 | } 96 | store.getAccessedAt.yields(null, Infinity); 97 | cache.leasedPopulate.yields(new Error('bad')); 98 | store.incrementPopulateInErrorCount.yields(null, 1); 99 | unit.leasedPopulate('k', check); 100 | }); 101 | 102 | it('should not continue to populate despite an error over attempts', function (done) { 103 | function check(err) { 104 | err.message.should.equal('bad'); 105 | timer.setTimeout.called.should.not.be.ok(); 106 | done(); 107 | } 108 | store.getAccessedAt.yields(null, Infinity); 109 | cache.leasedPopulate.yields(new Error('bad')); 110 | store.incrementPopulateInErrorCount.yields(null, 5); 111 | unit.leasedPopulate('k', check); 112 | }); 113 | 114 | it('should continue to populate despite an error', function (done) { 115 | function check(err) { 116 | err.message.should.equal('bad'); 117 | timer.setTimeout.calledOnce.should.be.ok(); 118 | done(); 119 | } 120 | store.getAccessedAt.yields(null, Infinity); 121 | cache.leasedPopulate.yields(new Error('bad')); 122 | store.incrementPopulateInErrorCount.yields(null, 1); 123 | unit.leasedPopulate('k', check); 124 | }); 125 | 126 | it('should propagate an incremement error', function (done) { 127 | function check(err) { 128 | err.message.should.equal('inc'); 129 | timer.setTimeout.called.should.not.be.ok(); 130 | done(); 131 | } 132 | store.getAccessedAt.yields(null, Infinity); 133 | cache.leasedPopulate.yields(new Error('bad')); 134 | store.incrementPopulateInErrorCount.yields(new Error('inc')); 135 | unit.leasedPopulate('k', check); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/decorators/PromiseDecorator-test.js: -------------------------------------------------------------------------------- 1 | var proxyquire = require('proxyquire').noCallThru(), 2 | stub = require('sinon').stub, 3 | UNIT_PATH = '../../lib/decorators/PromiseDecorator'; 4 | 5 | describe('decorators/PromiseDecorator', function () { 6 | var unit, BaseDeco; 7 | 8 | beforeEach(function () { 9 | BaseDeco = function () {}; 10 | BaseDeco.prototype.get = stub(); 11 | BaseDeco.prototype.set = stub(); 12 | BaseDeco.prototype.del = stub(); 13 | 14 | var Unit = proxyquire(UNIT_PATH, { 15 | './BaseDecorator': BaseDeco 16 | }); 17 | 18 | unit = new Unit(); 19 | }); 20 | 21 | it('should return err on reject', function (done) { 22 | BaseDeco.prototype.get.withArgs('ek').yields(new Error('el')); 23 | unit.get('ek').catch(function (err) { 24 | err.message.should.eql('el'); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should return promise on get', function (done) { 30 | BaseDeco.prototype.get.withArgs('gk').yields(null); 31 | unit.get('gk').then(done); 32 | }); 33 | 34 | it('should return promise on set', function (done) { 35 | BaseDeco.prototype.set.withArgs('sk', 'sv').yields(null); 36 | unit.set('sk', 'sv').then(done); 37 | }); 38 | 39 | it('should return promise on del', function (done) { 40 | BaseDeco.prototype.del.withArgs('dk').yields(null); 41 | unit.del('dk').then(done); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/helpers/wrap.js: -------------------------------------------------------------------------------- 1 | var slice = Array.prototype.slice; 2 | 3 | module.exports = function (workCb, doneCb) { 4 | return function (err) { 5 | if (err) return doneCb(err); 6 | var args = slice.call(arguments); 7 | args.shift(); 8 | workCb.apply(null, args); 9 | doneCb(); 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /test/index-test.js: -------------------------------------------------------------------------------- 1 | var proxyquire = require('proxyquire').noCallThru(); 2 | 3 | describe('Distribucache', function () { 4 | var distribucache; 5 | 6 | beforeEach(function () { 7 | distribucache = proxyquire('../lib', { 8 | './CacheClient': function () { 9 | this.answer = 42; 10 | } 11 | }); 12 | }); 13 | 14 | describe('createClient', function () { 15 | it('should create a new cache client', function () { 16 | distribucache.createClient().answer.should.equal(42); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/integration/events-test.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'), 2 | stub = sinon.stub, 3 | should = require('should'), 4 | w = require('../helpers/wrap'), 5 | dcache = require('../../'), 6 | slice = Array.prototype.slice, 7 | EVENT_NAMES = [ 8 | // GET 9 | 'get:before', 'get:stale', 'get:expire', 10 | 'get:hit', 'get:miss', 'get:after', 'get:error', 11 | 12 | // SET 13 | 'set:before', 'set:identical', 'set:after', 'set:error', 14 | 15 | // DEL 16 | 'del:before', 'del:after', 'del:error', 17 | 18 | // POPULATE 19 | 'populate:before', 'populate:after', 'populate:error', 20 | 21 | // POPULATE_IN 22 | 'populateIn:before', 'populateIn:pause', 'populateIn:maxAttempts', 23 | 'populateIn:after', 'populateIn:error' 24 | ]; 25 | 26 | describe('integration/events', function () { 27 | var cache, client, store, events, clock, timer, lease; 28 | 29 | beforeEach(function () { 30 | function noop() {} 31 | store = stub({ 32 | createLease: noop, 33 | createTimer: noop, 34 | on: noop, 35 | del: noop, 36 | expire: noop, 37 | delProp: noop, 38 | getProp: noop, 39 | setProp: noop, 40 | depProp: noop, 41 | incrPropBy: noop 42 | }); 43 | timer = stub({on: noop, setTimeout: noop}); 44 | lease = stub(); 45 | lease.yields(null, stub()); 46 | store.createTimer.returns(timer); 47 | store.createLease.returns(lease); 48 | clock = sinon.useFakeTimers(); 49 | client = dcache.createClient(store); 50 | }); 51 | 52 | afterEach(function () { 53 | clock.restore(); 54 | }); 55 | 56 | function createCache() { 57 | cache = client.create.apply(client, arguments); 58 | events = {}; 59 | EVENT_NAMES.forEach(function (name) { 60 | cache.on(name, function () { 61 | if (!events[name]) events[name] = {callCount: 0, args: []}; 62 | events[name].callCount++; 63 | events[name].args.push(slice.call(arguments)); 64 | }); 65 | }); 66 | } 67 | 68 | it('should emit a `create` event', function (done) { 69 | client.on('create', function (c, namespace) { 70 | c.should.be.type('object'); 71 | namespace.should.equal('nsp'); 72 | done(); 73 | }); 74 | client.create('nsp'); 75 | }); 76 | 77 | describe('get', function () { 78 | beforeEach(createCache.bind(null, 'n')); 79 | 80 | it('should emit `miss` if value is null', function (done) { 81 | store.getProp.withArgs('n:k').yields(null, null, 'e'); 82 | 83 | function verify(value) { 84 | arguments.length.should.equal(1); 85 | should(value).not.be.ok(); 86 | Object.keys(events).should.eql([ 87 | 'get:before', 'get:after', 'get:miss' 88 | ]); 89 | events.should.eql({ 90 | 'get:before': {callCount: 1, args: [['k']]}, 91 | 'get:after': {callCount: 1, args: [['k', 0]]}, 92 | 'get:miss': {callCount: 1, args: [['k']]} 93 | }); 94 | } 95 | 96 | cache.get('k', w(verify, done)); 97 | }); 98 | 99 | it('should emit hit event if value is not null', function (done) { 100 | store.getProp.withArgs('n:b').yields(null, '"v"', 'e'); 101 | 102 | function verify(value) { 103 | arguments.length.should.equal(1); 104 | should(value).be.ok(); 105 | Object.keys(events).should.eql([ 106 | 'get:before', 'get:after', 'get:hit' 107 | ]); 108 | events['get:hit'].should.eql({callCount: 1, args: [['b']]}); 109 | } 110 | 111 | cache.get('b', w(verify, done)); 112 | }); 113 | 114 | describe('with `staleIn` set', function () { 115 | beforeEach(createCache.bind(null, 'n', {staleIn: 100})); 116 | 117 | it('should emit a `miss` event when cache empty', function (done) { 118 | store.getProp.withArgs('n:b', 'createdAt').yields(null, null); 119 | 120 | function verify(value) { 121 | arguments.length.should.equal(1); 122 | should(value).not.be.ok(); 123 | Object.keys(events).should.eql([ 124 | 'get:before', 'get:after', 'get:miss' 125 | ]); 126 | events['get:miss'].should.eql({callCount: 1, args: [['b']]}); 127 | } 128 | 129 | cache.get('b', w(verify, done)); 130 | }); 131 | 132 | it('should not emit a `stale` event when cache not stale', function (done) { 133 | store.getProp.withArgs('n:b', 'createdAt').yields(null, Date.now()); 134 | store.getProp.withArgs('n:b', 'value').yields(null, '"v"'); 135 | 136 | function verify(value) { 137 | arguments.length.should.equal(1); 138 | value.should.equal('v'); 139 | Object.keys(events).should.eql([ 140 | 'get:before', 'get:after', 'get:hit' 141 | ]); 142 | } 143 | 144 | cache.get('b', w(verify, done)); 145 | }); 146 | 147 | it('should emit a `stale` event when cache empty and get', function (done) { 148 | store.getProp.withArgs('n:b', 'createdAt').yields(null, Date.now() - 200); 149 | store.getProp.withArgs('n:b', 'value').yields(null, '"v"'); 150 | 151 | function verify(value) { 152 | arguments.length.should.equal(1); 153 | value.should.equal('v'); 154 | Object.keys(events).should.eql([ 155 | 'get:before', 'get:stale', 'get:after', 'get:hit' 156 | ]); 157 | events['get:stale'].should.eql({callCount: 1, args: [['b']]}); 158 | } 159 | 160 | cache.get('b', w(verify, done)); 161 | }); 162 | }); 163 | 164 | describe('with `expireIn` set', function () { 165 | it('should emit an `get:expire` event and delete the cache when cache expires', function (done) { 166 | createCache('n', {expiresIn: 100}); 167 | 168 | store.getProp.withArgs('n:c', 'createdAt').yields(null, Date.now() - 200); 169 | store.del.withArgs('n:c').yields(null); 170 | 171 | function verify(value) { 172 | arguments.length.should.equal(1); 173 | should(value).not.be.ok(); 174 | //Object.keys(events).should.eql(['expire', 'del', 'miss']); 175 | Object.keys(events).should.eql([ 176 | 'get:before', 'get:expire', 177 | 'del:before', 'del:after', 178 | 'get:after', 'get:miss' 179 | ]); 180 | events['get:expire'].should.eql({callCount: 1, args: [['c']]}); 181 | events['del:before'].should.eql({callCount: 1, args: [['c']]}); 182 | events['del:after'].should.eql({callCount: 1, args: [['c', 0]]}); 183 | } 184 | 185 | cache.get('c', w(verify, done)); 186 | }); 187 | }); 188 | }); 189 | 190 | describe('set', function () { 191 | beforeEach(createCache.bind(null, 'n')); 192 | 193 | it('should emit set events and ignore extra store args', function (done) { 194 | store.getProp.withArgs('n:k', 'hash').yields(null, 'h', 'e'); 195 | store.setProp.withArgs('n:k', 'value').yields(null, 'e'); 196 | 197 | function verify() { 198 | arguments.length.should.equal(0); 199 | Object.keys(events).should.eql(['set:before', 'set:after']); 200 | events['set:before'].should.eql({callCount: 1, args: [['k', 'v']]}); 201 | events['set:after'].should.eql({callCount: 1, args: [['k', 'v', 0]]}); 202 | } 203 | 204 | cache.set('k', 'v', w(verify, done)); 205 | }); 206 | 207 | it('should emit set and set:identical if hash is the same', function (done) { 208 | store.getProp.withArgs('n:s', 'hash').yields(null, '59b943d2fe6aede1820f470ac1e94e1a'); 209 | store.setProp.withArgs('n:s', 'value').yields(null); 210 | 211 | function verify() { 212 | arguments.length.should.equal(0); 213 | Object.keys(events).should.eql(['set:before', 'set:identical', 'set:after']); 214 | events['set:identical'].should.eql({callCount: 1, args: [['s']]}); 215 | } 216 | 217 | cache.set('s', 'v', w(verify, done)); 218 | }); 219 | }); 220 | 221 | describe('populate', function () { 222 | it('should populate on a `get:miss`', function (done) { 223 | createCache('n', { 224 | populate: function (k, cb) { 225 | cb(null, 'z'); 226 | } 227 | }); 228 | 229 | function verify(value) { 230 | value.should.eql('z'); 231 | Object.keys(events).should.eql([ 232 | 'get:before', 'get:after', 'get:miss', 233 | 'populate:before', 234 | 'set:before', 'set:after', 235 | 'populate:after' 236 | ]); 237 | events['populate:before'].should.eql({callCount: 1, args: [['k']]}); 238 | events['populate:after'].should.eql({callCount: 1, args: [['k', 0]]}); 239 | } 240 | 241 | store.getProp.withArgs('n:k').yields(null, null); 242 | store.setProp.withArgs('n:k').yields(null); 243 | 244 | cache.get('k', w(verify, done)); 245 | }); 246 | 247 | it('should emit a `:error` event on an error', function (done) { 248 | createCache('n', { 249 | populate: function (k, cb) { 250 | cb(new Error('perr')); 251 | } 252 | }); 253 | 254 | function verify(err) { 255 | err.name.should.equal('PopulateError'); 256 | Object.keys(events).should.eql([ 257 | 'get:before', 'get:after', 'get:miss', 258 | 'populate:before', 'populate:error', 'populate:after' 259 | ]); 260 | events['populate:error'].callCount.should.equal(1); 261 | events['populate:error'].args[0][0].should.be.instanceOf(Error); 262 | done(); 263 | } 264 | 265 | store.getProp.withArgs('n:k').yields(null, null); 266 | 267 | cache.get('k', verify); 268 | }); 269 | }); 270 | 271 | describe('populateIn', function () { 272 | beforeEach(function () { 273 | createCache('n', { 274 | populateIn: 700, 275 | pausePopulateIn: 900, 276 | populateInAttempts: 0, 277 | populate: function (k, cb) { 278 | cb(null, 'z'); 279 | } 280 | }); 281 | }); 282 | 283 | it('should emit events in correct sequence when triggered', function (done) { 284 | store.getProp.withArgs('n:k', 'accessedAt').yields(null, 0); 285 | store.getProp.withArgs('n:k', 'hash').yields(null, null); 286 | store.setProp.withArgs('n:k', 'value').yields(null); 287 | store.delProp.withArgs('n:k', 'populateInErrorCount').yields(null); 288 | 289 | timer.on.secondCall.args[0].should.equal('timeout'); 290 | timer.on.secondCall.args[1]('k'); 291 | 292 | cache.on('populateIn:after', function () { 293 | Object.keys(events).should.eql([ 294 | 'populateIn:before', 295 | 'populate:before', 296 | 'set:before', 'set:after', 297 | 'populate:after', 298 | 'populateIn:after' 299 | ]); 300 | 301 | events['populateIn:before'].should.eql({callCount: 1, args: [['k']]}); 302 | events['populateIn:after'].should.eql({callCount: 1, args: [['k', 0]]}); 303 | 304 | done(); 305 | }); 306 | }); 307 | 308 | it('should emit a `:pause` when the cache hasn\'t been accessed', function (done) { 309 | clock.tick(1000); 310 | store.getProp.withArgs('n:k', 'accessedAt').yields(null, 1); 311 | 312 | timer.on.secondCall.args[0].should.equal('timeout'); 313 | timer.on.secondCall.args[1]('k'); 314 | 315 | cache.on('populateIn:after', function () { 316 | Object.keys(events).should.eql([ 317 | 'populateIn:before', 318 | 'populateIn:pause', 319 | 'populateIn:after' 320 | ]); 321 | 322 | events['populateIn:pause'].should.eql({callCount: 1, args: [['k']]}); 323 | 324 | done(); 325 | }); 326 | }); 327 | 328 | it('should emit a `:maxAttempts` when cache reaches `populateInAttempts`', function (done) { 329 | // for the PopulateError we're triggering 330 | client.on('error', function () {}); 331 | 332 | lease.yields(new Error('uncool')); 333 | store.getProp.withArgs('n:k', 'accessedAt').yields(null, 0); 334 | store.incrPropBy.withArgs('n:k', 'populateInErrorCount').yields(null, 1); 335 | 336 | cache.on('populateIn:after', function () { 337 | Object.keys(events).should.eql([ 338 | 'populateIn:before', 339 | 'populateIn:maxAttempts', 340 | 'populateIn:error', 341 | 'populateIn:after' 342 | ]); 343 | 344 | events['populateIn:maxAttempts'].should.eql({callCount: 1, args: [['k']]}); 345 | 346 | done(); 347 | }); 348 | 349 | timer.on.secondCall.args[0].should.equal('timeout'); 350 | timer.on.secondCall.args[1]('k'); 351 | }); 352 | }); 353 | }); 354 | -------------------------------------------------------------------------------- /test/util-test.js: -------------------------------------------------------------------------------- 1 | var proxyquire = require('proxyquire').noCallThru(), 2 | should = require('should'), 3 | sinon = require('sinon'), 4 | stub = sinon.stub; 5 | 6 | describe('util', function () { 7 | var noop, util, redis; 8 | 9 | beforeEach(function () { 10 | noop = function () {}; 11 | redis = {createClient: stub()}; 12 | redis.createClient.returns({auth: stub()}); 13 | util = proxyquire('../lib/util', { 14 | redis: redis 15 | }); 16 | }); 17 | 18 | describe('createHash', function () { 19 | it('should create the same hash for the same value', function () { 20 | util.createHash('a').should.eql(util.createHash('a')); 21 | }); 22 | 23 | it('should create a different hash for different values', function () { 24 | util.createHash('a').should.not.eql(util.createHash('b')); 25 | }); 26 | }); 27 | 28 | describe('completeWithin', function () { 29 | it('should yield an TimeoutError if cb takes longer than timeout', function (done) { 30 | function longTime(cb) { 31 | setTimeout(function () { 32 | cb(null); 33 | }, 3); 34 | } 35 | 36 | function onReady(err) { 37 | err.name.should.equal('TimeoutError'); 38 | done(); 39 | } 40 | 41 | longTime(util.completeWithin(1, onReady)); 42 | }); 43 | 44 | it('should call the `done` before timeout', function (done) { 45 | function longTime(cb) { 46 | setTimeout(function () { 47 | cb(null); 48 | }, 3); 49 | } 50 | longTime(util.completeWithin(6, done)); 51 | }); 52 | }); 53 | 54 | describe('intervalToMs', function () { 55 | it('should return the input if not a string', function () { 56 | util.intervalToMs(100).should.equal(100); 57 | }); 58 | 59 | it('should return the input if the input does not match the re', function () { 60 | util.intervalToMs('hello').should.equal('hello'); 61 | }); 62 | 63 | it('should parse and transform various formats', function () { 64 | var tests = ['1ms', '10 ms', '3 sec', '3 seconds', 65 | '1min', '1 minute', '1 hour', '1 day', '1 week', 66 | '1 month', '10 years']; 67 | tests.forEach(function (test) { 68 | util.intervalToMs(test).should.be.type('number'); 69 | }); 70 | }); 71 | 72 | it('should understand floats', function () { 73 | util.intervalToMs('1.5 seconds').should.equal(1500); 74 | }); 75 | }); 76 | 77 | describe('getNamespace', function () { 78 | it('should return an empty string if no input', function () { 79 | util.createNamespace().should.equal(''); 80 | }); 81 | 82 | it('should return the only arg if one arg', function () { 83 | util.createNamespace('a').should.equal('a'); 84 | }); 85 | 86 | it('should return a namespace provided multiple args', function () { 87 | util.createNamespace('a', 'b', 'c').should.equal('a:b:c'); 88 | }); 89 | 90 | it('should skip falsey args', function () { 91 | util.createNamespace('a', undefined, false, null, '', 'b').should.equal('a:b'); 92 | }); 93 | }); 94 | 95 | describe('propagateEvent', function () { 96 | it('should propagate a single event', function () { 97 | var source = stub({on: noop}), dest = stub({emit: noop}); 98 | source.on.yields('ooo'); 99 | util.propagateEvent(source, dest, 'nom'); 100 | dest.emit.firstCall.args.should.eql(['nom', 'ooo']); 101 | }); 102 | }); 103 | 104 | describe('propagateEvents', function () { 105 | it('should pass the sourceName as the last param', function () { 106 | var source = stub({on: noop}), dest = stub({emit: noop}); 107 | source.on.yields('ooo'); 108 | util.propagateEvents(source, dest, ['nom'], 'sn'); 109 | dest.emit.firstCall.args.should.eql(['nom', 'ooo', 'sn']); 110 | }); 111 | }); 112 | 113 | describe('eventWrap', function () { 114 | var clock; 115 | 116 | beforeEach(function () { 117 | clock = sinon.useFakeTimers(); 118 | }); 119 | 120 | afterEach(function () { 121 | clock.restore(); 122 | }); 123 | 124 | it('should wrap with before / after events', function(done) { 125 | var ev = {emit: stub()}, wRun; 126 | 127 | function takeTime(cb) { 128 | process.nextTick(function () { 129 | clock.tick(500); 130 | cb(null, 'u'); 131 | }); 132 | } 133 | 134 | function run(err, val) { 135 | if (err) return done(err); 136 | val.should.equal('u'); 137 | ev.emit.callCount.should.equal(2); 138 | ev.emit.firstCall.args.should.eql(['g:before', 'arg1', 'arg2']); 139 | ev.emit.secondCall.args.should.eql(['g:after', 'arg1', 'arg2', 500]); 140 | done(); 141 | } 142 | 143 | wRun = util.eventWrap(ev, 'g', ['arg1', 'arg2'], run); 144 | takeTime(wRun); 145 | }); 146 | 147 | it('should emit an error event on error', function(done) { 148 | var ev = {emit: stub()}, wRun; 149 | 150 | function takeTime(cb) { 151 | cb(new Error('b')); 152 | } 153 | 154 | function run(err) { 155 | should(err).be.ok(); 156 | ev.emit.callCount.should.equal(3); 157 | ev.emit.firstCall.args.should.eql(['g:before', 'arg1']); 158 | ev.emit.secondCall.args.should.eql(['g:error', new Error('b'), 'arg1']); 159 | ev.emit.thirdCall.args.should.eql(['g:after', 'arg1', 0]); 160 | done(); 161 | } 162 | 163 | wRun = util.eventWrap(ev, 'g', ['arg1'], run); 164 | takeTime(wRun); 165 | }); 166 | }); 167 | }); 168 | --------------------------------------------------------------------------------