├── docs ├── CNAME ├── README.md ├── flush.md ├── intro.md ├── misc.md ├── cachedump.md ├── items.md ├── elasticache.md ├── delete.md ├── getmulti.md ├── disconnect.md ├── replace.md ├── add.md ├── basics.md ├── incrdecr.md ├── get.md ├── appendprepend.md ├── set.md └── initialization.md ├── test ├── mocha.opts ├── connection.js └── client.js ├── index.js ├── .travis.yml ├── .github └── workflows │ └── npm-grunt.yml ├── .gitignore ├── Gruntfile.js ├── .jshintrc ├── LICENSE ├── README.md ├── package.json └── lib ├── misc.js ├── connection-pool.js ├── client.js └── connection.js /docs/CNAME: -------------------------------------------------------------------------------- 1 | memcache-plus.com 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --ui bdd -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies 4 | */ 5 | 6 | module.exports = require("./lib/client"); 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 8 5 | - 10 6 | - 12 7 | 8 | env: 9 | global: 10 | - GIT_NAME: Travis CI 11 | - GIT_EMAIL: mail@victorquinn.com 12 | - GITHUB_REPO: socialradar/memcache-plus 13 | - GIT_SOURCE: _docpress 14 | 15 | before_script: 16 | - npm install -g grunt-cli 17 | 18 | script: 19 | - grunt test 20 | 21 | services: 22 | - memcached 23 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | * [Memcache Plus](../README.md) 2 | * [Getting Started](intro.md) 3 | * [Initialization](initialization.md) 4 | * [Set](set.md) 5 | * [Get](get.md) 6 | * [Get Multi](getmulti.md) 7 | * [Delete](delete.md) 8 | * [Incr/Decr](incrdecr.md) 9 | * [Append/Prepend](appendprepend.md) 10 | * [Add](add.md) 11 | * [Replace](replace.md) 12 | * [Flush](flush.md) 13 | * [Items](items.md) 14 | * [Disconnect](disconnect.md) 15 | * [Elasticache](elasticache.md) 16 | * [Miscellaneous](misc.md) 17 | -------------------------------------------------------------------------------- /.github/workflows/npm-grunt.yml: -------------------------------------------------------------------------------- 1 | name: Main Workflow 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x, 16.x, 18.x, 20.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Install Memcached 26 | uses: niden/actions-memcached@v7 27 | 28 | - name: Run Test Suite 29 | run: | 30 | yarn 31 | grunt test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | # Netbeans project folder 28 | nbproject_docpress 29 | _docpress 30 | -------------------------------------------------------------------------------- /docs/flush.md: -------------------------------------------------------------------------------- 1 | # Flush 2 | 3 | ### Basic case 4 | 5 | Flush causes all items to expire. It does not free up or flush memory. 6 | 7 | ```javascript 8 | client 9 | .flush() 10 | .then(function() { 11 | console.log('Successfully cleared all data'); 12 | }); 13 | ``` 14 | 15 | ### Delayed flush 16 | 17 | You can add a delay in seconds before the flush is executed. 18 | 19 | ```javascript 20 | client 21 | .flush(1) 22 | .then(function() { 23 | console.log('Successfully cleared all data'); 24 | }); 25 | ``` 26 | 27 | ### Callbacks 28 | 29 | Memcache Plus will always return a [Promise](https://www.promisejs.org), but it 30 | can also take a traditional callback for any of its methods so it can work just 31 | like most of the other Memcache modules out there. For example: 32 | 33 | ```javascript 34 | client.flush(function() { 35 | console.log('Successfully cleared all data'); 36 | }); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | 5 | Memcache Plus is available on npm: 6 | 7 | [![NPM](https://nodei.co/npm/memcache-plus.png?downloads=true)](https://nodei.co/npm/memcache-plus?downloads=true) 8 | 9 | ## Usage 10 | 11 | After installing, it's easy to start using Memcache Plus: 12 | 13 | ```javascript 14 | const MemcachePlus = require('memcache-plus') 15 | 16 | const client = new MemcachePlus() 17 | ``` 18 | 19 | Instantiating the client will automatically establish a connection between your 20 | running application and your Memcache server. 21 | 22 | Then, right away you can start using its methods: 23 | 24 | ```javascript 25 | client 26 | .get('my-key') 27 | .then(function(value) { 28 | console.log('my-key has a value of ', value); 29 | }); 30 | ``` 31 | 32 | or with async/await 33 | 34 | ```javascript 35 | const value = await client.get('my-key') 36 | console.log(`my-key has a value of ${ value }`) 37 | ``` 38 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | var js_files = ['Gruntfile.js', 'lib/**/*.js', 'test/**/*.js']; 3 | 4 | grunt.initConfig({ 5 | jshint: { 6 | options: { 7 | jshintrc: '.jshintrc', 8 | reporter: require('jshint-stylish') 9 | }, 10 | all: js_files 11 | }, 12 | mochaTest: { 13 | test: { 14 | options: { 15 | reporter: 'spec' 16 | }, 17 | src: ['test/**/*.js'] 18 | } 19 | }, 20 | watch: { 21 | files: js_files, 22 | tasks: ['jshint', 'mochaTest'] 23 | } 24 | }); 25 | 26 | grunt.loadNpmTasks('grunt-contrib-jshint'); 27 | grunt.loadNpmTasks('grunt-contrib-watch'); 28 | grunt.loadNpmTasks('grunt-mocha-test'); 29 | 30 | grunt.registerTask('default', ['watch']); 31 | grunt.registerTask('test', ['jshint', 'mochaTest']); 32 | }; 33 | -------------------------------------------------------------------------------- /docs/misc.md: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | 3 | ## Version 4 | 5 | You can retrieve the version of Memcached currently running on the server by 6 | running `version()`. 7 | 8 | ```javascript 9 | // As a Promise 10 | cache.version().then(function(v) { 11 | console.log('This server is running version %s of Memcached', v); 12 | }); 13 | 14 | // With ESNext async/await 15 | let version = await cache.version(); 16 | 17 | // With standard callback 18 | cache.version(function(err, v) { 19 | console.log('This server is running version %s of Memcached', v); 20 | }); 21 | 22 | ``` 23 | 24 | Note, for simplicity, this will just query a single server and retrieve the 25 | version, so if you are connected to multiple servers it will still only return 26 | a single result (aka one version running on one of the servers). 27 | 28 | We may introduce `versions()` at some point which would query all servers and 29 | return the version running on each server but that seemed a bit complicated for 30 | the most likely use case of getting the version for a single server. 31 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "curly": true, 4 | "devel": false, 5 | "eqeqeq": true, 6 | "expr": true, 7 | "immed": true, 8 | "indent": 4, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "noempty": true, 13 | "node": true, 14 | "quotmark": "single", 15 | "sub": true, 16 | "trailing": true, 17 | "undef": true, 18 | "unused": true, 19 | "eqnull": true, 20 | "esversion": 8, 21 | "globals": { 22 | "_": true, 23 | "$": true, 24 | "after": true, 25 | "afterEach": true, 26 | "angular": true, 27 | "before": true, 28 | "beforeEach": true, 29 | "chance": true, 30 | "describe": true, 31 | "document": true, 32 | "escape": true, 33 | "expect": true, 34 | "it": true, 35 | "localStorage": true, 36 | "moment": true, 37 | "Promise": true, 38 | "qs": true, 39 | "request": true, 40 | "sinon": true, 41 | "URI": true, 42 | "window": true, 43 | "Zombie": true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Victor Quinn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/cachedump.md: -------------------------------------------------------------------------------- 1 | # Items 2 | 3 | `cachedump(, [])` 4 | 5 | ### Basic case 6 | 7 | Gets cache data for a given slabs id. 8 | 9 | ```javascript 10 | client 11 | .cachedump(1) 12 | .then(function(items) { 13 | /* Returns an (empty) array that looks like this: 14 | [{ 15 | key: 'test', 16 | bytes: 4, 17 | expiry_time_secs: 1476901980 18 | } ] 19 | */ 20 | }); 21 | ``` 22 | 23 | ### Limit items returned 24 | 25 | ```javascript 26 | client 27 | .cachedump(1, 1) 28 | .then(function(items) { 29 | /* Returns an (empty) array that looks like this: 30 | [{ 31 | key: 'test', 32 | bytes: 4, 33 | expiry_time_secs: 1476901980 34 | } ] 35 | */ 36 | }); 37 | ``` 38 | 39 | 40 | ### Callbacks 41 | 42 | Memcache Plus will always return a [Promise](https://www.promisejs.org), but it 43 | can also take a traditional callback for any of its methods so it can work just 44 | like most of the other Memcache modules out there. For example: 45 | 46 | ```javascript 47 | client.items(function(items) { 48 | ... 49 | }); 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/items.md: -------------------------------------------------------------------------------- 1 | # Items 2 | 3 | ### Basic case 4 | 5 | Gets items statistics. 6 | 7 | ```javascript 8 | client 9 | .items() 10 | .then(function(items) { 11 | /* Returns an (empty) array that looks like this: 12 | [{ 13 | slab_id: 1, 14 | data: { 15 | number: 2, 16 | age: 4918, 17 | evicted: 0, 18 | evicted_nonzero: 0, 19 | evicted_time: 0, 20 | outofmemory: 0, 21 | tailrepairs: 0, 22 | reclaimed: 0, 23 | expired_unfetched: 0, 24 | evicted_unfetched: 0, 25 | crawler_reclaimed: 0, 26 | lrutail_reflocked: 0 27 | }, 28 | server: 'localhost:11211' 29 | }] 30 | */ 31 | }); 32 | ``` 33 | 34 | ### Callbacks 35 | 36 | Memcache Plus will always return a [Promise](https://www.promisejs.org), but it 37 | can also take a traditional callback for any of its methods so it can work just 38 | like most of the other Memcache modules out there. For example: 39 | 40 | ```javascript 41 | client.items(function(items) { 42 | ... 43 | }); 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/elasticache.md: -------------------------------------------------------------------------------- 1 | # Elasticache 2 | 3 | Amazon provides Memcache (and Redis) as a hosted webservice they call 4 | [Elasticache](https://aws.amazon.com/elasticache/). 5 | 6 | Memcache Plus has some special enhancements which make connecting to Elasticache 7 | a bit more easy. 8 | 9 | ### Auto Discovery 10 | 11 | Amazon provides something called [Auto Discovery](https://aws.amazon.com/elasticache/faqs/#memcached-auto-discovery) 12 | which is an enhancement to Memcache and basically allows you to specify a single 13 | url for a cluster (rather than specifying each host url separately) and that single 14 | url can be queried for information on the other nodes in your cluster. 15 | 16 | Memcache Plus can handle the auto discovery for you! 17 | 18 | Simply specify a single host when you create your client and enable the 19 | `autodiscover` option: 20 | 21 | ```javascript 22 | var MemcachePlus = require('memcache-plus'); 23 | 24 | var client = new MemcachePlus({ 25 | hosts: ['victor-di6cba.cfg.use1.cache.amazonaws.com'], 26 | autodiscover: true 27 | }); 28 | ``` 29 | 30 | And that's it! Memcache Plus will use that discovery url to reach out to Amazon, 31 | retrieve information on all other nodes in the cluster, then automatically 32 | connect to all of them. 33 | -------------------------------------------------------------------------------- /docs/delete.md: -------------------------------------------------------------------------------- 1 | # Delete 2 | 3 | ### Basic case 4 | 5 | If you've got a key you want to delete, simply call the `delete()` method and 6 | supply a key: 7 | 8 | ```javascript 9 | client 10 | .delete('firstName') 11 | .then(function() { 12 | console.log('Successfully deleted the value associated with key firstName') 13 | }) 14 | ``` 15 | 16 | with async/await 17 | 18 | ```javascript 19 | await client.delete('firstName') 20 | console.log('Successfully deleted the value associated with key firstName') 21 | ``` 22 | 23 | 24 | ### Delete Multi 25 | 26 | However, if you need to delete multiple keys at once, calling `delete()` over 27 | and over again can be rather inefficient. For this reason, Memcache Plus 28 | supports `deleteMulti()` for which you provide an array of keys and that single 29 | command will delete all of them at once from Memcache: 30 | 31 | ```javascript 32 | client 33 | .deleteMulti(['firstName', 'middleName', 'lastName']) 34 | .then(function() { 35 | console.log('Successfully deleted all three values with the supplied keys'); 36 | }); 37 | ``` 38 | 39 | with async/await 40 | 41 | ```javascript 42 | await client.deleteMulti(['firstName', 'middleName', 'lastName']) 43 | console.log('Successfully deleted all three values with the supplied keys') 44 | ``` 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memcache Plus 2 | 3 | Memcache Plus - Better memcache for node 4 | 5 | Full Documentation at: [https://memcache-plus.com](https://memcache-plus.com) 6 | 7 | [![NPM](https://nodei.co/npm/memcache-plus.png)](https://nodei.co/npm/memcache-plus?downloads=true) 8 | 9 | 10 | 11 | ## What makes it "Plus"? 12 | 13 | * Native support for Promises or Callbacks 14 | * Elasticache auto discovery baked in 15 | * Actively developed and used 16 | * Focus on cleanliness and simplicity 17 | * Command buffering - start issuing commands right away, *memcache-plus* will automatically wait until connected then flush that buffer 18 | * Ability to disable with just a flag on init - sounds trivial, but nice to test with memcache off without altering any of your code 19 | * Compression built in on a per item basis 20 | * Cached retrieve (*coming soon!*) - simply pass a function for retrieving a value and a key and memcache-plus will do the whole "check key, if it exists return it, if not run the function to retrieve it, set the value, and return it" for you 21 | * Support for binaries (*coming soon!*) which the other memcache libraries for Node don't support 22 | -------------------------------------------------------------------------------- /docs/getmulti.md: -------------------------------------------------------------------------------- 1 | # Get Multiple 2 | 3 | Let's say you want to get 5 keys at once. The following is rather onerous: 4 | 5 | ```javascript 6 | Promise 7 | .all([ 8 | client.get('hydrogen'), client.get('helium'), 9 | client.get('oxygen'), client.get('carbon'), client.get('nitrogen') 10 | ]) 11 | .then(function(values) { 12 | console.log('Successfully retrieved these 5 keys'); 13 | }); 14 | ``` 15 | 16 | The above would also fire off 5 separate calls from your client to your Memcached 17 | server. 18 | 19 | Thankfully Memcache Plus provides a convenience method called `getMulti()` that 20 | will combine these into a single call: 21 | 22 | ```javascript 23 | client 24 | .getMulti(['hydrogen', 'helium', 'oxygen', 'carbon', 'nitrogen']) 25 | .then(function(values) { 26 | console.log('Successfully retrieved these 5 keys'); 27 | }); 28 | ``` 29 | 30 | or with async/await 31 | 32 | ```javascript 33 | const values = await client.getMulti(['hydrogen', 'helium', 'oxygen', 'carbon', 'nitrogen']) 34 | console.log('Successfully retrieved these 5 keys'); 35 | 36 | // You can also use destructuring if you'd like to get each of these as their 37 | // own variable 38 | const [hydrogen, lithium, sodium] = await client.get(['hydrogen', 'lithium', 'sodium']) 39 | console.log(`${ hydrogen } and ${ lithium } and ${ sodium }` 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/disconnect.md: -------------------------------------------------------------------------------- 1 | # Disconnect 2 | 3 | ### Basic case 4 | 5 | In order to disconnect and close any and all open connections, simply call the 6 | `disconnect()` method on the client: 7 | 8 | ```javascript 9 | client 10 | .disconnect() 11 | .then(function() { 12 | console.log('Successfully disconnected from all clients!'); 13 | }); 14 | ``` 15 | 16 | ### Disconnect from a specific host or hosts 17 | 18 | However, if you would like to disconnect from a single host or list of hosts 19 | but keep connections to the rest, you can do so by calling disconnect and 20 | specifying either a single connection (as a string) or multiple (as an array) 21 | and Memcache Plus will disconnect from only those you specify 22 | 23 | ```javascript 24 | // Single as a string 25 | client 26 | .disconnect('myserver.com:11211') 27 | .then(function() { 28 | console.log('Successfully disconnected from only myserver.com:11211'); 29 | }); 30 | 31 | // Multiple as an array 32 | client 33 | .disconnect(['myserver1.com:11211', 'myserver2.com:11211']) 34 | .then(function() { 35 | console.log('Disconnected from myserver1.com:11211 AND myserver2.com:11211'); 36 | }); 37 | ``` 38 | 39 | Note, if you specify a full disconnect `disconnect()` or specify all currently 40 | open connections, the `reconnect` option will be automatically set to `false`. 41 | Otherwise you'll close the connection and Memcache Plus will automatically try 42 | to reconnect which of course you don't want! 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memcache-plus", 3 | "version": "0.3.1", 4 | "description": "Better memcache for node", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "grunt", 8 | "test": "grunt test", 9 | "build": "docpress build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/victorquinn/memcache-plus" 14 | }, 15 | "keywords": [ 16 | "memcache", 17 | "memcached", 18 | "cache", 19 | "promise", 20 | "elasticache", 21 | "cluster" 22 | ], 23 | "author": "Victor Quinn ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/victorquinn/memcache-plus/issues" 27 | }, 28 | "docpress": { 29 | "github": "victorquinn/memcache-plus", 30 | "googleAnalytics": { 31 | "id": "UA-29142694-4", 32 | "domain": "memcache-plus.com" 33 | } 34 | }, 35 | "homepage": "https://github.com/victorquinn/memcache-plus", 36 | "devDependencies": { 37 | "chai": "^3.5.0", 38 | "chance": "^1.0.4", 39 | "docpress": "0.7.1", 40 | "git-update-ghpages": "1.3.0", 41 | "grunt": "^1.0.1", 42 | "grunt-contrib-jshint": "^3.0.0", 43 | "grunt-contrib-watch": "^1.0.0", 44 | "grunt-mocha-test": "^0.13.2", 45 | "jshint-stylish": "^2.2.0", 46 | "json-stringify-safe": "^5.0.0", 47 | "mocha": "^3.4.2", 48 | "sinon": "^1.10.3" 49 | }, 50 | "dependencies": { 51 | "bluebird": "^3.4.1", 52 | "carrier": "^0.3.0", 53 | "debug": "^3.1.0", 54 | "hashring": "^3.2.0", 55 | "immutable": "^4.1.0", 56 | "lodash": "^4.14.0", 57 | "ramda": "~0.28.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/misc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file misc.js 3 | * 4 | * Miscellaneous utility methods 5 | */ 6 | 7 | var Promise = require('bluebird'), 8 | zlib = Promise.promisifyAll(require('zlib')); 9 | 10 | function assert(cond, msg) { 11 | if(!cond) { throw new Error('AssertionError: ' + msg); } 12 | } 13 | exports.assert = assert; 14 | 15 | exports.defer = function defer(key) { 16 | key = key || null; 17 | 18 | var resolve, reject; 19 | var promise = new Promise(function() { 20 | resolve = arguments[0]; 21 | reject = arguments[1]; 22 | }); 23 | return { 24 | key: key, 25 | resolve: resolve, 26 | reject: reject, 27 | promise: promise 28 | }; 29 | }; 30 | 31 | /** 32 | * compress() - Compress the supplied value 33 | * 34 | * @param {Buffer} val - the buffer to compress 35 | * @returns {Promise} - Promise for the compressed buffer 36 | */ 37 | exports.compress = function compress(val) { 38 | assert(val instanceof Buffer, 'Memcache-Plus can only compress a Buffer'); 39 | return zlib.deflateAsync(val); 40 | }; 41 | 42 | /** 43 | * decompress() - Decompress the supplied value 44 | * 45 | * @param {Buffer} val - the buffer to decompress 46 | * @returns {Promise} - Promise for the decompressed buffer 47 | */ 48 | exports.decompress = function decompress(val) { 49 | assert(val instanceof Buffer, 'Memcache-Plus can only decompress a Buffer'); 50 | return zlib.inflateAsync(val); 51 | }; 52 | 53 | /** 54 | * truncateIfNecessary() - Truncate string if too long, for display purposes 55 | * only 56 | */ 57 | exports.truncateIfNecessary = function truncateIfNecessary(str, len) { 58 | assert(typeof str === 'string', 'str needs to be of type "string"'); 59 | len = len || 100; 60 | return str && str.length > len ? str.substr(0, len) + '...' : str; 61 | }; 62 | -------------------------------------------------------------------------------- /docs/replace.md: -------------------------------------------------------------------------------- 1 | # Replace 2 | 3 | ### Basic case 4 | 5 | Replace sets a new value for a key if and only if that key already exists 6 | 7 | ```javascript 8 | client 9 | .replace('firstName', 'Victor') 10 | .then(function() { 11 | console.log('Successfully replaced the value for the key firstName') 12 | }) 13 | ``` 14 | 15 | with async/await 16 | 17 | ```javascript 18 | await client.replace('firstName', 'Victor') 19 | console.log('Successfully replaced the value for the key firstName') 20 | ``` 21 | 22 | ### Error if key does not already exist 23 | 24 | If a key does not already exist and you try to use the `replace` command, 25 | Memcached will return an error which Memcache Plus will throw. 26 | 27 | ```javascript 28 | // If 'firstName' does not already exist 29 | client 30 | .replace('firstName', 'Victor') 31 | .then(function() { 32 | // This will not get hit because `replace` will throw on error 33 | console.log('Successfully replaced the key firstName'); 34 | }) 35 | .catch(function(err) { 36 | // Will print: 'Cannot "replace" for key "firstName" because it does not exist' 37 | console.error(err); 38 | }); 39 | ``` 40 | 41 | ### Callbacks 42 | 43 | Memcache Plus will always return a [Promise](https://www.promisejs.org), but it 44 | can also take a traditional callback for any of its methods so it can work just 45 | like most of the other Memcache modules out there. For example: 46 | 47 | ```javascript 48 | client.replace('firstName', 'Victor, function(err) { 49 | console.log('Successfully replaced the key firstName'); 50 | }); 51 | ``` 52 | 53 | And if you try to replace a key that does not already exist: 54 | 55 | ```javascript 56 | // If 'firstName' does not already exist 57 | client.replace('firstName', 'Victor', function(err) { 58 | // Will print: 'Cannot "replace" for key "firstName" because it does not exist' 59 | console.error(err); 60 | }); 61 | ``` 62 | -------------------------------------------------------------------------------- /lib/connection-pool.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('memcache-plus:connection-pool'); 2 | 3 | var Connection = require('./connection'); 4 | 5 | 6 | function ConnectionPool(opts) { 7 | // Initialise the pool 8 | this.pool = []; 9 | this.poolRR = 0; 10 | const poolSize = opts.poolSize || 1; 11 | debug('Creating a connection pool with %d connections', poolSize); 12 | for (let x = 0; x < poolSize; x++ ) { 13 | this.pool.push(new Connection(opts)); 14 | } 15 | 16 | // Methods to which we need to select a single connection 17 | [ 18 | 'autodiscovery', 'set', 'cas', 'incr', 'decr', 19 | 'gets', 'get', 'flush_all', 'add', 'replace', 'append', 20 | 'prepend', 'delete', 'version', 21 | ].forEach((method) => { 22 | this[method] = (...args) => { 23 | const connIdx = this.selectConn(this.pool); 24 | const conn = this.pool[connIdx]; 25 | return conn[method].apply(conn, args); 26 | }; 27 | }); 28 | 29 | // Methods to which we'll broadcast the operations 30 | [ 31 | 'disconnect', 'destroy', 'connect', 'flushBuffer' 32 | ].forEach((method) => { 33 | this[method] = (...args) => { 34 | this.pool.forEach(conn => conn[method].apply(conn, args)); 35 | }; 36 | }); 37 | 38 | // Methods that we are not implementing here on purpose (because they don't apply) 39 | [ 40 | 'write', 'read' 41 | ].forEach((method) => { 42 | this[method] = () => { 43 | throw new Error(`Method '${method}' not implemented in connection pools`); 44 | }; 45 | }); 46 | 47 | // TODO: We could make this event-driven 48 | setInterval(() => { 49 | this.ready = !!this.pool.find(conn => conn.ready); 50 | }, 100); 51 | 52 | return this; 53 | } 54 | 55 | ConnectionPool.prototype.selectConn = function() { 56 | const connIdx = this.poolRR++; 57 | if (this.poolRR >= this.pool.length) { 58 | this.poolRR = 0; 59 | } 60 | return connIdx; 61 | }; 62 | 63 | module.exports = ConnectionPool; 64 | -------------------------------------------------------------------------------- /docs/add.md: -------------------------------------------------------------------------------- 1 | # Add 2 | 3 | ### Basic case 4 | 5 | Add sets a new value for a key if and only if that key doesn't already exist 6 | 7 | ```javascript 8 | client 9 | .add('firstName', 'Victor') 10 | .then(function() { 11 | console.log('Successfully added the key firstName') 12 | }) 13 | ``` 14 | 15 | with async/await 16 | 17 | ```javascript 18 | await client.add('firstName', 'Victor') 19 | console.log('Successfully added the key firstName') 20 | ``` 21 | 22 | ### Error if key already exists 23 | 24 | If a key already exists and you try to use the `add` command, Memcached will 25 | return an error which Memcache Plus will throw. 26 | 27 | ```javascript 28 | // If 'firstName' already exists 29 | client 30 | .add('firstName', 'Victor') 31 | .then(function() { 32 | // This will not get hit because `add` will throw on error 33 | console.log('Successfully added the key firstName'); 34 | }) 35 | .catch(function(err) { 36 | // Will print: 'Cannot "add" for key "firstName" because it already exists' 37 | console.error(err); 38 | }); 39 | ``` 40 | 41 | with async/await 42 | 43 | ```javascript 44 | // If 'firstName' already exists 45 | try { 46 | await client.add('firstName', 'Victor') 47 | // This will not get hit because `add` will throw on error 48 | console.log('Successfully added the key firstName') 49 | } catch(err) { 50 | // Will print: 'Cannot "add" for key "firstName" because it already exists' 51 | console.error(err) 52 | } 53 | ``` 54 | 55 | ### Callbacks 56 | 57 | Memcache Plus will always return a [Promise](https://www.promisejs.org), but it 58 | can also take a traditional callback for any of its methods so it can work just 59 | like most of the other Memcache modules out there. For example: 60 | 61 | ```javascript 62 | client.add('firstName', 'Victor, function(err) { 63 | console.log('Successfully added the key firstName'); 64 | }); 65 | ``` 66 | 67 | And if you try to add to a key that already exists: 68 | 69 | ```javascript 70 | // If 'firstName' already exists 71 | client.add('firstName', 'Victor', function(err) { 72 | // Will print: 'Cannot "add" for key "firstName" because it already exists' 73 | console.error(err); 74 | }); 75 | ``` 76 | -------------------------------------------------------------------------------- /test/connection.js: -------------------------------------------------------------------------------- 1 | 2 | require('chai').should(); 3 | 4 | var Connection = require('../lib/connection'); 5 | 6 | describe('Connection', function() { 7 | var connection; 8 | beforeEach(function() { 9 | connection = new Connection(); 10 | }); 11 | 12 | it('initializes with defaults', function() { 13 | connection.should.have.property('host'); 14 | connection.host.should.be.a('string'); 15 | connection.should.have.property('port'); 16 | connection.port.should.be.a('string'); 17 | }); 18 | 19 | it('initiates connection', function() { 20 | connection.should.have.property('client'); 21 | connection.client.should.be.ok; 22 | 23 | }); 24 | 25 | it('does connect', function(done) { 26 | connection.on('connect', function() { 27 | done(); 28 | }); 29 | }); 30 | 31 | it('reconnects, if enabled and connection lost', function(done) { 32 | connection.on('connect', function() { 33 | connection.client.end(); 34 | connection.on('connect', function() { 35 | done(); 36 | }); 37 | }); 38 | }); 39 | 40 | describe('does not reconnect when', function() { 41 | it('reconnect is disabled and connection lost', function(done) { 42 | connection = new Connection({ reconnect: false }); 43 | connection.on('connect', function() { 44 | connection.client.end(); 45 | connection.on('connect', function() { 46 | done(new Error('The client should not have attempted to reconnect')); 47 | }); 48 | setTimeout(function() { 49 | done(); 50 | }, 50); 51 | }); 52 | }); 53 | 54 | it('intentionally disconnected', function(done) { 55 | connection = new Connection(); 56 | connection.on('connect', function() { 57 | connection.disconnect(); 58 | connection.on('connect', function() { 59 | done(new Error('The client should not have attempted to reconnect')); 60 | }); 61 | setTimeout(function() { 62 | done(); 63 | }, 50); 64 | }); 65 | }); 66 | 67 | }); 68 | 69 | afterEach(function() { 70 | connection.disconnect(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /docs/basics.md: -------------------------------------------------------------------------------- 1 | # Set 2 | 3 | ### Basic case 4 | 5 | After you've created a client (which will automatically establish a connection), 6 | you can start issuing commands! The most basic is to set a value: 7 | 8 | ```javascript 9 | client 10 | .set('firstName', 'Victor') 11 | .then(function() { 12 | console.log('Successfully set the key firstName'); 13 | }); 14 | ``` 15 | 16 | Set takes 2 arguments, the key and the value and it returns a Promise. 17 | 18 | Both the key and the value must be [strings](misc.md). So if you would like to set an 19 | Object as a value, you must stringify it first: 20 | 21 | ```javascript 22 | var myVal = { 23 | firstName: 'Victor', 24 | lastName: 'Quinn' 25 | }; 26 | 27 | client 28 | .set('user', JSON.stringify(myVal)) 29 | .then(function() { 30 | console.log('Successfully set the stringified object'); 31 | }); 32 | ``` 33 | 34 | ### Compression 35 | 36 | Optionally you can request that compression be enabled for a given item: 37 | 38 | ```javascript 39 | client.set('firstName', 'Victor', { compressed: true }) 40 | .then(function() { 41 | console.log('Successfully set the key firstName as compressed data'); 42 | }); 43 | ``` 44 | By enabling this option, every key will be compressed with Node's 45 | [zlib](https://nodejs.org/api/zlib.html) library prior to being stored. 46 | 47 | This is helpful in the event that you are attempting to store data (such as a 48 | stringified object) which is too large for the standard Memcache value limit 49 | size. It is also helpful in situations where the memory allocated to your 50 | Memcache instance is limited, such as on a Raspberry Pi or some other embedded 51 | hardware. 52 | 53 | For example, if your Memcache server is set with a limit of 1MB for a value and 54 | you attempt to store a 1.2MB object, the set will fail. However, enabling 55 | compression will cause the value to be compressed with zlib before it is stored 56 | so the size may be reduced significantly and save successfully. 57 | 58 | Notes: 59 | 60 | 1. If you store a value compressed with `set()` you have to `get()` it and 61 | specify that it was compressed. There is no automatic inspection of values to 62 | determine whether they were set with compression and decompress automatically 63 | (as this would incur significant performance penalty) 64 | 1. Enabling compression will reduce the size of the objects stored but it will 65 | also add a non-negligent performance hit to each `set()` and `get()` so use it 66 | judiciously! 67 | 1. The maximum key size is 250 bytes. This is a Memcache limitation and this 68 | library will throw an error if a key larger than that is provided. 69 | 70 | ### Errors 71 | 72 | Memcache Plus will throw errors for any unexpected or broken behavior so you 73 | know quickly if something is wrong with your application. These could be things 74 | like: trying to `set()` an item without a key, trying to `set()` an item that 75 | is too large, trying to `set()` with a key that is too long, and many more. 76 | -------------------------------------------------------------------------------- /docs/incrdecr.md: -------------------------------------------------------------------------------- 1 | # Incr/Decr 2 | 3 | ## Incr 4 | `incr` can be used to increment the value of a given key in a single command. 5 | 6 | ```javascript 7 | // Increment myCountValue by 1, returns a promise 8 | client.incr('myCountValue') 9 | ``` 10 | 11 | with async/await 12 | 13 | ```javascript 14 | // Increment myCountValue by 1, wait for it 15 | await client.incr('myCountValue') 16 | console.log('myCountValue increased by 1') 17 | ``` 18 | 19 | ## Decr 20 | `decr` can be used to decrement the value of a given key in a single command. 21 | 22 | ```javascript 23 | // Decrement myCountValue by 1, returns a promise 24 | client.decr('myCountValue') 25 | ``` 26 | 27 | with async/await 28 | 29 | ```javascript 30 | // Decrement myCountValue by 1, wait for it 31 | await client.decr('myCountValue') 32 | console.log('myCountValue decreased by 1') 33 | ``` 34 | 35 | ## Notes 36 | 37 | In cases where you just want to increase or decrease the value of an item, using 38 | incr/decr makes a lot more sense than performing a `get` then increasing or 39 | decreasing its value then performing a `set`. 40 | 41 | It is also helpful to eliminate race conditions since it is an atomic operation. 42 | For example, if your Memcached server is shared by multiple hosts and multiple 43 | want to increment a key, you could have a scenario where both hosts perform the 44 | `get`, then increment then both perform the `set`. In this case where you'd 45 | expect the value to be incremented by 2, it may instead be incremented only by 1 46 | because the two fought and neither won. 47 | 48 | Using increment eliminates those kinds of race conditions since the whole thing 49 | is performed in a single Memcached command. 50 | 51 | #### Defaults to 1 52 | 53 | In the simplest case, if you provide no increment amount, Memcache Plus will 54 | increment or decrement the supplied key by `1`. It will then resolve the Promise 55 | with the updated value of that item. 56 | 57 | ```javascript 58 | client 59 | .incr('myCountValue') 60 | .then(function(val) { 61 | // The new value will be exactly 1 more than the old one 62 | console.log('the new value of myCountValue is', val) 63 | }) 64 | ``` 65 | 66 | with async/await 67 | 68 | ```javascript 69 | const val = await client.incr('myCountValue') 70 | // The new value will be exactly 1 more than the old one 71 | console.log('the new value of myCountValue is', val) 72 | ``` 73 | 74 | #### Can specify incr/decr amount 75 | 76 | However, you can also specify an increment/decrement amount. For instance, to 77 | decrement a number by 5: 78 | 79 | ```javascript 80 | client 81 | .decr('myCountValue', 5) 82 | .then(function(val) { 83 | // The new value will be exactly 5 less than the old one 84 | console.log('the new value of myCountValue is', val); 85 | }); 86 | ``` 87 | 88 | with async/await 89 | 90 | ```javascript 91 | const val = await client.decr('myCountValue', 5) 92 | // The new value will be exactly 5 less than the old one 93 | console.log('the new value of myCountValue is', val) 94 | ``` 95 | 96 | #### Only works on numeric types 97 | 98 | Since it makes no sense to increment something like `"banana"` or 99 | `{ "foo": "bar" }`, `incr` and `decr` can only be used to increment numeric 100 | types. 101 | -------------------------------------------------------------------------------- /docs/get.md: -------------------------------------------------------------------------------- 1 | # Get 2 | 3 | ### Basic case 4 | 5 | Now that you've used `set()` to set some values, let's use `get()` to retrieve them! 6 | 7 | ```javascript 8 | client 9 | .get('firstName') 10 | .then(function(firstName) { 11 | console.log('Successfully got the key firstName: ', firstName); 12 | // Would print: "Successfully got the key firstName: Victor" 13 | }); 14 | ``` 15 | 16 | or with async/await 17 | 18 | ```javascript 19 | const firstName = await client.get('firstName') 20 | console.log(`Successfully got the key firstName: ${ firstName }`) 21 | // Would print: "Successfully got the key firstName: Victor" 22 | ``` 23 | 24 | Get takes 1 argument, the key, and it returns a Promise. It has an optional 25 | second argument which is an object to specify options for this retrieval. 26 | 27 | The key must be a string. The value you get back from the resolution 28 | of this promise will have the same type it had when you `set` it. 29 | 30 | For example, if you had previously `set()` with an object, you'll get back an 31 | object. 32 | 33 | ```javascript 34 | client 35 | .get('user') 36 | .then(function(user) { 37 | console.log('Successfully got the object: ', user); 38 | // Would print: "Successfully got the object: { firstName: 'Victor', lastName: 'Quinn' }" 39 | }); 40 | ``` 41 | 42 | or with async/await 43 | 44 | ```javascript 45 | const user = await client.get('user') 46 | console.log('Successfully got the object: ', user); 47 | // Would print: "Successfully got the object: { firstName: 'Victor', lastName: 'Quinn' }" 48 | ``` 49 | 50 | ### Callbacks 51 | 52 | Memcache Plus will always return a [Promise](https://www.promisejs.org), but it 53 | can also take a traditional callback for any of its methods so it can work just 54 | like most of the other Memcache modules out there. For example: 55 | 56 | ```javascript 57 | client.get('firstName', function(firstName) { 58 | console.log('Successfully got the value for key firstName: ', firstName); 59 | }); 60 | ``` 61 | 62 | ### No value 63 | 64 | When there is no value set for a key, Memcache Plus will simply return `null` as 65 | the value. 66 | 67 | For example: 68 | 69 | ```javascript 70 | client 71 | .get('keyThatDoesNotExist') 72 | .then(function(value) { 73 | console.log('The value is: ', value); 74 | // Would print: "The value is: null" 75 | }); 76 | ``` 77 | 78 | with async/await 79 | 80 | ```javascript 81 | const value = await client.get('keyThatDoesNotExist') 82 | console.log('The value is: ', value); 83 | // Would print: "The value is: null" 84 | ``` 85 | 86 | ### Compression 87 | 88 | If an item was written with `set()` with compression enabled, you can specify 89 | that fact when retrieving the object or it will not be decompressed by Memcache 90 | Plus: 91 | 92 | ```javascript 93 | client.get('firstName', { compressed: true }) 94 | .then(function(firstName) { 95 | console.log('Successfully got the key firstName as compressed data: ', firstName); 96 | // Would print: "Successfully got the key firstName as compressed data: Victor" 97 | }); 98 | ``` 99 | 100 | with async/await 101 | 102 | ```javascript 103 | const firstName = await client.get('firstName', { compressed: true }) 104 | console.log('Successfully got the key firstName as compressed data and automatically uncompressed it: ', firstName); 105 | // Would print: "Successfully got the key firstName as compressed data: Victor" 106 | ``` 107 | 108 | However, compressed objects set by newer versions of Memcache Plus will 109 | automatically be decompressed without having to provide this flag. 110 | 111 | By enabling this option, every value will be compressed with Node's 112 | [zlib](https://nodejs.org/api/zlib.html) library after being retrieved. 113 | 114 | Notes: 115 | 116 | 1. Enabling compression will reduce the size of the objects stored but it will 117 | also add a non-negligent performance hit to each `set()` and `get()` since 118 | compression is rather CPU intensive so use it judiciously! 119 | -------------------------------------------------------------------------------- /docs/appendprepend.md: -------------------------------------------------------------------------------- 1 | # Append 2 | 3 | Will append data to the supplied key if and only if it already exists. 4 | 5 | In other words, add the value you supply to the end of the value currently 6 | residing in the supplied key. 7 | 8 | ### Basic case 9 | 10 | ```javascript 11 | // Assuming you have a key called `milkshake` and it currently has the value 12 | // `vanilla` 13 | 14 | client 15 | .append('milkshake', ' malt') 16 | .then(function() { 17 | // now milkshake has a value of `vanilla malt` 18 | console.log('Successfully appended to the key milkshake'); 19 | }); 20 | ``` 21 | 22 | with async/await 23 | 24 | ```javascript 25 | // Assuming you have a key called `milkshake` and it currently has the value 26 | // `vanilla` 27 | 28 | await client.append('milkshake', ' malt') 29 | // now milkshake has a value of `vanilla malt` 30 | console.log('Successfully appended to the key milkshake') 31 | ``` 32 | 33 | ### Error if key doesn't yet exist 34 | 35 | If a key does not already exist and you try to use the `append` command, 36 | Memcached will return an error which Memcache Plus will throw. 37 | 38 | ```javascript 39 | // If 'milkshake' does not already exist 40 | client 41 | .append('milkshake', ' malt') 42 | .then(function() { 43 | // This will not get hit because `append` will throw on error 44 | console.log('Successfully replaced the key milkshake') 45 | }) 46 | .catch(function(err) { 47 | // Will print: 'Cannot "replace" for key "milkshake" because it does not exist' 48 | console.error(err) 49 | }) 50 | ``` 51 | 52 | with async/await 53 | 54 | ```javascript 55 | try { 56 | // If 'milkshake' does not already exist 57 | await client.append('milkshake', ' malt') 58 | // This will not get hit because `append` will throw on error 59 | console.log('Successfully replaced the key milkshake') 60 | } catch (err) { 61 | // Will print: 'Cannot "append" for key "milkshake" because it does not exist' 62 | console.error(err) 63 | } 64 | ``` 65 | 66 | ### Callbacks 67 | 68 | Memcache Plus will always return a [Promise](https://www.promisejs.org) which 69 | means it will work seamlessly with async/await as in many of the examples here 70 | but it can also take a traditional callback for any of its methods so it can work 71 | just like most of the other Memcache modules out there. For example: 72 | 73 | ```javascript 74 | client.append('milkshake', ' malt', function(err) { 75 | console.log('Successfully appended to the key milkshake'); 76 | }) 77 | ``` 78 | 79 | And if you try to append a key that does not already exist: 80 | 81 | ```javascript 82 | // If 'milkshake' does not already exist 83 | client.append('milkshake', ' malt', function(err) { 84 | // Will print: 'Cannot "append" to key "milkshake" because it does not exist' 85 | console.error(err); 86 | }); 87 | ``` 88 | 89 | # Prepend 90 | 91 | Will prepend data to the supplied key if and only if it already exists 92 | 93 | ### Basic case 94 | 95 | ```javascript 96 | // Assuming you have a key called `gauge` and it currently has the value 97 | // `meter` 98 | 99 | client 100 | .prepend('gauge', 'thermo') 101 | .then(function() { 102 | // now gauge has a value of `thermometer` 103 | console.log('Successfully prepended to the key meter'); 104 | }); 105 | ``` 106 | 107 | with async/await 108 | 109 | ```javascript 110 | // Assuming you have a key called `gauge` and it currently has the value 111 | // `meter` 112 | 113 | await client.prepend('gauge', 'thermo') 114 | // now gauge has a value of `thermometer` 115 | console.log('Successfully prepended to the key gauge') 116 | ``` 117 | 118 | ### Error if key doesn't yet exist 119 | 120 | If a key does not already exist and you try to use the `prepend` command, 121 | Memcached will return an error which Memcache Plus will throw. 122 | 123 | ```javascript 124 | // If 'gauge' does not already exist 125 | client 126 | .prepend('gauge', 'thermo') 127 | .then(function() { 128 | // This will not get hit because `prepend` will throw on error 129 | console.log('Successfully replaced the key gauge'); 130 | }) 131 | .catch(function(err) { 132 | // Will print: 'Cannot "replace" for key "gauge" because it does not exist' 133 | console.error(err); 134 | }); 135 | ``` 136 | 137 | ```javascript 138 | // If 'gauge' does not already exist 139 | try { 140 | await client.prepend('gauge', 'thermo') 141 | // This will not get hit because `prepend` will throw on error 142 | console.log('Successfully replaced the key gauge') 143 | } catch (err) { 144 | // Will print: 'Cannot "prepend" for key "gauge" because it does not exist' 145 | console.error(err) 146 | } 147 | ``` 148 | 149 | ### Callbacks 150 | 151 | Memcache Plus will always return a [Promise](https://www.promisejs.org), but it 152 | can also take a traditional callback for any of its methods so it can work just 153 | like most of the other Memcache modules out there. For example: 154 | 155 | ```javascript 156 | client.prepend('gauge', 'thermo', function(err) { 157 | console.log('Successfully prepended to the key gauge'); 158 | }); 159 | ``` 160 | 161 | And if you try to prepend a key that does not already exist: 162 | 163 | ```javascript 164 | // If 'gauge' does not already exist 165 | client.prepend('gauge', 'thermo', function(err) { 166 | // Will print: 'Cannot "prepend" to key "gauge" because it does not exist' 167 | console.error(err); 168 | }); 169 | ``` 170 | -------------------------------------------------------------------------------- /docs/set.md: -------------------------------------------------------------------------------- 1 | # Set 2 | 3 | ### Basic case 4 | 5 | After you've created a client (which will automatically establish a connection), 6 | you can start issuing commands! The most basic is to set a value: 7 | 8 | ```javascript 9 | client 10 | .set('firstName', 'Victor') 11 | .then(function() { 12 | console.log('Successfully set the key firstName'); 13 | }); 14 | ``` 15 | 16 | or with async/await 17 | 18 | ```javascript 19 | await client.set('firstName', 'Victor') 20 | console.log('Successfully set the key firstName') 21 | ``` 22 | 23 | ### Arguments 24 | 25 | `set()` requires 2 arguments and could have up to 4. 26 | 27 | The first is always the key and must be a string. 28 | The second is always the value and must be a string. 29 | The third is optional and could be either: (1) a ttl for this key (2) an options object or (3) a callback 30 | The fourth is only present if there is a third argument for ttl or options and a callback is provided. 31 | 32 | ### Key must be a string 33 | 34 | Non-string keys are not allowed and Memcache Plus will throw an error if you 35 | try to provide a non-string key. 36 | 37 | ```javascript 38 | client 39 | .set({ foo: 'bar' }, myVal) 40 | .then(function() { 41 | // This will never happen because an error will be thrown 42 | }) 43 | .catch(function(err) { 44 | // This will get hit! 45 | console.error('Oops we have an error', err); 46 | }); 47 | ``` 48 | 49 | or with async/await 50 | 51 | ```javascript 52 | try { 53 | await client.set({ foo: 'bar' }, myVal) 54 | // This will never happen because an error will be thrown 55 | } catch (err) { 56 | // This will get hit! 57 | console.error('Oops we have an error', err); 58 | } 59 | ``` 60 | 61 | ### Value can be of any type 62 | 63 | The value can be of any type (numeric, string, object, array, null, etc.) 64 | 65 | Memcache Plus will handle converting the value (if necessary) before sending to 66 | the Memcached server and converting it back upon retrieval. 67 | 68 | For instance, with Memcache Plus you can go ahead and set an object 69 | 70 | ```javascript 71 | var myVal = { 72 | firstName: 'Victor', 73 | lastName: 'Quinn' 74 | }; 75 | 76 | client 77 | .set('user', myVal) 78 | .then(function() { 79 | console.log('Successfully set the object'); 80 | }); 81 | ``` 82 | 83 | or with async/await 84 | 85 | ```javascript 86 | var myVal = { 87 | firstName: 'Victor', 88 | lastName: 'Quinn', 89 | } 90 | 91 | await client.set('user', myVal) 92 | console.log('Successfully set the object') 93 | ``` 94 | 95 | Then when you get it out it'll be an object: 96 | 97 | ```javascript 98 | client 99 | .get('user') 100 | .then(function(user) { 101 | // The user is a JS object: 102 | // { firstName: 'Victor', lastName: 'Quinn' } 103 | console.log('Successfully got the object', user); 104 | }); 105 | ``` 106 | with async/await 107 | 108 | ```javascript 109 | let user = await client.get('user') 110 | // The user is a JS object: 111 | // { firstName: 'Victor', lastName: 'Quinn' } 112 | console.log('Successfully got the object', user) 113 | ``` 114 | 115 | Same goes for numbers, arrays, etc. Memcache Plus will always return the exact 116 | type you put into it. 117 | 118 | ### TTL 119 | 120 | A key/value pair can be specified with an optional ttl which will specify how 121 | long (in seconds) that object persists before it is automatically purged from the cache. 122 | 123 | For example, to set a value that will stay around in the cache for only 10 seconds: 124 | 125 | ```javascript 126 | client.set('firstName', 'Victor', 10) 127 | ``` 128 | 129 | If you perform a `get()` within 10 seconds for `firstName`, you'll get back 130 | `"Victor"` but after 10 seconds, you will get `null`. 131 | 132 | ### Callbacks 133 | 134 | Memcache Plus will always return a [Promise](https://www.promisejs.org), but it 135 | can also take a traditional callback for any of its methods so it can work just 136 | like most of the other Memcache modules out there. For example: 137 | 138 | ```javascript 139 | client.set('firstName', 'Victor', function(err) { 140 | console.log('Successfully set the key firstName') 141 | }) 142 | ``` 143 | 144 | ### Compression 145 | 146 | Optionally you can request that compression be enabled for a given item: 147 | 148 | ```javascript 149 | client.set('firstName', 'Victor', { compressed: true }) 150 | .then(function() { 151 | console.log('Successfully set the key firstName as compressed data'); 152 | }); 153 | ``` 154 | 155 | or with async/await 156 | 157 | ```javascript 158 | await client.set('firstName', 'Victor', { compressed: true }) 159 | console.log('Successfully set the key firstName as compressed data') 160 | ``` 161 | 162 | By enabling this option, every key will be compressed with Node's 163 | [zlib](https://nodejs.org/api/zlib.html) library prior to being stored. 164 | 165 | This is helpful in the event that you are attempting to store data (such as a 166 | stringified object) which is too large for the standard Memcache value limit 167 | size. It is also helpful in situations where the memory allocated to your 168 | Memcache instance is limited, such as on a Raspberry Pi or some other embedded 169 | hardware. 170 | 171 | Note the maximum allowed value for a memcache item is set by the Memcached 172 | server and not something that can be tuned on the client alone. The default 173 | [is 1MB](https://docs.oracle.com/cd/E17952_01/mysql-5.6-en/ha-memcached-faq.html#faq-memcached-max-object-size) 174 | but it can be increased to up to 5MB 175 | 176 | For example, if your Memcache server is set with a limit of 1MB for a value and 177 | you attempt to store a 1.2MB object, the set will fail. However, enabling 178 | compression will cause the value to be compressed with zlib before it is stored 179 | so the size may be reduced significantly and save successfully. 180 | 181 | Notes: 182 | 183 | 1. If you store a value compressed with `set()` you have to `get()` it and 184 | specify that it was compressed. There is no automatic inspection of values to 185 | determine whether they were set with compression and decompress automatically 186 | (as this would incur significant performance penalty) 187 | 1. Enabling compression will reduce the size of the objects stored but it will 188 | also add a non-negligent performance hit to each `set()` and `get()` so use it 189 | judiciously! 190 | 1. The maximum key size is 250 bytes. This is a Memcache limitation and this 191 | library will throw an error if a key larger than that is provided. 192 | 193 | ### Errors 194 | 195 | Memcache Plus will throw errors for any unexpected or broken behavior so you 196 | know quickly if something is wrong with your application. These could be things 197 | like: trying to `set()` an item without a key, trying to `set()` an item that 198 | is too large, trying to `set()` with a key that is too long, and many more. 199 | -------------------------------------------------------------------------------- /docs/initialization.md: -------------------------------------------------------------------------------- 1 | # Initialization 2 | 3 | When you initialize Memcache Plus, you provide the address of the server(s) you 4 | are connecting to along with a series of options which can alter the behavior of 5 | the library. 6 | 7 | Below is a description of the options and some details about initialization. 8 | 9 | ## Kicking it off 10 | ```javascript 11 | const MemcachePlus = require('memcache-plus') 12 | 13 | const client = new MemcachePlus() 14 | ``` 15 | 16 | Instantiating the client will automatically establish a connection between your 17 | running application and your Memcache server. Make sure you do not have a 18 | firewall rule blocking port 11211 on your Memcache server. 19 | 20 | If you do not specify a host, Memcache Plus will default to connecting to 21 | `localhost:11211`. 22 | 23 | ## Specifying a Host 24 | 25 | #### As a string 26 | 27 | You can optionally provide a host as a single string argument. 28 | 29 | ```javascript 30 | const MemcachePlus = require('memcache-plus') 31 | 32 | // Will initiate a connection to 'my-memcache-server.com' on port 11211 33 | const client = new MemcachePlus('my-memcache-server.com') 34 | ``` 35 | 36 | If you do not specify a port with the host, Memcache Plus will attempt to 37 | connect to port 11211, the default port. 38 | 39 | You can also specify a port if for some reason your Memcached is running on 40 | some other port: 41 | 42 | ```javascript 43 | const MemcachePlus = require('memcache-plus') 44 | 45 | // Will initiate a connection to 'my-memcache-server.com' on port 12345 46 | const client = new MemcachePlus('my-memcache-server.com:12345') 47 | ``` 48 | 49 | #### As an array 50 | You can optionally provide an array of hosts to connect to multiple. 51 | 52 | More details on connecting to multiple hosts below, but the following will make 53 | this happen: 54 | 55 | ```javascript 56 | const MemcachePlus = require('memcache-plus') 57 | 58 | // Will initiate connections to both 'my-memcache-server1.com' and 59 | // 'my-memcache-server2.com' on port 11211 60 | const client = new MemcachePlus([ 61 | 'my-memcache-server1.com', 62 | 'my-memcache-server2.com' 63 | ]) 64 | ``` 65 | 66 | You can also specify ports if your Memcached servers are running on a 67 | non-default port: 68 | 69 | ```javascript 70 | const MemcachePlus = require('memcache-plus') 71 | 72 | // Will initiate connections to both 'my-memcache-server1.com' and 73 | // 'my-memcache-server2.com' on port 12345 74 | const client = new MemcachePlus([ 75 | 'my-memcache-server1.com:12345', 76 | 'my-memcache-server2.com:12345' 77 | ]) 78 | ``` 79 | 80 | 81 | #### As options 82 | Below we'll lay out the available options, but one of them is a key of `hosts` 83 | and you can provide an array of hosts to which you'd like to connect. 84 | 85 | ```javascript 86 | const MemcachePlus = require('memcache-plus') 87 | 88 | // Will initiate connections to both 'my-memcache-server1.com' and 89 | // 'my-memcache-server2.com' on port 11211 90 | const client = new MemcachePlus({ 91 | hosts: [ 'my-memcache-server1.com', 'my-memcache-server2.com' ] 92 | }) 93 | ``` 94 | 95 | This is useful in case you also want to specify other options. 96 | 97 | ## Connecting to multiple hosts 98 | 99 | Memcache Plus can automatically connect to multiple hosts. 100 | 101 | In doing so, it will use a hash ring to handle even distribution of keys among 102 | multiple servers. It is easy, simply specify multiple hosts when connecting and 103 | Memcache Plus will automatically handle the rest! 104 | 105 | ```javascript 106 | const MemcachePlus = require('memcache-plus') 107 | 108 | const client = new MemcachePlus({ 109 | // Specify 3 hosts 110 | hosts: ['10.0.0.1', '10.0.0.2', '10.0.0.3'] 111 | }) 112 | ``` 113 | If you've using Amazon's Elasticache for your Memcache hosting, you can also 114 | enable [Auto Discovery](elasticache.md) and Memcache Plus will automatically 115 | connect to your discovery url, find all of the hosts in your cluster, and 116 | establish connections to all of them. 117 | 118 | 119 | 120 | ## Command Queueing 121 | 122 | Memcache Plus will automatically queue and then execute (in order) any commands 123 | you make before a connection can be established. This means that you can 124 | instantiate the client and immediately start issuing commands and they will 125 | automatically execute as soon as a connection is established to your Memcache 126 | server(s). 127 | 128 | This makes it a lot easier to just get going with Memcache Plus than with many 129 | other Memcache clients for Node since they either require you to write code to 130 | ensure a connection is established before executing commands or they issue 131 | failures when commands fail due to lack of connection. 132 | 133 | Memcache Plus maintains an internal command queue which it will use until a 134 | connection is established. This same command queue is utilized if there is a 135 | momentary drop in the connection, so your code doesn't have to worry about a 136 | momentary blip like this. 137 | 138 | ## Options 139 | When instantiating Memcache Plus, you can optionally provide the client with an 140 | object containing any of the following options (default values in parentheses): 141 | 142 | | Key | Default Value | Description | 143 | |---|---|--- | 144 | |`autodiscover` | `false` | Whether or not to use [Elasticache Auto Discovery](http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/AutoDiscovery.html). [More details on this feature](elasticache.md). | 145 | |`backoffLimit`|10000| Memcache Plus uses an exponential backoff. This is the maximum limit in milliseconds it will wait before declaring a connection dead| 146 | |`bufferBeforeError`|1000|Memcache Plus will buffer and not reject or return errors until it hits this limit. Set to 0 to basically disable the buffer and throw an error on any single failed request.| 147 | |`disabled` | `false` | Whether or not Memcache is disabled. If it is disabled, all of the commands will simply return `null` as if the key does not exist | 148 | |`hosts` | `null` | The list of hosts to connect to. Can be a string for a single host or an array for multiple hosts. If none provided, defaults to `localhost` | 149 | |`maxValueSize`|1048576| The max value that can be stored, in bytes. This is configurable on the Memcached server but this library will help prevent you from storing objects over the default size in Memcache. If you have increased this limit on your server, you'll need to increase it here as well before setting anything over the default limit. | 150 | |`onNetError`| `function onNetError(err) { console.error(err) 151 | |`queue`| `true` | Whether or not to queue commands issued before a connection is established or if the connection is dropped momentarily. | 152 | |`netTimeout`|500| Number of milliseconds to wait before assuming there is a network timeout. | 153 | |`reconnect` | `true` | Whether or not to automatically reconnect if the connection is lost. Memcache Plus includes an exponential backoff to prevent it from spamming a server that is offline | 154 | 155 | Example: 156 | 157 | ```javascript 158 | const MemcachePlus = require('memcache-plus') 159 | 160 | const client = new MemcachePlus({ 161 | // Specify 2 hosts 162 | hosts: ['10.0.0.1', '10.0.0.2'], 163 | // Decrease the netTimeout from the 500ms default to 200ms 164 | netTimeout: 200 165 | }) 166 | ``` 167 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Main file for the Memcache Client 3 | */ 4 | 5 | var debug = require('debug')('memcache-plus:client'); 6 | 7 | var _ = require('lodash'), 8 | HashRing = require('hashring'), 9 | misc = require('./misc'), 10 | Immutable = require('immutable'), 11 | Promise = require('bluebird'), 12 | R = require('ramda'); 13 | 14 | var Connection = require('./connection'); 15 | var ConnectionPool = require('./connection-pool'); 16 | 17 | function validateKey(key, operation) { 18 | misc.assert(key, 'Cannot "' + operation + '" without key!'); 19 | misc.assert(typeof key === 'string', 'Key needs to be of type "string"'); 20 | misc.assert(Buffer.byteLength(key) < 250, 'Key must be less than 250 bytes long'); 21 | misc.assert(key.length < 250, 'Key must be less than 250 bytes long'); 22 | misc.assert(!/[\x00-\x20]/.test(key), 'Key must not include control characters or whitespace'); 23 | } 24 | 25 | /** 26 | * Constructor - Initiate client 27 | */ 28 | function Client(opts) { 29 | if (!(this instanceof Client)) { 30 | return new Client(opts); 31 | } 32 | 33 | var options = {}; 34 | 35 | // If single connection provided, array-ify it 36 | if (typeof opts === 'string') { 37 | options.hosts = [opts]; 38 | opts = options; 39 | } else if (typeof opts === 'undefined') { 40 | opts = {}; 41 | } else if (_.isArray(opts)) { 42 | options.hosts = opts; 43 | opts = options; 44 | } 45 | 46 | _.defaults(opts, { 47 | autodiscover: false, 48 | bufferBeforeError: 1000, 49 | disabled: false, 50 | hosts: null, 51 | reconnect: true, 52 | onNetError: function onNetError(err) { console.error(err); }, 53 | queue: true, 54 | netTimeout: 500, 55 | backoffLimit: 10000, 56 | maxValueSize: 1048576, 57 | poolSize: 1, 58 | tls: null 59 | }); 60 | 61 | // Iterate over options, assign each to this object 62 | R.keys(opts).forEach(function(key) { 63 | this[key] = opts[key]; 64 | }, this); 65 | 66 | if (this.queue) { 67 | this.buffer = new Immutable.List(); 68 | } 69 | 70 | debug('Connect options', opts); 71 | this.connect(); 72 | } 73 | 74 | /** 75 | * connect() - Iterate over all hosts, connect to each. 76 | * 77 | * @api private 78 | */ 79 | Client.prototype.connect = function() { 80 | debug('starting connection'); 81 | this.connections = {}; 82 | 83 | if (this.hosts === null) { 84 | this.hosts = ['localhost:11211']; 85 | } 86 | 87 | if (this.autodiscover) { 88 | // First connect to the servers provided 89 | this.getHostList() 90 | .bind(this) 91 | .then(function() { 92 | debug('got host list, connecting to hosts'); 93 | // Connect to these hosts 94 | this.connectToHosts(); 95 | this.ring = new HashRing(_.keys(this.connections)); 96 | }); 97 | 98 | // Then get the list of servers 99 | 100 | // Then connect to those 101 | } else { 102 | this.connectToHosts(); 103 | } 104 | }; 105 | 106 | /** 107 | * disconnect() - Iterate over all hosts, disconnect from each. 108 | * 109 | * @api private 110 | */ 111 | Client.prototype.disconnect = function(opts) { 112 | debug('starting disconnection'); 113 | var connections; 114 | 115 | if (typeof opts === 'string') { 116 | // If single connection provided, array-ify it 117 | connections = [opts]; 118 | } else if (typeof opts === 'undefined') { 119 | // No connections specified so client wants to disconnect from all 120 | connections = R.keys(this.connections); 121 | } else if (_.isArray(opts)) { 122 | connections = opts; 123 | } 124 | 125 | if (connections.length === R.keys(this.connections).length) { 126 | // Fair to assume if client is requesting a full disconnect, they don't 127 | // want it to just reconnect 128 | this.reconnect = false; 129 | } 130 | 131 | connections.map(function(ckey) { 132 | debug('disconnecting from %s', ckey); 133 | // Check that host exists before disconnecting from it 134 | if (this.connections[ckey] === undefined) { 135 | debug('failure trying to disconnect from server [%s] because not connected', ckey); 136 | throw new Error('Cannot disconnect from server unless connected'); 137 | } 138 | this.connections[ckey].disconnect(); 139 | // Remove this connection 140 | delete this.connections[ckey]; 141 | 142 | // Remove this host from the list of hosts 143 | this.hosts = this.hosts.filter(function(host) { 144 | return host !== ckey; 145 | }); 146 | }.bind(this)); 147 | 148 | return Promise.resolve(null); 149 | }; 150 | 151 | /** 152 | * getHostList() - Given a list of hosts, contact them via Elasticache 153 | * autodiscover and retrieve the list of hosts 154 | * 155 | * @api private 156 | */ 157 | Client.prototype.getHostList = function() { 158 | 159 | var client = this; 160 | var connections = {}; 161 | // Promise.any because we don't care which completes first, as soon as we get 162 | // a list of hosts we can stop 163 | return Promise.any(this.hosts.map(function(host) { 164 | var h = this.splitHost(host); 165 | var deferred = misc.defer(); 166 | const ConnClass = this.poolSize > 1 ? ConnectionPool : Connection; 167 | connections[host] = new ConnClass({ 168 | host: h.host, 169 | port: h.port, 170 | netTimeout: this.netTimeout, 171 | reconnect: false, 172 | tls: this.tls, 173 | poolSize: this.poolSize, 174 | onConnect: function() { 175 | // Do the autodiscovery, then resolve with hosts 176 | return deferred.resolve(this.autodiscovery()); 177 | }, 178 | onError: function (err) { 179 | client.onNetError(err); 180 | deferred.reject(err); 181 | } 182 | }); 183 | 184 | return deferred.promise; 185 | }, this)).bind(this).then( 186 | function(hosts) { 187 | this.hosts = hosts; 188 | this.connectToHosts(); 189 | this.flushBuffer(); 190 | }, 191 | function (err) { 192 | var wrappedError = new Error('Autodiscovery failed. Errors were:\n' + err.join('\n---\n')); 193 | this.flushBuffer(wrappedError); 194 | } 195 | ); 196 | }; 197 | 198 | /** 199 | * connectToHosts() - Given a list of hosts, actually connect to them 200 | * 201 | * @api private 202 | */ 203 | Client.prototype.connectToHosts = function() { 204 | debug('connecting to all hosts'); 205 | 206 | this.hosts.forEach(function(host) { 207 | var h = this.splitHost(host); 208 | var client = this; 209 | 210 | // Connect to host 211 | const ConnClass = this.poolSize > 1 ? ConnectionPool : Connection; 212 | this.connections[host] = new ConnClass({ 213 | host: h.host, 214 | port: h.port, 215 | reconnect: this.reconnect, 216 | onConnect: function() { 217 | client.flushBuffer(); 218 | }, 219 | bufferBeforeError: this.bufferBeforeError, 220 | netTimeout: this.netTimeout, 221 | onError: this.onNetError, 222 | maxValueSize: this.maxValueSize, 223 | tls: this.tls, 224 | poolSize: this.poolSize, 225 | }); 226 | }, this); 227 | 228 | this.ring = new HashRing(_.keys(this.connections)); 229 | }; 230 | 231 | /** 232 | * flushBuffer() - Flush the current buffer of commands, if any 233 | * 234 | * @api private 235 | */ 236 | Client.prototype.flushBuffer = function(err) { 237 | this.bufferedError = err; 238 | 239 | if (this.buffer && this.buffer.size > 0) { 240 | debug('flushing client write buffer'); 241 | // @todo Watch out for and handle how this behaves with a very long buffer 242 | while(this.buffer.size > 0) { 243 | var item = this.buffer.first(); 244 | this.buffer = this.buffer.shift(); 245 | 246 | // Something bad happened before things got a chonce to run. We 247 | // need to cancel all pending operations. 248 | if (err) { 249 | item.deferred.reject(err); 250 | continue; 251 | } 252 | 253 | // First, retrieve the correct connection out of the hashring 254 | var connection = this.connections[this.ring.get(item.key)]; 255 | var promise = connection[item.cmd].apply(connection, item.args); 256 | promise.then(item.deferred.resolve, item.deferred.reject); 257 | } 258 | } 259 | }; 260 | 261 | /** 262 | * splitHost() - Helper to split a host string into port and host 263 | * 264 | * @api private 265 | */ 266 | Client.prototype.splitHost = function(str) { 267 | var host = str.split(':'); 268 | 269 | if (host.length === 1 && host.indexOf(':') === -1) { 270 | host.push('11211'); 271 | } else if (host[0].length === 0) { 272 | host[0] = 'localhost'; 273 | } 274 | return { 275 | host: host[0], 276 | port: host[1] 277 | }; 278 | }; 279 | 280 | /** 281 | * ready() - Predicate function, returns true if Client is ready, false otherwise. 282 | * Client is ready when all of its connections are open and ready. If autodiscovery 283 | * is enabled, Client is ready once it has contacted Elasticache and then initialized 284 | * all of the connections 285 | */ 286 | Client.prototype.ready = function() { 287 | var size = _.size(this.connections); 288 | 289 | if (size < 1) { 290 | return false; 291 | } else { 292 | return _.reduce(this.connections, function(ready, conn) { 293 | ready = ready && conn.ready; 294 | return ready; 295 | }, true); 296 | } 297 | }; 298 | 299 | /** 300 | * delete() - Delete an item from the cache 301 | * 302 | * @param {String} key - The key of the item to delete 303 | * @param {Function} [cb] - Callback to call when we have a value 304 | * @returns {Promise} 305 | */ 306 | Client.prototype.delete = function(key, cb) { 307 | validateKey(key, 'delete'); 308 | 309 | return this.run('delete', [key], cb); 310 | }; 311 | 312 | /** 313 | * deleteMulti() - Delete multiple items from the cache 314 | * 315 | * @param {Array} keys - The keys of the items to delete 316 | * @param {Function} [cb] - Callback to call when we have a value 317 | * @returns {Promise} 318 | */ 319 | Client.prototype.deleteMulti = function(keys, cb) { 320 | var self = this; 321 | misc.assert(keys, 'Cannot delete without keys!'); 322 | 323 | return Promise.props(R.reduce(function(acc, key) { 324 | validateKey(key, 'deleteMulti'); 325 | acc[key] = self.run('delete', [key], null); 326 | return acc; 327 | }, {}, keys)).nodeify(cb); 328 | }; 329 | 330 | /** 331 | * set() - Set a value for the provided key 332 | * 333 | * @param {String} key - The key to set 334 | * @param {*} value - The value to set for this key. Can be of any type 335 | * @param {Number|Object|Function} [ttl = 0] - The time to live for this key or callback 336 | * @param {Function} [cb] - Callback to call when we have a value 337 | * @returns {Promise} 338 | */ 339 | Client.prototype.set = function(key, val, ttl, cb) { 340 | validateKey(key, 'set'); 341 | 342 | if (typeof ttl === 'function') { 343 | cb = ttl; 344 | ttl = 0; 345 | } 346 | 347 | return this.run('set', [key, val, ttl], cb); 348 | }; 349 | 350 | /** 351 | * cas() - Set a value for the provided key if the CAS value matches 352 | * 353 | * @param {String} key - The key to set 354 | * @param {*} value - The value to set for this key. Can be of any type 355 | * @param {String} cas - A CAS value returned from a 'gets' call 356 | * @param {Number|Object|Function} [ttl = 0] - The time to live for this key or callback 357 | * @param {Function} [cb] - Callback to call when we have a value 358 | * @returns {Promise} with a boolean value indicating if the value was stored (true) or not (false) 359 | */ 360 | Client.prototype.cas = function(key, val, cas, ttl, cb) { 361 | validateKey(key, 'cas'); 362 | 363 | if (typeof ttl === 'function') { 364 | cb = ttl; 365 | ttl = 0; 366 | } 367 | 368 | return this.run('cas', [key, val, cas, ttl], cb); 369 | }; 370 | 371 | 372 | /** 373 | * gets() - Get the value and CAS id for the provided key 374 | * 375 | * @param {String} key - The key to get 376 | * @param {Object} opts - Any options for this request 377 | * @param {Function} [cb] - The (optional) callback called on completion 378 | * @returns {Promise} which is an array containing the value and CAS id 379 | */ 380 | Client.prototype.gets = function(key, opts, cb) { 381 | validateKey(key, 'gets'); 382 | 383 | if (typeof opts === 'function' && typeof cb === 'undefined') { 384 | cb = opts; 385 | opts = {}; 386 | } 387 | 388 | return this.run('gets', [key, opts], cb); 389 | }; 390 | 391 | /** 392 | * get() - Get the value for the provided key 393 | * 394 | * @param {String} key - The key to get 395 | * @param {Object} opts - Any options for this request 396 | * @param {Function} [cb] - The (optional) callback called on completion 397 | * @returns {Promise} 398 | */ 399 | Client.prototype.get = function(key, opts, cb) { 400 | if (typeof opts === 'function' && typeof cb === 'undefined') { 401 | cb = opts; 402 | opts = {}; 403 | } 404 | 405 | if (_.isArray(key)) { 406 | return this.getMulti(key, opts, cb); 407 | } else { 408 | validateKey(key, 'get'); 409 | return this.run('get', [key, opts], cb); 410 | } 411 | }; 412 | 413 | /** 414 | * getMulti() - Get multiple values for the provided array of keys 415 | * 416 | * @param {Array} keys - The keys to get 417 | * @param {Function} [cb] - The value to set for this key. Can be of any type 418 | * @returns {Promise} 419 | */ 420 | Client.prototype.getMulti = function(keys, opts, cb) { 421 | var self = this; 422 | misc.assert(keys, 'Cannot get without key!'); 423 | 424 | if (typeof opts === 'function' && typeof cb === 'undefined') { 425 | cb = opts; 426 | opts = {}; 427 | } 428 | 429 | return Promise.props(R.reduce(function(acc, key) { 430 | validateKey(key, 'getMulti'); 431 | acc[key] = self.run('get', [key, opts], null); 432 | return acc; 433 | }, {}, keys)).nodeify(cb); 434 | }; 435 | 436 | /** 437 | * incr() - Increment a value for the provided key 438 | * 439 | * @param {String} key - The key to incr 440 | * @param {Number|Function} [value = 1] - The value to increment this key by. Must be an integer 441 | * @param {Function} [cb] - Callback to call when we have a value 442 | * @returns {Promise} 443 | */ 444 | Client.prototype.incr = function(key, val, cb) { 445 | validateKey(key, 'incr'); 446 | 447 | if (typeof val === 'function' || typeof val === 'undefined') { 448 | cb = val; 449 | val = 1; 450 | } 451 | 452 | misc.assert(typeof val === 'number', 'Cannot incr in memcache with a non number value'); 453 | 454 | return this.run('incr', [key, val], cb); 455 | }; 456 | 457 | /** 458 | * decr() - Decrement a value for the provided key 459 | * 460 | * @param {String} key - The key to decr 461 | * @param {Number|Function} [value = 1] - The value to decrement this key by. Must be an integer 462 | * @param {Function} [cb] - Callback to call when we have a value 463 | * @returns {Promise} 464 | */ 465 | Client.prototype.decr = function(key, val, cb) { 466 | validateKey(key, 'decr'); 467 | 468 | if (typeof val === 'function' || typeof val === 'undefined') { 469 | cb = val; 470 | val = 1; 471 | } 472 | 473 | misc.assert(typeof val === 'number', 'Cannot decr in memcache with a non number value'); 474 | 475 | return this.run('decr', [key, val], cb); 476 | }; 477 | 478 | /** 479 | * flush() - Removes all stored values 480 | * @param {Number|Function} [delay = 0] - Delay invalidation by specified seconds 481 | * @param {Function} [cb] - The (optional) callback called on completion 482 | * @returns {Promise} 483 | */ 484 | Client.prototype.flush = function (delay, cb) { 485 | if (typeof delay === 'function' || typeof delay === 'undefined') { 486 | cb = delay; 487 | delay = 0; 488 | } 489 | 490 | return this.run('flush_all', [delay], cb); 491 | }; 492 | 493 | /** 494 | * items() - Gets items statistics 495 | * @param {Function} [cb] - The (optional) callback called on completion 496 | * @returns {Promise} 497 | */ 498 | Client.prototype.items = function(cb) { 499 | return this.run('stats items', [], cb); 500 | }; 501 | 502 | /** 503 | * add() - Add value for the provided key only if it didn't already exist 504 | * 505 | * @param {String} key - The key to set 506 | * @param {*} value - The value to set for this key. Can be of any type 507 | * @param {Number|Object|Function} [ttl = 0] - The time to live for this key or callback 508 | * @param {Function} [cb] - Callback to call when we have a value 509 | * @returns {Promise} 510 | */ 511 | Client.prototype.add = function(key, val, ttl, cb) { 512 | validateKey(key, 'add'); 513 | 514 | if (typeof ttl === 'function') { 515 | cb = ttl; 516 | ttl = 0; 517 | } 518 | 519 | return this.run('add', [key, val, ttl], cb); 520 | }; 521 | 522 | /** 523 | * replace() - Replace value for the provided key only if it already exists 524 | * 525 | * @param {String} key - The key to replace 526 | * @param {*} value - The value to replace for this key. Can be of any type 527 | * @param {Number|Object|Function} [ttl = 0] - The time to live for this key or callback 528 | * @param {Function} [cb] - Callback to call when we have a value 529 | * @returns {Promise} 530 | */ 531 | Client.prototype.replace = function(key, val, ttl, cb) { 532 | validateKey(key, 'replace'); 533 | 534 | if (typeof ttl === 'function') { 535 | cb = ttl; 536 | ttl = 0; 537 | } 538 | 539 | return this.run('replace', [key, val, ttl], cb); 540 | }; 541 | 542 | /** 543 | * append() - Append value for the provided key only if it already exists 544 | * 545 | * @param {String} key - The key to append 546 | * @param {*} value - The value to append for this key. Can be of any type 547 | * @param {Number|Object|Function} [ttl = 0] - The time to live for this key or callback 548 | * @param {Function} [cb] - Callback to call when we have a value 549 | * @returns {Promise} 550 | */ 551 | Client.prototype.append = function(key, val, ttl, cb) { 552 | validateKey(key, 'append'); 553 | 554 | if (typeof ttl === 'function') { 555 | cb = ttl; 556 | ttl = 0; 557 | } 558 | 559 | return this.run('append', [key, val, ttl], cb); 560 | }; 561 | 562 | /** 563 | * prepend() - Prepend value for the provided key only if it already exists 564 | * 565 | * @param {String} key - The key to prepend 566 | * @param {*} value - The value to prepend for this key. Can be of any type 567 | * @param {Number|Object|Function} [ttl = 0] - The time to live for this key or callback 568 | * @param {Function} [cb] - Callback to call when we have a value 569 | * @returns {Promise} 570 | */ 571 | Client.prototype.prepend = function(key, val, ttl, cb) { 572 | validateKey(key, 'prepend'); 573 | 574 | if (typeof ttl === 'function') { 575 | cb = ttl; 576 | ttl = 0; 577 | } 578 | 579 | return this.run('prepend', [key, val, ttl], cb); 580 | }; 581 | 582 | /** 583 | * cachedump() - get cache information for a given slabs id 584 | * @param {number} slabsId 585 | * @param {number} [limit] Limit result to number of entries. Default is 0 (unlimited). 586 | * @param {Function} [cb] - The (optional) callback called on completion 587 | * @returns {Promise} 588 | */ 589 | Client.prototype.cachedump = function(slabsId, limit, cb) { 590 | misc.assert(slabsId, 'Cannot cachedump without slabId!'); 591 | 592 | if (typeof limit === 'function' || typeof limit === 'undefined') { 593 | cb = limit; 594 | limit = 0; 595 | } 596 | 597 | return this.run('stats cachedump', [slabsId, limit], cb); 598 | }; 599 | 600 | /** 601 | * version() - Get current Memcached version from the server 602 | * @param {Function} [cb] - The (optional) callback called on completion 603 | * @returns {Promise} 604 | */ 605 | Client.prototype.version = function(cb) { 606 | return this.run('version', [], cb); 607 | }; 608 | 609 | /** 610 | * run() - Run this command on the appropriate connection. Will buffer command 611 | * if connection(s) are not ready 612 | * 613 | * @param {String} command - The command to run 614 | * @param {Array} args - The arguments to send with this command 615 | * @returns {Promise} 616 | */ 617 | Client.prototype.run = function(command, args, cb) { 618 | if (this.disabled) { 619 | return Promise.resolve(null).nodeify(cb); 620 | } 621 | 622 | if (this.ready()) { 623 | // First, retrieve the correct connection out of the hashring 624 | var connection = this.connections[this.ring.get(args[0])]; 625 | 626 | // Run this command 627 | return connection[command].apply(connection, args).nodeify(cb); 628 | } else if (this.bufferBeforeError === 0 || !this.queue) { 629 | return Promise.reject(new Error('Connection is not ready, either not connected yet or disconnected')).nodeify(cb); 630 | } else if (this.bufferedError) { 631 | return Promise.reject(this.bufferedError).nodeify(cb); 632 | } else { 633 | var deferred = misc.defer(args[0]); 634 | 635 | this.buffer = this.buffer.push({ 636 | cmd: command, 637 | args: args, 638 | key: args[0], 639 | deferred: deferred 640 | }); 641 | 642 | return deferred.promise.nodeify(cb); 643 | } 644 | }; 645 | 646 | module.exports = Client; 647 | -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @file Main file for a Memcache Connection 4 | */ 5 | 6 | var debug = require('debug')('memcache-plus:connection'); 7 | 8 | var _ = require('lodash'), 9 | carrier = require('carrier'), 10 | misc = require('./misc'), 11 | net = require('net'), 12 | tls = require('tls'), 13 | Immutable = require('immutable'), 14 | Promise = require('bluebird'), 15 | util = require('util'), 16 | EventEmitter = require('events').EventEmitter; 17 | 18 | // Note, these first few flags intentionally mirror the memcached module so 19 | // they are cross compatible 20 | var FLAG_JSON = 1<<1; 21 | var FLAG_BINARY = 1<<2; 22 | var FLAG_NUMERIC = 1<<3; 23 | // These are new flags we add for Memcache Plus specific data 24 | var FLAG_COMPRESSED = 1<<4; 25 | 26 | var metadataTemp = {}; 27 | 28 | /** 29 | * Miscellaneous helper functions 30 | */ 31 | 32 | /** 33 | * getFlag() - Given a value and whether it's compressed, return the correct 34 | * flag 35 | * 36 | * @param {*} val - the value to inspect and on which to set the flag 37 | * @param {bool} compressed - whether or not this value is to be compressed 38 | * @returns {Number} the value of the flag 39 | */ 40 | function getFlag(val, compressed) { 41 | var flag = 0; 42 | 43 | if (typeof val === 'number') { 44 | flag = FLAG_NUMERIC; 45 | } else if (Buffer.isBuffer(val)) { 46 | flag = FLAG_BINARY; 47 | } else if (typeof val !== 'string') { 48 | flag = FLAG_JSON; 49 | } 50 | 51 | if (compressed === true) { 52 | flag = flag | FLAG_COMPRESSED; 53 | } 54 | 55 | return flag; 56 | } 57 | 58 | /** 59 | * formatValue() - Given a value and whether it's compressed, return a Promise 60 | * which will resolve to that value formatted correctly, either compressed or 61 | * not and as the correct type of string 62 | * 63 | * @param {*} val - the value to inspect and on which to set the flag 64 | * @param {bool} compressed - whether or not this value is to be compressed 65 | * @returns {Promise} resolves to the value after being converted and, if 66 | * necessary, compressed 67 | */ 68 | function formatValue(val, compressed) { 69 | var value = val; 70 | 71 | if (typeof val === 'number') { 72 | value = val.toString(); 73 | } else if (Buffer.isBuffer(val)) { 74 | value = val.toString('binary'); 75 | } else if (typeof val !== 'string') { 76 | value = JSON.stringify(val); 77 | } 78 | 79 | var bVal = Buffer.from(value); 80 | 81 | var compression = Promise.resolve(bVal); 82 | 83 | if (compressed) { 84 | // Compress 85 | debug('compression enabled, compressing'); 86 | compression = misc.compress(bVal); 87 | } 88 | 89 | return compression; 90 | } 91 | 92 | /** 93 | * Connection constructor 94 | * 95 | * With the supplied options, connect. 96 | * 97 | * @param {object} opts - The options for this Connection instance 98 | */ 99 | function Connection(opts) { 100 | 101 | EventEmitter.call(this); 102 | 103 | opts = opts || {}; 104 | 105 | _.defaults(opts, { 106 | host: 'localhost', 107 | port: '11211', 108 | reconnect: true, 109 | maxValueSize: 1048576, 110 | tls: null 111 | }); 112 | 113 | this.host = opts.host; 114 | this.port = opts.port; 115 | this.tls = opts.tls; 116 | 117 | this.commandBuffer = new Immutable.List(); 118 | 119 | if (opts.onConnect) { 120 | this.onConnect = opts.onConnect; 121 | } 122 | 123 | if (opts.onError) { 124 | this.onError = opts.onError; 125 | } else { 126 | this.onError = function onError(err) { this.emit('error'); console.error(err); }; 127 | } 128 | this.bufferBeforeError = opts.bufferBeforeError; 129 | this.netTimeout = ('netTimeout' in opts) ? opts.netTimeout : 500; 130 | this.backoffLimit = ('backoffLimit' in opts) ? opts.backoffLimit : 10000; 131 | this.reconnect = opts.reconnect; 132 | this.disconnecting = false; 133 | this.ready = false; 134 | this.backoff = ('backoff' in opts) ? opts.backoff : 10; 135 | this.maxValueSize = opts.maxValueSize; 136 | this.writeBuffer = new Immutable.List(); 137 | 138 | this.connect(); 139 | } 140 | 141 | util.inherits(Connection, EventEmitter); 142 | 143 | /** 144 | * Disconnect connection 145 | */ 146 | Connection.prototype.disconnect = function() { 147 | this.ready = false; 148 | this.disconnecting = true; 149 | this.client.end(); 150 | }; 151 | 152 | /** 153 | * Destroy connection immediately 154 | */ 155 | Connection.prototype.destroy = function() { 156 | this.client.destroy(); 157 | this.client = null; 158 | 159 | this.commandBuffer.forEach(function(deferred) { 160 | deferred.reject(new Error('Memcache connection lost')); 161 | }); 162 | this.commandBuffer = this.commandBuffer.clear(); 163 | }; 164 | 165 | /** 166 | * Initialize connection 167 | * 168 | * @api private 169 | */ 170 | Connection.prototype.connect = function() { 171 | var self = this; 172 | var params = { 173 | port: self.port 174 | }; 175 | 176 | if (self.host) { 177 | params.host = self.host; 178 | } 179 | 180 | debug('connecting to host %s:%s', params.host, params.port); 181 | 182 | // If a client already exists, we just want to reconnect 183 | if (this.client) { 184 | this.client.connect(params); 185 | 186 | } else { 187 | // Initialize a new client, connect 188 | if (self.tls === null) { 189 | self.client = net.connect(params); 190 | } else { 191 | params.tls = self.tls; 192 | self.client = tls.connect(params); 193 | } 194 | self.client.setTimeout(self.netTimeout); 195 | self.client.on('error', self.onError); 196 | self.client.setNoDelay(true); 197 | 198 | // If reconnect is enabled, we want to re-initiate connection if it is ended 199 | if (self.reconnect) { 200 | self.client.on('close', function() { 201 | self.emit('close'); 202 | self.ready = false; 203 | // Wait before retrying and double each time. Backoff starts at 10ms and will 204 | // plateau at 1 minute. 205 | if (self.backoff < self.backoffLimit) { 206 | self.backoff *= 2; 207 | } 208 | debug('connection to memcache lost, reconnecting in %sms...', self.backoff); 209 | setTimeout(function() { 210 | // Only want to do this if a disconnect was not triggered intentionally 211 | if (!self.disconnecting) { 212 | debug('attempting to reconnect to memcache now.', self.backoff); 213 | self.destroy(); 214 | self.connect(); 215 | } 216 | }, self.backoff); 217 | }); 218 | } 219 | } 220 | 221 | self.client.on('connect', function() { 222 | self.emit('connect'); 223 | debug('successfully (re)connected!'); 224 | self.ready = true; 225 | // Reset backoff if we connect successfully 226 | self.backoff = 10; 227 | 228 | // If an onConnect handler was specified, execute it 229 | if (self.onConnect) { 230 | self.onConnect(); 231 | } 232 | self.flushBuffer(); 233 | }); 234 | 235 | carrier.carry(self.client, self.read.bind(self)); 236 | }; 237 | 238 | /** 239 | * read() - Called as soon as we get data back from this connection from the 240 | * server. The response parsing is a bit of a beast. 241 | */ 242 | Connection.prototype.read = function(data) { 243 | debug('got data: "%s" and the queue now has "%d" elements', 244 | misc.truncateIfNecessary(data), 245 | this.commandBuffer.size 246 | ); 247 | 248 | var deferred = this.commandBuffer.first(); 249 | 250 | var done = true; 251 | var err = null; 252 | var resp = data; 253 | 254 | if (deferred.type === 'autodiscovery') { 255 | this.autodiscover = this.autodiscover || {}; 256 | done = false; 257 | // Treat this a bit like a special snowflake 258 | if (data.match(/^CONFIG .+/)) { 259 | // Do nothing with this for now 260 | var configItems = data.match(/^CONFIG cluster ([0-9]+) ([0-9]+)/); 261 | this.autodiscover.len = Number(configItems[2]); 262 | } else if (this.autodiscover.len !== undefined && this.autodiscover.version === undefined && data.match(/^([0-9])+/)) { 263 | this.autodiscover.version = Number(data); 264 | } else if (data.match(/^END$/)) { 265 | resp = this.autodiscover; 266 | this.autodiscover = null; 267 | done = true; 268 | } else if (data.length > 20) { 269 | this.autodiscover.info = data; 270 | } 271 | } else if (data.match(/^ERROR$/) && this.commandBuffer.size > 0) { 272 | debug('got an error from memcached'); 273 | // We only want to do this if the last thing was not an error, 274 | // as if it were, we already would have notified about the error 275 | // last time so now we want to ignore it 276 | err = new Error(util.format('Memcache returned an error: %s\r\nFor key %s', data, deferred.key)); 277 | } else if (data.match(/^VALUE .+/)) { 278 | var spl = data.match(/^VALUE ([^ ]+) ([0-9]+) ([0-9]+)( ([0-9]+))?$/); 279 | debug('Got some metadata', spl); 280 | metadataTemp[spl[1]] = { 281 | flag: Number(spl[2]), 282 | len: Number(spl[3]) 283 | }; 284 | if (spl.length === 6) { 285 | metadataTemp[spl[1]].cas = spl[5]; 286 | } 287 | done = false; 288 | } else if (data.match(/^END$/)) { 289 | if (deferred.type === 'items') { 290 | resp = this.data; 291 | } else if (metadataTemp[deferred.key]) { 292 | var metadata = metadataTemp[deferred.key]; 293 | resp = [ this.data, metadata.flag, metadata.len, metadata.cas ]; 294 | // After we've used this metadata, purge it 295 | delete metadataTemp[deferred.key]; 296 | } else { 297 | resp = [ this.data ]; 298 | } 299 | } else if (data.match(/^STAT items.+/)) { 300 | // format is 301 | // STAT items:SLAB_ID: 302 | 303 | var splData = data.match(/^STAT items:([0-9]+):(.+) ([0-9]+)$/); 304 | 305 | var slabId = splData[1]; 306 | var slabName = splData[2]; 307 | var slabValue = splData[3]; 308 | 309 | if (this.data) { 310 | resp = this.data; 311 | } else { 312 | resp = { 313 | items: {}, 314 | ids: [] 315 | }; 316 | } 317 | 318 | if (!resp.items[slabId]) { 319 | resp.items[slabId] = {}; 320 | resp.ids.push(slabId); 321 | } 322 | 323 | // set the slab key and value to the slab id 324 | resp.items[slabId][slabName] = parseInt(slabValue, 10); 325 | done = false; 326 | } else if (data.match(/^ITEM .+/)) { 327 | // ITEM [ b; s] 328 | 329 | var cacheData = data.match(/^ITEM\s(.+)\s\[(\d+)\sb\;\s(\d+)\ss\]$/); 330 | 331 | var key = cacheData[1]; 332 | 333 | // Item size (including key) in bytes 334 | var itemSize = parseInt(cacheData[2], 10); 335 | 336 | // Expiration timestamp in second 337 | var expiry = parseInt(cacheData[3], 10); 338 | 339 | if (this.data) { 340 | resp = this.data; 341 | } else { 342 | resp = { 343 | items: {}, 344 | ids: [] 345 | }; 346 | } 347 | 348 | resp.items[key] = { 349 | key: key, 350 | bytes: itemSize, 351 | expiry_time_secs: expiry 352 | }; 353 | 354 | resp.ids.push(key); 355 | done = false; 356 | } else if (data.match(/^SERVER_ERROR|CLIENT_ERROR .+/)) { 357 | err = new Error(util.format('Memcache returned an error: %s', data)); 358 | } else if (data.match(/^VERSION .+/)) { 359 | resp = data.match(/^VERSION ([0-9|\.]+)/)[1]; 360 | } else { 361 | // If this is a special response that we expect, handle it 362 | if (data.match(/^(STORED|NOT_STORED|DELETED|EXISTS|TOUCHED|NOT_FOUND|OK|INCRDECR|STAT)$/)) { 363 | // Do nothing currently... 364 | debug('misc response, passing along to client'); 365 | } else { 366 | if (data !== '') { 367 | if (deferred.type !== 'incr' && deferred.type !== 'decr') { 368 | done = false; 369 | } 370 | } 371 | } 372 | } 373 | 374 | if (done) { 375 | // Pull this guy off the queue 376 | this.commandBuffer = this.commandBuffer.shift(); 377 | // Reset for next loop 378 | this.data = null; 379 | } else { 380 | this.data = resp; 381 | } 382 | 383 | if (err) { 384 | // If we have an error, reject 385 | deferred.reject(err); 386 | } else { 387 | // If we don't have an error, resolve if done 388 | if (done) { 389 | deferred.resolve(resp); 390 | } 391 | } 392 | debug('responded and the queue now has "%s" elements', this.commandBuffer.size); 393 | }; 394 | 395 | /** 396 | * flushBuffer() - Flush the queue for this connection 397 | */ 398 | Connection.prototype.flushBuffer = function() { 399 | debug('trying to flush buffer for %s', this.host); 400 | if (this.writeBuffer && this.writeBuffer.size > 0) { 401 | debug('flushing write buffer for %s', this.host); 402 | // @todo Watch out for and handle how this behaves with a very long buffer 403 | while(this.writeBuffer.size > 0) { 404 | this.client.write(this.writeBuffer.first()); 405 | this.writeBuffer = this.writeBuffer.shift(); 406 | this.client.write('\r\n'); 407 | } 408 | debug('flushed write buffer for %s', this.host); 409 | } 410 | }; 411 | 412 | /** 413 | * write() - Write a command to this connection 414 | */ 415 | Connection.prototype.write = function(str) { 416 | // If for some reason this connection is not yet ready and a request is tried, 417 | // we don't want to fire it off so we write it to a buffer and then will fire 418 | // them off when we finally do connect. And even if we are connected we don't 419 | // want to fire off requests unless the write buffer is emptied. So if say we 420 | // buffer 100 requests, then connect and chug through 10, there are 90 left to 421 | // be flushed before we send it new requests so we'll just keep pushing on the 422 | // end until it's flushed 423 | if (this.ready && this.writeBuffer.size < 1) { 424 | debug('sending: "%s"', misc.truncateIfNecessary(str)); 425 | this.client.write(str); 426 | this.client.write('\r\n'); 427 | } else if (this.writeBuffer.size < this.bufferBeforeError) { 428 | debug('buffering to [%s]: "%s" ', this.host, misc.truncateIfNecessary(str)); 429 | this.writeBuffer = this.writeBuffer.push(str); 430 | // Check if we should flush this queue. Useful in case it gets stuck for 431 | // some reason 432 | if (this.ready) { 433 | this.flushBuffer(); 434 | } 435 | } else { 436 | this.commandBuffer.first().reject('Error, Connection to memcache lost and buffer over ' + this.bufferBeforeError + ' items'); 437 | this.commandBuffer = this.commandBuffer.shift(); 438 | } 439 | }; 440 | 441 | Connection.prototype.autodiscovery = function() { 442 | debug('starting autodiscovery'); 443 | var deferred = misc.defer('autodiscovery'); 444 | deferred.type = 'autodiscovery'; 445 | this.commandBuffer = this.commandBuffer.push(deferred); 446 | 447 | this.write('config get cluster'); 448 | return deferred.promise 449 | .then(function(config) { 450 | debug('got autodiscovery response from elasticache', config); 451 | // Elasticache returns hosts as a string like the following: 452 | // victor.di6cba.0001.use1.cache.amazonaws.com|10.10.8.18|11211 victor.di6cba.0002.use1.cache.amazonaws.com|10.10.8.133|11211 453 | // We want to break it into the correct pieces 454 | var hosts = config.info.toString().split(' '); 455 | return hosts.map(function(host) { 456 | host = host.split('|'); 457 | return util.format('%s:%s', host[0], host[2]); 458 | }); 459 | }); 460 | }; 461 | 462 | /** 463 | * set() - Set a value on this connection 464 | */ 465 | Connection.prototype.set = function(key, val, ttl) { 466 | var self = this; 467 | 468 | ttl = ttl || 0; 469 | var opts = {}; 470 | 471 | if (_.isObject(ttl)) { 472 | opts = ttl; 473 | ttl = opts.ttl || 0; 474 | } 475 | 476 | var flag = getFlag(val, opts.compressed); 477 | 478 | return formatValue(val, opts.compressed) 479 | .bind(this) 480 | .then(function(v) { 481 | if (opts.compressed) { 482 | // Note, we use base64 encoding because this allows us to compress it 483 | // but also safely store/retrieve it in memcache. Not quite as efficient 484 | // as if we just used the zlib default, but it also includes a bunch of 485 | // funky characters that didn't seem to be safe to set/get from memcache 486 | // without messing with the data. 487 | v = Buffer.from(v.toString('base64')); 488 | } 489 | if (v.length > self.maxValueSize) { 490 | throw new Error(util.format('Value too large to set in memcache: %s > %s', v.length, self.maxValueSize)); 491 | } 492 | 493 | // What we're doing here is a bit tricky as we need to invert control. 494 | // We are going to basically return a Promise that itself is made up of a 495 | // chain of Promises, most resolved here (the initial communication with 496 | // memcache), but the last is not resolved until some time in the future. 497 | // This Promise is put into a queue which will be processed whenever the 498 | // socket responds (usually immediately). This because we don't know 499 | // exactly when it's going to respond since it's an event emitter. So we 500 | // are doing some funky promise trickery to convert event emmitter into 501 | // Promise/or Callback. Since all actions in this library share the same 502 | // queue, order should be maintained and this trick should work! 503 | 504 | var deferred = misc.defer(key); 505 | deferred.key = key; 506 | this.commandBuffer = this.commandBuffer.push(deferred); 507 | 508 | // First send the metadata for this request 509 | this.write(util.format('set %s %d %d %d', key, flag, ttl, v.length)); 510 | 511 | // Then the actual value (as a string) 512 | this.write(util.format('%s', v)); 513 | return deferred.promise 514 | .then(function(data) { 515 | // data will be a buffer 516 | if (data !== 'STORED') { 517 | throw new Error(util.format('Something went wrong with the set. Expected STORED, got :%s:', data.toString())); 518 | } else { 519 | return Promise.resolve(); 520 | } 521 | }); 522 | }); 523 | }; 524 | 525 | /** 526 | * cas() - Set a value on this connection if the CAS value matches 527 | */ 528 | Connection.prototype.cas = function(key, val, cas, ttl) { 529 | var self = this; 530 | 531 | ttl = ttl || 0; 532 | var opts = {}; 533 | 534 | if (_.isObject(ttl)) { 535 | opts = ttl; 536 | ttl = opts.ttl || 0; 537 | } 538 | 539 | var flag = getFlag(val, opts.compressed); 540 | 541 | return formatValue(val, opts.compressed) 542 | .bind(this) 543 | .then(function(v) { 544 | if (opts.compressed) { 545 | // Note, we use base64 encoding because this allows us to compress it 546 | // but also safely store/retrieve it in memcache. Not quite as efficient 547 | // as if we just used the zlib default, but it also includes a bunch of 548 | // funky characters that didn't seem to be safe to set/get from memcache 549 | // without messing with the data. 550 | v = Buffer.from(v.toString('base64')); 551 | } 552 | if (v.length > self.maxValueSize) { 553 | throw new Error(util.format('Value too large to set in memcache: %s > %s', v.length, self.maxValueSize)); 554 | } 555 | 556 | // What we're doing here is a bit tricky as we need to invert control. 557 | // We are going to basically return a Promise that itself is made up of a 558 | // chain of Promises, most resolved here (the initial communication with 559 | // memcache), but the last is not resolved until some time in the future. 560 | // This Promise is put into a queue which will be processed whenever the 561 | // socket responds (usually immediately). This because we don't know 562 | // exactly when it's going to respond since it's an event emitter. So we 563 | // are doing some funky promise trickery to convert event emmitter into 564 | // Promise/or Callback. Since all actions in this library share the same 565 | // queue, order should be maintained and this trick should work! 566 | 567 | var deferred = misc.defer(key); 568 | deferred.key = key; 569 | this.commandBuffer = this.commandBuffer.push(deferred); 570 | 571 | // First send the metadata for this request 572 | this.write(util.format('cas %s %d %d %d %s', key, flag, ttl, v.length, cas)); 573 | 574 | // Then the actual value (as a string) 575 | this.write(util.format('%s', v)); 576 | return deferred.promise 577 | .then(function(data) { 578 | // data will be a buffer 579 | if ((data === 'EXISTS') || (data === 'NOT_FOUND')) { 580 | return Promise.resolve(false); 581 | 582 | } else if (data !== 'STORED') { 583 | throw new Error(util.format('Something went wrong with the set. Expected STORED, got :%s:', data.toString())); 584 | 585 | } else { 586 | return Promise.resolve(true); 587 | } 588 | }); 589 | }); 590 | }; 591 | 592 | function convert(data, flag, length) { 593 | if (flag & FLAG_NUMERIC) { 594 | return Number(data); 595 | } else if (flag & FLAG_BINARY) { 596 | var buff = Buffer.alloc(length); 597 | buff.write(data, 0, 'binary'); 598 | return buff; 599 | } else if (flag & FLAG_JSON) { 600 | return JSON.parse(data); 601 | } else { 602 | return data.toString(); 603 | } 604 | } 605 | 606 | /** 607 | * incr() - Increment a value on this connection 608 | */ 609 | Connection.prototype.incr = function(key, amount) { 610 | // Do the delete 611 | var deferred = misc.defer(key); 612 | deferred.type = 'incr'; 613 | this.commandBuffer = this.commandBuffer.push(deferred); 614 | 615 | this.write(util.format('incr %s %d', key, amount)); 616 | 617 | return deferred.promise 618 | .then(function(v) { 619 | if (v === 'NOT FOUND') { 620 | throw new Error('key %s not found to incr', key); 621 | } else { 622 | return Number(v); 623 | } 624 | }) 625 | .timeout(this.netTimeout); 626 | }; 627 | 628 | /** 629 | * decr() - Decrement a value on this connection 630 | */ 631 | Connection.prototype.decr = function(key, amount) { 632 | // Do the delete 633 | var deferred = misc.defer(key); 634 | deferred.type = 'decr'; 635 | this.commandBuffer = this.commandBuffer.push(deferred); 636 | 637 | this.write(util.format('decr %s %d', key, amount)); 638 | 639 | return deferred.promise 640 | .then(function(v) { 641 | if (v === 'NOT FOUND') { 642 | throw new Error('key %s not found to decr', key); 643 | } else { 644 | return Number(v); 645 | } 646 | }) 647 | .timeout(this.netTimeout); 648 | }; 649 | 650 | /** 651 | * gets() - Get a value and CID on this connection 652 | * 653 | * @param {String} key - The key for the value to retrieve 654 | * @param {Object} [opts] - Any additional options for this get 655 | * @returns {Promise} - containing an array with the value and CID 656 | */ 657 | Connection.prototype.gets = function(key, opts) { 658 | opts = opts || {}; 659 | // Do the gets 660 | var deferred = misc.defer(key); 661 | this.commandBuffer = this.commandBuffer.push(deferred); 662 | 663 | this.write('gets ' + key); 664 | 665 | return deferred.promise 666 | .timeout(this.netTimeout) 667 | .spread(function(data, flag, length, cas) { 668 | if (data) { 669 | if (opts.compressed || (flag & FLAG_COMPRESSED)) { 670 | // @todo compressed data should still be able to utilize the 671 | // flags to return data of same type set 672 | return misc.decompress(Buffer.from(data, 'base64')) 673 | .then(function(d) { 674 | return [ d.toString(), cas ]; 675 | }) 676 | .catch(function(err) { 677 | if (err.toString().indexOf('Error: incorrect header check') > -1) { 678 | // Basically we get this OperationalError when we try to decompress a value 679 | // which was not previously compressed. We return null instead of bubbling 680 | // this error up the chain because we want to represent it as a cache miss 681 | // rather than an actual error. By looking like a cache miss like this, it 682 | // makes it so someone can turn on compression and it'll work fine, looking 683 | // like a cache miss. So in the usual workflow the client tries to set it 684 | // again which means the second time around the compressed version is set 685 | // as the client wanted/expected. 686 | return [ null, null ]; 687 | } else { 688 | throw err; 689 | } 690 | }); 691 | } else { 692 | return [ convert(data, flag, length), cas ]; 693 | } 694 | } else { 695 | return [ null, null ]; 696 | } 697 | }); 698 | }; 699 | 700 | /** 701 | * get() - Get a value on this connection 702 | * 703 | * @param {String} key - The key for the value to retrieve 704 | * @param {Object} [opts] - Any additional options for this get 705 | * @returns {Promise} 706 | */ 707 | Connection.prototype.get = function(key, opts) { 708 | opts = opts || {}; 709 | // Do the get 710 | var deferred = misc.defer(key); 711 | this.commandBuffer = this.commandBuffer.push(deferred); 712 | 713 | this.write('get ' + key); 714 | 715 | return deferred.promise 716 | .timeout(this.netTimeout) 717 | .spread(function(data, flag, length) { 718 | if (data) { 719 | if (opts.compressed || (flag & FLAG_COMPRESSED)) { 720 | // @todo compressed data should still be able to utilize the 721 | // flags to return data of same type set 722 | return misc.decompress(Buffer.from(data, 'base64')) 723 | .then(function(d) { 724 | return d.toString(); 725 | }) 726 | .catch(function(err) { 727 | if (err.toString().indexOf('Error: incorrect header check') > -1) { 728 | // Basically we get this OperationalError when we try to decompress a value 729 | // which was not previously compressed. We return null instead of bubbling 730 | // this error up the chain because we want to represent it as a cache miss 731 | // rather than an actual error. By looking like a cache miss like this, it 732 | // makes it so someone can turn on compression and it'll work fine, looking 733 | // like a cache miss. So in the usual workflow the client tries to set it 734 | // again which means the second time around the compressed version is set 735 | // as the client wanted/expected. 736 | return null; 737 | } else { 738 | throw err; 739 | } 740 | }); 741 | } else { 742 | return convert(data, flag, length); 743 | } 744 | } else { 745 | return null; 746 | } 747 | }); 748 | }; 749 | 750 | /** 751 | * flush() - delete all values on this connection 752 | * @param delay 753 | * @returns {Promise} 754 | */ 755 | Connection.prototype.flush_all = function(delay) { 756 | var deferred = misc.defer(delay); 757 | this.commandBuffer = this.commandBuffer.push(deferred); 758 | 759 | this.write(util.format('flush_all %s', delay)); 760 | 761 | return deferred.promise 762 | .then(function(v) { 763 | return v === 'OK'; 764 | }); 765 | }; 766 | 767 | /** 768 | * add() - Add a value on this connection 769 | */ 770 | Connection.prototype.add = function(key, val, ttl) { 771 | var self = this; 772 | 773 | ttl = ttl || 0; 774 | var opts = {}; 775 | 776 | if (_.isObject(ttl)) { 777 | opts = ttl; 778 | ttl = opts.ttl || 0; 779 | } 780 | 781 | var flag = getFlag(val, opts.compressed); 782 | 783 | return formatValue(val, opts.compressed) 784 | .bind(this) 785 | .then(function(v) { 786 | if (opts.compressed) { 787 | v = Buffer.from(v.toString('base64')); 788 | } 789 | if (v.length > self.maxValueSize) { 790 | throw new Error(util.format('Value too large to set in memcache: %s > %s', v.length, self.maxValueSize)); 791 | } 792 | 793 | var deferred = misc.defer(key); 794 | deferred.key = key; 795 | this.commandBuffer = this.commandBuffer.push(deferred); 796 | 797 | // First send the metadata for this request 798 | this.write(util.format('add %s %d %d %d', key, flag, ttl, v.length)); 799 | 800 | // Then the actual value 801 | this.write(util.format('%s', v)); 802 | return deferred.promise 803 | .then(function(data) { 804 | // data will be a buffer 805 | if (data === 'NOT_STORED') { 806 | throw new Error(util.format('Cannot "add" for key "%s" because it already exists', key)); 807 | } else if (data !== 'STORED') { 808 | throw new Error(util.format('Something went wrong with the add. Expected STORED, got :%s:', data.toString())); 809 | } else { 810 | return Promise.resolve(); 811 | } 812 | }); 813 | }); 814 | }; 815 | 816 | /** 817 | * replace() - Replace a value on this connection 818 | */ 819 | Connection.prototype.replace = function(key, val, ttl) { 820 | var self = this; 821 | 822 | ttl = ttl || 0; 823 | var opts = {}; 824 | 825 | if (_.isObject(ttl)) { 826 | opts = ttl; 827 | ttl = opts.ttl || 0; 828 | } 829 | 830 | var flag = getFlag(val, opts.compressed); 831 | 832 | return formatValue(val, opts.compressed) 833 | .bind(this) 834 | .then(function(v) { 835 | if (opts.compressed) { 836 | v = Buffer.from(v.toString('base64')); 837 | } 838 | if (v.length > self.maxValueSize) { 839 | throw new Error(util.format('Value too large to replace in memcache: %s > %s', v.length, self.maxValueSize)); 840 | } 841 | 842 | var deferred = misc.defer(key); 843 | deferred.key = key; 844 | this.commandBuffer = this.commandBuffer.push(deferred); 845 | 846 | // First send the metadata for this request 847 | this.write(util.format('replace %s %d %d %d', key, flag, ttl, v.length)); 848 | 849 | // Then the actual value 850 | this.write(util.format('%s', v)); 851 | return deferred.promise 852 | .then(function(data) { 853 | // data will be a buffer 854 | if (data === 'NOT_STORED') { 855 | throw new Error(util.format('Cannot "replace" for key "%s" because it does not exist', key)); 856 | } else if (data !== 'STORED') { 857 | throw new Error(util.format('Something went wrong with the replace. Expected STORED, got :%s:', data.toString())); 858 | } else { 859 | return Promise.resolve(); 860 | } 861 | }); 862 | }); 863 | }; 864 | 865 | /** 866 | * append() - Append a value on this connection 867 | */ 868 | Connection.prototype.append = function(key, val, ttl) { 869 | var self = this; 870 | 871 | ttl = ttl || 0; 872 | var opts = {}; 873 | 874 | if (_.isObject(ttl)) { 875 | opts = ttl; 876 | ttl = opts.ttl || 0; 877 | } 878 | 879 | var flag = getFlag(val, opts.compressed); 880 | 881 | return formatValue(val, opts.compressed) 882 | .bind(this) 883 | .then(function(v) { 884 | if (opts.compressed) { 885 | v = Buffer.from(v.toString('base64')); 886 | } 887 | if (v.length > self.maxValueSize) { 888 | throw new Error(util.format('Value too large to append in memcache: %s > %s', v.length, self.maxValueSize)); 889 | } 890 | 891 | var deferred = misc.defer(key); 892 | deferred.key = key; 893 | this.commandBuffer = this.commandBuffer.push(deferred); 894 | 895 | // First send the metadata for this request 896 | this.write(util.format('append %s %d %d %d', key, flag, ttl, v.length)); 897 | 898 | // Then the actual value 899 | this.write(util.format('%s', v)); 900 | return deferred.promise 901 | .then(function(data) { 902 | // data will be a buffer 903 | if (data === 'NOT_STORED') { 904 | throw new Error(util.format('Cannot "append" for key "%s" because it does not exist', key)); 905 | } else if (data !== 'STORED') { 906 | throw new Error(util.format('Something went wrong with the append. Expected STORED, got :%s:', data.toString())); 907 | } else { 908 | return Promise.resolve(); 909 | } 910 | }); 911 | }); 912 | }; 913 | 914 | /** 915 | * prepend() - Prepend a value on this connection 916 | */ 917 | Connection.prototype.prepend = function(key, val, ttl) { 918 | var self = this; 919 | 920 | ttl = ttl || 0; 921 | var opts = {}; 922 | 923 | if (_.isObject(ttl)) { 924 | opts = ttl; 925 | ttl = opts.ttl || 0; 926 | } 927 | 928 | var flag = getFlag(val, opts.compressed); 929 | 930 | return formatValue(val, opts.compressed) 931 | .bind(this) 932 | .then(function(v) { 933 | if (opts.compressed) { 934 | v = Buffer.from(v.toString('base64')); 935 | } 936 | if (v.length > self.maxValueSize) { 937 | throw new Error(util.format('Value too large to prepend in memcache: %s > %s', v.length, self.maxValueSize)); 938 | } 939 | 940 | var deferred = misc.defer(key); 941 | deferred.key = key; 942 | this.commandBuffer = this.commandBuffer.push(deferred); 943 | 944 | // First send the metadata for this request 945 | this.write(util.format('prepend %s %d %d %d', key, flag, ttl, v.length)); 946 | 947 | // Then the actual value 948 | this.write(util.format('%s', v)); 949 | return deferred.promise 950 | .then(function(data) { 951 | // data will be a buffer 952 | if (data === 'NOT_STORED') { 953 | throw new Error(util.format('Cannot "prepend" for key "%s" because it does not exist', key)); 954 | } else if (data !== 'STORED') { 955 | throw new Error(util.format('Something went wrong with the prepend. Expected STORED, got :%s:', data.toString())); 956 | } else { 957 | return Promise.resolve(); 958 | } 959 | }); 960 | }); 961 | }; 962 | 963 | /** 964 | * delete() - Delete value for this key on this connection 965 | * 966 | * @param {String} key - The key to delete 967 | * @returns {Promise} 968 | */ 969 | Connection.prototype.delete = function(key) { 970 | // Do the delete 971 | var deferred = misc.defer(key); 972 | this.commandBuffer = this.commandBuffer.push(deferred); 973 | 974 | this.write(util.format('delete %s', key)); 975 | 976 | return deferred.promise 977 | .then(function(v) { 978 | if (v === 'DELETED') { 979 | return true; 980 | } else { 981 | return false; 982 | } 983 | }) 984 | .timeout(this.netTimeout); 985 | }; 986 | 987 | /** 988 | * 'stats items'() - return items statistics 989 | * @returns {Promise} 990 | */ 991 | Connection.prototype['stats items'] = function() { 992 | debug('stats items'); 993 | 994 | var deferred = misc.defer(); 995 | deferred.type = 'items'; 996 | 997 | this.commandBuffer = this.commandBuffer.push(deferred); 998 | 999 | this.write(util.format('stats items')); 1000 | 1001 | return deferred.promise 1002 | .then(function(slabData) { 1003 | var slabItems = []; 1004 | 1005 | // slabData.items - object containing slab data 1006 | // slabData.ids - array of slab ids in which the results came in 1007 | 1008 | if (slabData && slabData.ids) { 1009 | slabData.ids.forEach(function(slabId) { 1010 | var item = {}; 1011 | 1012 | item.slab_id = parseInt(slabId, 10); 1013 | item.data = slabData.items[slabId]; 1014 | item.server = this.host + ':' + this.port; 1015 | 1016 | slabItems.push(item); 1017 | }.bind(this)); 1018 | } 1019 | 1020 | return slabItems; 1021 | }.bind(this)); 1022 | }; 1023 | 1024 | /** 1025 | * 'stats cachedump'() - dump cache info for a given slabs id 1026 | * @param {number} slabsId 1027 | * @param {number} limit number of entries to show 1028 | */ 1029 | Connection.prototype['stats cachedump'] = function(slabsId, limit) { 1030 | var deferred = misc.defer(slabsId); 1031 | deferred.type = 'items'; 1032 | this.commandBuffer = this.commandBuffer.push(deferred); 1033 | 1034 | this.write(util.format('stats cachedump %s %s', slabsId, limit)); 1035 | 1036 | return deferred.promise 1037 | .then(function(cacheMetaData) { 1038 | var cacheItems = []; 1039 | 1040 | // cacheMetaData.items - object containing cache metadata 1041 | // cacheMetaData.ids - array of cache keys in which the results came in 1042 | 1043 | if (cacheMetaData && cacheMetaData.ids) { 1044 | cacheMetaData.ids.forEach(function(cacheKey) { 1045 | cacheItems.push(cacheMetaData.items[cacheKey]); 1046 | }.bind(this)); 1047 | } 1048 | 1049 | return cacheItems; 1050 | }.bind(this)); 1051 | }; 1052 | 1053 | /** 1054 | * version() - Get current Memcached version from the server 1055 | * 1056 | * @returns {Promise} 1057 | */ 1058 | Connection.prototype.version = function() { 1059 | var deferred = misc.defer(); 1060 | deferred.type = 'version'; 1061 | this.commandBuffer = this.commandBuffer.push(deferred); 1062 | 1063 | this.write('version'); 1064 | 1065 | return deferred.promise; 1066 | }; 1067 | 1068 | 1069 | module.exports = Connection; 1070 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | require('chai').should(); 2 | var _ = require('lodash'), 3 | chance = require('chance').Chance(), 4 | expect = require('chai').expect, 5 | misc = require('../lib/misc'), 6 | Promise = require('bluebird'); 7 | 8 | var Client = require('../lib/client'); 9 | 10 | function sleep(ms) { 11 | return new Promise(resolve => setTimeout(resolve, ms)); 12 | } 13 | 14 | describe('Client', function() { 15 | var keys = []; 16 | // We want a method for generating keys which will store them so we can 17 | // do cleanup later and not litter memcache with a bunch of garbage data 18 | var getKey = function(opts) { 19 | var key; 20 | if (opts) { 21 | key = chance.word(opts); 22 | } else { 23 | key = chance.guid(); 24 | } 25 | keys.push(key); 26 | return key; 27 | }; 28 | 29 | describe('initialization', function() { 30 | it('with defaults', function() { 31 | var cache = new Client(); 32 | cache.should.have.property('reconnect'); 33 | cache.reconnect.should.be.a('boolean'); 34 | cache.should.have.property('hosts'); 35 | cache.hosts.should.be.an('array'); 36 | }); 37 | 38 | it('initiates connection', function(done) { 39 | var cache = new Client(); 40 | cache.should.have.property('connections'); 41 | cache.connections.should.be.an('object'); 42 | _.sample(cache.connections).client.on('connect', function() { 43 | done(); 44 | }); 45 | }); 46 | 47 | it('has a disconnect method', function(done) { 48 | var cache = new Client(); 49 | cache.should.have.property('disconnect'); 50 | cache.disconnect.should.be.a('function'); 51 | _.sample(cache.connections).client.on('connect', function() { 52 | cache.disconnect() 53 | .then(function() { 54 | cache.connections.should.be.an('object'); 55 | _.keys(cache.connections).should.have.length(0); 56 | }) 57 | .then(done); 58 | }); 59 | }); 60 | 61 | it('can disconnect from a specific client with string', function(done) { 62 | var cache = new Client({ hosts: ['localhost:11211', '127.0.0.1:11211'] }); 63 | cache.should.have.property('disconnect'); 64 | cache.disconnect.should.be.a('function'); 65 | cache.disconnect('127.0.0.1:11211') 66 | .then(function() { 67 | cache.connections.should.be.an('object'); 68 | _.keys(cache.connections).should.have.length(1); 69 | cache.hosts.should.have.length(1); 70 | _.keys(cache.connections)[0].should.equal('localhost:11211'); 71 | cache.hosts[0].should.equal('localhost:11211'); 72 | }) 73 | .then(done); 74 | }); 75 | 76 | it('can disconnect from a specific client with array', function(done) { 77 | var cache = new Client({ hosts: ['localhost:11211', '127.0.0.1:11211'] }); 78 | cache.should.have.property('disconnect'); 79 | cache.disconnect.should.be.a('function'); 80 | cache.disconnect(['127.0.0.1:11211']) 81 | .then(function() { 82 | cache.connections.should.be.an('object'); 83 | _.keys(cache.connections).should.have.length(1); 84 | _.keys(cache.connections)[0].should.equal('localhost:11211'); 85 | }) 86 | .then(done); 87 | }); 88 | 89 | it('throws an error if attempting to disconnect from a bogus host', function() { 90 | var cache = new Client({ 91 | hosts: ['localhost:11211', '127.0.0.1:11211'], 92 | onNetError: function() {} 93 | }); 94 | cache.should.have.property('disconnect'); 95 | cache.disconnect.should.be.a('function'); 96 | expect(function() { cache.disconnect(['badserver:11211']); }).to.throw('Cannot disconnect from server unless connected'); 97 | }); 98 | 99 | it('has a dictionary of connections', function() { 100 | var cache = new Client(); 101 | cache.should.have.property('hosts'); 102 | cache.connections.should.be.an('object'); 103 | }); 104 | 105 | it('has a hashring of connections', function() { 106 | var cache = new Client(); 107 | cache.should.have.property('ring'); 108 | cache.ring.should.be.an.instanceof(require('hashring')); 109 | }); 110 | 111 | it('with default port with single connection', function() { 112 | var cache = new Client('localhost'); 113 | cache.connections['localhost'].should.have.property('port'); 114 | cache.connections['localhost'].port.should.equal('11211'); 115 | }); 116 | 117 | it('with default port with multiple connections', function() { 118 | var cache = new Client(['localhost']); 119 | cache.connections['localhost'].should.have.property('port'); 120 | cache.connections['localhost'].port.should.equal('11211'); 121 | }); 122 | 123 | /** 124 | * To run this test, start up a local memcached server with TLS enabled 125 | * on port 11212 with a trusted certificate 126 | */ 127 | it('with TLS enabled', async function() { 128 | var cache_client = new Client({ 129 | hosts: ['127.0.0.1:11212'], 130 | tls: { 131 | checkServerIdentity: () => { 132 | return undefined; 133 | } 134 | } 135 | }); 136 | var val = chance.word(); 137 | await cache_client.set('key', val); 138 | let v = await cache_client.get('key'); 139 | val.should.equal(v); 140 | }); 141 | 142 | /** 143 | * Only comment this out when we have an Elasticache autodiscovery cluster to test against. 144 | * Ideally one day this can be mocked, but for now just selectively enabling it 145 | it('supports autodiscovery', function() { 146 | var cache = new Client({ hosts: ['test-memcache.di6cba.cfg.use1.cache.amazonaws.com'], autodiscover: true }); 147 | var val = chance.word(); 148 | 149 | return cache.set('test', val) 150 | .then(function() { 151 | return cache.get('test'); 152 | }) 153 | .then(function(v) { 154 | val.should.equal(v); 155 | }); 156 | }); 157 | */ 158 | 159 | it('throws on autodiscovery failure', function() { 160 | var cache = new Client({ 161 | hosts: ['badserver:11211'], 162 | autodiscover: true, 163 | onNetError: function() {} 164 | }); 165 | var val = chance.word(); 166 | 167 | return cache.set('test', val) 168 | .then(function() { throw new Error('should not get here'); }) 169 | .catch(function(err) { 170 | err.should.be.ok; 171 | err.should.be.an.instanceof(Error); 172 | err.message.should.match(/Autodiscovery failed/); 173 | }) 174 | .then(function() { 175 | // try again to ensure that subsequent ops also fail 176 | return cache.set('test', val); 177 | }) 178 | .then(function() { throw new Error('should not get here'); }) 179 | .catch(function(err) { 180 | err.should.be.ok; 181 | err.should.be.an.instanceof(Error); 182 | err.message.should.match(/Autodiscovery failed/); 183 | }); 184 | }); 185 | }); 186 | 187 | describe('set and get', function() { 188 | var cache; 189 | beforeEach(function() { 190 | cache = new Client(); 191 | }); 192 | 193 | it('exists', function() { 194 | cache.should.have.property('set'); 195 | }); 196 | 197 | describe('should throw an error if called', function() { 198 | it('without a key', function() { 199 | expect(function() { cache.set(); }).to.throw('AssertionError: Cannot "set" without key!'); 200 | }); 201 | 202 | it('with a key that is too long', function() { 203 | expect(function() { cache.set(chance.string({length: 251}), chance.word()); }).to.throw('less than 250 bytes'); 204 | }); 205 | 206 | it('with a non-string key', function() { 207 | expect(function() { cache.set({blah: 'test'}, 'val'); }).to.throw('AssertionError: Key needs to be of type "string"'); 208 | expect(function() { cache.set([1, 2], 'val'); }).to.throw('AssertionError: Key needs to be of type "string"'); 209 | expect(function() { cache.set(_.noop, 'val'); }).to.throw('AssertionError: Key needs to be of type "string"'); 210 | }); 211 | }); 212 | 213 | it('should work', async function() { 214 | var key = getKey(), val = chance.word(); 215 | 216 | await cache.set(key, val); 217 | let v = await cache.get(key); 218 | val.should.equal(v); 219 | }); 220 | 221 | it.skip('works with values with newlines', async function() { 222 | var key = getKey(), val = 'value\nwith newline'; 223 | 224 | await cache.set(key, val); 225 | let v = await cache.get(key); 226 | val.should.equal(v); 227 | }); 228 | 229 | it('works with very large values', async function() { 230 | var key = getKey(), val = chance.word({ length: 1000000 }); 231 | 232 | await cache.set(key, val); 233 | let v = await cache.get(key); 234 | val.should.equal(v); 235 | }); 236 | 237 | describe('compression', function() { 238 | it('does not throw an error if compression specified', function() { 239 | var key = getKey(), val = chance.word({ length: 1000 }); 240 | return cache.set(key, val, { compressed: true }); 241 | }); 242 | 243 | it('works of its own accord', async function() { 244 | var val = chance.word({ length: 1000 }); 245 | 246 | let v = await misc.compress(Buffer.from(val)); 247 | let d = await misc.decompress(v); 248 | d.toString().should.equal(val); 249 | }); 250 | 251 | it('get works with compression', async function() { 252 | var key = getKey(), val = chance.word({ length: 1000 }); 253 | 254 | await cache.set(key, val, { compressed: true }); 255 | let v = await cache.get(key, { compressed: true }); 256 | val.should.equal(v); 257 | }); 258 | 259 | it('get works with compression without explicit get compressed flag', async function() { 260 | var key = getKey(), val = chance.word({ length: 1000 }); 261 | 262 | await cache.set(key, val, { compressed: true }); 263 | let v = await cache.get(key); 264 | val.should.equal(v); 265 | }); 266 | 267 | it('getMulti works with compression', async function() { 268 | var key1 = getKey(), key2 = getKey(), 269 | val1 = chance.word(), val2 = chance.word(); 270 | 271 | await Promise.all([cache.set(key1, val1, { compressed: true }), cache.set(key2, val2, { compressed: true })]); 272 | let vals = await cache.getMulti([key1, key2], { compressed: true }); 273 | vals.should.be.an('object'); 274 | vals[key1].should.equal(val1); 275 | vals[key2].should.equal(val2); 276 | }); 277 | 278 | it('get works with a callback', function(done) { 279 | var key = getKey(), val = chance.word({ length: 1000 }); 280 | 281 | cache.set(key, val, { compressed: true }, function() { 282 | cache.get(key, { compressed: true }, function(err, v) { 283 | val.should.equal(v); 284 | done(err); 285 | }); 286 | }); 287 | }); 288 | 289 | it('get for key that should be compressed but is not returns null', async function() { 290 | var key = getKey(), val = chance.word({ length: 1000 }); 291 | 292 | await cache.set(key, val); 293 | let v = await cache.get(key, { compressed: true }); 294 | expect(v).to.be.null; 295 | }); 296 | }); 297 | 298 | it('does not throw an error when setting a value number', function() { 299 | var key = chance.guid(), val = chance.natural(); 300 | 301 | expect(function() { cache.set(key, val); }).to.not.throw(); 302 | }); 303 | 304 | it('get for val set as number returns number', async function() { 305 | var key = getKey(), val = chance.integer(); 306 | 307 | await cache.set(key, val); 308 | let v = await cache.get(key); 309 | expect(v).to.be.a('number'); 310 | v.should.equal(val); 311 | }); 312 | 313 | it('get for val set as floating number returns number', async function() { 314 | var key = getKey(), val = chance.floating(); 315 | 316 | await cache.set(key, val); 317 | let v = await cache.get(key); 318 | expect(v).to.be.a.number; 319 | v.should.equal(val); 320 | }); 321 | 322 | it('get for val set as object returns object', async function() { 323 | var key = getKey(), val = { num: chance.integer() }; 324 | 325 | await cache.set(key, val); 326 | let v = await cache.get(key); 327 | expect(v).to.be.an.object; 328 | (v.num).should.equal(val.num); 329 | }); 330 | 331 | it('get for val set as Buffer returns Buffer', async function() { 332 | var key = getKey(), val = Buffer.from('blah blah test'); 333 | 334 | await cache.set(key, val); 335 | let v = await cache.get(key); 336 | expect(v).to.be.an.instanceof(Buffer); 337 | (v.toString()).should.equal(val.toString()); 338 | }); 339 | 340 | it('get for val set as null returns null', async function() { 341 | var key = getKey(), val = null; 342 | 343 | await cache.set(key, val); 344 | let v = await cache.get(key); 345 | expect(v).to.be.null; 346 | }); 347 | 348 | it('get for val set as array returns array', async function() { 349 | var key = getKey(), val = [ chance.integer(), chance.integer() ]; 350 | 351 | await cache.set(key, val); 352 | let v = await cache.get(key); 353 | expect(v).to.be.an.array; 354 | expect(v).to.deep.equal(val); 355 | }); 356 | 357 | it('throws error with enormous values (over memcache limit)', async function() { 358 | // Limit is 1048577, 1 byte more throws error. We'll go up a few just to be safe 359 | var key = getKey(), val = chance.word({ length: 1048590 }); 360 | try { 361 | await cache.set(key, val); 362 | throw new Error('this code should never get hit'); 363 | } catch (err) { 364 | err.should.be.ok; 365 | err.should.be.an.instanceof(Error); 366 | err.should.deep.equal(new Error('Value too large to set in memcache')); 367 | } 368 | }); 369 | 370 | it('works fine with special characters', async function() { 371 | var key = getKey(), 372 | val = chance.string({ pool: 'ÀÈÌÒÙàèìòÁÉÍÓÚáéíóúÂÊÎÔÛâêîôûÃÑÕãñõÄËÏÖÜŸäëïöüÿæ☃', length: 1000 }); 373 | 374 | await cache.set(key, val); 375 | let v = await cache.get(key); 376 | val.should.equal(v); 377 | }); 378 | 379 | it('works with callbacks as well', function(done) { 380 | var key = getKey(), val = chance.word(); 381 | 382 | cache.set(key, val, function(err) { 383 | if (err !== null) { 384 | done(err); 385 | } 386 | cache.get(key, function(err, v) { 387 | if (err !== null) { 388 | done(err); 389 | } 390 | val.should.equal(v); 391 | done(); 392 | }); 393 | }); 394 | }); 395 | 396 | it('multiple should not conflict', function() { 397 | var key1 = getKey(), key2 = getKey(), key3 = getKey(), 398 | val1 = chance.word(), val2 = chance.word(), val3 = chance.word(); 399 | 400 | var item1 = cache.set(key1, val1) 401 | .then(function() { 402 | return cache.get(key1); 403 | }) 404 | .then(function(v) { 405 | val1.should.equal(v); 406 | }); 407 | 408 | var item2 = cache.set(key2, val2) 409 | .then(function() { 410 | return cache.get(key2); 411 | }) 412 | .then(function(v) { 413 | val2.should.equal(v); 414 | }); 415 | 416 | var item3 = cache.set(key3, val3) 417 | .then(function() { 418 | return cache.get(key3); 419 | }) 420 | .then(function(v) { 421 | val3.should.equal(v); 422 | }); 423 | 424 | return Promise.all([item1, item2, item3]); 425 | }); 426 | 427 | it('many multiple operations should not conflict', function() { 428 | var key = getKey(), key1 = getKey(), key2 = getKey(), key3 = getKey(), 429 | val1 = chance.word(), val2 = chance.word(), val3 = chance.word(); 430 | 431 | 432 | return cache.set(key, val1) 433 | .then(function() { 434 | return Promise.all([ 435 | cache.delete(key), 436 | cache.set(key1, val1), 437 | cache.set(key2, val2), 438 | cache.set(key3, val3) 439 | ]); 440 | }) 441 | .then(function() { 442 | return Promise.all([cache.get(key1), cache.get(key2), cache.get(key3)]); 443 | }) 444 | .then(function(v) { 445 | v[0].should.equal(val1); 446 | v[1].should.equal(val2); 447 | v[2].should.equal(val3); 448 | 449 | return Promise.all([ 450 | cache.get(key1), 451 | cache.deleteMulti([key1, key3]) 452 | ]); 453 | }) 454 | .then(function(v) { 455 | v[0].should.equal(val1); 456 | }); 457 | }); 458 | 459 | describe('get to key that does not exist returns null', function() { 460 | it('with Promise', async function() { 461 | let v = await cache.get(chance.guid()); 462 | expect(v).to.be.null; 463 | }); 464 | 465 | it('with Callback', function(done) { 466 | cache.get(chance.word(), function(err, response) { 467 | expect(response).to.be.null; 468 | done(err); 469 | }); 470 | }); 471 | }); 472 | 473 | describe('getMulti', function() { 474 | it('exists', function() { 475 | cache.should.have.property('getMulti'); 476 | }); 477 | 478 | it('works', async function() { 479 | var key1 = getKey(), key2 = getKey(), 480 | val1 = chance.word(), val2 = chance.word(); 481 | 482 | await Promise.all([cache.set(key1, val1), cache.set(key2, val2)]); 483 | let vals = await cache.getMulti([key1, key2]); 484 | vals.should.be.an('object'); 485 | vals[key1].should.equal(val1); 486 | vals[key2].should.equal(val2); 487 | }); 488 | 489 | it('get with array of keys delegates to getMulti', async function() { 490 | var key1 = getKey(), key2 = getKey(), 491 | val1 = chance.word(), val2 = chance.word(); 492 | 493 | await Promise.all([cache.set(key1, val1), cache.set(key2, val2)]); 494 | let vals = await cache.get([key1, key2]); 495 | vals.should.be.an('object'); 496 | vals[key1].should.equal(val1); 497 | vals[key2].should.equal(val2); 498 | }); 499 | 500 | it('works if some values not found', async function() { 501 | var key1 = getKey(), key2 = getKey(), 502 | val = chance.word(); 503 | 504 | await cache.set(key1, val); 505 | let vals = await cache.getMulti([key1, key2]); 506 | vals.should.be.an('object'); 507 | vals[key1].should.equal(val); 508 | expect(vals[key2]).to.equal(null); 509 | }); 510 | 511 | it('works if all values not found', async function() { 512 | var key = getKey(), key2 = getKey(), key3 = getKey(), 513 | val = chance.word(); 514 | 515 | await cache.set(key, val); 516 | let vals = await cache.getMulti([key2, key3]); 517 | vals.should.be.an('object'); 518 | _.size(vals).should.equal(2); 519 | expect(vals[key2]).to.equal(null); 520 | expect(vals[key3]).to.equal(null); 521 | }); 522 | 523 | it('works if all values not found with callback', function(done) { 524 | var key = getKey(), key2 = getKey(), key3 = getKey(), 525 | val = chance.word(); 526 | 527 | cache.set(key, val) 528 | .then(function() { 529 | cache.getMulti([key2, key3], function(err, vals) { 530 | vals.should.be.an('object'); 531 | _.size(vals).should.equal(2); 532 | expect(vals[key2]).to.equal(null); 533 | expect(vals[key3]).to.equal(null); 534 | done(err); 535 | }); 536 | }); 537 | }); 538 | }); 539 | 540 | describe('works with expiration', function() { 541 | it('expires', async function() { 542 | var key = getKey(), val = chance.word(); 543 | 544 | await cache.set(key, val, 1); 545 | let v = await cache.get(key); 546 | val.should.equal(v); 547 | await sleep(1001); 548 | v = await cache.get(key); 549 | expect(v).to.be.null; 550 | }); 551 | }); 552 | }); 553 | 554 | describe('cas and gets', function() { 555 | var cache; 556 | beforeEach(function() { 557 | cache = new Client(); 558 | }); 559 | 560 | it('exists', function() { 561 | cache.should.have.property('gets'); 562 | }); 563 | 564 | it('should return a cas value', async function() { 565 | var key = getKey(), val = chance.word(); 566 | 567 | await cache.set(key, val); 568 | let [v, cas] = await cache.gets(key); 569 | val.should.equal(v); 570 | expect(cas).to.exist; 571 | }); 572 | 573 | it('should store new value when given a matching cas', async function() { 574 | var key = getKey(), val = chance.word(), updatedVal = chance.word(); 575 | 576 | await cache.set(key, val); 577 | let [v, cas] = await cache.gets(key); 578 | expect(v).to.not.be.null; 579 | let success = await cache.cas(key, updatedVal, cas); 580 | expect(success).to.be.true; 581 | let v2 = await cache.get(key); 582 | expect(v2).to.equal(updatedVal); 583 | }); 584 | 585 | it('should not store the new value when given an invalid cas value', async function() { 586 | var key = getKey(), val = chance.word(), updatedVal = chance.word(); 587 | 588 | await cache.set(key, val); 589 | let [v, cas] = await cache.gets(key); 590 | expect(v).to.not.be.null; 591 | var invalidCas; 592 | do { 593 | invalidCas = chance.string({pool: '0123456789', length: 15}); 594 | } while (invalidCas === cas); 595 | 596 | let success = await cache.cas(key, updatedVal, invalidCas); 597 | expect(success).to.be.false; 598 | }); 599 | 600 | it('should not store a value when given an invalid key value', async function() { 601 | var key = getKey(), invalidKey = getKey(), 602 | val = chance.word(), updatedVal = chance.word(); 603 | 604 | await cache.set(key, val); 605 | let [v, cas] = await cache.gets(key); 606 | expect(v).to.not.be.null; 607 | let success = await cache.cas(invalidKey, updatedVal, cas); 608 | expect(success).to.be.false; 609 | }); 610 | }); 611 | 612 | // @todo should have cleanup jobs to delete keys we set in memcache 613 | describe('delete', function() { 614 | var cache; 615 | beforeEach(function() { 616 | cache = new Client(); 617 | }); 618 | 619 | it('exists', function() { 620 | cache.should.have.property('delete'); 621 | cache.delete.should.be.a('function'); 622 | }); 623 | 624 | it('works', async function() { 625 | var key = getKey(); 626 | 627 | await cache.set(key, 'myvalue'); 628 | await cache.delete(key); 629 | let v = await cache.get(key); 630 | expect(v).to.be.null; 631 | }); 632 | 633 | it('does not blow up if deleting key that does not exist', function() { 634 | var key = chance.guid(); 635 | return cache.delete(key); 636 | }); 637 | }); 638 | 639 | describe('deleteMulti', function() { 640 | var cache; 641 | beforeEach(function() { 642 | cache = new Client(); 643 | }); 644 | 645 | it('exists', function() { 646 | cache.should.have.property('deleteMulti'); 647 | cache.deleteMulti.should.be.a('function'); 648 | }); 649 | 650 | it('works', function() { 651 | var key1 = getKey(), key2 = getKey(); 652 | 653 | return Promise.all([cache.set(key1, 'myvalue'), cache.set(key2, 'myvalue')]) 654 | .then(function() { 655 | return cache.deleteMulti([key1, key2]); 656 | }) 657 | .then(function(d) { 658 | d.should.be.an.object; 659 | _.values(d).indexOf(null).should.equal(-1); 660 | _.every(d).should.be.true; 661 | return Promise.all([cache.get(key1), cache.get(key2)]); 662 | }) 663 | .spread(function(v1, v2) { 664 | expect(v1).to.be.null; 665 | expect(v2).to.be.null; 666 | return; 667 | }); 668 | }); 669 | }); 670 | 671 | // @todo these are placeholders for now until I can figure out a good way 672 | // to adequeately test these. 673 | describe('Client buffer', function() { 674 | it('works'); 675 | it('can be flushed'); 676 | }); 677 | 678 | describe('Connection buffer', function() { 679 | it('works'); 680 | it('can be flushed'); 681 | }); 682 | 683 | describe('Helpers', function() { 684 | describe('splitHost()', function() { 685 | it('exists', function() { 686 | var client = new Client(); 687 | client.should.have.property('splitHost'); 688 | }); 689 | 690 | it('works with no port', function() { 691 | var client = new Client(); 692 | var hostName = chance.word(); 693 | 694 | var host = client.splitHost(hostName); 695 | host.should.have.property('host'); 696 | host.should.have.property('port'); 697 | host.host.should.equal(hostName); 698 | host.port.should.equal('11211'); 699 | }); 700 | 701 | it('works with just a port', function() { 702 | var client = new Client(); 703 | var port = chance.natural({ max: 65536 }).toString(); 704 | 705 | var host = client.splitHost(':' + port); 706 | host.should.have.property('host'); 707 | host.should.have.property('port'); 708 | host.host.should.equal('localhost'); 709 | host.port.should.equal(port); 710 | }); 711 | 712 | it('works with both a host and port', function() { 713 | var client = new Client(); 714 | var hostName = chance.word(); 715 | var port = chance.natural({ max: 65536 }).toString(); 716 | 717 | var host = client.splitHost(hostName + ':' + port); 718 | host.should.have.property('host'); 719 | host.should.have.property('port'); 720 | host.host.should.equal(hostName); 721 | host.port.should.equal(port); 722 | }); 723 | }); 724 | }); 725 | 726 | describe('Options', function() { 727 | it('can be disabled', async function() { 728 | var client = new Client({ disabled: true }); 729 | var key = getKey(), val = chance.word(); 730 | 731 | await client.set(key, val); 732 | let v = await client.get(key); 733 | expect(v).to.be.null; 734 | }); 735 | }); 736 | 737 | describe('incr', function() { 738 | var cache; 739 | beforeEach(function() { 740 | cache = new Client(); 741 | }); 742 | 743 | it('exists', function() { 744 | cache.should.have.property('incr'); 745 | }); 746 | 747 | describe('should throw an error if called', function() { 748 | it('without a key', function() { 749 | expect(function() { cache.incr(); }).to.throw('AssertionError: Cannot "incr" without key!'); 750 | }); 751 | 752 | it('with a key that is too long', function() { 753 | expect(function() { cache.incr(chance.string({length: 251})); }).to.throw('less than 250 bytes'); 754 | }); 755 | 756 | it('with a non-string key', function() { 757 | expect(function() { cache.incr({blah: 'test'}); }).to.throw('AssertionError: Key needs to be of type "string"'); 758 | expect(function() { cache.incr([1, 2]); }).to.throw('AssertionError: Key needs to be of type "string"'); 759 | expect(function() { cache.incr(_.noop); }).to.throw('AssertionError: Key needs to be of type "string"'); 760 | }); 761 | 762 | it('with a val that is not a number', function() { 763 | expect(function() { cache.incr(chance.string(), chance.word()); }).to.throw('AssertionError: Cannot incr in memcache with a non number value'); 764 | }); 765 | }); 766 | 767 | describe('should work', function() { 768 | it('without an increment value', async function() { 769 | var key = getKey(), val = chance.natural(); 770 | 771 | await cache.set(key, val); 772 | let v = await cache.incr(key); 773 | v.should.equal(val + 1); 774 | }); 775 | 776 | it('with an increment value', async function() { 777 | var key = getKey(), val = chance.natural({ max: 20000000}), incr = chance.natural({ max: 1000 }); 778 | 779 | await cache.set(key, val); 780 | let v = await cache.incr(key, incr); 781 | v.should.equal(val + incr); 782 | }); 783 | }); 784 | }); 785 | 786 | describe('decr', function() { 787 | var cache; 788 | beforeEach(function() { 789 | cache = new Client(); 790 | }); 791 | 792 | it('exists', function() { 793 | cache.should.have.property('decr'); 794 | }); 795 | 796 | describe('should throw an error if called', function() { 797 | it('without a key', function() { 798 | expect(function() { cache.decr(); }).to.throw('AssertionError: Cannot "decr" without key!'); 799 | }); 800 | 801 | it('with a key that is too long', function() { 802 | expect(function() { cache.decr(chance.string({length: 251})); }).to.throw('less than 250 bytes'); 803 | }); 804 | 805 | it('with a non-string key', function() { 806 | expect(function() { cache.decr({blah: 'test'}); }).to.throw('AssertionError: Key needs to be of type "string"'); 807 | expect(function() { cache.decr([1, 2]); }).to.throw('AssertionError: Key needs to be of type "string"'); 808 | expect(function() { cache.decr(_.noop); }).to.throw('AssertionError: Key needs to be of type "string"'); 809 | }); 810 | 811 | it('with a val that is not a number', function() { 812 | expect(function() { cache.decr(chance.string(), chance.word()); }).to.throw('AssertionError: Cannot decr in memcache with a non number value'); 813 | }); 814 | }); 815 | 816 | describe('should work', function() { 817 | it('without a decrement value', async function() { 818 | var key = getKey(), val = chance.natural(); 819 | 820 | await cache.set(key, val); 821 | let v = await cache.decr(key); 822 | v.should.equal(val - 1); 823 | }); 824 | 825 | it('with a decrement value', async function() { 826 | var key = getKey(), val = chance.natural({ max: 20000000}), decr = chance.natural({ max: 1000 }); 827 | 828 | await cache.set(key, val); 829 | let v = await cache.decr(key, decr); 830 | v.should.equal(val - decr); 831 | }); 832 | }); 833 | }); 834 | 835 | describe('flush', function() { 836 | var cache; 837 | beforeEach(function() { 838 | cache = new Client(); 839 | }); 840 | 841 | it('exists', function() { 842 | cache.should.have.property('flush'); 843 | }); 844 | 845 | describe('should work', function() { 846 | it('removes all data', async function () { 847 | var key = getKey(), val = chance.natural(); 848 | 849 | await cache.set(key, val); 850 | let v = await cache.get(key); 851 | expect(v).to.equal(val); 852 | await cache.flush(); 853 | let v2 = await cache.get(key); 854 | expect(v2).to.equal(null); 855 | }); 856 | 857 | it('removes all data after a specified number of seconds', async function() { 858 | var key = getKey(), val = chance.natural(); 859 | 860 | await cache.set(key, val); 861 | let v = await cache.get(key); 862 | expect(v).to.equal(val); 863 | 864 | await cache.flush(1); 865 | 866 | await sleep(1001); 867 | let v3 = await cache.get(key); 868 | expect(v3).to.be.null; 869 | }); 870 | }); 871 | }); 872 | 873 | describe('add', function() { 874 | var cache; 875 | beforeEach(function() { 876 | cache = new Client(); 877 | }); 878 | 879 | it('exists', function() { 880 | cache.should.have.property('add'); 881 | }); 882 | 883 | describe('should throw an error if called', function() { 884 | it('without a key', function() { 885 | expect(function() { cache.add(); }).to.throw('AssertionError: Cannot "add" without key!'); 886 | }); 887 | 888 | it('with a key that is too long', function() { 889 | expect(function() { cache.add(chance.string({length: 251})); }).to.throw('less than 250 bytes'); 890 | }); 891 | 892 | it('with a non-string key', function() { 893 | expect(function() { cache.add({blah: 'test'}); }).to.throw('AssertionError: Key needs to be of type "string"'); 894 | expect(function() { cache.add([1, 2]); }).to.throw('AssertionError: Key needs to be of type "string"'); 895 | expect(function() { cache.add(_.noop); }).to.throw('AssertionError: Key needs to be of type "string"'); 896 | }); 897 | }); 898 | 899 | describe('should work', function() { 900 | it('with a brand new key', async function() { 901 | var key = getKey(), val = chance.natural(); 902 | 903 | await cache.add(key, val); 904 | let v = await cache.get(key); 905 | v.should.equal(val); 906 | }); 907 | 908 | it('should behave properly when add over existing key', async function() { 909 | var key = getKey(), val = chance.natural(); 910 | 911 | await cache.add(key, val); 912 | try { 913 | await cache.add(key, val); 914 | } catch (err) { 915 | expect(err.toString()).to.contain('it already exists'); 916 | } 917 | }); 918 | }); 919 | }); 920 | 921 | describe('replace', function() { 922 | var cache; 923 | beforeEach(function() { 924 | cache = new Client(); 925 | }); 926 | 927 | it('exists', function() { 928 | cache.should.have.property('replace'); 929 | }); 930 | 931 | describe('should throw an error if called', function() { 932 | it('without a key', function() { 933 | expect(function() { cache.replace(); }).to.throw('AssertionError: Cannot "replace" without key!'); 934 | }); 935 | 936 | it('with a key that is too long', function() { 937 | expect(function() { cache.replace(chance.string({length: 251})); }).to.throw('less than 250 bytes'); 938 | }); 939 | 940 | it('with a non-string key', function() { 941 | expect(function() { cache.replace({blah: 'test'}); }).to.throw('AssertionError: Key needs to be of type "string"'); 942 | expect(function() { cache.replace([1, 2]); }).to.throw('AssertionError: Key needs to be of type "string"'); 943 | expect(function() { cache.replace(_.noop); }).to.throw('AssertionError: Key needs to be of type "string"'); 944 | }); 945 | }); 946 | 947 | describe('should work', function() { 948 | it('as normal', async function() { 949 | var key = getKey(), val = chance.natural(), val2 = chance.natural(); 950 | 951 | await cache.set(key, val); 952 | await cache.replace(key, val2); 953 | let v = await cache.get(key); 954 | v.should.equal(val2); 955 | }); 956 | 957 | it('should behave properly when replace over non-existent key', async function() { 958 | var key = getKey(), val = chance.natural(); 959 | 960 | try { 961 | await cache.replace(key, val); 962 | } catch (err) { 963 | expect(err.toString()).to.contain('does not exist'); 964 | } 965 | }); 966 | }); 967 | }); 968 | 969 | describe('append', function() { 970 | var cache; 971 | beforeEach(function() { 972 | cache = new Client(); 973 | }); 974 | 975 | it('exists', function() { 976 | cache.should.have.property('append'); 977 | }); 978 | 979 | describe('should throw an error if called', function() { 980 | it('without a key', function() { 981 | expect(function() { cache.append(); }).to.throw('AssertionError: Cannot "append" without key!'); 982 | }); 983 | 984 | it('with a key that is too long', function() { 985 | expect(function() { cache.append(chance.string({length: 251})); }).to.throw('less than 250 bytes'); 986 | }); 987 | 988 | it('with a non-string key', function() { 989 | expect(function() { cache.append({blah: 'test'}); }).to.throw('AssertionError: Key needs to be of type "string"'); 990 | expect(function() { cache.append([1, 2]); }).to.throw('AssertionError: Key needs to be of type "string"'); 991 | expect(function() { cache.append(_.noop); }).to.throw('AssertionError: Key needs to be of type "string"'); 992 | }); 993 | }); 994 | 995 | describe('should work', function() { 996 | it('as normal', function() { 997 | var key = getKey(), val = chance.string(), val2 = chance.string(); 998 | 999 | return cache.set(key, val) 1000 | .then(function() { 1001 | return cache.append(key, val2); 1002 | }) 1003 | .then(function() { 1004 | return cache.get(key); 1005 | }) 1006 | .then(function(v) { 1007 | v.should.equal(val + val2); 1008 | }); 1009 | }); 1010 | 1011 | it('should behave properly when append over non-existent key', function() { 1012 | var key = getKey(), val = chance.natural(); 1013 | 1014 | return cache.append(key, val) 1015 | .catch(function(err) { 1016 | expect(err.toString()).to.contain('does not exist'); 1017 | }); 1018 | }); 1019 | }); 1020 | }); 1021 | 1022 | describe('prepend', function() { 1023 | var cache; 1024 | beforeEach(function() { 1025 | cache = new Client(); 1026 | }); 1027 | 1028 | it('exists', function() { 1029 | cache.should.have.property('prepend'); 1030 | }); 1031 | 1032 | describe('should throw an error if called', function() { 1033 | it('without a key', function() { 1034 | expect(function() { cache.prepend(); }).to.throw('AssertionError: Cannot "prepend" without key!'); 1035 | }); 1036 | 1037 | it('with a key that is too long', function() { 1038 | expect(function() { cache.prepend(chance.string({length: 251})); }).to.throw('less than 250 bytes'); 1039 | }); 1040 | 1041 | it('with a non-string key', function() { 1042 | expect(function() { cache.prepend({blah: 'test'}); }).to.throw('AssertionError: Key needs to be of type "string"'); 1043 | expect(function() { cache.prepend([1, 2]); }).to.throw('AssertionError: Key needs to be of type "string"'); 1044 | expect(function() { cache.prepend(_.noop); }).to.throw('AssertionError: Key needs to be of type "string"'); 1045 | }); 1046 | }); 1047 | 1048 | describe('should work', function() { 1049 | it('as normal', function() { 1050 | var key = getKey(), val = chance.string(), val2 = chance.string(); 1051 | 1052 | return cache.set(key, val) 1053 | .then(function() { 1054 | return cache.prepend(key, val2); 1055 | }) 1056 | .then(function() { 1057 | return cache.get(key); 1058 | }) 1059 | .then(function(v) { 1060 | v.should.equal(val2 + val); 1061 | }); 1062 | }); 1063 | 1064 | it('should behave properly when prepend over non-existent key', function() { 1065 | var key = getKey(), val = chance.natural(); 1066 | 1067 | return cache.prepend(key, val) 1068 | .catch(function(err) { 1069 | expect(err.toString()).to.contain('does not exist'); 1070 | }); 1071 | }); 1072 | }); 1073 | }); 1074 | 1075 | describe('items', function () { 1076 | var cache; 1077 | beforeEach(function() { 1078 | cache = new Client(); 1079 | }); 1080 | 1081 | it('exists', function() { 1082 | cache.should.have.property('items'); 1083 | }); 1084 | 1085 | describe('should work', function() { 1086 | it('gets slab stats', function (done) { 1087 | cache.set('test', 'test').then(function() { 1088 | return cache.items(); 1089 | }).then(function (items) { 1090 | expect(items.length).to.be.above(0); 1091 | expect(items[0].slab_id).to.exist; 1092 | expect(items[0].server).to.exist; 1093 | expect(items[0].data.number).to.exist; 1094 | expect(items[0].data.age).to.exist; 1095 | done(); 1096 | }); 1097 | }); 1098 | }); 1099 | }); 1100 | 1101 | describe('cachedump', function () { 1102 | var cache; 1103 | beforeEach(function() { 1104 | cache = new Client(); 1105 | }); 1106 | 1107 | it('exists', function() { 1108 | cache.should.have.property('items'); 1109 | }); 1110 | 1111 | // Comment this test out because `stats cachedump` has been threatened to be removed 1112 | // for years and does not appear to be present on the memcached in Github Actions 1113 | // See https://groups.google.com/g/memcached/c/1-T8I-RVGKM?pli=1 1114 | 1115 | // describe('should work', function() { 1116 | // it('gets cache metadata', function (done) { 1117 | // var key = getKey(); 1118 | 1119 | // // guarantee that we will at least have one result 1120 | // cache.set(key, 'test').then(function() { 1121 | // return cache.items(); 1122 | // }).then(function (items) { 1123 | // return cache.cachedump(items[0].slab_id); 1124 | // }).then(function (data) { 1125 | // expect(data[0].key).to.be.defined; 1126 | // done(); 1127 | // }); 1128 | // }); 1129 | 1130 | // it('gets cache metadata with limit', function (done) { 1131 | // var key = getKey(); 1132 | 1133 | // cache.set(key, 'test').then(function() { 1134 | // return cache.items(); 1135 | // }).then(function (items) { 1136 | // return cache.cachedump(items[0].slab_id, 1); 1137 | // }).then(function (data) { 1138 | // expect(data.length).to.equal(1); 1139 | // done(); 1140 | // }); 1141 | // }); 1142 | // }); 1143 | }); 1144 | 1145 | describe('version', function () { 1146 | var cache; 1147 | beforeEach(function() { 1148 | cache = new Client(); 1149 | }); 1150 | 1151 | it('exists', function() { 1152 | cache.should.have.property('version'); 1153 | }); 1154 | 1155 | describe('should work', function() { 1156 | it('gets version', function () { 1157 | return cache.version().then(function(v) { 1158 | expect(v).to.be.a.string; 1159 | }); 1160 | }); 1161 | }); 1162 | }); 1163 | 1164 | after(function() { 1165 | var cache = new Client(); 1166 | 1167 | // Clean up all of the keys we created 1168 | return cache.deleteMulti(keys); 1169 | }); 1170 | }); 1171 | --------------------------------------------------------------------------------