├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.js ├── index.js ├── package.json └── test ├── emojis.json └── index.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, workflow_dispatch] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [22.14.0] 12 | redis-version: [6.0.14] 13 | 14 | env: 15 | DO_COVERALLS: 22.14.0/6.0.14 16 | 17 | steps: 18 | - name: Setup Node ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Checkout from Git 24 | uses: actions/checkout@v2 25 | with: 26 | persist-credentials: false 27 | ssh-key: ${{ secrets.MERCATALYST_DEPLOY_KEY }} 28 | 29 | - name: Install ESLint and Mocha 30 | run: npm install -g eslint mocha 31 | 32 | - name: Install ESLine dependencies 33 | run: npm install globals @eslint/js 34 | 35 | - name: Run ESLint 36 | run: eslint . 37 | 38 | - name: Run npm install 39 | run: npm install 40 | 41 | - name: Setup Redis v${{ matrix.redis-version }} 42 | uses: supercharge/redis-github-action@1.1.0 43 | with: 44 | redis-version: ${{ matrix.redis-version }} 45 | 46 | - name: Run coverage report 47 | run: |- 48 | if [ "${{ matrix.node-version }}/${{ matrix.redis-version }}"x == "${DO_COVERALLS}"x ] 49 | then 50 | npm run coveralls 51 | else 52 | true 53 | fi 54 | env: 55 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 56 | 57 | - name: Run tests without coverage report 58 | run: |- 59 | if [ ! "${{ matrix.node-version }}/${{ matrix.redis-version }}"x == "${DO_COVERALLS}"x ] 60 | then 61 | npm test 62 | else 63 | true 64 | fi 65 | 66 | 67 | notify_slack: 68 | if: always() 69 | runs-on: ubuntu-latest 70 | needs: test 71 | steps: 72 | - name: Notify Slack 73 | uses: homoluctus/slatify@master 74 | with: 75 | type: ${{ needs.test.result }} 76 | job_name: '${{ github.repository }} - Tests ' 77 | channel: '#petty-cache' 78 | url: ${{ secrets.SLACK_WEBHOOK_URL }} 79 | commit: true 80 | token: ${{ secrets.GITHUB_TOKEN }} 81 | icon_emoji: ':mediocrebot:' 82 | username: 'mediocrebot' 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslint* 2 | .github 3 | .travis.yml 4 | test -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [3.5.0] - 2025-05-01 10 | ### Changed 11 | - Added the ability for `pettyCache.del` functions to support callbacks and promises. 12 | 13 | ## [3.4.0] - 2025-02-21 14 | ### Changed 15 | - Added the ability for `pettyCache.mutex` functions to support callbacks and promises. 16 | 17 | ## [3.3.0] - 2024-07-03 18 | ### Changed 19 | - Added the ability for `pettyCache.fetch` to support async functions. 20 | 21 | ## [3.2.0] - 2021-04-05 22 | ### Changed 23 | - Upgraded `redis` version to `~3.1.0`. 24 | 25 | ## [3.1.0] - 2021-02-16 26 | ### Added 27 | - Added the ability for `pettyCache.bulkFetch` to specify TTL options. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # petty-cache 2 | 3 | [![Build Status](https://github.com/mediocre/petty-cache/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/mediocre/petty-cache/actions?query=workflow%3Abuild+branch%3Amain) 4 | [![Coverage Status](https://coveralls.io/repos/github/mediocre/petty-cache/badge.svg?branch=main)](https://coveralls.io/github/mediocre/petty-cache?branch=main) 5 | 6 | A cache module for Node.js that uses a two-level cache (in-memory cache for recently accessed data plus Redis for distributed caching) with automatic serialization plus some extra features to avoid cache stampedes and thundering herds. 7 | 8 | Also includes mutex and semaphore distributed locking primitives. 9 | 10 | ## Features 11 | 12 | **Two-level cache** 13 | Data is cached for 2 to 5 seconds in memory to reduce the amount of calls to Redis. 14 | 15 | **Jitter** 16 | By default, cache values expire from Redis at a random time between 30 and 60 seconds. This helps to prevent a large amount of keys from expiring at the same time in order to avoid thundering herds (http://en.wikipedia.org/wiki/Thundering_herd_problem). 17 | 18 | **Double-checked locking** 19 | Functions executed on cache misses are wrapped in double-checked locking (http://en.wikipedia.org/wiki/Double-checked_locking). This ensures the function called on cache miss will only be executed once in order to prevent cache stampedes (http://en.wikipedia.org/wiki/Cache_stampede). 20 | 21 | **Mutex** 22 | Provides a distributed lock (mutex) with the ability to retry a specified number of times after a specified interval of time when acquiring a lock. 23 | 24 | **Semaphore** 25 | Provides a pool of distributed locks with the ability to release a slot back to the pool or remove the slot from the pool so that it's not used again. 26 | 27 | ## Getting Started 28 | 29 | ```javascript 30 | // Setup petty-cache 31 | var PettyCache = require('petty-cache'); 32 | var pettyCache = new PettyCache(); 33 | 34 | // Fetch some data 35 | pettyCache.fetch('key', function(callback) { 36 | // This function is called on a cache miss 37 | fs.readFile('file.txt', callback); 38 | }, function(err, value) { 39 | // This callback is called once petty-cache has loaded data from cache or executed the specified cache miss function 40 | console.log(value); 41 | }); 42 | ``` 43 | 44 | ## API 45 | 46 | ### new PettyCache([port, [host, [options]]]) 47 | 48 | Creates a new petty-cache client. `port`, `host`, and `options` are passed directly to [redis.createClient()](https://www.npmjs.com/package/redis#rediscreateclient). 49 | 50 | **Example** 51 | ```javascript 52 | const pettyCache = new PettyCache(6379, 'localhost', { auth_pass: 'secret' }); 53 | ``` 54 | 55 | ### new PettyCache(RedisClient) 56 | 57 | Alternatively, you can inject your own [RedisClient](https://www.npmjs.com/package/redis) into Petty Cache. 58 | 59 | **Example** 60 | ```javascript 61 | const redisClient = redis.createClient(); 62 | const pettyCache = new PettyCache(redisClient); 63 | ``` 64 | 65 | ### pettyCache.bulkFetch(keys, cacheMissFunction, [options,] callback) 66 | 67 | Attempts to retrieve the values of the keys specified in the `keys` array. Any keys that aren't found are passed to cacheMissFunction as an array along with a callback that takes an error and an object, expecting the keys of the object to be the keys passed to `cacheMissFunction` and the values to be the values that should be stored in cache for the corresponding key. Either way, the resulting error or key-value hash of all requested keys is passed to `callback`. 68 | 69 | **Example** 70 | 71 | ```javascript 72 | // Let's assume a and b are already cached as 1 and 2 73 | pettyCache.bulkFetch(['a', 'b', 'c', 'd'], function(keys, callback) { 74 | var results = {}; 75 | 76 | keys.forEach(function(key) { 77 | results[key] = key.toUpperCase(); 78 | }); 79 | 80 | callback(null, results); 81 | }, function(err, values) { 82 | console.log(values); // {a: 1, b: 2, c: 'C', d: 'D'} 83 | }); 84 | ``` 85 | 86 | **Options** 87 | 88 | ``` 89 | { 90 | ttl: 30000 // How long it should take for the cache entry to expire in milliseconds. Defaults to a random value between 30000 and 60000 (for jitter). 91 | } 92 | ``` 93 | 94 | ``` 95 | { 96 | // TTL can optional be specified with a range to pick a random value between `min` and `max` (for jitter). 97 | ttl: { 98 | min: 5000, 99 | max: 10000 100 | } 101 | } 102 | ``` 103 | 104 | ### pettyCache.bulkGet(keys, callback) 105 | 106 | Attempts to retrieve the values of the keys specified in the `keys` array. Returns a key-value hash of all specified keys with either the corresponding values from cache or `undefined` if a key was not found. 107 | 108 | **Example** 109 | 110 | ```javascript 111 | pettyCache.get(['key1', 'key2', 'key3'], function(err, values) { 112 | console.log(values); 113 | }); 114 | ``` 115 | 116 | ### pettyCache.bulkSet(values, [options,] callback) 117 | 118 | Unconditionally sets the values for the specified keys. 119 | 120 | **Example** 121 | 122 | ```javascript 123 | pettyCache.set({ key1: 'one', key2: 2, key3: 'three' }, function(err) { 124 | if (err) { 125 | // Handle error 126 | } 127 | }); 128 | ``` 129 | 130 | **Options** 131 | 132 | ``` 133 | { 134 | ttl: 30000 // How long it should take for the cache entries to expire in milliseconds. Defaults to a random value between 30000 and 60000 (for jitter). 135 | } 136 | ``` 137 | 138 | ``` 139 | { 140 | // TTL can optional be specified with a range to pick a random value between `min` and `max` (for jitter). 141 | ttl: { 142 | min: 5000, 143 | max: 10000 144 | } 145 | } 146 | ``` 147 | 148 | ### pettyCache.fetch(key, cacheMissFunction, [options,] callback) 149 | 150 | Attempts to retrieve the value from cache at the specified key. If it doesn't exist, it executes the specified cacheMissFunction that takes two parameters: an error and a value. `cacheMissFunction` should retrieve the expected value for the key from another source and pass it to the given callback. Either way, the resulting error or value is passed to `callback`. 151 | 152 | **Example** 153 | 154 | ```javascript 155 | pettyCache.fetch('key', function(callback) { 156 | // This function is called on a cache miss 157 | fs.readFile('file.txt', callback); 158 | }, function(err, value) { 159 | // This callback is called once petty-cache has loaded data from cache or executed the specified cache miss function 160 | console.log(value); 161 | }); 162 | ``` 163 | 164 | ```javascript 165 | pettyCache.fetch('key', async () => { 166 | // This function is called on a cache miss 167 | return await fs.readFile('file.txt'); 168 | }, function(err, value) { 169 | // This callback is called once petty-cache has loaded data from cache or executed the specified cache miss function 170 | console.log(value); 171 | }); 172 | ``` 173 | 174 | **Options** 175 | 176 | ``` 177 | { 178 | ttl: 30000 // How long it should take for the cache entry to expire in milliseconds. Defaults to a random value between 30000 and 60000 (for jitter). 179 | } 180 | ``` 181 | 182 | ``` 183 | { 184 | // TTL can optional be specified with a range to pick a random value between `min` and `max` (for jitter). 185 | ttl: { 186 | min: 5000, 187 | max: 10000 188 | } 189 | } 190 | ``` 191 | 192 | ### pettyCache.fetchAndRefresh(key, cacheMissFunction, [options,] callback) 193 | 194 | Similar to `pettyCache.fetch` but this method continually refreshes the data in cache by executing the specified cacheMissFunction before the TTL expires. 195 | 196 | **Example** 197 | 198 | ```javascript 199 | pettyCache.fetchAndRefresh('key', function(callback) { 200 | // This function is called on a cache miss and every TTL/2 milliseconds 201 | fs.readFile('file.txt', callback); 202 | }, function(err, value) { 203 | console.log(value); 204 | }); 205 | ``` 206 | 207 | **Options** 208 | 209 | ``` 210 | { 211 | ttl: 30000 // How long it should take for the cache entry to expire in milliseconds. Defaults to a random value between 30000 and 60000 (for jitter). 212 | } 213 | ``` 214 | 215 | ``` 216 | { 217 | // TTL can optional be specified with a range to pick a random value between `min` and `max` (for jitter). 218 | ttl: { 219 | min: 5000, 220 | max: 10000 221 | } 222 | } 223 | ``` 224 | 225 | ### pettyCache.get(key, callback) 226 | 227 | Attempts to retrieve the value from cache at the specified key. Returns `null` if the key doesn't exist. 228 | 229 | **Example** 230 | 231 | ```javascript 232 | pettyCache.get('key', function(err, value) { 233 | // `value` contains the value of the key if it was found in the in-memory cache or Redis. `value` is `null` if the key was not found. 234 | console.log(value); 235 | }); 236 | ``` 237 | 238 | ### pettyCache.patch(key, value, [options,] callback) 239 | 240 | Updates an object at the given key with the property values provided. Sends an error to the callback if the key does not exist. 241 | 242 | **Example** 243 | 244 | ```javascript 245 | pettyCache.patch('key', { a: 1 }, function(callback) { 246 | if (err) { 247 | // Handle redis or key not found error 248 | } 249 | 250 | // The object stored at 'key' now has a property 'a' with the value 1. Its other values are intact. 251 | }); 252 | ``` 253 | 254 | **Options** 255 | 256 | ``` 257 | { 258 | ttl: 30000 // How long it should take for the cache entry to expire in milliseconds. Defaults to a random value between 30000 and 60000 (for jitter). 259 | } 260 | ``` 261 | 262 | ``` 263 | { 264 | // TTL can optional be specified with a range to pick a random value between `min` and `max` (for jitter). 265 | ttl: { 266 | min: 5000, 267 | max: 10000 268 | } 269 | } 270 | ``` 271 | 272 | ### pettyCache.set(key, value, [options,] callback) 273 | 274 | Unconditionally sets a value for a given key. 275 | 276 | **Example** 277 | 278 | ```javascript 279 | pettyCache.set('key', { a: 'b' }, function(err) { 280 | if (err) { 281 | // Handle redis error 282 | } 283 | }); 284 | ``` 285 | 286 | **Options** 287 | 288 | ``` 289 | { 290 | ttl: 30000 // How long it should take for the cache entry to expire in milliseconds. Defaults to a random value between 30000 and 60000 (for jitter). 291 | } 292 | ``` 293 | 294 | ``` 295 | { 296 | // TTL can optional be specified with a range to pick a random value between `min` and `max` (for jitter). 297 | ttl: { 298 | min: 5000, 299 | max: 10000 300 | } 301 | } 302 | ``` 303 | 304 | ## Mutex 305 | 306 | ### pettyCache.mutex.lock(key, [options, [callback]]) 307 | 308 | Attempts to acquire a distributed lock for the specified key. Optionally retries a specified number of times by waiting a specified amount of time between attempts. 309 | 310 | ```javascript 311 | pettyCache.mutex.lock('key', { retry: { interval: 100, times: 5 }, ttl: 1000 }, function(err) { 312 | if (err) { 313 | // We weren't able to acquire the lock (even after trying 5 times every 100 milliseconds). 314 | } 315 | 316 | // We were able to acquire the lock. Do work and then unlock. 317 | pettyCache.mutex.unlock('key'); 318 | }); 319 | ``` 320 | 321 | **Options** 322 | 323 | ```javascript 324 | { 325 | retry: { 326 | interval: 100, // The time in milliseconds between attempts to acquire the lock. 327 | times: 1 // The number of attempts to acquire the lock. 328 | }, 329 | ttl: 1000 // The maximum amount of time to keep the lock locked before automatically being unlocked. 330 | } 331 | ``` 332 | 333 | ### pettyCache.mutex.unlock(key, [callback]) 334 | 335 | Releases the distributed lock for the specified key. 336 | 337 | ```javascript 338 | pettyCache.mutex.unlock('key', function(err) { 339 | if (err) { 340 | // We weren't able to reach Redis. Your lock will expire after its TTL, but you might want to log this error. 341 | } 342 | }); 343 | ``` 344 | 345 | ## Semaphore 346 | 347 | Provides a pool of distributed locks. Once a consumer acquires a lock they have the ability to release the lock back to the pool or mark the lock as "consumed" so that it's not used again. 348 | 349 | **Example** 350 | 351 | ```javascript 352 | // Create a new semaphore 353 | pettyCache.semaphore.retrieveOrCreate('key', { size: 10 }, function(err) { 354 | if (err) { 355 | // Aw, snap! We couldn't create the semaphore 356 | } 357 | 358 | // Acquire a lock from the semaphore's pool 359 | pettyCache.semaphore.acquireLock('key', { retry: { interval: 100, times: 5 }, ttl: 1000 }, function(err, index) { 360 | if (err) { 361 | // We couldn't acquire a lock from the semaphore's pool (even after trying 5 times every 100 milliseconds). 362 | } 363 | 364 | // We were able to acquire a lock from the semaphore's pool. Do work and then release the lock. 365 | pettyCache.semaphore.releaseLock('key', index, function(err) { 366 | if (err) { 367 | // We weren't able to reach Redis. Your lock will expire after its TTL, but you might want to log this error. 368 | } 369 | }); 370 | 371 | // Or, rather than releasing the lock back to the semaphore's pool you can mark the lock as "consumed" to prevent it from being used again. 372 | pettyCache.semaphore.consumeLock('key', index, function(err) { 373 | if (err) { 374 | // We weren't able to reach Redis. Your lock will expire after its TTL, but you might want to log this error. 375 | } 376 | }); 377 | }); 378 | }); 379 | ``` 380 | 381 | ### pettyCache.semaphore.acquireLock(key, [options, [callback]]) 382 | 383 | Attempts to acquire a lock from the semaphore's pool. Optionally retries a specified number of times by waiting a specified amount of time between attempts. 384 | 385 | ```javascript 386 | // Acquire a lock from the semaphore's pool 387 | pettyCache.semaphore.acquireLock('key', { retry: { interval: 100, times: 5 }, ttl: 1000 }, function(err, index) { 388 | if (err) { 389 | // We couldn't acquire a lock from the semaphore's pool (even after trying 5 times every 100 milliseconds). 390 | } 391 | 392 | // We were able to acquire a lock from the semaphore's pool. Do work and then release the lock. 393 | }); 394 | ``` 395 | 396 | **Options** 397 | 398 | ```javascript 399 | { 400 | retry: { 401 | interval: 100, // The time in milliseconds between attempts to acquire the lock. 402 | times: 1 // The number of attempts to acquire the lock. 403 | }, 404 | ttl: 1000 // The maximum amount of time to keep the lock locked before automatically being unlocked. 405 | } 406 | ``` 407 | 408 | ### pettyCache.semaphore.consumeLock(key, index, [callback]) 409 | 410 | Mark the lock at the specified index as "consumed" to prevent it from being used again. 411 | 412 | ```javascript 413 | pettyCache.semaphore.consumeLock('key', index, function(err) { 414 | if (err) { 415 | // We weren't able to reach Redis. Your lock will expire after its TTL, but you might want to log this error. 416 | } 417 | }); 418 | ``` 419 | 420 | ### pettyCache.semaphore.expand(key, size, [callback]) 421 | 422 | Expand the number of locks in the specified semaphore's pool. 423 | 424 | ```javascript 425 | pettyCache.semaphore.expand(key, 100, function(err) { 426 | if (err) { 427 | // We weren't able to expand the semaphore. 428 | } 429 | }); 430 | ``` 431 | 432 | ### pettyCache.semaphore.releaseLock(key, index, [callback]) 433 | 434 | Releases the lock at the specified index back to the semaphore's pool so that it can be used again. 435 | 436 | ```javascript 437 | pettyCache.semaphore.releaseLock('key', index, function(err) { 438 | if (err) { 439 | // We weren't able to reach Redis. Your lock will expire after its TTL, but you might want to log this error. 440 | } 441 | }); 442 | ``` 443 | 444 | ### pettyCache.semaphore.reset(key, [callback]) 445 | 446 | Resets the semaphore to its initial state effectively releasing all locks (even those that have been marked as "consumed"). 447 | 448 | ```javascript 449 | pettyCache.semaphore.reset('key', function(err) { 450 | if (err) { 451 | // We weren't able to reset the semaphore. 452 | } 453 | }); 454 | ``` 455 | 456 | ### pettyCache.semaphore.retrieveOrCreate(key, [options, [callback]]) 457 | 458 | Retrieves a previously created semaphore or creates a new semaphore with the optionally specified number of locks in its pool. 459 | 460 | ```javascript 461 | // Create a new semaphore 462 | pettyCache.semaphore.retrieveOrCreate('key', { size: 10 }, function(err) { 463 | if (err) { 464 | // Aw, snap! We couldn't create the semaphore 465 | } 466 | 467 | // Your semaphore was created. 468 | }); 469 | ``` 470 | **Options** 471 | 472 | ```javascript 473 | { 474 | size: 1 || function() { var x = 1 + 1; callback(null, x); } // The number of locks to create in the semaphore's pool. Optionally, size can be a `callback(err, size)` function. 475 | } 476 | ``` 477 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const globals = require('globals'); 2 | const js = require('@eslint/js'); 3 | 4 | module.exports = [ 5 | js.configs.recommended, 6 | { 7 | ignores: ['node_modules/*'], 8 | languageOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: 'module', 11 | parserOptions: { 12 | ecmaFeatures: { 13 | jsx: true 14 | } 15 | }, 16 | globals: { 17 | ...globals.es2020, 18 | ...globals.mocha, 19 | ...globals.node, 20 | //added for 'fetch()' access 21 | ...globals.serviceworker 22 | } 23 | }, 24 | rules: { 25 | 'brace-style': ['error', '1tbs', { allowSingleLine: true }], 26 | 'comma-dangle': ['error', 'never'], 27 | 'dot-notation': 'error', 28 | 'no-array-constructor': 'error', 29 | 'no-console': 'error', 30 | 'no-fallthrough': 'off', 31 | 'no-inline-comments': 'warn', 32 | 'no-trailing-spaces': 'error', 33 | 'no-unused-vars': ['error', { caughtErrors: 'none' }], 34 | 'object-curly-spacing': ['error', 'always'], 35 | quotes: ['error', 'single'], 36 | semi: ['error', 'always'], 37 | 'space-before-function-paren': ['error', { 38 | anonymous: 'never', 39 | named: 'never', 40 | asyncArrow: 'always' 41 | }] 42 | } 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const async = require('async'); 2 | const lock = require('lock').Lock(); 3 | const memoryCache = require('memory-cache'); 4 | const redis = require('redis'); 5 | 6 | function PettyCache() { 7 | const intervals = {}; 8 | let redisClient; 9 | 10 | if (arguments[0] instanceof redis.RedisClient) { 11 | redisClient = arguments[0]; 12 | } else { 13 | redisClient = redis.createClient(...arguments); 14 | } 15 | 16 | //eslint-disable-next-line no-console 17 | redisClient.on('error', err => console.warn(`Warning: Redis reported a client error: ${err}`)); 18 | 19 | function bulkGetFromRedis(keys, callback) { 20 | // Try to get values from Redis 21 | redisClient.mget(keys, function(err, data) { 22 | if (err) { 23 | return callback(err); 24 | } 25 | 26 | const values = {}; 27 | 28 | for (var i = 0; i < keys.length; i++) { 29 | var key = keys[i]; 30 | var value = data[i]; 31 | 32 | if (value === null) { 33 | values[key] = { exists: false }; 34 | continue; 35 | } 36 | 37 | values[key] = { exists: true, value: PettyCache.parse(value) }; 38 | } 39 | 40 | callback(null, values); 41 | }); 42 | } 43 | 44 | function getFromMemoryCache(key) { 45 | // Try to get value from memory cache 46 | const value = memoryCache.get(key); 47 | 48 | // Return value from the memory cache if it's not null 49 | if (value !== null) { 50 | return { exists: true, value }; 51 | } 52 | 53 | // If the key exists, the value in the memory cache is null 54 | if (memoryCache.keys().includes(key)) { 55 | return { exists: true, value: null }; 56 | } 57 | 58 | // The key wasn't found in memory cache 59 | return { exists: false }; 60 | } 61 | 62 | function getFromRedis(key, callback) { 63 | // Try to get value from Redis 64 | redisClient.get(key, function(err, data) { 65 | if (err) { 66 | return callback(err); 67 | } 68 | 69 | // Return if the key wasn't found in Redis 70 | if (data === null) { 71 | return callback(null, { exists: false }); 72 | } 73 | 74 | callback(null, { exists: true, value: PettyCache.parse(data) }); 75 | }); 76 | } 77 | 78 | function getTtl(options) { 79 | // Default TTL is 30-60 seconds 80 | const ttl = { 81 | max: 60000, 82 | min: 30000 83 | }; 84 | 85 | if (Object.prototype.hasOwnProperty.call(options, 'ttl')) { 86 | if (typeof options.ttl === 'number') { 87 | ttl.max = options.ttl; 88 | ttl.min = options.ttl; 89 | } else { 90 | if (Object.prototype.hasOwnProperty.call(options.ttl, 'max')) { 91 | ttl.max = options.ttl.max; 92 | } 93 | 94 | if (Object.prototype.hasOwnProperty.call(options.ttl, 'min')) { 95 | ttl.min = options.ttl.min; 96 | } 97 | } 98 | } 99 | 100 | return ttl; 101 | } 102 | 103 | /** 104 | * @param {Array} keys - An array of keys. 105 | */ 106 | this.bulkFetch = function(keys, func, options, callback) { 107 | // Options are optional 108 | if (!callback) { 109 | callback = options; 110 | options = {}; 111 | } 112 | 113 | // If there aren't any keys, return 114 | if (!keys.length) { 115 | return callback(null, {}); 116 | } 117 | 118 | const _keys = Array.from(new Set(keys)); 119 | const values = {}; 120 | 121 | // Try to get values from memory cache 122 | for (var i = _keys.length - 1; i >= 0; i--) { 123 | const key = _keys[i]; 124 | const result = getFromMemoryCache(key); 125 | 126 | if (result.exists) { 127 | values[key] = result.value; 128 | _keys.splice(i, 1); 129 | } 130 | } 131 | 132 | // If there aren't any keys left, return 133 | if (!_keys.length) { 134 | return callback(null, values); 135 | } 136 | 137 | const _this = this; 138 | 139 | // Try to get values from Redis 140 | bulkGetFromRedis(_keys, function(err, results) { 141 | if (err) { 142 | return callback(err); 143 | } 144 | 145 | for (var i = _keys.length - 1; i >= 0; i--) { 146 | const key = _keys[i]; 147 | const result = results[key]; 148 | 149 | if (result.exists) { 150 | _keys.splice(i, 1); 151 | values[key] = result.value; 152 | 153 | // Store value in memory cache with a short expiration 154 | memoryCache.put(key, result.value, random(2000, 5000)); 155 | } 156 | } 157 | 158 | // If there aren't any keys left, return 159 | if (!_keys.length) { 160 | return callback(null, values); 161 | } 162 | 163 | // Execute the specified function for remaining keys 164 | func(_keys, function(err, data) { 165 | if (err) { 166 | return callback(err); 167 | } 168 | 169 | Object.keys(data).forEach(key => values[key] = data[key]); 170 | 171 | _this.bulkSet(data, options, err => callback(err, values)); 172 | }); 173 | }); 174 | }; 175 | 176 | /** 177 | * @param {Array} keys - An array of keys. 178 | */ 179 | this.bulkGet = function(keys, callback) { 180 | // If there aren't any keys, return 181 | if (!keys.length) { 182 | return callback(null, {}); 183 | } 184 | 185 | const _keys = Array.from(new Set(keys)); 186 | const values = {}; 187 | 188 | // Try to get values from memory cache 189 | for (var i = _keys.length - 1; i >= 0; i--) { 190 | const key = _keys[i]; 191 | const result = getFromMemoryCache(key); 192 | 193 | if (result.exists) { 194 | values[key] = result.value; 195 | _keys.splice(i, 1); 196 | } 197 | } 198 | 199 | // If there aren't any keys left, return 200 | if (!_keys.length) { 201 | return callback(null, values); 202 | } 203 | 204 | // Try to get values from Redis 205 | bulkGetFromRedis(_keys, function(err, results) { 206 | if (err) { 207 | return callback(err); 208 | } 209 | 210 | for (var i = 0; i < _keys.length; i++) { 211 | var key = _keys[i]; 212 | var result = results[key]; 213 | 214 | if (!result.exists) { 215 | values[key] = null; 216 | continue; 217 | } 218 | 219 | values[key] = result.value; 220 | 221 | // Store value in memory cache with a short expiration 222 | memoryCache.put(key, result.value, random(2000, 5000)); 223 | } 224 | 225 | callback(null, values); 226 | }); 227 | }; 228 | 229 | this.bulkSet = function(values, options, callback) { 230 | // Options are optional 231 | if (!callback) { 232 | callback = options; 233 | options = {}; 234 | } 235 | 236 | // Get TTL based on specified options 237 | const ttl = getTtl(options); 238 | 239 | // Redis does not have a MSETEX command so we batch commands: http://redis.js.org/#api-clientbatchcommands 240 | const batch = redisClient.batch(); 241 | 242 | Object.keys(values).forEach(key => { 243 | const value = values[key]; 244 | 245 | // Store value in memory cache with a short expiration 246 | memoryCache.put(key, value, random(2000, 5000)); 247 | 248 | // Add Redis command 249 | batch.psetex(key, random(ttl.min, ttl.max), PettyCache.stringify(value)); 250 | }); 251 | 252 | batch.exec(function(err) { 253 | callback(err); 254 | }); 255 | }; 256 | 257 | this.del = function(key, callback) { 258 | const executor = () => { 259 | return new Promise((resolve, reject) => { 260 | redisClient.del(key, function(err) { 261 | if (err) { 262 | return reject(err); 263 | } 264 | 265 | memoryCache.del(key); 266 | resolve(); 267 | }); 268 | }); 269 | }; 270 | 271 | if (callback) { 272 | executor().then(result => callback(null, result)).catch(callback); 273 | } else { 274 | return executor(); 275 | } 276 | }; 277 | 278 | // Returns data from cache if available; 279 | // otherwise executes the specified function and places the results in cache before returning the data. 280 | this.fetch = function(key, func, options, callback) { 281 | options = options || {}; 282 | 283 | if (typeof options === 'function') { 284 | callback = options; 285 | options = {}; 286 | } 287 | 288 | // Default callback is a noop 289 | callback = callback || function() {}; 290 | 291 | // Try to get value from memory cache 292 | var result = getFromMemoryCache(key); 293 | 294 | // Return value from memory cache if it exists 295 | if (result.exists) { 296 | return callback(null, result.value); 297 | } 298 | 299 | const _this = this; 300 | 301 | // Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking 302 | lock(`fetch-memory-cache-lock-${key}`, function(releaseMemoryCacheLock) { 303 | async.reflect(function(callback) { 304 | // Try to get value from memory cache 305 | result = getFromMemoryCache(key); 306 | 307 | // Return value from memory cache if it exists 308 | if (result.exists) { 309 | return callback(null, result.value); 310 | } 311 | 312 | // Try to get value from Redis 313 | getFromRedis(key, function(err, result) { 314 | if (err) { 315 | return callback(err); 316 | } 317 | 318 | // Return value from Redis if it exists 319 | if (result.exists) { 320 | memoryCache.put(key, result.value, random(2000, 5000)); 321 | return callback(null, result.value); 322 | } 323 | 324 | // Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking 325 | lock(`fetch-redis-lock-${key}`, function(releaseRedisLock) { 326 | async.reflect(function(callback) { 327 | // Try to get value from memory cache 328 | result = getFromMemoryCache(key); 329 | 330 | // Return value from memory cache if it exists 331 | if (result.exists) { 332 | return callback(null, result.value); 333 | } 334 | 335 | // Try to get value from Redis 336 | getFromRedis(key, async function(err, result) { 337 | if (err) { 338 | return callback(err); 339 | } 340 | 341 | // Return value from Redis if it exists 342 | if (result.exists) { 343 | memoryCache.put(key, result.value, random(2000, 5000)); 344 | return callback(null, result.value); 345 | } 346 | 347 | // Execute the specified function and place the results in cache before returning the data 348 | if (func.length === 0) { 349 | // If the function doesn't have any arguments, there wasn't a callback provided 350 | try { 351 | const data = await func(); 352 | 353 | _this.set(key, data, options, function(err) { 354 | callback(err, data); 355 | }); 356 | } catch(err) { 357 | callback(err); 358 | } 359 | } else { 360 | // If the function has arguments, there was a callback provided 361 | func(function(err, data) { 362 | if (err) { 363 | return callback(err); 364 | } 365 | 366 | _this.set(key, data, options, function(err) { 367 | callback(err, data); 368 | }); 369 | }); 370 | } 371 | }); 372 | })(releaseRedisLock(function(err, result) { 373 | if (result.error) { 374 | return callback(result.error); 375 | } 376 | 377 | callback(null, result.value); 378 | })); 379 | }); 380 | }); 381 | })(releaseMemoryCacheLock(function(err, result) { 382 | if (result.error) { 383 | return callback(result.error); 384 | } 385 | 386 | callback(null, result.value); 387 | })); 388 | }); 389 | }; 390 | 391 | this.fetchAndRefresh = function(key, func, options, callback) { 392 | options = options || {}; 393 | 394 | if (typeof options === 'function') { 395 | callback = options; 396 | options = {}; 397 | } 398 | 399 | // Get TTL based on specified options 400 | const ttl = getTtl(options); 401 | 402 | // Default callback is a noop 403 | callback = callback || function() {}; 404 | 405 | const _this = this; 406 | 407 | if (!intervals[key]) { 408 | const delay = ttl.min / 2; 409 | 410 | intervals[key] = setInterval(function() { 411 | // This distributed lock prevents multiple clients from executing func at the same time 412 | _this.mutex.lock(`interval-${key}`, { ttl: delay - 100 }, function(err) { 413 | if (err) { 414 | return; 415 | } 416 | 417 | // Execute the specified function and update cache 418 | func(function(err, data) { 419 | if (err) { 420 | return; 421 | } 422 | 423 | _this.set(key, data, options); 424 | }); 425 | }); 426 | }, delay); 427 | } 428 | 429 | this.fetch(key, func, options, callback); 430 | }; 431 | 432 | this.get = function(key, callback) { 433 | // Try to get value from memory cache 434 | let result = getFromMemoryCache(key); 435 | 436 | // Return value from memory cache if it exists 437 | if (result.exists) { 438 | return callback(null, result.value); 439 | } 440 | 441 | // Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking 442 | lock(`get-memory-cache-lock-${key}`, function(releaseMemoryCacheLock) { 443 | async.reflect(function(callback) { 444 | // Try to get value from memory cache 445 | result = getFromMemoryCache(key); 446 | 447 | // Return value from memory cache if it exists 448 | if (result.exists) { 449 | return callback(null, result.value); 450 | } 451 | 452 | getFromRedis(key, function(err, result) { 453 | if (err) { 454 | return callback(err); 455 | } 456 | 457 | if (!result.exists) { 458 | return callback(null, null); 459 | } 460 | 461 | memoryCache.put(key, result.value, random(2000, 5000)); 462 | callback(null, result.value); 463 | }); 464 | })(releaseMemoryCacheLock(function(err, result) { 465 | if (result.error) { 466 | return callback(result.error); 467 | } 468 | 469 | callback(null, result.value); 470 | })); 471 | }); 472 | }; 473 | 474 | this.mutex = { 475 | lock: (key, options, callback) => { 476 | // Options are optional 477 | if (!callback && typeof options === 'function') { 478 | callback = options; 479 | options = {}; 480 | } 481 | 482 | options = options || {}; 483 | 484 | options.retry = Object.hasOwn(options, 'retry') ? options.retry : {}; 485 | options.retry.interval = Object.hasOwn(options.retry, 'interval') ? options.retry.interval : 100; 486 | options.retry.times = Object.hasOwn(options.retry, 'times') ? options.retry.times : 1; 487 | options.ttl = Object.hasOwn(options, 'ttl') ? options.ttl : 1000; 488 | 489 | const executor = () => { 490 | return new Promise((resolve, reject) => { 491 | async.retry({ interval: options.retry.interval, times: options.retry.times }, callback => { 492 | redisClient.set(key, '1', 'NX', 'PX', options.ttl, function(err, res) { 493 | if (err) { 494 | return callback(err); 495 | } 496 | 497 | if (!res) { 498 | return callback(new Error()); 499 | } 500 | 501 | if (res !== 'OK') { 502 | return callback(new Error(res)); 503 | } 504 | 505 | callback(); 506 | }); 507 | }, function(err) { 508 | if (err) { 509 | return reject(err); 510 | } 511 | 512 | resolve(); 513 | }); 514 | }); 515 | }; 516 | 517 | if (callback) { 518 | executor().then(result => callback(null, result)).catch(callback); 519 | } else { 520 | return executor(); 521 | } 522 | }, 523 | unlock: (key, callback) => { 524 | const executor = () => { 525 | return new Promise((resolve, reject) => { 526 | redisClient.del(key, function(err) { 527 | if (err) { 528 | return reject(err); 529 | } 530 | 531 | resolve(); 532 | }); 533 | }); 534 | }; 535 | 536 | if (callback) { 537 | executor().then(result => callback(null, result)).catch(callback); 538 | } else { 539 | return executor(); 540 | } 541 | } 542 | }; 543 | 544 | this.patch = function(key, value, options, callback) { 545 | if (!callback) { 546 | callback = options; 547 | options = {}; 548 | } 549 | 550 | const _this = this; 551 | 552 | this.get(key, function(err, data) { 553 | if (err) { 554 | return callback(err); 555 | } 556 | 557 | if (!data) { 558 | return callback(new Error(`Key ${key} does not exist`)); 559 | } 560 | 561 | for (var k in value) { 562 | data[k] = value[k]; 563 | } 564 | 565 | _this.set(key, data, options, callback); 566 | }); 567 | }; 568 | 569 | this.semaphore = { 570 | acquireLock: function(key, options, callback) { 571 | // Options are optional 572 | if (!callback && typeof options === 'function') { 573 | callback = options; 574 | options = {}; 575 | } 576 | 577 | options = options || {}; 578 | 579 | options.retry = Object.prototype.hasOwnProperty.call(options, 'retry') ? options.retry : {}; 580 | options.retry.interval = Object.prototype.hasOwnProperty.call(options.retry, 'interval') ? options.retry.interval : 100; 581 | options.retry.times = Object.prototype.hasOwnProperty.call(options.retry, 'times') ? options.retry.times : 1; 582 | options.ttl = Object.prototype.hasOwnProperty.call(options, 'ttl') ? options.ttl : 1000; 583 | 584 | const _this = this; 585 | 586 | async.retry({ interval: options.retry.interval, times: options.retry.times }, function(callback) { 587 | // Mutex lock around semaphore 588 | _this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, function(err) { 589 | if (err) { 590 | return callback(err); 591 | } 592 | 593 | redisClient.get(key, function(err, data) { 594 | // If we encountered an error, unlock the mutex lock and return error 595 | if (err) { 596 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 597 | } 598 | 599 | // If we don't have a previously created semaphore, unlock the mutex lock and return error 600 | if (!data) { 601 | return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't exist.`)); }); 602 | } 603 | 604 | var pool = JSON.parse(data); 605 | 606 | // Try to find a slot that's available. 607 | var index = pool.findIndex(s => s.status === 'available'); 608 | 609 | if (index === -1) { 610 | index = pool.findIndex(s => s.ttl <= Date.now()); 611 | } 612 | 613 | // If we don't have a previously created semaphore, unlock the mutex lock and return error 614 | if (index === -1) { 615 | return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't have any available slots.`)); }); 616 | } 617 | 618 | pool[index] = { status: 'acquired', ttl: Date.now() + options.ttl }; 619 | 620 | redisClient.set(key, JSON.stringify(pool), function(err) { 621 | if (err) { 622 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 623 | } 624 | 625 | _this.mutex.unlock(`lock:${key}`, () => { callback(null, index); }); 626 | }); 627 | }); 628 | }); 629 | }, callback); 630 | }, 631 | consumeLock: function(key, index, callback) { 632 | callback = callback || function() {}; 633 | 634 | const _this = this; 635 | 636 | // Mutex lock around semaphore 637 | _this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, function(err) { 638 | if (err) { 639 | return callback(err); 640 | } 641 | 642 | redisClient.get(key, function(err, data) { 643 | // If we encountered an error, unlock the mutex lock and return error 644 | if (err) { 645 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 646 | } 647 | 648 | // If we don't have a previously created semaphore, unlock the mutex lock and return error 649 | if (!data) { 650 | return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't exist.`)); }); 651 | } 652 | 653 | var pool = JSON.parse(data); 654 | 655 | // Ensure index exists. 656 | if (pool.length <= index) { 657 | return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Index ${index} for semaphore ${key} is invalid.`)); }); 658 | } 659 | 660 | pool[index] = { status: 'consumed' }; 661 | 662 | // Ensure at least one slot isn't consumed 663 | if (pool.every(s => s.status === 'consumed')) { 664 | pool[index] = { status: 'available' }; 665 | } 666 | 667 | redisClient.set(key, JSON.stringify(pool), function(err) { 668 | if (err) { 669 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 670 | } 671 | 672 | _this.mutex.unlock(`lock:${key}`, () => { callback(); }); 673 | }); 674 | }); 675 | }); 676 | }, 677 | expand: function(key, size, callback) { 678 | callback = callback || function() {}; 679 | 680 | const _this = this; 681 | 682 | _this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, function(err) { 683 | if (err) { 684 | return callback(err); 685 | } 686 | 687 | redisClient.get(key, function(err, data) { 688 | // If we encountered an error, unlock the mutex lock and return error 689 | if (err) { 690 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 691 | } 692 | 693 | // If we don't have a previously created semaphore, unlock the mutex lock and return error 694 | if (!data) { 695 | return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't exist.`)); }); 696 | } 697 | 698 | var pool = JSON.parse(data); 699 | 700 | if (pool.length > size) { 701 | return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Cannot shrink pool, size is ${pool.length} and you requested a size of ${size}.`)); }); 702 | } 703 | 704 | if (pool.length === size) { 705 | return _this.mutex.unlock(`lock:${key}`, () => callback()); 706 | } 707 | 708 | pool = pool.concat(Array(size - pool.length).fill({ status: 'available' })); 709 | 710 | redisClient.set(key, JSON.stringify(pool), function(err) { 711 | if (err) { 712 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 713 | } 714 | 715 | _this.mutex.unlock(`lock:${key}`, () => { callback(); }); 716 | }); 717 | }); 718 | }); 719 | }, 720 | releaseLock: function(key, index, callback) { 721 | callback = callback || function() {}; 722 | 723 | const _this = this; 724 | 725 | // Mutex lock around semaphore 726 | _this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, function(err) { 727 | if (err) { 728 | return callback(err); 729 | } 730 | 731 | redisClient.get(key, function(err, data) { 732 | // If we encountered an error, unlock the mutex lock and return error 733 | if (err) { 734 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 735 | } 736 | 737 | // If we don't have a previously created semaphore, unlock the mutex lock and return error 738 | if (!data) { 739 | return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't exist.`)); }); 740 | } 741 | 742 | var pool = JSON.parse(data); 743 | 744 | // Ensure index exists. 745 | if (pool.length <= index) { 746 | return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Index ${index} for semaphore ${key} is invalid.`)); }); 747 | } 748 | 749 | pool[index] = { status: 'available' }; 750 | 751 | redisClient.set(key, JSON.stringify(pool), function(err) { 752 | if (err) { 753 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 754 | } 755 | 756 | _this.mutex.unlock(`lock:${key}`, () => { callback(); }); 757 | }); 758 | }); 759 | }); 760 | }, 761 | reset: function(key, callback) { 762 | callback = callback || function() {}; 763 | 764 | const _this = this; 765 | 766 | // Mutex lock around semaphore 767 | this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, function(err) { 768 | if (err) { 769 | return callback(err); 770 | } 771 | 772 | // Try to get previously created semaphore 773 | redisClient.get(key, function(err, data) { 774 | // If we encountered an error, unlock the mutex lock and return error 775 | if (err) { 776 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 777 | } 778 | 779 | // If we don't have a previously created semaphore, unlock the mutex lock and return error 780 | if (!data) { 781 | return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't exist.`)); }); 782 | } 783 | 784 | var pool = JSON.parse(data); 785 | pool = Array(pool.length).fill({ status: 'available' }); 786 | 787 | redisClient.set(key, JSON.stringify(pool), function(err) { 788 | if (err) { 789 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 790 | } 791 | 792 | _this.mutex.unlock(`lock:${key}`, () => { callback(null, pool); }); 793 | }); 794 | }); 795 | }); 796 | }, 797 | retrieveOrCreate: function(key, options, callback) { 798 | // Options are optional 799 | if (!callback && typeof options === 'function') { 800 | callback = options; 801 | options = {}; 802 | } 803 | 804 | callback = callback || function() {}; 805 | options = options || {}; 806 | 807 | const _this = this; 808 | 809 | // Mutex lock around semaphore retrival or creation 810 | this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, function(err) { 811 | if (err) { 812 | return callback(err); 813 | } 814 | 815 | // Try to get previously created semaphore 816 | redisClient.get(key, function(err, data) { 817 | // If we encountered an error, unlock the mutex lock and return error 818 | if (err) { 819 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 820 | } 821 | 822 | // If we retreived a previously created semaphore, unlock the mutex lock and return 823 | if (data) { 824 | return _this.mutex.unlock(`lock:${key}`, () => { callback(null, JSON.parse(data)); }); 825 | } 826 | 827 | var getSize = function(callback) { 828 | if (typeof options.size === 'function') { 829 | return options.size(callback); 830 | } 831 | 832 | callback(null, Object.prototype.hasOwnProperty.call(options, 'size') ? options.size : 1); 833 | }; 834 | 835 | getSize(function(err, size) { 836 | // If we encountered an error, unlock the mutex lock and return error 837 | if (err) { 838 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 839 | } 840 | 841 | var pool = Array(Math.max(size, 1)).fill({ status: 'available' }); 842 | 843 | redisClient.set(key, JSON.stringify(pool), function(err) { 844 | if (err) { 845 | return _this.mutex.unlock(`lock:${key}`, () => { callback(err); }); 846 | } 847 | 848 | _this.mutex.unlock(`lock:${key}`, () => { callback(null, pool); }); 849 | }); 850 | }); 851 | }); 852 | }); 853 | } 854 | }; 855 | 856 | this.set = function(key, value, options, callback) { 857 | options = options || {}; 858 | 859 | if (typeof options === 'function') { 860 | callback = options; 861 | options = {}; 862 | } 863 | 864 | // Get TTL based on specified options 865 | const ttl = getTtl(options); 866 | 867 | // Default callback is a noop 868 | callback = callback || function() {}; 869 | 870 | // Store value in memory cache with a short expiration 871 | memoryCache.put(key, value, random(2000, 5000)); 872 | 873 | // Store value is Redis 874 | redisClient.psetex(key, random(ttl.min, ttl.max), PettyCache.stringify(value), callback); 875 | }; 876 | 877 | // Semaphore functions need to be bound to the main PettyCache object 878 | for (var method in this.semaphore) { 879 | this.semaphore[method] = this.semaphore[method].bind(this); 880 | } 881 | } 882 | 883 | function random(min, max) { 884 | if (min === max) { 885 | return min; 886 | } 887 | 888 | return Math.floor(Math.random() * (max - min + 1) + min); 889 | } 890 | 891 | PettyCache.parse = function(text) { 892 | return JSON.parse(text, function(k, v) { 893 | if (v === '__NaN') { 894 | return NaN; 895 | } else if (v === '__null') { 896 | return null; 897 | } else if (v === '__undefined') { 898 | return undefined; 899 | } 900 | 901 | return v; 902 | }); 903 | }; 904 | 905 | PettyCache.stringify = function(value) { 906 | return JSON.stringify(value, function(k, v) { 907 | if (typeof v === 'number' && isNaN(v)) { 908 | return '__NaN'; 909 | } else if (v === null) { 910 | return '__null'; 911 | } else if (v === undefined) { 912 | return '__undefined'; 913 | } 914 | 915 | return v; 916 | }); 917 | }; 918 | 919 | module.exports = PettyCache; 920 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "A cache module for node.js that uses a two-level cache (in-memory cache for recently accessed data plus Redis for distributed caching) with some extra features to avoid cache stampedes and thundering herds.", 3 | "dependencies": { 4 | "async": "~3.2.6", 5 | "lock": "~1.1.0", 6 | "memory-cache": "~0.2.0", 7 | "redis": "~3.1.0" 8 | }, 9 | "devDependencies": { 10 | "@eslint/js": "*", 11 | "globals": "*", 12 | "coveralls": "*", 13 | "mocha": "*", 14 | "nyc": "*" 15 | }, 16 | "homepage": "https://github.com/mediocre/petty-cache", 17 | "keywords": [ 18 | "cache", 19 | "lock", 20 | "mutex", 21 | "redis", 22 | "semaphore" 23 | ], 24 | "license": "Apache-2.0", 25 | "main": "index.js", 26 | "name": "petty-cache", 27 | "scripts": { 28 | "coveralls": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 29 | "test": "mocha --exit --reporter spec" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/mediocre/petty-cache.git" 34 | }, 35 | "version": "3.5.0" 36 | } 37 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const timers = require('node:timers/promises'); 3 | 4 | const async = require('async'); 5 | const memoryCache = require('memory-cache'); 6 | const redis = require('redis'); 7 | 8 | const PettyCache = require('../index.js'); 9 | 10 | const redisClient = redis.createClient(); 11 | const pettyCache = new PettyCache(redisClient); 12 | 13 | describe('new PettyCache()', function() { 14 | it('new PettyCache()', function(done) { 15 | this.timeout(7000); 16 | 17 | const key = Math.random().toString(); 18 | const newPettyCache = new PettyCache(); 19 | 20 | newPettyCache.fetch(key, function(callback) { 21 | return callback(null, { foo: 'bar' }); 22 | }, function() { 23 | newPettyCache.fetch(key, function() { 24 | throw 'This function should not be called'; 25 | }, function(err, data) { 26 | assert.equal(data.foo, 'bar'); 27 | 28 | // Wait for memory cache to expire 29 | setTimeout(function() { 30 | newPettyCache.fetch(key, function() { 31 | throw 'This function should not be called'; 32 | }, function(err, data) { 33 | assert.strictEqual(data.foo, 'bar'); 34 | done(); 35 | }); 36 | }, 6000); 37 | }); 38 | }); 39 | }); 40 | 41 | it('new PettyCache(port, host)', function(done) { 42 | this.timeout(7000); 43 | 44 | const key = Math.random().toString(); 45 | const newPettyCache = new PettyCache(6379, 'localhost'); 46 | 47 | newPettyCache.fetch(key, function(callback) { 48 | return callback(null, { foo: 'bar' }); 49 | }, function() { 50 | newPettyCache.fetch(key, function() { 51 | throw 'This function should not be called'; 52 | }, function(err, data) { 53 | assert.equal(data.foo, 'bar'); 54 | 55 | // Wait for memory cache to expire 56 | setTimeout(function() { 57 | newPettyCache.fetch(key, function() { 58 | throw 'This function should not be called'; 59 | }, function(err, data) { 60 | assert.strictEqual(data.foo, 'bar'); 61 | done(); 62 | }); 63 | }, 6000); 64 | }); 65 | }); 66 | }); 67 | 68 | it('new PettyCache(redisClient)', function(done) { 69 | this.timeout(7000); 70 | 71 | const key = Math.random().toString(); 72 | const redisClient = redis.createClient(); 73 | const newPettyCache = new PettyCache(redisClient); 74 | 75 | newPettyCache.fetch(key, function(callback) { 76 | return callback(null, { foo: 'bar' }); 77 | }, function() { 78 | newPettyCache.fetch(key, function() { 79 | throw 'This function should not be called'; 80 | }, function(err, data) { 81 | assert.equal(data.foo, 'bar'); 82 | 83 | // Wait for memory cache to expire 84 | setTimeout(function() { 85 | newPettyCache.fetch(key, function() { 86 | throw 'This function should not be called'; 87 | }, function(err, data) { 88 | assert.strictEqual(data.foo, 'bar'); 89 | done(); 90 | }); 91 | }, 6000); 92 | }); 93 | }); 94 | }); 95 | }); 96 | 97 | describe('memory-cache', function() { 98 | it('memoryCache.put(key, \'\')', function(done) { 99 | var key = Math.random().toString(); 100 | 101 | memoryCache.put(key, '', 1000); 102 | assert(memoryCache.keys().includes(key)); 103 | assert.strictEqual(memoryCache.get(key), ''); 104 | 105 | // Wait for memory cache to expire 106 | setTimeout(function() { 107 | assert(!memoryCache.keys().includes(key)); 108 | assert.strictEqual(memoryCache.get(key), null); 109 | done(); 110 | }, 1001); 111 | }); 112 | 113 | it('memoryCache.put(key, 0)', function(done) { 114 | var key = Math.random().toString(); 115 | 116 | memoryCache.put(key, 0, 1000); 117 | assert(memoryCache.keys().includes(key)); 118 | assert.strictEqual(memoryCache.get(key), 0); 119 | 120 | // Wait for memory cache to expire 121 | setTimeout(function() { 122 | assert(!memoryCache.keys().includes(key)); 123 | assert.strictEqual(memoryCache.get(key), null); 124 | done(); 125 | }, 1001); 126 | }); 127 | 128 | it('memoryCache.put(key, false)', function(done) { 129 | var key = Math.random().toString(); 130 | 131 | memoryCache.put(key, false, 1000); 132 | assert(memoryCache.keys().includes(key)); 133 | assert.strictEqual(memoryCache.get(key), false); 134 | 135 | // Wait for memory cache to expire 136 | setTimeout(function() { 137 | assert(!memoryCache.keys().includes(key)); 138 | assert.strictEqual(memoryCache.get(key), null); 139 | done(); 140 | }, 1001); 141 | }); 142 | 143 | it('memoryCache.put(key, NaN)', function(done) { 144 | var key = Math.random().toString(); 145 | 146 | memoryCache.put(key, NaN, 1000); 147 | assert(memoryCache.keys().includes(key)); 148 | assert(isNaN(memoryCache.get(key))); 149 | 150 | // Wait for memory cache to expire 151 | setTimeout(function() { 152 | assert(!memoryCache.keys().includes(key)); 153 | assert.strictEqual(memoryCache.get(key), null); 154 | done(); 155 | }, 1001); 156 | }); 157 | 158 | it('memoryCache.put(key, null)', function(done) { 159 | var key = Math.random().toString(); 160 | 161 | memoryCache.put(key, null, 1000); 162 | assert(memoryCache.keys().includes(key)); 163 | assert.strictEqual(memoryCache.get(key), null); 164 | 165 | // Wait for memory cache to expire 166 | setTimeout(function() { 167 | assert(!memoryCache.keys().includes(key)); 168 | assert.strictEqual(memoryCache.get(key), null); 169 | done(); 170 | }, 1001); 171 | }); 172 | 173 | it('memoryCache.put(key, undefined)', function(done) { 174 | var key = Math.random().toString(); 175 | 176 | memoryCache.put(key, undefined, 1000); 177 | assert(memoryCache.keys().includes(key)); 178 | assert.strictEqual(memoryCache.get(key), undefined); 179 | 180 | // Wait for memory cache to expire 181 | setTimeout(function() { 182 | assert(!memoryCache.keys().includes(key)); 183 | assert.strictEqual(memoryCache.get(key), null); 184 | done(); 185 | }, 1001); 186 | }); 187 | }); 188 | 189 | describe('PettyCache.bulkFetch', function() { 190 | it('PettyCache.bulkFetch', function(done) { 191 | this.timeout(7000); 192 | 193 | pettyCache.set('a', 1, function() { 194 | pettyCache.set('b', '2', function() { 195 | pettyCache.bulkFetch(['a', 'b', 'c', 'd'], function(keys, callback) { 196 | assert(keys.length === 2); 197 | 198 | callback(null, { 'c': [3], 'd': { num: 4 } }); 199 | }, function(err, values) { 200 | assert.strictEqual(values.a, 1); 201 | assert.strictEqual(values.b, '2'); 202 | assert.strictEqual(values.c[0], 3); 203 | assert.strictEqual(values.d.num, 4); 204 | 205 | // Call bulkFetch again to ensure memory serialization is working as expected. 206 | pettyCache.bulkFetch(['a', 'b', 'c', 'd'], function() { 207 | throw 'This function should not be called'; 208 | }, function(err, values) { 209 | assert.strictEqual(values.a, 1); 210 | assert.strictEqual(values.b, '2'); 211 | assert.strictEqual(values.c[0], 3); 212 | assert.strictEqual(values.d.num, 4); 213 | 214 | // Wait for memory cache to expire 215 | setTimeout(function() { 216 | pettyCache.bulkFetch(['a', 'b', 'c', 'd'], function() { 217 | throw 'This function should not be called'; 218 | }, function(err, values) { 219 | assert.strictEqual(values.a, 1); 220 | assert.strictEqual(values.b, '2'); 221 | assert.strictEqual(values.c[0], 3); 222 | assert.strictEqual(values.d.num, 4); 223 | 224 | // Call bulkFetch again to ensure memory serialization is working as expected. 225 | pettyCache.bulkFetch(['a', 'b', 'c', 'd'], function() { 226 | throw 'This function should not be called'; 227 | }, function(err, values) { 228 | assert.strictEqual(values.a, 1); 229 | assert.strictEqual(values.b, '2'); 230 | assert.strictEqual(values.c[0], 3); 231 | assert.strictEqual(values.d.num, 4); 232 | done(); 233 | }); 234 | }); 235 | }, 5001); 236 | }); 237 | }); 238 | }); 239 | }); 240 | }); 241 | 242 | it('PettyCache.bulkFetch should cache null values returned by func', function(done) { 243 | this.timeout(7000); 244 | 245 | var key1 = Math.random().toString(); 246 | var key2 = Math.random().toString(); 247 | 248 | pettyCache.bulkFetch([key1, key2], function(keys, callback) { 249 | assert.strictEqual(keys.length, 2); 250 | assert(keys.some(k => k === key1)); 251 | assert(keys.some(k => k === key2)); 252 | 253 | var values = {}; 254 | 255 | values[key1] = '1'; 256 | values[key2] = null; 257 | 258 | callback(null, values); 259 | }, function(err) { 260 | assert.ifError(err); 261 | 262 | pettyCache.bulkFetch([key1, key2], function() { 263 | throw 'This function should not be called'; 264 | }, function(err, data) { 265 | assert.strictEqual(Object.keys(data).length, 2); 266 | assert.strictEqual(data[key1], '1'); 267 | assert.strictEqual(data[key2], null); 268 | 269 | // Wait for memory cache to expire 270 | setTimeout(function() { 271 | pettyCache.bulkFetch([key1, key2], function() { 272 | throw 'This function should not be called'; 273 | }, function(err, data) { 274 | assert.strictEqual(Object.keys(data).length, 2); 275 | assert.strictEqual(data[key1], '1'); 276 | assert.strictEqual(data[key2], null); 277 | 278 | done(); 279 | }); 280 | }, 6000); 281 | }); 282 | }); 283 | }); 284 | 285 | it('PettyCache.bulkFetch should return empty object when no keys are passed', function(done) { 286 | pettyCache.bulkFetch([], function() { 287 | throw 'This function should not be called'; 288 | }, function(err, values) { 289 | assert.ifError(err); 290 | assert.deepEqual(values, {}); 291 | done(); 292 | }); 293 | }); 294 | 295 | it('PettyCache.bulkFetch should return error if func returns error', function(done) { 296 | pettyCache.bulkFetch([Math.random().toString()], function(keys, callback) { 297 | callback(new Error('PettyCache.bulkFetch should return error if func returns error')); 298 | }, function(err, values) { 299 | assert(err); 300 | assert.strictEqual(err.message, 'PettyCache.bulkFetch should return error if func returns error'); 301 | assert(!values); 302 | done(); 303 | }); 304 | }); 305 | 306 | it('PettyCache.bulkFetch should run func again after TTL', function(done) { 307 | this.timeout(7000); 308 | 309 | const keys = [Math.random().toString(), Math.random().toString()]; 310 | var numberOfFuncCalls = 0; 311 | 312 | const func = function(keys, callback) { 313 | numberOfFuncCalls++; 314 | 315 | const results = {}; 316 | results[keys[0]] = numberOfFuncCalls; 317 | results[keys[1]] = numberOfFuncCalls; 318 | 319 | callback(null, results); 320 | }; 321 | 322 | pettyCache.bulkFetch(keys, func, { ttl: 6000 }, function(err, results) { 323 | assert.ifError(err); 324 | assert.strictEqual(results[keys[0]], 1); 325 | assert.strictEqual(results[keys[1]], 1); 326 | 327 | pettyCache.bulkGet(keys, function(err, results) { 328 | assert.ifError(err); 329 | assert.strictEqual(results[keys[0]], 1); 330 | assert.strictEqual(results[keys[1]], 1); 331 | }); 332 | 333 | setTimeout(function() { 334 | pettyCache.bulkGet(keys, function(err, results) { 335 | assert.ifError(err); 336 | assert.strictEqual(results[keys[0]], null); 337 | assert.strictEqual(results[keys[1]], null); 338 | 339 | pettyCache.bulkFetch(keys, func, { ttl: 6000 }, function(err, results) { 340 | assert.ifError(err); 341 | assert.strictEqual(results[keys[0]], 2); 342 | assert.strictEqual(results[keys[1]], 2); 343 | 344 | pettyCache.bulkGet(keys, function(err, results) { 345 | assert.ifError(err); 346 | assert.strictEqual(results[keys[0]], 2); 347 | assert.strictEqual(results[keys[1]], 2); 348 | done(); 349 | }); 350 | }); 351 | }); 352 | }, 6001); 353 | }); 354 | }); 355 | }); 356 | 357 | describe('PettyCache.bulkGet', function() { 358 | it('PettyCache.bulkGet should return values', function(done) { 359 | this.timeout(6000); 360 | 361 | var key1 = Math.random().toString(); 362 | var key2 = Math.random().toString(); 363 | var key3 = Math.random().toString(); 364 | 365 | pettyCache.set(key1, '1', function() { 366 | pettyCache.set(key2, '2', function() { 367 | pettyCache.set(key3, '3', function() { 368 | pettyCache.bulkGet([key1, key2, key3], function(err, values) { 369 | assert.strictEqual(Object.keys(values).length, 3); 370 | assert.strictEqual(values[key1], '1'); 371 | assert.strictEqual(values[key2], '2'); 372 | assert.strictEqual(values[key3], '3'); 373 | 374 | // Call bulkGet again while values are still in memory cache 375 | pettyCache.bulkGet([key1, key2, key3], function(err, values) { 376 | assert.strictEqual(Object.keys(values).length, 3); 377 | assert.strictEqual(values[key1], '1'); 378 | assert.strictEqual(values[key2], '2'); 379 | assert.strictEqual(values[key3], '3'); 380 | 381 | // Wait for memory cache to expire 382 | setTimeout(function() { 383 | // Ensure keys are still in Redis 384 | pettyCache.bulkGet([key1, key2, key3], function(err, values) { 385 | assert.strictEqual(Object.keys(values).length, 3); 386 | assert.strictEqual(values[key1], '1'); 387 | assert.strictEqual(values[key2], '2'); 388 | assert.strictEqual(values[key3], '3'); 389 | done(); 390 | }); 391 | }, 5001); 392 | }); 393 | }); 394 | }); 395 | }); 396 | }); 397 | }); 398 | 399 | it('PettyCache.bulkGet should return null for missing keys', function(done) { 400 | this.timeout(6000); 401 | 402 | var key1 = Math.random().toString(); 403 | var key2 = Math.random().toString(); 404 | var key3 = Math.random().toString(); 405 | 406 | pettyCache.set(key1, '1', function() { 407 | pettyCache.set(key2, '2', function() { 408 | pettyCache.bulkGet([key1, key2, key3], function(err, values) { 409 | assert.strictEqual(Object.keys(values).length, 3); 410 | assert.strictEqual(values[key1], '1'); 411 | assert.strictEqual(values[key2], '2'); 412 | assert.strictEqual(values[key3], null); 413 | 414 | // Call bulkGet again while values are still in memory cache 415 | pettyCache.bulkGet([key1, key2, key3], function(err, values) { 416 | assert.strictEqual(Object.keys(values).length, 3); 417 | assert.strictEqual(values[key1], '1'); 418 | assert.strictEqual(values[key2], '2'); 419 | assert.strictEqual(values[key3], null); 420 | 421 | // Wait for memory cache to expire 422 | setTimeout(function() { 423 | // Ensure keys are still in Redis 424 | pettyCache.bulkGet([key1, key2, key3], function(err, values) { 425 | assert.strictEqual(Object.keys(values).length, 3); 426 | assert.strictEqual(values[key1], '1'); 427 | assert.strictEqual(values[key2], '2'); 428 | assert.strictEqual(values[key3], null); 429 | done(); 430 | }); 431 | }, 5001); 432 | }); 433 | }); 434 | }); 435 | }); 436 | }); 437 | 438 | it('PettyCache.bulkGet should correctly handle falsy values', function(done) { 439 | this.timeout(12000); 440 | 441 | var key1 = Math.random().toString(); 442 | var key2 = Math.random().toString(); 443 | var key3 = Math.random().toString(); 444 | var key4 = Math.random().toString(); 445 | var key5 = Math.random().toString(); 446 | var key6 = Math.random().toString(); 447 | var values = {}; 448 | 449 | values[key1] = ''; 450 | values[key2] = 0; 451 | values[key3] = false; 452 | values[key4] = NaN; 453 | values[key5] = null; 454 | values[key6] = undefined; 455 | 456 | async.each(Object.keys(values), function(key, callback) { 457 | pettyCache.set(key, values[key], { ttl: 6000 }, callback); 458 | }, function(err) { 459 | assert.ifError(err); 460 | 461 | var keys = Object.keys(values); 462 | 463 | // Add an additional key to check handling of missing keys 464 | var key7 = Math.random().toString(); 465 | keys.push(key7); 466 | 467 | pettyCache.bulkGet(keys, function(err, data) { 468 | assert.ifError(err); 469 | assert.strictEqual(keys.length, 7); 470 | assert.strictEqual(Object.keys(data).length, 7); 471 | assert.strictEqual(data[key1], ''); 472 | assert.strictEqual(data[key2], 0); 473 | assert.strictEqual(data[key3], false); 474 | assert.strictEqual(typeof data[key4], 'number'); 475 | assert(isNaN(data[key4])); 476 | assert.strictEqual(data[key5], null); 477 | assert.strictEqual(data[key6], undefined); 478 | assert.strictEqual(data[key7], null); 479 | 480 | // Wait for memory cache to expire 481 | setTimeout(function() { 482 | // Ensure keys are still in Redis 483 | pettyCache.bulkGet(keys, function(err, data) { 484 | assert.ifError(err); 485 | assert.strictEqual(Object.keys(data).length, 7); 486 | assert.strictEqual(data[key1], ''); 487 | assert.strictEqual(data[key2], 0); 488 | assert.strictEqual(data[key3], false); 489 | assert.strictEqual(typeof data[key4], 'number'); 490 | assert(isNaN(data[key4])); 491 | assert.strictEqual(data[key5], null); 492 | assert.strictEqual(data[key6], undefined); 493 | assert.strictEqual(data[key7], null); 494 | 495 | // Wait for Redis cache to expire 496 | setTimeout(function() { 497 | // Ensure keys are not in Redis 498 | pettyCache.bulkGet(keys, function(err, data) { 499 | assert.ifError(err); 500 | assert.strictEqual(Object.keys(data).length, 7); 501 | assert.strictEqual(data[key1], null); 502 | assert.strictEqual(data[key2], null); 503 | assert.strictEqual(data[key3], null); 504 | assert.strictEqual(data[key4], null); 505 | assert.strictEqual(data[key5], null); 506 | assert.strictEqual(data[key6], null); 507 | assert.strictEqual(data[key7], null); 508 | done(); 509 | }); 510 | }, 6001); 511 | }); 512 | }, 5001); 513 | }); 514 | }); 515 | }); 516 | 517 | it('PettyCache.bulkGet should return empty object when no keys are passed', function(done) { 518 | pettyCache.bulkGet([], function(err, values) { 519 | assert.ifError(err); 520 | assert.deepEqual(values, {}); 521 | done(); 522 | }); 523 | }); 524 | }); 525 | 526 | describe('PettyCache.bulkSet', function() { 527 | it('PettyCache.bulkSet should set values', function(done) { 528 | this.timeout(6000); 529 | 530 | var key1 = Math.random().toString(); 531 | var key2 = Math.random().toString(); 532 | var key3 = Math.random().toString(); 533 | var values = {}; 534 | 535 | values[key1] = '1'; 536 | values[key2] = 2; 537 | values[key3] = '3'; 538 | 539 | pettyCache.bulkSet(values, function(err) { 540 | assert.ifError(err); 541 | 542 | pettyCache.get(key1, function(err, value) { 543 | assert.ifError(err); 544 | assert.strictEqual(value, '1'); 545 | 546 | pettyCache.get(key2, function(err, value) { 547 | assert.ifError(err); 548 | assert.strictEqual(value, 2); 549 | 550 | pettyCache.get(key3, function(err, value) { 551 | assert.ifError(err); 552 | assert.strictEqual(value, '3'); 553 | 554 | // Wait for memory cache to expire 555 | setTimeout(function() { 556 | pettyCache.get(key1, function(err, value) { 557 | assert.ifError(err); 558 | assert.strictEqual(value, '1'); 559 | 560 | pettyCache.get(key2, function(err, value) { 561 | assert.ifError(err); 562 | assert.strictEqual(value, 2); 563 | 564 | pettyCache.get(key3, function(err, value) { 565 | assert.ifError(err); 566 | assert.strictEqual(value, '3'); 567 | done(); 568 | }); 569 | }); 570 | }); 571 | }, 5001); 572 | }); 573 | }); 574 | }); 575 | }); 576 | }); 577 | 578 | it('PettyCache.bulkSet should set values with the specified TTL option', function(done) { 579 | this.timeout(7000); 580 | 581 | var key1 = Math.random().toString(); 582 | var key2 = Math.random().toString(); 583 | var key3 = Math.random().toString(); 584 | var values = {}; 585 | 586 | values[key1] = '1'; 587 | values[key2] = 2; 588 | values[key3] = '3'; 589 | 590 | pettyCache.bulkSet(values, { ttl: 6000 }, function(err) { 591 | assert.ifError(err); 592 | 593 | pettyCache.get(key1, function(err, value) { 594 | assert.ifError(err); 595 | assert.strictEqual(value, '1'); 596 | 597 | pettyCache.get(key2, function(err, value) { 598 | assert.ifError(err); 599 | assert.strictEqual(value, 2); 600 | 601 | pettyCache.get(key3, function(err, value) { 602 | assert.ifError(err); 603 | assert.strictEqual(value, '3'); 604 | 605 | // Wait for Redis cache to expire 606 | setTimeout(function() { 607 | pettyCache.get(key1, function(err, value) { 608 | assert.ifError(err); 609 | assert.strictEqual(value, null); 610 | 611 | pettyCache.get(key2, function(err, value) { 612 | assert.ifError(err); 613 | assert.strictEqual(value, null); 614 | 615 | pettyCache.get(key3, function(err, value) { 616 | assert.ifError(err); 617 | assert.strictEqual(value, null); 618 | done(); 619 | }); 620 | }); 621 | }); 622 | }, 6001); 623 | }); 624 | }); 625 | }); 626 | }); 627 | }); 628 | 629 | it('PettyCache.bulkSet should set values with the specified TTL option using max and min', function(done) { 630 | this.timeout(10000); 631 | 632 | var key1 = Math.random().toString(); 633 | var key2 = Math.random().toString(); 634 | var key3 = Math.random().toString(); 635 | var values = {}; 636 | 637 | values[key1] = '1'; 638 | values[key2] = 2; 639 | values[key3] = '3'; 640 | 641 | pettyCache.bulkSet(values, { ttl: { max: 7000, min: 6000 } }, function(err) { 642 | assert.ifError(err); 643 | 644 | pettyCache.get(key1, function(err, value) { 645 | assert.ifError(err); 646 | assert.strictEqual(value, '1'); 647 | 648 | pettyCache.get(key2, function(err, value) { 649 | assert.ifError(err); 650 | assert.strictEqual(value, 2); 651 | 652 | pettyCache.get(key3, function(err, value) { 653 | assert.ifError(err); 654 | assert.strictEqual(value, '3'); 655 | 656 | // Wait for Redis cache to expire 657 | setTimeout(function() { 658 | pettyCache.get(key1, function(err, value) { 659 | assert.ifError(err); 660 | assert.strictEqual(value, null); 661 | 662 | pettyCache.get(key2, function(err, value) { 663 | assert.ifError(err); 664 | assert.strictEqual(value, null); 665 | 666 | pettyCache.get(key3, function(err, value) { 667 | assert.ifError(err); 668 | assert.strictEqual(value, null); 669 | done(); 670 | }); 671 | }); 672 | }); 673 | }, 7001); 674 | }); 675 | }); 676 | }); 677 | }); 678 | }); 679 | 680 | it('PettyCache.bulkSet should set values with the specified TTL option using max only', function(done) { 681 | this.timeout(10000); 682 | 683 | var key1 = Math.random().toString(); 684 | var key2 = Math.random().toString(); 685 | var key3 = Math.random().toString(); 686 | var values = {}; 687 | 688 | values[key1] = '1'; 689 | values[key2] = 2; 690 | values[key3] = '3'; 691 | 692 | pettyCache.bulkSet(values, { ttl: { max: 10000 } }, function(err) { 693 | assert.ifError(err); 694 | 695 | pettyCache.get(key1, function(err, value) { 696 | assert.ifError(err); 697 | assert.strictEqual(value, '1'); 698 | 699 | done(); 700 | }); 701 | }); 702 | }); 703 | 704 | it('PettyCache.bulkSet should set values with the specified TTL option using min only', function(done) { 705 | this.timeout(10000); 706 | 707 | var key1 = Math.random().toString(); 708 | var key2 = Math.random().toString(); 709 | var key3 = Math.random().toString(); 710 | var values = {}; 711 | 712 | values[key1] = '1'; 713 | values[key2] = 2; 714 | values[key3] = '3'; 715 | 716 | pettyCache.bulkSet(values, { ttl: { min: 6000 } }, function(err) { 717 | assert.ifError(err); 718 | 719 | pettyCache.get(key1, function(err, value) { 720 | assert.ifError(err); 721 | assert.strictEqual(value, '1'); 722 | 723 | done(); 724 | }); 725 | }); 726 | }); 727 | }); 728 | 729 | describe('PettyCache.del', function() { 730 | it('PettyCache.del', function(done) { 731 | const key = Math.random().toString(); 732 | 733 | pettyCache.set(key, key.split('').reverse().join(''), function(err) { 734 | assert.ifError(err); 735 | 736 | pettyCache.get(key, function(err, value) { 737 | assert.strictEqual(value, key.split('').reverse().join('')); 738 | 739 | pettyCache.del(key, function(err) { 740 | assert.ifError(err); 741 | 742 | pettyCache.get(key, function(err, value) { 743 | assert.ifError(err); 744 | assert.strictEqual(value, null); 745 | 746 | pettyCache.del(key, function(err) { 747 | assert.ifError(err); 748 | done(); 749 | }); 750 | }); 751 | }); 752 | }); 753 | }); 754 | }); 755 | 756 | it('PettyCache.del', function(done) { 757 | const key = Math.random().toString(); 758 | 759 | pettyCache.set(key, key.split('').reverse().join(''), function(err) { 760 | assert.ifError(err); 761 | 762 | pettyCache.get(key, async function(err, value) { 763 | assert.strictEqual(value, key.split('').reverse().join('')); 764 | 765 | await pettyCache.del(key); 766 | 767 | pettyCache.get(key, async function(err, value) { 768 | assert.ifError(err); 769 | assert.strictEqual(value, null); 770 | 771 | await pettyCache.del(key); 772 | 773 | done(); 774 | }); 775 | }); 776 | }); 777 | }); 778 | }); 779 | 780 | describe('PettyCache.fetch', function() { 781 | it('PettyCache.fetch', function(done) { 782 | this.timeout(7000); 783 | 784 | var key = Math.random().toString(); 785 | 786 | pettyCache.fetch(key, function(callback) { 787 | return callback(null, { foo: 'bar' }); 788 | }, function() { 789 | pettyCache.fetch(key, function() { 790 | throw 'This function should not be called'; 791 | }, function(err, data) { 792 | assert.equal(data.foo, 'bar'); 793 | 794 | // Wait for memory cache to expire 795 | setTimeout(function() { 796 | pettyCache.fetch(key, function() { 797 | throw 'This function should not be called'; 798 | }, function(err, data) { 799 | assert.strictEqual(data.foo, 'bar'); 800 | done(); 801 | }); 802 | }, 6000); 803 | }); 804 | }); 805 | }); 806 | 807 | it('PettyCache.fetch should cache null values returned by func', function(done) { 808 | this.timeout(7000); 809 | 810 | var key = Math.random().toString(); 811 | 812 | pettyCache.fetch(key, function(callback) { 813 | return callback(null, null); 814 | }, function() { 815 | pettyCache.fetch(key, function() { 816 | throw 'This function should not be called'; 817 | }, function(err, data) { 818 | assert.strictEqual(data, null); 819 | 820 | // Wait for memory cache to expire 821 | setTimeout(function() { 822 | pettyCache.fetch(key, function() { 823 | throw 'This function should not be called'; 824 | }, function(err, data) { 825 | assert.strictEqual(data, null); 826 | done(); 827 | }); 828 | }, 6000); 829 | }); 830 | }); 831 | }); 832 | 833 | it('PettyCache.fetch should cache undefined values returned by func', function(done) { 834 | this.timeout(7000); 835 | 836 | var key = Math.random().toString(); 837 | 838 | pettyCache.fetch(key, function(callback) { 839 | return callback(null, undefined); 840 | }, function() { 841 | pettyCache.fetch(key, function() { 842 | throw 'This function should not be called'; 843 | }, function(err, data) { 844 | assert.strictEqual(data, undefined); 845 | 846 | // Wait for memory cache to expire 847 | setTimeout(function() { 848 | pettyCache.fetch(key, function() { 849 | throw 'This function should not be called'; 850 | }, function(err, data) { 851 | assert.strictEqual(data, undefined); 852 | done(); 853 | }); 854 | }, 6000); 855 | }); 856 | }); 857 | }); 858 | 859 | it('PettyCache.fetch should lock around func', function(done) { 860 | var key = Math.random().toString(); 861 | var numberOfFuncCalls = 0; 862 | 863 | var func = function(callback) { 864 | setTimeout(function() { 865 | callback(null, ++numberOfFuncCalls); 866 | }, 100); 867 | }; 868 | 869 | pettyCache.fetch(key, func, function() {}); 870 | pettyCache.fetch(key, func, function() {}); 871 | pettyCache.fetch(key, func, function() {}); 872 | pettyCache.fetch(key, func, function() {}); 873 | pettyCache.fetch(key, func, function() {}); 874 | pettyCache.fetch(key, func, function() {}); 875 | pettyCache.fetch(key, func, function() {}); 876 | pettyCache.fetch(key, func, function() {}); 877 | pettyCache.fetch(key, func, function() {}); 878 | 879 | pettyCache.fetch(key, func, function(err, data) { 880 | assert.equal(data, 1); 881 | done(); 882 | }); 883 | }); 884 | 885 | it('PettyCache.fetch should run func again after TTL', function(done) { 886 | this.timeout(7000); 887 | 888 | var key = Math.random().toString(); 889 | var numberOfFuncCalls = 0; 890 | 891 | var func = function(callback) { 892 | setTimeout(function() { 893 | callback(null, ++numberOfFuncCalls); 894 | }, 100); 895 | }; 896 | 897 | pettyCache.fetch(key, func, { ttl: 6000 }, function() {}); 898 | 899 | pettyCache.fetch(key, func, { ttl: 6000 }, function(err, data) { 900 | assert.equal(data, 1); 901 | 902 | setTimeout(function() { 903 | pettyCache.fetch(key, func, { ttl: 6000 }, function(err, data) { 904 | assert.equal(data, 2); 905 | 906 | pettyCache.fetch(key, func, { ttl: 6000 }, function(err, data) { 907 | assert.equal(data, 2); 908 | done(); 909 | }); 910 | }); 911 | }, 6001); 912 | }); 913 | }); 914 | 915 | it('PettyCache.fetch should lock around Redis', function(done) { 916 | redisClient.info('commandstats', function(err, info) { 917 | var lineBefore = info.split('\n').find(i => i.startsWith('cmdstat_get:')); 918 | var tokenBefore = lineBefore.split(/:|,/).find(i => i.startsWith('calls=')); 919 | var callsBefore = parseInt(tokenBefore.split('=')[1]); 920 | 921 | var key = Math.random().toString(); 922 | var numberOfFuncCalls = 0; 923 | 924 | var func = function(callback) { 925 | setTimeout(function() { 926 | callback(null, ++numberOfFuncCalls); 927 | }, 100); 928 | }; 929 | 930 | pettyCache.fetch(key, func); 931 | pettyCache.fetch(key, func); 932 | pettyCache.fetch(key, func); 933 | pettyCache.fetch(key, func); 934 | pettyCache.fetch(key, func); 935 | pettyCache.fetch(key, func); 936 | pettyCache.fetch(key, func); 937 | pettyCache.fetch(key, func); 938 | pettyCache.fetch(key, func); 939 | 940 | pettyCache.fetch(key, func, function(err, data) { 941 | assert.equal(data, 1); 942 | 943 | redisClient.info('commandstats', function(err, info) { 944 | var lineAfter = info.split('\n').find(i => i.startsWith('cmdstat_get:')); 945 | var tokenAfter = lineAfter.split(/:|,/).find(i => i.startsWith('calls=')); 946 | var callsAfter = parseInt(tokenAfter.split('=')[1]); 947 | 948 | assert.strictEqual(callsBefore + 2, callsAfter); 949 | 950 | done(); 951 | }); 952 | }); 953 | }); 954 | }); 955 | 956 | it('PettyCache.fetch should return error if func returns error', function(done) { 957 | pettyCache.fetch(Math.random().toString(), function(callback) { 958 | callback(new Error('PettyCache.fetch should return error if func returns error')); 959 | }, function(err, values) { 960 | assert(err); 961 | assert.strictEqual(err.message, 'PettyCache.fetch should return error if func returns error'); 962 | assert(!values); 963 | done(); 964 | }); 965 | }); 966 | 967 | it('PettyCache.fetch should support async func', function(done) { 968 | this.timeout(7000); 969 | 970 | const key = Math.random().toString(); 971 | 972 | pettyCache.fetch(key, async () => { 973 | return { foo: 'bar' }; 974 | }, function() { 975 | pettyCache.fetch(key, async () => { 976 | throw 'This function should not be called'; 977 | }, function(err, data) { 978 | assert.ifError(err); 979 | assert.equal(data.foo, 'bar'); 980 | 981 | // Wait for memory cache to expire 982 | setTimeout(function() { 983 | pettyCache.fetch(key, async () => { 984 | throw 'This function should not be called'; 985 | }, function(err, data) { 986 | assert.ifError(err); 987 | assert.strictEqual(data.foo, 'bar'); 988 | done(); 989 | }); 990 | }, 6000); 991 | }); 992 | }); 993 | }); 994 | 995 | it('PettyCache.fetch should support async func with callback', function(done) { 996 | this.timeout(7000); 997 | 998 | const key = Math.random().toString(); 999 | 1000 | pettyCache.fetch(key, async (callback) => { 1001 | return callback(null, { foo: 'bar' }); 1002 | }, function() { 1003 | pettyCache.fetch(key, async () => { 1004 | throw 'This function should not be called'; 1005 | }, function(err, data) { 1006 | assert.ifError(err); 1007 | assert.equal(data.foo, 'bar'); 1008 | 1009 | // Wait for memory cache to expire 1010 | setTimeout(function() { 1011 | pettyCache.fetch(key, async () => { 1012 | throw 'This function should not be called'; 1013 | }, function(err, data) { 1014 | assert.ifError(err); 1015 | assert.strictEqual(data.foo, 'bar'); 1016 | done(); 1017 | }); 1018 | }, 6000); 1019 | }); 1020 | }); 1021 | }); 1022 | 1023 | it('PettyCache.fetch should support sync func without callback', function(done) { 1024 | this.timeout(7000); 1025 | 1026 | const key = Math.random().toString(); 1027 | 1028 | pettyCache.fetch(key, () => { 1029 | return { foo: 'bar' }; 1030 | }, function() { 1031 | pettyCache.fetch(key, () => { 1032 | throw 'This function should not be called'; 1033 | }, function(err, data) { 1034 | assert.ifError(err); 1035 | assert.equal(data.foo, 'bar'); 1036 | 1037 | // Wait for memory cache to expire 1038 | setTimeout(function() { 1039 | pettyCache.fetch(key, () => { 1040 | throw 'This function should not be called'; 1041 | }, function(err, data) { 1042 | assert.ifError(err); 1043 | assert.strictEqual(data.foo, 'bar'); 1044 | done(); 1045 | }); 1046 | }, 6000); 1047 | }); 1048 | }); 1049 | }); 1050 | }); 1051 | 1052 | describe('PettyCache.fetchAndRefresh', function() { 1053 | it('PettyCache.fetchAndRefresh', function(done) { 1054 | this.timeout(7000); 1055 | 1056 | var key = Math.random().toString(); 1057 | 1058 | pettyCache.fetchAndRefresh(key, function(callback) { 1059 | return callback(null, { foo: 'bar' }); 1060 | }, function() { 1061 | pettyCache.fetchAndRefresh(key, function() { 1062 | throw 'This function should not be called'; 1063 | }, function(err, data) { 1064 | assert.equal(data.foo, 'bar'); 1065 | 1066 | // Wait for memory cache to expire 1067 | setTimeout(function() { 1068 | pettyCache.fetchAndRefresh(key, function() { 1069 | throw 'This function should not be called'; 1070 | }, function(err, data) { 1071 | assert.strictEqual(data.foo, 'bar'); 1072 | done(); 1073 | }); 1074 | }, 6000); 1075 | }); 1076 | }); 1077 | }); 1078 | 1079 | it('PettyCache.fetchAndRefresh should run func again to refresh', function(done) { 1080 | this.timeout(7000); 1081 | 1082 | const key = Math.random().toString(); 1083 | var numberOfFuncCalls = 0; 1084 | 1085 | const func = function(callback) { 1086 | setTimeout(function() { 1087 | callback(null, ++numberOfFuncCalls); 1088 | }, 100); 1089 | }; 1090 | 1091 | pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }); 1092 | 1093 | pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, function(err, data) { 1094 | assert.equal(data, 1); 1095 | 1096 | setTimeout(function() { 1097 | pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, function(err, data) { 1098 | assert.equal(data, 2); 1099 | 1100 | pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, function(err, data) { 1101 | assert.equal(data, 2); 1102 | done(); 1103 | }); 1104 | }); 1105 | }, 4001); 1106 | }); 1107 | }); 1108 | 1109 | it('PettyCache.fetchAndRefresh should not allow multiple clients to execute func at the same time', function(done) { 1110 | this.timeout(7000); 1111 | 1112 | const key = Math.random().toString(); 1113 | let numberOfFuncCalls = 0; 1114 | 1115 | const func = function(callback) { 1116 | setTimeout(function() { 1117 | callback(null, ++numberOfFuncCalls); 1118 | }, 100); 1119 | }; 1120 | 1121 | pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, function(err, data) { 1122 | assert.ifError(err); 1123 | assert.equal(data, 1); 1124 | 1125 | const pettyCache2 = new PettyCache(redisClient); 1126 | 1127 | pettyCache2.fetchAndRefresh(key, func, { ttl: 6000 }, function(err, data) { 1128 | assert.ifError(err); 1129 | assert.equal(data, 1); 1130 | 1131 | setTimeout(function() { 1132 | pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, function(err, data) { 1133 | assert.ifError(err); 1134 | assert.equal(data, 2); 1135 | 1136 | pettyCache2.fetchAndRefresh(key, func, { ttl: 6000 }, function(err, data) { 1137 | assert.ifError(err); 1138 | assert.equal(data, 2); 1139 | done(); 1140 | }); 1141 | }); 1142 | }, 5001); 1143 | }); 1144 | }); 1145 | }); 1146 | 1147 | it('PettyCache.fetchAndRefresh should return error if func returns error', function(done) { 1148 | this.timeout(7000); 1149 | 1150 | const key = Math.random().toString(); 1151 | 1152 | const func = function(callback) { 1153 | callback(new Error('PettyCache.fetchAndRefresh should return error if func returns error')); 1154 | }; 1155 | 1156 | pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, function(err, data) { 1157 | assert(err); 1158 | assert.strictEqual(err.message, 'PettyCache.fetchAndRefresh should return error if func returns error'); 1159 | assert(!data); 1160 | 1161 | setTimeout(function() { 1162 | pettyCache.fetchAndRefresh(key, func, { ttl: 6000 }, function(err, data) { 1163 | assert(err); 1164 | assert.strictEqual(err.message, 'PettyCache.fetchAndRefresh should return error if func returns error'); 1165 | assert(!data); 1166 | 1167 | done(); 1168 | }); 1169 | }, 4001); 1170 | }); 1171 | }); 1172 | 1173 | it('PettyCache.fetchAndRefresh should not require options', function(done) { 1174 | pettyCache.fetchAndRefresh(Math.random().toString(), function(callback) { 1175 | return callback(null, { foo: 'bar' }); 1176 | }); 1177 | 1178 | done(); 1179 | }); 1180 | }); 1181 | 1182 | describe('PettyCache.get', function() { 1183 | it('PettyCache.get should return value', function(done) { 1184 | this.timeout(7000); 1185 | 1186 | var key = Math.random().toString(); 1187 | 1188 | pettyCache.set(key, 'hello world', function() { 1189 | pettyCache.get(key, function(err, value) { 1190 | assert.equal(value, 'hello world'); 1191 | 1192 | // Wait for memory cache to expire 1193 | setTimeout(function() { 1194 | pettyCache.get(key, function(err, value) { 1195 | assert.equal(value, 'hello world'); 1196 | done(); 1197 | }); 1198 | }, 6000); 1199 | }); 1200 | }); 1201 | }); 1202 | 1203 | it('PettyCache.get should return null for missing keys', function(done) { 1204 | var key = Math.random().toString(); 1205 | 1206 | pettyCache.get(key, function(err, value) { 1207 | assert.strictEqual(value, null); 1208 | 1209 | pettyCache.get(key, function(err, value) { 1210 | assert.strictEqual(value, null); 1211 | done(); 1212 | }); 1213 | }); 1214 | }); 1215 | }); 1216 | 1217 | describe('PettyCache.mutex', function() { 1218 | describe('PettyCache.mutex.lock (callbacks)', function() { 1219 | it('PettyCache.mutex.lock should lock for 1 second by default', done => { 1220 | const key = Math.random().toString(); 1221 | 1222 | pettyCache.mutex.lock(key, err => { 1223 | assert.ifError(err); 1224 | 1225 | pettyCache.mutex.lock(key, err => { 1226 | assert(err); 1227 | 1228 | setTimeout(() => { 1229 | pettyCache.mutex.lock(key, err => { 1230 | assert.ifError(err); 1231 | done(); 1232 | }); 1233 | }, 1001); 1234 | }); 1235 | }); 1236 | }); 1237 | 1238 | it('PettyCache.mutex.lock should lock for 2 seconds when ttl parameter is specified', function(done) { 1239 | this.timeout(3000); 1240 | 1241 | const key = Math.random().toString(); 1242 | 1243 | pettyCache.mutex.lock(key, { ttl: 2000 }, err => { 1244 | assert.ifError(err); 1245 | 1246 | pettyCache.mutex.lock(key, err => { 1247 | assert(err); 1248 | 1249 | setTimeout(() => { 1250 | pettyCache.mutex.lock(key, err => { 1251 | assert(err); 1252 | }); 1253 | }, 1001); 1254 | 1255 | setTimeout(() => { 1256 | pettyCache.mutex.lock(key, err => { 1257 | assert.ifError(err); 1258 | done(); 1259 | }); 1260 | }, 2001); 1261 | }); 1262 | }); 1263 | }); 1264 | 1265 | it('PettyCache.mutex.lock should acquire a lock after retries', function(done) { 1266 | this.timeout(3000); 1267 | const key = Math.random().toString(); 1268 | 1269 | pettyCache.mutex.lock(key, { ttl: 2000 } , err => { 1270 | assert.ifError(err); 1271 | 1272 | pettyCache.mutex.lock(key, err => { 1273 | assert(err); 1274 | 1275 | pettyCache.mutex.lock(key, { retry: { interval: 500, times: 10 } }, err => { 1276 | assert.ifError(err); 1277 | done(); 1278 | }); 1279 | }); 1280 | }); 1281 | }); 1282 | }); 1283 | 1284 | describe('PettyCache.mutex.lock (promises)', function() { 1285 | it('PettyCache.mutex.lock should lock for 1 second by default', async () => { 1286 | const key = Math.random().toString(); 1287 | 1288 | await pettyCache.mutex.lock(key); 1289 | 1290 | try { 1291 | await pettyCache.mutex.lock(key); 1292 | assert.fail('Should have thrown an error'); 1293 | } catch(err) { 1294 | assert.notStrictEqual(err.message, 'Should have thrown an error'); 1295 | assert(err); 1296 | } 1297 | 1298 | await timers.setTimeout(1001); 1299 | 1300 | await pettyCache.mutex.lock(key); 1301 | }); 1302 | 1303 | it('PettyCache.mutex.lock should lock for 2 seconds when ttl parameter is specified', async function() { 1304 | this.timeout(4000); 1305 | 1306 | const key = Math.random().toString(); 1307 | 1308 | await pettyCache.mutex.lock(key, { ttl: 2000 }); 1309 | 1310 | try { 1311 | await pettyCache.mutex.lock(key); 1312 | assert.fail('Should have thrown an error'); 1313 | } catch(err) { 1314 | assert.notStrictEqual(err.message, 'Should have thrown an error'); 1315 | assert(err); 1316 | } 1317 | 1318 | await timers.setTimeout(1001); 1319 | 1320 | try { 1321 | await pettyCache.mutex.lock(key); 1322 | assert.fail('Should have thrown an error'); 1323 | } catch(err) { 1324 | assert.notStrictEqual(err.message, 'Should have thrown an error'); 1325 | assert(err); 1326 | } 1327 | 1328 | await timers.setTimeout(1001); 1329 | 1330 | await pettyCache.mutex.lock(key); 1331 | }); 1332 | 1333 | it('PettyCache.mutex.lock should acquire a lock after retries', async function() { 1334 | this.timeout(4000); 1335 | const key = Math.random().toString(); 1336 | 1337 | await pettyCache.mutex.lock(key, { ttl: 2000 }); 1338 | 1339 | try { 1340 | await pettyCache.mutex.lock(key); 1341 | assert.fail('Should have thrown an error'); 1342 | } catch(err) { 1343 | assert.notStrictEqual(err.message, 'Should have thrown an error'); 1344 | assert(err); 1345 | } 1346 | 1347 | await pettyCache.mutex.lock(key, { retry: { interval: 500, times: 10 } }); 1348 | }); 1349 | }); 1350 | 1351 | describe('PettyCache.mutex.unlock (callbacks)', function() { 1352 | it('PettyCache.mutex.unlock should unlock', function(done) { 1353 | const key = Math.random().toString(); 1354 | 1355 | pettyCache.mutex.lock(key, { ttl: 10000 }, err => { 1356 | assert.ifError(err); 1357 | 1358 | pettyCache.mutex.lock(key, err => { 1359 | assert(err); 1360 | 1361 | pettyCache.mutex.unlock(key, () => { 1362 | pettyCache.mutex.lock(key, err => { 1363 | assert.ifError(err); 1364 | done(); 1365 | }); 1366 | }); 1367 | }); 1368 | }); 1369 | }); 1370 | 1371 | it('PettyCache.mutex.unlock should work without a callback', function(done) { 1372 | const key = Math.random().toString(); 1373 | 1374 | pettyCache.mutex.lock(key, { ttl: 10000 }, err => { 1375 | assert.ifError(err); 1376 | 1377 | pettyCache.mutex.unlock(key); 1378 | done(); 1379 | }); 1380 | }); 1381 | }); 1382 | 1383 | describe('PettyCache.mutex.unlock (promises)', function() { 1384 | it('PettyCache.mutex.unlock should unlock', async () => { 1385 | const key = Math.random().toString(); 1386 | 1387 | await pettyCache.mutex.lock(key, { ttl: 10000 }); 1388 | 1389 | try { 1390 | await pettyCache.mutex.lock(key); 1391 | assert.fail('Should have thrown an error'); 1392 | } catch(err) { 1393 | assert.notStrictEqual(err.message, 'Should have thrown an error'); 1394 | assert(err); 1395 | } 1396 | 1397 | await pettyCache.mutex.unlock(key); 1398 | await pettyCache.mutex.lock(key); 1399 | }); 1400 | }); 1401 | }); 1402 | 1403 | describe('PettyCache.patch', function() { 1404 | var key = Math.random().toString(); 1405 | 1406 | before(function(done) { 1407 | pettyCache.set(key, { a: 1, b: 2, c: 3 }, done); 1408 | }); 1409 | 1410 | it('PettyCache.patch should fail if the key does not exist', function(done) { 1411 | pettyCache.patch('xyz', { b: 3 }, function(err) { 1412 | assert(err, 'No error provided'); 1413 | done(); 1414 | }); 1415 | }); 1416 | 1417 | it('PettyCache.patch should update the values of given object keys', function(done) { 1418 | pettyCache.patch(key, { b: 4, c: 5 }, function(err) { 1419 | assert(!err, 'Error: ' + err); 1420 | 1421 | pettyCache.get(key, function(err, data) { 1422 | assert(!err, 'Error: ' + err); 1423 | assert.deepEqual(data, { a: 1, b: 4, c: 5 }); 1424 | done(); 1425 | }); 1426 | }); 1427 | }); 1428 | 1429 | it('PettyCache.patch should update the values of given object keys with options', function(done) { 1430 | pettyCache.patch(key, { b: 5, c: 6 }, { ttl: 10000 }, function(err) { 1431 | assert(!err, 'Error: ' + err); 1432 | 1433 | pettyCache.get(key, function(err, data) { 1434 | assert(!err, 'Error: ' + err); 1435 | assert.deepEqual(data, { a: 1, b: 5, c: 6 }); 1436 | done(); 1437 | }); 1438 | }); 1439 | }); 1440 | }); 1441 | 1442 | describe('PettyCache.semaphore', function() { 1443 | describe('PettyCache.semaphore.acquireLock', function() { 1444 | it('should aquire a lock', function(done) { 1445 | var key = Math.random().toString(); 1446 | 1447 | pettyCache.semaphore.retrieveOrCreate(key, { size: 10 }, function(err) { 1448 | assert.ifError(err); 1449 | 1450 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1451 | assert.ifError(err); 1452 | assert.equal(index, 0); 1453 | 1454 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1455 | assert.ifError(err); 1456 | assert.equal(index, 1); 1457 | done(); 1458 | }); 1459 | }); 1460 | }); 1461 | }); 1462 | 1463 | it('should not aquire a lock', function(done) { 1464 | var key = Math.random().toString(); 1465 | 1466 | pettyCache.semaphore.retrieveOrCreate(key, function(err) { 1467 | assert.ifError(err); 1468 | 1469 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1470 | assert.ifError(err); 1471 | assert.equal(index, 0); 1472 | 1473 | pettyCache.semaphore.acquireLock(key, function(err) { 1474 | assert(err); 1475 | done(); 1476 | }); 1477 | }); 1478 | }); 1479 | }); 1480 | 1481 | it('should aquire a lock after ttl', function(done) { 1482 | var key = Math.random().toString(); 1483 | 1484 | pettyCache.semaphore.retrieveOrCreate(key, function(err) { 1485 | assert.ifError(err); 1486 | 1487 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1488 | assert.ifError(err); 1489 | assert.equal(index, 0); 1490 | 1491 | pettyCache.semaphore.acquireLock(key, function(err) { 1492 | assert(err); 1493 | 1494 | setTimeout(function() { 1495 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1496 | assert.ifError(err); 1497 | assert.equal(index, 0); 1498 | done(); 1499 | }); 1500 | }, 1001); 1501 | }); 1502 | }); 1503 | }); 1504 | }); 1505 | 1506 | it('should aquire a lock with specified options', function(done) { 1507 | this.timeout(5000); 1508 | 1509 | var key = Math.random().toString(); 1510 | 1511 | pettyCache.semaphore.retrieveOrCreate(key, { size: 10 }, function(err) { 1512 | assert.ifError(err); 1513 | 1514 | // callback is optional 1515 | pettyCache.semaphore.acquireLock(key); 1516 | 1517 | setTimeout(function() { 1518 | pettyCache.semaphore.acquireLock(key, { retry: { interval: 500, times: 10 }, ttl: 500 }, function(err, index) { 1519 | assert.ifError(err); 1520 | assert.equal(index, 1); 1521 | done(); 1522 | }); 1523 | }, 1000); 1524 | }); 1525 | }); 1526 | 1527 | it('should fail if the semaphore does not exist', function(done) { 1528 | var key = Math.random().toString(); 1529 | 1530 | pettyCache.semaphore.acquireLock(key, 0, function(err) { 1531 | assert(err); 1532 | assert.strictEqual(err.message, `Semaphore ${key} doesn't exist.`); 1533 | done(); 1534 | }); 1535 | }); 1536 | }); 1537 | 1538 | describe('PettyCache.semaphore.consumeLock', function() { 1539 | it('should consume a lock', function(done) { 1540 | var key = Math.random().toString(); 1541 | 1542 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err) { 1543 | assert.ifError(err); 1544 | 1545 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1546 | assert.ifError(err); 1547 | assert.equal(index, 0); 1548 | 1549 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1550 | assert.ifError(err); 1551 | assert.equal(index, 1); 1552 | 1553 | pettyCache.semaphore.acquireLock(key, function(err) { 1554 | assert(err); 1555 | 1556 | pettyCache.semaphore.consumeLock(key, 0, function(err) { 1557 | assert.ifError(err); 1558 | 1559 | pettyCache.semaphore.acquireLock(key, function(err) { 1560 | assert(err); 1561 | done(); 1562 | }); 1563 | }); 1564 | }); 1565 | }); 1566 | }); 1567 | }); 1568 | }); 1569 | 1570 | it('should ensure at least one lock is not consumed', function(done) { 1571 | var key = Math.random().toString(); 1572 | 1573 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err) { 1574 | assert.ifError(err); 1575 | 1576 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1577 | assert.ifError(err); 1578 | assert.equal(index, 0); 1579 | 1580 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1581 | assert.ifError(err); 1582 | assert.equal(index, 1); 1583 | 1584 | pettyCache.semaphore.acquireLock(key, function(err) { 1585 | assert(err); 1586 | 1587 | pettyCache.semaphore.consumeLock(key, 0, function(err) { 1588 | assert.ifError(err); 1589 | 1590 | pettyCache.semaphore.consumeLock(key, 1, function(err) { 1591 | assert.ifError(err); 1592 | 1593 | pettyCache.semaphore.acquireLock(key, function(err) { 1594 | assert.ifError(err); 1595 | assert.equal(index, 1); 1596 | done(); 1597 | }); 1598 | }); 1599 | }); 1600 | }); 1601 | }); 1602 | }); 1603 | }); 1604 | }); 1605 | 1606 | it('should fail if the semaphore does not exist', function(done) { 1607 | var key = Math.random().toString(); 1608 | 1609 | pettyCache.semaphore.consumeLock(key, 0, function(err) { 1610 | assert(err); 1611 | assert.strictEqual(err.message, `Semaphore ${key} doesn't exist.`); 1612 | done(); 1613 | }); 1614 | }); 1615 | 1616 | it('should fail if index is larger than semaphore', function(done) { 1617 | var key = Math.random().toString(); 1618 | 1619 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err) { 1620 | assert.ifError(err); 1621 | 1622 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1623 | assert.ifError(err); 1624 | assert.equal(index, 0); 1625 | 1626 | pettyCache.semaphore.consumeLock(key, 10, function(err) { 1627 | assert(err); 1628 | assert.strictEqual(err.message, `Index 10 for semaphore ${key} is invalid.`); 1629 | done(); 1630 | }); 1631 | }); 1632 | }); 1633 | }); 1634 | 1635 | it('callback is optional', function(done) { 1636 | var key = Math.random().toString(); 1637 | 1638 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err) { 1639 | assert.ifError(err); 1640 | 1641 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1642 | assert.ifError(err); 1643 | assert.equal(index, 0); 1644 | 1645 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1646 | assert.ifError(err); 1647 | assert.equal(index, 1); 1648 | 1649 | pettyCache.semaphore.acquireLock(key, function(err) { 1650 | assert(err); 1651 | 1652 | pettyCache.semaphore.consumeLock(key, 0); 1653 | 1654 | pettyCache.semaphore.acquireLock(key, function(err) { 1655 | assert(err); 1656 | done(); 1657 | }); 1658 | }); 1659 | }); 1660 | }); 1661 | }); 1662 | }); 1663 | }); 1664 | 1665 | describe('PettyCache.semaphore.expand', function() { 1666 | it('should increase the size of a semaphore pool', function(done) { 1667 | var key = Math.random().toString(); 1668 | 1669 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err, pool) { 1670 | assert.ifError(err); 1671 | assert.strictEqual(pool.length, 2); 1672 | 1673 | pettyCache.semaphore.expand(key, 3, function(err) { 1674 | assert.ifError(err); 1675 | 1676 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err, pool) { 1677 | assert.ifError(err); 1678 | assert.strictEqual(pool.length, 3); 1679 | done(); 1680 | }); 1681 | }); 1682 | }); 1683 | }); 1684 | 1685 | it('should refuse to shrink a pool', function(done) { 1686 | var key = Math.random().toString(); 1687 | 1688 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err, pool) { 1689 | assert.ifError(err); 1690 | assert.strictEqual(pool.length, 2); 1691 | 1692 | pettyCache.semaphore.expand(key, 1, function(err) { 1693 | assert(err); 1694 | assert.strictEqual(err.message, 'Cannot shrink pool, size is 2 and you requested a size of 1.'); 1695 | done(); 1696 | }); 1697 | }); 1698 | }); 1699 | 1700 | it('should succeed if pool size is already equal to the specified size', function(done) { 1701 | var key = Math.random().toString(); 1702 | 1703 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err, pool) { 1704 | assert.ifError(err); 1705 | assert.strictEqual(pool.length, 2); 1706 | 1707 | pettyCache.semaphore.expand(key, 2, function(err) { 1708 | assert.ifError(err); 1709 | 1710 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err, pool) { 1711 | assert.ifError(err); 1712 | assert.strictEqual(pool.length, 2); 1713 | done(); 1714 | }); 1715 | }); 1716 | }); 1717 | }); 1718 | 1719 | it('callback is optional', function(done) { 1720 | var key = Math.random().toString(); 1721 | 1722 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err, pool) { 1723 | assert.ifError(err); 1724 | assert.strictEqual(pool.length, 2); 1725 | 1726 | pettyCache.semaphore.expand(key, 3); 1727 | 1728 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err, pool) { 1729 | assert.ifError(err); 1730 | assert.strictEqual(pool.length, 3); 1731 | done(); 1732 | }); 1733 | }); 1734 | }); 1735 | 1736 | it('should fail if the semaphore does not exist', function(done) { 1737 | var key = Math.random().toString(); 1738 | 1739 | pettyCache.semaphore.expand(key, 10, function(err) { 1740 | assert(err); 1741 | assert.strictEqual(err.message, `Semaphore ${key} doesn't exist.`); 1742 | done(); 1743 | }); 1744 | }); 1745 | }); 1746 | 1747 | describe('PettyCache.semaphore.releaseLock', function() { 1748 | it('should release a lock', function(done) { 1749 | var key = Math.random().toString(); 1750 | 1751 | pettyCache.semaphore.retrieveOrCreate(key, function(err) { 1752 | assert.ifError(err); 1753 | 1754 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1755 | assert.ifError(err); 1756 | assert.equal(index, 0); 1757 | 1758 | pettyCache.semaphore.acquireLock(key, function(err) { 1759 | assert(err); 1760 | 1761 | pettyCache.semaphore.releaseLock(key, 0, function(err) { 1762 | assert.ifError(err); 1763 | 1764 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1765 | assert.ifError(err); 1766 | assert.equal(index, 0); 1767 | done(); 1768 | }); 1769 | }); 1770 | }); 1771 | }); 1772 | }); 1773 | }); 1774 | 1775 | it('should fail to release a lock outside of the semaphore size', function(done) { 1776 | var key = Math.random().toString(); 1777 | 1778 | pettyCache.semaphore.retrieveOrCreate(key, function(err) { 1779 | assert.ifError(err); 1780 | 1781 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1782 | assert.ifError(err); 1783 | assert.equal(index, 0); 1784 | 1785 | pettyCache.semaphore.releaseLock(key, 10, function(err) { 1786 | assert(err); 1787 | assert.strictEqual(err.message, `Index 10 for semaphore ${key} is invalid.`); 1788 | done(); 1789 | }); 1790 | }); 1791 | }); 1792 | }); 1793 | 1794 | it('callback is optional', function(done) { 1795 | var key = Math.random().toString(); 1796 | 1797 | pettyCache.semaphore.retrieveOrCreate(key, function(err) { 1798 | assert.ifError(err); 1799 | 1800 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1801 | assert.ifError(err); 1802 | assert.equal(index, 0); 1803 | 1804 | pettyCache.semaphore.acquireLock(key, function(err) { 1805 | assert(err); 1806 | 1807 | pettyCache.semaphore.releaseLock(key, 0); 1808 | 1809 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1810 | assert.ifError(err); 1811 | assert.equal(index, 0); 1812 | done(); 1813 | }); 1814 | }); 1815 | }); 1816 | }); 1817 | }); 1818 | 1819 | it('should fail if the semaphore does not exist', function(done) { 1820 | var key = Math.random().toString(); 1821 | 1822 | pettyCache.semaphore.releaseLock(key, 10, function(err) { 1823 | assert(err); 1824 | assert.strictEqual(err.message, `Semaphore ${key} doesn't exist.`); 1825 | done(); 1826 | }); 1827 | }); 1828 | }); 1829 | 1830 | describe('PettyCache.semaphore.reset', function() { 1831 | it('should reset all locks', function(done) { 1832 | var key = Math.random().toString(); 1833 | 1834 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err) { 1835 | assert.ifError(err); 1836 | 1837 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1838 | assert.ifError(err); 1839 | assert.equal(index, 0); 1840 | 1841 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1842 | assert.ifError(err); 1843 | assert.equal(index, 1); 1844 | 1845 | pettyCache.semaphore.acquireLock(key, function(err) { 1846 | assert(err); 1847 | 1848 | pettyCache.semaphore.reset(key, function(err) { 1849 | assert.ifError(err); 1850 | 1851 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1852 | assert.ifError(err); 1853 | assert.equal(index, 0); 1854 | done(); 1855 | }); 1856 | }); 1857 | }); 1858 | }); 1859 | }); 1860 | }); 1861 | }); 1862 | 1863 | it('callback is optional', function(done) { 1864 | var key = Math.random().toString(); 1865 | 1866 | pettyCache.semaphore.retrieveOrCreate(key, { size: 2 }, function(err) { 1867 | assert.ifError(err); 1868 | 1869 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1870 | assert.ifError(err); 1871 | assert.equal(index, 0); 1872 | 1873 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1874 | assert.ifError(err); 1875 | assert.equal(index, 1); 1876 | 1877 | pettyCache.semaphore.acquireLock(key, function(err) { 1878 | assert(err); 1879 | 1880 | pettyCache.semaphore.reset(key); 1881 | 1882 | pettyCache.semaphore.acquireLock(key, function(err, index) { 1883 | assert.ifError(err); 1884 | assert.equal(index, 0); 1885 | done(); 1886 | }); 1887 | }); 1888 | }); 1889 | }); 1890 | }); 1891 | }); 1892 | 1893 | it('should fail if the semaphore does not exist', function(done) { 1894 | var key = Math.random().toString(); 1895 | 1896 | pettyCache.semaphore.reset(key, function(err) { 1897 | assert(err); 1898 | assert.strictEqual(err.message, `Semaphore ${key} doesn't exist.`); 1899 | done(); 1900 | }); 1901 | }); 1902 | }); 1903 | 1904 | describe('PettyCache.semaphore.retrieveOrCreate', function() { 1905 | it('should create a new semaphore', function(done) { 1906 | var key = Math.random().toString(); 1907 | 1908 | pettyCache.semaphore.retrieveOrCreate(key, { size: 100 }, function(err, semaphore) { 1909 | assert.ifError(err); 1910 | assert(semaphore); 1911 | assert.equal(semaphore.length, 100); 1912 | assert(semaphore.every(s => s.status === 'available')); 1913 | 1914 | pettyCache.semaphore.retrieveOrCreate(key, function(err, semaphore) { 1915 | assert.ifError(err); 1916 | assert(semaphore); 1917 | assert.equal(semaphore.length, 100); 1918 | assert(semaphore.every(s => s.status === 'available')); 1919 | done(); 1920 | }); 1921 | }); 1922 | }); 1923 | 1924 | it('should have a min size of 1', function(done) { 1925 | var key = Math.random().toString(); 1926 | 1927 | pettyCache.semaphore.retrieveOrCreate(key, { size: 0 }, function(err, semaphore) { 1928 | assert.ifError(err); 1929 | assert(semaphore); 1930 | assert.equal(semaphore.length, 1); 1931 | assert(semaphore.every(s => s.status === 'available')); 1932 | 1933 | pettyCache.semaphore.retrieveOrCreate(key, function(err, semaphore) { 1934 | assert.ifError(err); 1935 | assert(semaphore); 1936 | assert.equal(semaphore.length, 1); 1937 | assert(semaphore.every(s => s.status === 'available')); 1938 | done(); 1939 | }); 1940 | }); 1941 | }); 1942 | 1943 | it('should allow options.size to provide a function', function(done) { 1944 | var key = Math.random().toString(); 1945 | 1946 | pettyCache.semaphore.retrieveOrCreate(key, { size: (callback) => callback(null, 1 + 1) }, function(err, semaphore) { 1947 | assert.ifError(err); 1948 | assert(semaphore); 1949 | assert.equal(semaphore.length, 2); 1950 | assert(semaphore.every(s => s.status === 'available')); 1951 | 1952 | pettyCache.semaphore.retrieveOrCreate(key, function(err, semaphore) { 1953 | assert.ifError(err); 1954 | assert(semaphore); 1955 | assert.equal(semaphore.length, 2); 1956 | assert(semaphore.every(s => s.status === 'available')); 1957 | done(); 1958 | }); 1959 | }); 1960 | }); 1961 | 1962 | it('callback is optional', function(done) { 1963 | var key = Math.random().toString(); 1964 | 1965 | pettyCache.semaphore.retrieveOrCreate(key); 1966 | 1967 | pettyCache.semaphore.retrieveOrCreate(key, { size: 100 }, function(err, semaphore) { 1968 | assert.ifError(err); 1969 | assert(semaphore); 1970 | assert.equal(semaphore.length, 1); 1971 | assert(semaphore.every(s => s.status === 'available')); 1972 | done(); 1973 | }); 1974 | }); 1975 | }); 1976 | }); 1977 | 1978 | describe('PettyCache.set', function() { 1979 | it('PettyCache.set should set a value', function(done) { 1980 | this.timeout(7000); 1981 | 1982 | var key = Math.random().toString(); 1983 | 1984 | pettyCache.set(key, 'hello world', function() { 1985 | pettyCache.get(key, function(err, value) { 1986 | assert.equal(value, 'hello world'); 1987 | 1988 | // Wait for memory cache to expire 1989 | setTimeout(function() { 1990 | pettyCache.get(key, function(err, value) { 1991 | assert.equal(value, 'hello world'); 1992 | done(); 1993 | }); 1994 | }, 6000); 1995 | }); 1996 | }); 1997 | }); 1998 | 1999 | it('PettyCache.set should work without a callback', function(done) { 2000 | pettyCache.set(Math.random().toString(), 'hello world'); 2001 | done(); 2002 | }); 2003 | 2004 | it('PettyCache.set should set a value with the specified TTL option', function(done) { 2005 | this.timeout(7000); 2006 | 2007 | var key = Math.random().toString(); 2008 | 2009 | pettyCache.set(key, 'hello world', { ttl: 6000 },function() { 2010 | pettyCache.get(key, function(err, value) { 2011 | assert.equal(value, 'hello world'); 2012 | 2013 | // Wait for memory cache to expire 2014 | setTimeout(function() { 2015 | pettyCache.get(key, function(err, value) { 2016 | assert.equal(value, null); 2017 | done(); 2018 | }); 2019 | }, 6001); 2020 | }); 2021 | }); 2022 | }); 2023 | 2024 | it('PettyCache.set should set a value with the specified TTL option using max and min', function(done) { 2025 | this.timeout(10000); 2026 | 2027 | var key = Math.random().toString(); 2028 | 2029 | pettyCache.set(key, 'hello world', { ttl: { max: 7000, min: 6000 } },function() { 2030 | pettyCache.get(key, function(err, value) { 2031 | assert.strictEqual(value, 'hello world'); 2032 | 2033 | // Get again before cache expires 2034 | setTimeout(function() { 2035 | pettyCache.get(key, function(err, value) { 2036 | assert.strictEqual(value, 'hello world'); 2037 | 2038 | // Wait for memory cache to expire 2039 | setTimeout(function() { 2040 | pettyCache.get(key, function(err, value) { 2041 | assert.strictEqual(value, null); 2042 | done(); 2043 | }); 2044 | }, 6001); 2045 | }); 2046 | }, 1000); 2047 | }); 2048 | }); 2049 | }); 2050 | 2051 | it('PettyCache.set should set a value with the specified TTL option using min only', function(done) { 2052 | this.timeout(10000); 2053 | 2054 | var key = Math.random().toString(); 2055 | 2056 | pettyCache.set(key, 'hello world', { ttl: { min: 6000 } },function() { 2057 | pettyCache.get(key, function(err, value) { 2058 | assert.strictEqual(value, 'hello world'); 2059 | done(); 2060 | }); 2061 | }); 2062 | }); 2063 | 2064 | it('PettyCache.set should set a value with the specified TTL option using max only', function(done) { 2065 | this.timeout(10000); 2066 | 2067 | var key = Math.random().toString(); 2068 | 2069 | pettyCache.set(key, 'hello world', { ttl: { max: 10000 } },function() { 2070 | pettyCache.get(key, function(err, value) { 2071 | assert.strictEqual(value, 'hello world'); 2072 | done(); 2073 | }); 2074 | }); 2075 | }); 2076 | 2077 | it('PettyCache.set(key, \'\')', function(done) { 2078 | this.timeout(11000); 2079 | 2080 | var key = Math.random().toString(); 2081 | 2082 | pettyCache.set(key, '', { ttl: 7000 }, function(err) { 2083 | assert.ifError(err); 2084 | 2085 | pettyCache.get(key, function(err, value) { 2086 | assert.ifError(err); 2087 | assert.strictEqual(value, ''); 2088 | 2089 | // Wait for memory cache to expire 2090 | setTimeout(function() { 2091 | pettyCache.get(key, function(err, value) { 2092 | assert.ifError(err); 2093 | assert.strictEqual(value, ''); 2094 | 2095 | // Wait for memory cache and Redis cache to expire 2096 | setTimeout(function() { 2097 | pettyCache.get(key, function(err, value) { 2098 | assert.ifError(err); 2099 | assert.strictEqual(value, null); 2100 | done(); 2101 | }); 2102 | }, 5001); 2103 | }); 2104 | }, 5001); 2105 | }); 2106 | }); 2107 | }); 2108 | 2109 | it('PettyCache.set(key, 0)', function(done) { 2110 | this.timeout(11000); 2111 | 2112 | var key = Math.random().toString(); 2113 | 2114 | pettyCache.set(key, 0, { ttl: 7000 }, function(err) { 2115 | assert.ifError(err); 2116 | 2117 | pettyCache.get(key, function(err, value) { 2118 | assert.ifError(err); 2119 | assert.strictEqual(value, 0); 2120 | 2121 | // Wait for memory cache to expire 2122 | setTimeout(function() { 2123 | pettyCache.get(key, function(err, value) { 2124 | assert.ifError(err); 2125 | assert.strictEqual(value, 0); 2126 | 2127 | // Wait for memory cache and Redis cache to expire 2128 | setTimeout(function() { 2129 | pettyCache.get(key, function(err, value) { 2130 | assert.ifError(err); 2131 | assert.strictEqual(value, null); 2132 | done(); 2133 | }); 2134 | }, 5001); 2135 | }); 2136 | }, 5001); 2137 | }); 2138 | }); 2139 | }); 2140 | 2141 | it('PettyCache.set(key, false)', function(done) { 2142 | this.timeout(11000); 2143 | 2144 | var key = Math.random().toString(); 2145 | 2146 | pettyCache.set(key, false, { ttl: 7000 }, function(err) { 2147 | assert.ifError(err); 2148 | 2149 | pettyCache.get(key, function(err, value) { 2150 | assert.ifError(err); 2151 | assert.strictEqual(value, false); 2152 | 2153 | // Wait for memory cache to expire 2154 | setTimeout(function() { 2155 | pettyCache.get(key, function(err, value) { 2156 | assert.ifError(err); 2157 | assert.strictEqual(value, false); 2158 | 2159 | // Wait for memory cache and Redis cache to expire 2160 | setTimeout(function() { 2161 | pettyCache.get(key, function(err, value) { 2162 | assert.ifError(err); 2163 | assert.strictEqual(value, null); 2164 | done(); 2165 | }); 2166 | }, 5001); 2167 | }); 2168 | }, 5001); 2169 | }); 2170 | }); 2171 | }); 2172 | 2173 | it('PettyCache.set(key, NaN)', function(done) { 2174 | this.timeout(11000); 2175 | 2176 | var key = Math.random().toString(); 2177 | 2178 | pettyCache.set(key, NaN, { ttl: 7000 }, function(err) { 2179 | assert.ifError(err); 2180 | 2181 | pettyCache.get(key, function(err, value) { 2182 | assert.ifError(err); 2183 | assert(typeof value === 'number' && isNaN(value)); 2184 | 2185 | // Wait for memory cache to expire 2186 | setTimeout(function() { 2187 | pettyCache.get(key, function(err, value) { 2188 | assert.ifError(err); 2189 | assert(typeof value === 'number' && isNaN(value)); 2190 | 2191 | // Wait for memory cache and Redis cache to expire 2192 | setTimeout(function() { 2193 | pettyCache.get(key, function(err, value) { 2194 | assert.ifError(err); 2195 | assert.strictEqual(value, null); 2196 | done(); 2197 | }); 2198 | }, 5001); 2199 | }); 2200 | }, 5001); 2201 | }); 2202 | }); 2203 | }); 2204 | 2205 | it('PettyCache.set(key, null)', function(done) { 2206 | this.timeout(11000); 2207 | 2208 | var key = Math.random().toString(); 2209 | 2210 | pettyCache.set(key, null, { ttl: 7000 }, function(err) { 2211 | assert.ifError(err); 2212 | 2213 | pettyCache.get(key, function(err, value) { 2214 | assert.ifError(err); 2215 | assert.strictEqual(value, null); 2216 | 2217 | // Wait for memory cache to expire 2218 | setTimeout(function() { 2219 | pettyCache.get(key, function(err, value) { 2220 | assert.ifError(err); 2221 | assert.strictEqual(value, null); 2222 | 2223 | // Wait for memory cache and Redis cache to expire 2224 | setTimeout(function() { 2225 | pettyCache.get(key, function(err, value) { 2226 | assert.ifError(err); 2227 | assert.strictEqual(value, null); 2228 | done(); 2229 | }); 2230 | }, 5001); 2231 | }); 2232 | }, 5001); 2233 | }); 2234 | }); 2235 | }); 2236 | 2237 | it('PettyCache.set(key, undefined)', function(done) { 2238 | this.timeout(11000); 2239 | 2240 | var key = Math.random().toString(); 2241 | 2242 | pettyCache.set(key, undefined, { ttl: 7000 }, function(err) { 2243 | assert.ifError(err); 2244 | 2245 | pettyCache.get(key, function(err, value) { 2246 | assert.ifError(err); 2247 | assert.strictEqual(value, undefined); 2248 | 2249 | // Wait for memory cache to expire 2250 | setTimeout(function() { 2251 | pettyCache.get(key, function(err, value) { 2252 | assert.ifError(err); 2253 | assert.strictEqual(value, undefined); 2254 | 2255 | // Wait for memory cache and Redis cache to expire 2256 | setTimeout(function() { 2257 | pettyCache.get(key, function(err, value) { 2258 | assert.ifError(err); 2259 | assert.strictEqual(value, null); 2260 | done(); 2261 | }); 2262 | }, 5001); 2263 | }); 2264 | }, 5001); 2265 | }); 2266 | }); 2267 | }); 2268 | }); 2269 | 2270 | describe('redisClient', function() { 2271 | it('redisClient.mget(falsy keys)', function(done) { 2272 | var key1 = Math.random().toString(); 2273 | var key2 = Math.random().toString(); 2274 | var key3 = Math.random().toString(); 2275 | var key4 = Math.random().toString(); 2276 | var key5 = Math.random().toString(); 2277 | var key6 = Math.random().toString(); 2278 | var values = {}; 2279 | 2280 | values[key1] = ''; 2281 | values[key2] = 0; 2282 | values[key3] = false; 2283 | values[key4] = NaN; 2284 | values[key5] = null; 2285 | values[key6] = undefined; 2286 | 2287 | async.each(Object.keys(values), function(key, callback) { 2288 | redisClient.psetex(key, 1000, PettyCache.stringify(values[key]), callback); 2289 | }, function(err) { 2290 | assert.ifError(err); 2291 | 2292 | var keys = Object.keys(values); 2293 | 2294 | // Add an additional key to check handling of missing keys 2295 | keys.push(Math.random().toString()); 2296 | 2297 | redisClient.mget(keys, function(err, data) { 2298 | assert.ifError(err); 2299 | assert.strictEqual(data.length, 7); 2300 | assert.strictEqual(data[0], '""'); 2301 | assert.strictEqual(PettyCache.parse(data[0]), ''); 2302 | assert.strictEqual(data[1], '0'); 2303 | assert.strictEqual(PettyCache.parse(data[1]), 0); 2304 | assert.strictEqual(data[2], 'false'); 2305 | assert.strictEqual(PettyCache.parse(data[2]), false); 2306 | assert.strictEqual(data[3], '"__NaN"'); 2307 | assert.strictEqual(typeof PettyCache.parse(data[3]), 'number'); 2308 | assert(isNaN(PettyCache.parse(data[3]))); 2309 | assert.strictEqual(data[4], '"__null"'); 2310 | assert.strictEqual(PettyCache.parse(data[4]), null); 2311 | assert.strictEqual(data[5], '"__undefined"'); 2312 | assert.strictEqual(PettyCache.parse(data[5]), undefined); 2313 | assert.strictEqual(data[6], null); 2314 | done(); 2315 | }); 2316 | }); 2317 | }); 2318 | 2319 | it('redisClient.psetex(key, \'\')', function(done) { 2320 | var key = Math.random().toString(); 2321 | 2322 | redisClient.psetex(key, 1000, PettyCache.stringify(''), function(err) { 2323 | assert.ifError(err); 2324 | 2325 | redisClient.get(key, function(err, data) { 2326 | assert.ifError(err); 2327 | assert.strictEqual(data, '""'); 2328 | assert.strictEqual(PettyCache.parse(data), ''); 2329 | 2330 | // Wait for Redis cache to expire 2331 | setTimeout(function() { 2332 | redisClient.get(key, function(err, data) { 2333 | assert.ifError(err); 2334 | assert.strictEqual(data, null); 2335 | done(); 2336 | }); 2337 | }, 1001); 2338 | }); 2339 | }); 2340 | }); 2341 | 2342 | it('redisClient.psetex(key, 0)', function(done) { 2343 | var key = Math.random().toString(); 2344 | 2345 | redisClient.psetex(key, 1000, PettyCache.stringify(0), function(err) { 2346 | assert.ifError(err); 2347 | 2348 | redisClient.get(key, function(err, data) { 2349 | assert.ifError(err); 2350 | assert.strictEqual(data, '0'); 2351 | assert.strictEqual(PettyCache.parse(data), 0); 2352 | 2353 | // Wait for Redis cache to expire 2354 | setTimeout(function() { 2355 | redisClient.get(key, function(err, data) { 2356 | assert.ifError(err); 2357 | assert.strictEqual(data, null); 2358 | done(); 2359 | }); 2360 | }, 1001); 2361 | }); 2362 | }); 2363 | }); 2364 | 2365 | it('redisClient.psetex(key, false)', function(done) { 2366 | var key = Math.random().toString(); 2367 | 2368 | redisClient.psetex(key, 1000, PettyCache.stringify(false), function(err) { 2369 | assert.ifError(err); 2370 | 2371 | redisClient.get(key, function(err, data) { 2372 | assert.ifError(err); 2373 | assert.strictEqual(data, 'false'); 2374 | assert.strictEqual(PettyCache.parse(data), false); 2375 | 2376 | // Wait for Redis cache to expire 2377 | setTimeout(function() { 2378 | redisClient.get(key, function(err, data) { 2379 | assert.ifError(err); 2380 | assert.strictEqual(data, null); 2381 | done(); 2382 | }); 2383 | }, 1001); 2384 | }); 2385 | }); 2386 | }); 2387 | 2388 | it('redisClient.psetex(key, NaN)', function(done) { 2389 | var key = Math.random().toString(); 2390 | 2391 | redisClient.psetex(key, 1000, PettyCache.stringify(NaN), function(err) { 2392 | assert.ifError(err); 2393 | 2394 | redisClient.get(key, function(err, data) { 2395 | assert.ifError(err); 2396 | assert.strictEqual(data, '"__NaN"'); 2397 | assert(isNaN(PettyCache.parse(data))); 2398 | 2399 | // Wait for Redis cache to expire 2400 | setTimeout(function() { 2401 | redisClient.get(key, function(err, data) { 2402 | assert.ifError(err); 2403 | assert.strictEqual(data, null); 2404 | done(); 2405 | }); 2406 | }, 1001); 2407 | }); 2408 | }); 2409 | }); 2410 | 2411 | it('redisClient.psetex(key, null)', function(done) { 2412 | var key = Math.random().toString(); 2413 | 2414 | redisClient.psetex(key, 1000, PettyCache.stringify(null), function(err) { 2415 | assert.ifError(err); 2416 | 2417 | redisClient.get(key, function(err, data) { 2418 | assert.ifError(err); 2419 | assert.strictEqual(data, '"__null"'); 2420 | assert.strictEqual(PettyCache.parse(data), null); 2421 | 2422 | // Wait for Redis cache to expire 2423 | setTimeout(function() { 2424 | redisClient.get(key, function(err, data) { 2425 | assert.ifError(err); 2426 | assert.strictEqual(data, null); 2427 | done(); 2428 | }); 2429 | }, 1001); 2430 | }); 2431 | }); 2432 | }); 2433 | 2434 | it('redisClient.psetex(key, undefined)', function(done) { 2435 | var key = Math.random().toString(); 2436 | 2437 | redisClient.psetex(key, 1000, PettyCache.stringify(undefined), function(err) { 2438 | assert.ifError(err); 2439 | 2440 | redisClient.get(key, function(err, data) { 2441 | assert.ifError(err); 2442 | assert.strictEqual(data, '"__undefined"'); 2443 | assert.strictEqual(PettyCache.parse(data), undefined); 2444 | 2445 | // Wait for Redis cache to expire 2446 | setTimeout(function() { 2447 | redisClient.get(key, function(err, data) { 2448 | assert.ifError(err); 2449 | assert.strictEqual(data, null); 2450 | done(); 2451 | }); 2452 | }, 1001); 2453 | }); 2454 | }); 2455 | }); 2456 | }); 2457 | 2458 | describe('Benchmark', function() { 2459 | const emojis = require('./emojis.json'); 2460 | 2461 | it('PettyCache should be faster than node-redis', function(done) { 2462 | var pettyCacheEnd; 2463 | var pettyCacheKey = Math.random().toString(); 2464 | var pettyCacheStart; 2465 | var redisEnd; 2466 | var redisKey = Math.random().toString(); 2467 | var redisStart = Date.now(); 2468 | 2469 | redisClient.psetex(redisKey, 30000, JSON.stringify(emojis), function(err) { 2470 | assert.ifError(err); 2471 | 2472 | async.times(500, function(n, callback) { 2473 | redisClient.get(redisKey, function(err, data) { 2474 | if (err) { 2475 | return callback(err); 2476 | } 2477 | 2478 | callback(null, JSON.parse(data)); 2479 | }); 2480 | }, function(err) { 2481 | redisEnd = Date.now(); 2482 | assert.ifError(err); 2483 | pettyCacheStart = Date.now(); 2484 | 2485 | pettyCache.set(pettyCacheKey, emojis, function(err) { 2486 | assert.ifError(err); 2487 | 2488 | async.times(500, function(n, callback) { 2489 | pettyCache.get(pettyCacheKey, function(err, data) { 2490 | if (err) { 2491 | return callback(err); 2492 | } 2493 | 2494 | callback(null, data); 2495 | }); 2496 | }, function(err) { 2497 | pettyCacheEnd = Date.now(); 2498 | assert.ifError(err); 2499 | assert(pettyCacheEnd - pettyCacheStart < redisEnd - redisStart); 2500 | done(); 2501 | }); 2502 | }); 2503 | }); 2504 | }); 2505 | }); 2506 | }); --------------------------------------------------------------------------------