memcache-plus
Advanced tools
Comparing version 0.2.12 to 0.2.13
@@ -19,8 +19,7 @@ # Get | ||
The key must be a [string](misc.md). The value you get back from the resolution | ||
of this promise will also be a string, it is up to you to convert it into any | ||
specific type you need. | ||
The key must be a string. The value you get back from the resolution | ||
of this promise will have the same type it had when you `set` it. | ||
For example, if you had previously `set()` with an object, you'll need to | ||
deserialize it to get your object back. | ||
For example, if you had previously `set()` with an object, you'll get back an | ||
object. | ||
@@ -30,4 +29,3 @@ ```javascript | ||
.get('user') | ||
.then(function(u) { | ||
var user = JSON.parse(u); | ||
.then(function(user) { | ||
console.log('Successfully got the object: ', user); | ||
@@ -46,3 +44,3 @@ // Would print: "Successfully got the object: { firstName: 'Victor', lastName: 'Quinn' }" | ||
client.get('firstName', function(firstName) { | ||
console.log('Successfully gott the value for key firstName: ', firstName); | ||
console.log('Successfully got the value for key firstName: ', firstName); | ||
}); | ||
@@ -69,3 +67,3 @@ ``` | ||
If an item was written with `set()` with compression enabled, you must specify | ||
If an item was written with `set()` with compression enabled, you can specify | ||
that fact when retrieving the object or it will not be decompressed by Memcache | ||
@@ -81,2 +79,6 @@ Plus: | ||
``` | ||
However, compressed objects set by newer versions of Memcache Plus will | ||
automatically be decompressed without having to provide this flag. | ||
By enabling this option, every value will be compressed with Node's | ||
@@ -87,8 +89,4 @@ [zlib](https://nodejs.org/api/zlib.html) library after being retrieved. | ||
1. If you store a value compressed with `set()` you have to `get()` it and | ||
specify that it was compressed. There is no automatic inspection of values to | ||
determine whether they were set with compression and decompress automatically | ||
(as this would incur significant performance penalty) | ||
1. Enabling compression will reduce the size of the objects stored but it will | ||
also add a non-negligent performance hit to each `set()` and `get()` so use it | ||
judiciously! | ||
also add a non-negligent performance hit to each `set()` and `get()` since | ||
compression is rather CPU intensive so use it judiciously! |
@@ -20,4 +20,3 @@ # Getting Started | ||
Instantiating the client will automatically establish a connection between your | ||
running application and your Memcache server. Make sure you do not have a | ||
firewall rule blocking port 11211 on your Memcache server. | ||
running application and your Memcache server. | ||
@@ -33,69 +32,1 @@ Then, right away you can start using its methods: | ||
``` | ||
## Command Queueing | ||
Memcache Plus will automatically queue and then execute (in order) any commands | ||
you make before a connection can be established. This means that you can | ||
instantiate the client and immediately start issuing commands and they will | ||
automatically execute as soon as a connection is established to your Memcache | ||
server(s). | ||
This makes it a lot easier to just get going with Memcache Plus than with many | ||
other Memcache clients for Node since they either require you to write code to | ||
ensure a connection is established before executing commands or they issue | ||
failures when commands fail due to lack of connection. | ||
Memcache Plus maintains an internal command queue which it will use until a | ||
connection is established. This same command queue is utilized if there is a | ||
momentary drop in the connection, so your code doesn't have to worry about a | ||
momentary blip like this. | ||
## Options | ||
When instantiating Memcache Plus, you can optionally provide the client with an | ||
object containing any of the following options (default values in parentheses): | ||
| Key | Default Value | Description | | ||
|---|---|--- | | ||
|`autodiscover` | `false` | Whether or not to use [Elasticache Auto Discovery](http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/AutoDiscovery.html) | | ||
|`backoffLimit`|10000| Memcache Plus uses an exponential backoff. This is the maximum limit in milliseconds it will wait before declaring a connection dead| | ||
|`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.| | ||
|`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 | | ||
|`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` | | ||
|`maxValueSize`|1048576| The max value that can be stored, in bytes. This is configurable in Memcache but this library will help prevent you from storing objects over the configured siize in Memcache | | ||
|`onNetError`| `function onNetError(err) { console.error(err); }`| Function to call in the event of a network error. | | ||
|`queue`| `true` | Whether or not to queue commands issued before a connection is established or if the connection is dropped momentarily. | | ||
|`netTimeout`|500| Number of milliseconds to wait before assuming there is a network timeout. | | ||
|`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 | | ||
Example: | ||
```javascript | ||
var MemcachePlus = require('memcache-plus'); | ||
var client = new MemcachePlus({ | ||
// Specify 2 hosts | ||
hosts: ['10.0.0.1', '10.0.0.2'], | ||
// Decrease the netTimeout from the 500ms default to 200ms | ||
netTimeout: 200 | ||
}); | ||
``` | ||
## Connecting to multiple hosts | ||
Memcache Plus can automatically connect to multiple hosts. | ||
In doing so, it will use a hash ring to handle even distribution of keys among | ||
multiple servers. It is easy, simply specify multiple hostswhen connecting and | ||
Memcache Plus will automatically handle the rest! | ||
```javascript | ||
var MemcachePlus = require('memcache-plus'); | ||
var client = new MemcachePlus({ | ||
// Specify 3 hosts | ||
hosts: ['10.0.0.1', '10.0.0.2', '10.0.0.3'] | ||
}); | ||
``` | ||
If you've using Amazon's Elasticache for your Memcache hosting, you can also | ||
enable [Auto Discovery](elasticache.md) and Memcache Plus will automatically | ||
connect to your discovery url, find all of the hosts in your cluster, and | ||
establish connections to all of them. |
# Miscellaneous | ||
### Why are only string keys and values allowed? | ||
#### Background | ||
This is a debate we had when writing this module as we wanted to be able to set | ||
anything (number, null, boolean, string, object, array) and get it back as the | ||
same thing. | ||
*tl;dr We made the decision to only allow string values as it simplifies things | ||
and maintains maximum interoperability with other libraries/systems.* | ||
The problem is that, as values are written to Memcache, they are written as just | ||
byte buffers so they lose any of their "type" in the process. So if I write a | ||
number like `34.29` to Memcache, it will come out as a Buffer containing `34.29` | ||
but does not have a type. So the library can't distinguish whether what | ||
originally was set was the number `34.29` or the string `"34.29"`. | ||
We made the design decision to enforce the constraint that all things stored are | ||
strings and leave it to the user to deal with the conversions. This way at least | ||
it is consistent. | ||
#### Other libraries | ||
Some other Memcache modules will allow users to `set()` numbers and then return | ||
strings back with `get()`. So you could do (in pseudocode since obviously the | ||
following wouldn't work without promises or callbacks): | ||
```javascript | ||
// Set a number | ||
client.set('myNumber', 12.54); | ||
// Get back a string | ||
var myNumber = client.get('myNumber') | ||
console.log(typeof myNumber) | ||
// Would print "string", wtf? | ||
``` | ||
As you can see this is rather confusing! We think it makes more sense to just | ||
enforce the constraint that all values must be strings, then there is never | ||
confusion. | ||
#### A possible solution | ||
We have considered taking each value and storing it as a more complex object so | ||
it could be deserialized later. | ||
For instance, instead of writing just the raw value to Memcache, we'd do | ||
something like the following: | ||
```javascript | ||
// Trying to set a number | ||
client.set('myNumber', 12.54) | ||
// Would actually do the following under the hood: | ||
client.set('myNumber', JSON.stringify({ type: 'number', value: 12.54 }); | ||
// This way, when it's extracted from Memcache, we can reassemble it with the | ||
// correct type | ||
var myNumber = client.get('myNumber'); | ||
console.log(typeof myNumber); | ||
// Would print "number" because this library would get back the stringified | ||
// object which was set in Memcache and be able to re-constitute the value to | ||
// the correct type. | ||
// This would also enable things like the following to set and get appropriately: | ||
client.set('myUser', { firstName: 'Victor', lastName: 'Quinn' }); | ||
client.set('myUser', ['Harley', 'Kawasaki', 'Triumph', 'BMW']); | ||
client.set('myUser', 5); | ||
client.set('myUser', true); | ||
client.set('myUser', null); | ||
``` | ||
However, it could break interoperability with other systems as they would expect | ||
to retrieve just a value and instead get back a complex object. | ||
So we'd like to get this enabled for this module at some point in the future as | ||
an option (or more likely as a companion module dependent on Memcache Plus) but | ||
for now we only allow string values. |
* [Memcache Plus](../README.md) | ||
* [Getting Started](intro.md) | ||
* [Initialization](initialization.md) | ||
* [Set](set.md) | ||
@@ -7,4 +8,7 @@ * [Get](get.md) | ||
* [Delete](delete.md) | ||
* [Incr/Decr](incrdecr.md) | ||
* [Add](add.md) | ||
* [Replace](replace.md) | ||
* [Flush](flush.md) | ||
* [Disconnect](disconnect.md) | ||
* [Elasticache](elasticache.md) | ||
* [Miscellaneous](misc.md) |
@@ -25,8 +25,29 @@ # Set | ||
### Key and Value must be strings | ||
### Key must be a string | ||
Both the key and the value must be [strings](misc.md). So if you would like to set an | ||
Object as a value, you must stringify it first: | ||
Non-string keys are not allowed and Memcache Plus will throw an error if you | ||
try to provide a non-string key. | ||
```javascript | ||
client | ||
.set({ foo: 'bar' }, myVal) | ||
.then(function() { | ||
// This will never happen because an error will be thrown | ||
}) | ||
.catch(function(err) { | ||
// This will get hit! | ||
console.error('Oops we have an error', err); | ||
}); | ||
``` | ||
### Value can be of any type | ||
The value can be of any type (numeric, string, object, array, null, etc.) | ||
Memcache Plus will handle converting the value (if necessary) before sending to | ||
the Memcached server and converting it back upon retrieval. | ||
For instance, with Memcache Plus you can go ahead and set an object | ||
```javascript | ||
var myVal = { | ||
@@ -38,10 +59,23 @@ firstName: 'Victor', | ||
client | ||
.set('user', JSON.stringify(myVal)) | ||
.set('user', myVal) | ||
.then(function() { | ||
console.log('Successfully set the stringified object'); | ||
console.log('Successfully set the object'); | ||
}); | ||
``` | ||
There is more discussion of the rationale behind this [here](misc.md). | ||
Then when you get it out it'll be an object: | ||
```javascript | ||
client | ||
.get('user') | ||
.then(function(user) { | ||
// The user is a JS object: | ||
// { firstName: 'Victor', lastName: 'Quinn' } | ||
console.log('Successfully got the object', user); | ||
}); | ||
``` | ||
Same goes for numbers, arrays, etc. Memcache Plus will always return the exact | ||
type you put into it. | ||
### TTL | ||
@@ -48,0 +82,0 @@ |
@@ -11,4 +11,4 @@ /** | ||
misc = require('./misc'), | ||
Immutable = require('immutable'), | ||
Promise = require('bluebird'), | ||
Queue = require('collections/deque'), | ||
R = require('ramda'); | ||
@@ -57,2 +57,6 @@ | ||
if (this.queue) { | ||
this.buffer = new Immutable.List(); | ||
} | ||
debug('Connect options', opts); | ||
@@ -206,7 +210,8 @@ this.connect(); | ||
Client.prototype.flushBuffer = function() { | ||
if (this.buffer && this.buffer.length > 0) { | ||
if (this.buffer && this.buffer.size > 0) { | ||
debug('flushing client write buffer'); | ||
// @todo Watch out for and handle how this behaves with a very long buffer | ||
while(this.buffer.length > 0) { | ||
var item = this.buffer.shift(); | ||
while(this.buffer.size > 0) { | ||
var item = this.buffer.first(); | ||
this.buffer = this.buffer.shift(); | ||
@@ -391,2 +396,57 @@ // First, retrieve the correct connection out of the hashring | ||
/** | ||
* flush() - Removes all stored values | ||
* @param {Number|Function} [delay = 0] - Delay invalidation by specified seconds | ||
* @param {Function} [cb] - The (optional) callback called on completion | ||
* @returns {Promise} | ||
*/ | ||
Client.prototype.flush = function (delay, cb) { | ||
if (typeof delay === 'function' || typeof delay === 'undefined') { | ||
cb = delay; | ||
delay = 0; | ||
} | ||
return this.run('flush_all', [delay], cb); | ||
}; | ||
/** | ||
* add() - Add value for the provided key only if it didn't already exist | ||
* | ||
* @param {String} key - The key to set | ||
* @param {*} value - The value to set for this key. Can be of any type | ||
* @param {Number|Object|Function} [ttl = 0] - The time to live for this key or callback | ||
* @param {Function} [cb] - Callback to call when we have a value | ||
* @returns {Promise} | ||
*/ | ||
Client.prototype.add = function(key, val, ttl, cb) { | ||
assert(key, 'Cannot add without key!'); | ||
if (typeof ttl === 'function') { | ||
cb = ttl; | ||
ttl = 0; | ||
} | ||
return this.run('add', [key, val, ttl], cb); | ||
}; | ||
/** | ||
* replace() - Replace value for the provided key only if it already exists | ||
* | ||
* @param {String} key - The key to replace | ||
* @param {*} value - The value to replace for this key. Can be of any type | ||
* @param {Number|Object|Function} [ttl = 0] - The time to live for this key or callback | ||
* @param {Function} [cb] - Callback to call when we have a value | ||
* @returns {Promise} | ||
*/ | ||
Client.prototype.replace = function(key, val, ttl, cb) { | ||
assert(key, 'Cannot replace without key!'); | ||
if (typeof ttl === 'function') { | ||
cb = ttl; | ||
ttl = 0; | ||
} | ||
return this.run('replace', [key, val, ttl], cb); | ||
}; | ||
/** | ||
* run() - Run this command on the appropriate connection. Will buffer command | ||
@@ -410,13 +470,8 @@ * if connection(s) are not ready | ||
return connection[command].apply(connection, args).nodeify(cb); | ||
} else if (this.bufferBeforeError === 0) { | ||
} else if (this.bufferBeforeError === 0 || !this.queue) { | ||
return Promise.reject(new Error('Connection is not ready, either not connected yet or disconnected')).nodeify(cb); | ||
} | ||
// @todo remove this? | ||
if (this.queue) { | ||
} else { | ||
var deferred = misc.defer(args[0]); | ||
this.buffer = this.buffer || new Queue(); | ||
this.buffer.push({ | ||
this.buffer = this.buffer.push({ | ||
cmd: command, | ||
@@ -430,6 +485,4 @@ args: args, | ||
} | ||
return Promise.resolve(null).nodeify(cb); | ||
}; | ||
module.exports = Client; |
@@ -0,1 +1,2 @@ | ||
/** | ||
@@ -12,4 +13,4 @@ * @file Main file for a Memcache Connection | ||
net = require('net'), | ||
Immutable = require('immutable'), | ||
Promise = require('bluebird'), | ||
Queue = require('collections/deque'), | ||
util = require('util'), | ||
@@ -29,2 +30,66 @@ EventEmitter = require('events').EventEmitter; | ||
/** | ||
* Miscellaneous helper functions | ||
*/ | ||
/** | ||
* getFlag() - Given a value and whether it's compressed, return the correct | ||
* flag | ||
* | ||
* @param {*} val - the value to inspect and on which to set the flag | ||
* @param {bool} compressed - whether or not this value is to be compressed | ||
* @returns {Number} the value of the flag | ||
*/ | ||
function getFlag(val, compressed) { | ||
var flag = 0; | ||
if (typeof val === 'number') { | ||
flag = FLAG_NUMERIC; | ||
} else if (Buffer.isBuffer(val)) { | ||
flag = FLAG_BINARY; | ||
} else if (typeof val !== 'string') { | ||
flag = FLAG_JSON; | ||
} | ||
if (compressed === true) { | ||
flag = flag | FLAG_COMPRESSED; | ||
} | ||
return flag; | ||
} | ||
/** | ||
* formatValue() - Given a value and whether it's compressed, return a Promise | ||
* which will resolve to that value formatted correctly, either compressed or | ||
* not and as the correct type of string | ||
* | ||
* @param {*} val - the value to inspect and on which to set the flag | ||
* @param {bool} compressed - whether or not this value is to be compressed | ||
* @returns {Promise} resolves to the value after being converted and, if | ||
* necessary, compressed | ||
*/ | ||
function formatValue(val, compressed) { | ||
var value = val; | ||
if (typeof val === 'number') { | ||
value = val.toString(); | ||
} else if (Buffer.isBuffer(val)) { | ||
value = val.toString('binary'); | ||
} else if (typeof val !== 'string') { | ||
value = JSON.stringify(val); | ||
} | ||
var bVal = new Buffer(value); | ||
var compression = Promise.resolve(bVal); | ||
if (compressed) { | ||
// Compress | ||
debug('compression enabled, compressing'); | ||
compression = misc.compress(bVal); | ||
} | ||
return compression; | ||
} | ||
/** | ||
* Connection constructor | ||
@@ -52,3 +117,3 @@ * | ||
this.queue = new Queue(); | ||
this.buffer = new Immutable.List(); | ||
@@ -72,2 +137,3 @@ if (opts.onConnect) { | ||
this.maxValueSize = opts.maxValueSize; | ||
this.writeBuffer = new Immutable.List(); | ||
@@ -95,6 +161,6 @@ this.connect(); | ||
this.queue.forEach(function(deferred) { | ||
this.buffer.forEach(function(deferred) { | ||
deferred.reject(new Error('Memcache connection lost')); | ||
}); | ||
this.queue.clear(); | ||
this.buffer = this.buffer.clear(); | ||
}; | ||
@@ -164,3 +230,3 @@ | ||
} | ||
self.flushQueue(); | ||
self.flushBuffer(); | ||
}); | ||
@@ -171,23 +237,27 @@ | ||
/** | ||
* read() - Called as soon as we get data back from this connection from the | ||
* server. The response parsing is a bit of a beast. | ||
*/ | ||
Connection.prototype.read = function(data) { | ||
debug('got data: %s', data); | ||
var deferred = this.queue.peek(); | ||
if (deferred === undefined) { | ||
// We should not get here. | ||
debug('Got data we did not expect.'); | ||
return; | ||
} | ||
debug('got data: "%s" and the queue now has "%d" elements', | ||
misc.truncateIfNecessary(data), | ||
this.buffer.size | ||
); | ||
if (data === 'ERROR' && this.queue.toArray().length > 0) { | ||
var deferred = this.buffer.first(); | ||
var done = true; | ||
var err = null; | ||
var resp = data; | ||
if (data.match(/^ERROR$/) && this.buffer.size > 0) { | ||
debug('got an error from memcached'); | ||
// We only want to do this if the last thing was not an error, | ||
// as if it were, we already would have notified about the error | ||
// last time so now we want to ignore it | ||
this.queue.shift(); | ||
deferred.reject(new Error(util.format('Memcache returned an error: %s\r\nFor key %s', data, deferred.key))); | ||
this.data = null; | ||
} else if (data.substr(0, 5) === 'VALUE') { | ||
err = new Error(util.format('Memcache returned an error: %s\r\nFor key %s', data, deferred.key)); | ||
} else if (data.match(/^VALUE .+/)) { | ||
var spl = data.match(/^VALUE (.+) ([0-9]+) ([0-9]+)$/); | ||
// Do nothing, this is just metadata. May want to somehow store this | ||
// and send it back somehow in the future for debugging purposes | ||
debug('Got some metadata'); | ||
debug('Got some metadata', spl); | ||
metadataTemp[spl[1]] = { | ||
@@ -197,53 +267,59 @@ flag: Number(spl[2]), | ||
}; | ||
} else if (data.substr(0, 3) === 'END') { | ||
this.queue.shift(); | ||
done = false; | ||
} else if (data.match(/^END$/)) { | ||
if (metadataTemp[deferred.key]) { | ||
var metadata = metadataTemp[deferred.key]; | ||
deferred.resolve([ this.data, metadata.flag, metadata.len ]); | ||
resp = [ this.data, metadata.flag, metadata.len ]; | ||
// After we've used this metadata, purge it | ||
delete metadataTemp[deferred.key]; | ||
} else { | ||
deferred.resolve([ this.data ]); | ||
resp = [ this.data ]; | ||
} | ||
} else if (data.match(/^SERVER_ERROR|CLIENT_ERROR .+/)) { | ||
err = new Error('Memcache returned an error: %s', data); | ||
} else { | ||
// If this is a special response that we expect, handle it | ||
if (data.match(/^(STORED|NOT_STORED|DELETED|EXISTS|TOUCHED|NOT_FOUND|OK|INCRDECR|ITEM|STAT|VERSION)$/)) { | ||
// Do nothing currently... | ||
debug('misc response, passing along to client'); | ||
} else { | ||
if (data !== '') { | ||
if (deferred.type !== 'incr' && deferred.type !== 'decr') { | ||
done = false; | ||
} | ||
} | ||
} | ||
} | ||
if (done) { | ||
// Pull this guy off the queue | ||
this.buffer = this.buffer.shift(); | ||
// Reset for next loop | ||
this.data = null; | ||
} else if (data === 'STORED' || data === 'DELETED' || | ||
data === 'EXISTS' || data === 'TOUCHED'|| | ||
data.substr(0, 9) === 'NOT_FOUND') { | ||
this.queue.shift(); | ||
deferred.resolve(data); | ||
this.data = null; | ||
} else if (data === 'NOT_STORED') { | ||
this.queue.shift(); | ||
deferred.reject(new Error('Issue writing to memcache, returned: NOT_STORED')); | ||
this.data = null; | ||
} else if (data.substr(0, 12) === 'SERVER_ERROR' || data.substr(0, 12) === 'CLIENT_ERROR') { | ||
this.queue.shift(); | ||
deferred.reject(data.substr(13)); | ||
this.data = null; | ||
} else if (data === 'INCRDECR' || data === 'ITEM' || data === 'STAT' || data === 'VERSION') { | ||
// For future expansion. For now, discarding | ||
this.queue.shift(); | ||
deferred.resolve(data); | ||
this.data = null; | ||
} else if (data !== '') { | ||
this.data = data; | ||
if (deferred.type === 'incr' || deferred.type === 'decr') { | ||
this.queue.shift(); | ||
deferred.resolve(data); | ||
this.data = null; | ||
} | ||
} else { | ||
// If this is a response we do not recognize, what to do with it... | ||
this.queue.shift(); | ||
deferred.reject(data); | ||
this.data = null; | ||
this.data = resp; | ||
} | ||
if (err) { | ||
// If we have an error, reject | ||
deferred.reject(err); | ||
} else { | ||
// If we don't have an error, resolve if done | ||
if (done) { | ||
deferred.resolve(resp); | ||
} | ||
} | ||
debug('responded and the queue now has "%s" elements', this.buffer.size); | ||
}; | ||
Connection.prototype.flushQueue = function() { | ||
if (this.writeBuffer && this.writeBuffer.length > 0) { | ||
/** | ||
* flushBuffer() - Flush the queue for this connection | ||
*/ | ||
Connection.prototype.flushBuffer = function() { | ||
if (this.writeBuffer && this.writeBuffer.size > 0) { | ||
debug('flushing connection write buffer'); | ||
// @todo Watch out for and handle how this behaves with a very long buffer | ||
while(this.writeBuffer.length > 0) { | ||
this.client.write(this.writeBuffer.shift()); | ||
while(this.writeBuffer.size > 0) { | ||
this.client.write(this.writeBuffer.first()); | ||
this.writeBuffer = this.writeBuffer.shift(); | ||
this.client.write('\r\n'); | ||
@@ -254,5 +330,6 @@ } | ||
/** | ||
* write() - Write a command to this connection | ||
*/ | ||
Connection.prototype.write = function(str) { | ||
debug('sending data: %s', str); | ||
this.writeBuffer = this.writeBuffer || new Queue(); | ||
// If for some reason this connection is not yet ready and a request is tried, | ||
@@ -265,6 +342,8 @@ // we don't want to fire it off so we write it to a buffer and then will fire | ||
// end until it's flushed | ||
if (this.ready && this.writeBuffer.length < 1) { | ||
if (this.ready && this.writeBuffer.size < 1) { | ||
debug('sending: "%s"', misc.truncateIfNecessary(str)); | ||
this.client.write(str); | ||
this.client.write('\r\n'); | ||
} else if (this.writeBuffer.length < this.bufferBeforeError) { | ||
} else if (this.writeBuffer.size < this.bufferBeforeError) { | ||
debug('buffering: "%s"', misc.truncateIfNecessary(str)); | ||
this.writeBuffer.push(str); | ||
@@ -274,6 +353,7 @@ // Check if we should flush this queue. Useful in case it gets stuck for | ||
if (this.ready) { | ||
this.flushQueue(); | ||
this.flushBuffer(); | ||
} | ||
} else { | ||
this.queue.shift().reject('Error, Connection to memcache lost and buffer over ' + this.bufferBeforeError + ' items'); | ||
this.buffer.first().reject('Error, Connection to memcache lost and buffer over ' + this.bufferBeforeError + ' items'); | ||
this.buffer = this.buffer.shift(); | ||
} | ||
@@ -285,3 +365,3 @@ }; | ||
var deferred = misc.defer('autodiscovery'); | ||
this.queue.push(deferred); | ||
this.buffer = this.buffer.push(deferred); | ||
@@ -308,18 +388,5 @@ this.write('config get cluster'); | ||
var self = this; | ||
debug('set %s:%s', key, val); | ||
assert(typeof key === 'string', 'Cannot set in memcache with a not string key'); | ||
assert(key.length < 250, 'Key must be less than 250 characters long'); | ||
var flag = 0; | ||
if (typeof val === 'number') { | ||
flag = FLAG_NUMERIC; | ||
val = val.toString(); | ||
} else if (Buffer.isBuffer(val)) { | ||
flag = FLAG_BINARY; | ||
val = val.toString('binary'); | ||
} else if (typeof val !== 'string') { | ||
flag = FLAG_JSON; | ||
val = JSON.stringify(val); | ||
} | ||
ttl = ttl || 0; | ||
@@ -333,18 +400,5 @@ var opts = {}; | ||
if (opts.compressed === true) { | ||
flag = flag | FLAG_COMPRESSED; | ||
} | ||
var flag = getFlag(val, opts.compressed); | ||
var bVal = new Buffer(val); | ||
var compression = Promise.resolve(bVal); | ||
if (opts.compressed) { | ||
// Compress | ||
debug('compression enabled, compressing'); | ||
bVal = new Buffer(val); | ||
compression = misc.compress(bVal); | ||
} | ||
return compression | ||
return formatValue(val, opts.compressed) | ||
.bind(this) | ||
@@ -377,3 +431,3 @@ .then(function(v) { | ||
deferred.key = key; | ||
this.queue.push(deferred); | ||
this.buffer = this.buffer.push(deferred); | ||
@@ -383,4 +437,4 @@ // First send the metadata for this request | ||
// Then the actual value | ||
this.write(v); | ||
// Then the actual value (as a string) | ||
this.write(util.format('%s', v)); | ||
return deferred.promise | ||
@@ -413,7 +467,5 @@ .then(function(data) { | ||
/** | ||
* incr() - Set a value on this connection | ||
* incr() - Increment a value on this connection | ||
*/ | ||
Connection.prototype.incr = function(key, amount) { | ||
debug('incr %s by %d', key, amount); | ||
assert(typeof key === 'string', 'Cannot set in memcache with a not string key'); | ||
@@ -425,3 +477,3 @@ assert(key.length < 250, 'Key must be less than 250 characters long'); | ||
deferred.type = 'incr'; | ||
this.queue.push(deferred); | ||
this.buffer = this.buffer.push(deferred); | ||
@@ -442,7 +494,5 @@ this.write(util.format('incr %s %d', key, amount)); | ||
/** | ||
* decr() - Set a value on this connection | ||
* decr() - Decrement a value on this connection | ||
*/ | ||
Connection.prototype.decr = function(key, amount) { | ||
debug('decr %s by %d', key, amount); | ||
assert(typeof key === 'string', 'Cannot set in memcache with a not string key'); | ||
@@ -454,3 +504,3 @@ assert(key.length < 250, 'Key must be less than 250 characters long'); | ||
deferred.type = 'decr'; | ||
this.queue.push(deferred); | ||
this.buffer = this.buffer.push(deferred); | ||
@@ -478,7 +528,6 @@ this.write(util.format('decr %s %d', key, amount)); | ||
Connection.prototype.get = function(key, opts) { | ||
debug('get %s', key); | ||
opts = opts || {}; | ||
// Do the get | ||
var deferred = misc.defer(key); | ||
this.queue.push(deferred); | ||
this.buffer = this.buffer.push(deferred); | ||
@@ -523,2 +572,121 @@ this.write('get ' + key); | ||
/** | ||
* flush() - delete all values on this connection | ||
* @param delay | ||
* @returns {Promise} | ||
*/ | ||
Connection.prototype.flush_all = function(delay) { | ||
var deferred = misc.defer(delay); | ||
this.buffer = this.buffer.push(deferred); | ||
this.write(util.format('flush_all %s', delay)); | ||
return deferred.promise | ||
.then(function(v) { | ||
return v === 'OK'; | ||
}); | ||
}; | ||
/** | ||
* add() - Add a value on this connection | ||
*/ | ||
Connection.prototype.add = function(key, val, ttl) { | ||
var self = this; | ||
assert(typeof key === 'string', 'Cannot add in memcache with a not string key'); | ||
assert(key.length < 250, 'Key must be less than 250 characters long'); | ||
ttl = ttl || 0; | ||
var opts = {}; | ||
if (_.isObject(ttl)) { | ||
opts = ttl; | ||
ttl = opts.ttl || 0; | ||
} | ||
var flag = getFlag(val, opts.compressed); | ||
return formatValue(val, opts.compressed) | ||
.bind(this) | ||
.then(function(v) { | ||
if (opts.compressed) { | ||
v = new Buffer(v.toString('base64')); | ||
} | ||
if (v.length > self.maxValueSize) { | ||
throw new Error(util.format('Value too large to set in memcache: %s > %s', v.length, self.maxValueSize)); | ||
} | ||
var deferred = misc.defer(key); | ||
deferred.key = key; | ||
this.buffer = this.buffer.push(deferred); | ||
// First send the metadata for this request | ||
this.write(util.format('add %s %d %d %d', key, flag, ttl, v.length)); | ||
// Then the actual value | ||
this.write(util.format('%s', v)); | ||
return deferred.promise | ||
.then(function(data) { | ||
// data will be a buffer | ||
if (data === 'NOT_STORED') { | ||
throw new Error(util.format('Cannot "add" for key "%s" because it already exists', key)); | ||
} else if (data !== 'STORED') { | ||
throw new Error(util.format('Something went wrong with the add. Expected STORED, got :%s:', data.toString())); | ||
} else { | ||
return Promise.resolve(); | ||
} | ||
}); | ||
}); | ||
}; | ||
/** | ||
* replace() - Replace a value on this connection | ||
*/ | ||
Connection.prototype.replace = function(key, val, ttl) { | ||
var self = this; | ||
assert(typeof key === 'string', 'Cannot replace in memcache with a not string key'); | ||
assert(key.length < 250, 'Key must be less than 250 characters long'); | ||
ttl = ttl || 0; | ||
var opts = {}; | ||
if (_.isObject(ttl)) { | ||
opts = ttl; | ||
ttl = opts.ttl || 0; | ||
} | ||
var flag = getFlag(val, opts.compressed); | ||
return formatValue(val, opts.compressed) | ||
.bind(this) | ||
.then(function(v) { | ||
if (opts.compressed) { | ||
v = new Buffer(v.toString('base64')); | ||
} | ||
if (v.length > self.maxValueSize) { | ||
throw new Error(util.format('Value too large to replace in memcache: %s > %s', v.length, self.maxValueSize)); | ||
} | ||
var deferred = misc.defer(key); | ||
deferred.key = key; | ||
this.buffer = this.buffer.push(deferred); | ||
// First send the metadata for this request | ||
this.write(util.format('replace %s %d %d %d', key, flag, ttl, v.length)); | ||
// Then the actual value | ||
this.write(util.format('%s', v)); | ||
return deferred.promise | ||
.then(function(data) { | ||
// data will be a buffer | ||
if (data === 'NOT_STORED') { | ||
throw new Error(util.format('Cannot "replace" for key "%s" because it does not exist', key)); | ||
} else if (data !== 'STORED') { | ||
throw new Error(util.format('Something went wrong with the replace. Expected STORED, got :%s:', data.toString())); | ||
} else { | ||
return Promise.resolve(); | ||
} | ||
}); | ||
}); | ||
}; | ||
/** | ||
* delete() - Delete value for this key on this connection | ||
@@ -530,6 +698,5 @@ * | ||
Connection.prototype.delete = function(key) { | ||
debug('delete %s', key); | ||
// Do the delete | ||
var deferred = misc.defer(key); | ||
this.queue.push(deferred); | ||
this.buffer = this.buffer.push(deferred); | ||
@@ -536,0 +703,0 @@ this.write(util.format('delete %s', key)); |
@@ -48,1 +48,11 @@ /** | ||
}; | ||
/** | ||
* truncateIfNecessary() - Truncate string if too long, for display purposes | ||
* only | ||
*/ | ||
exports.truncateIfNecessary = function(str, len) { | ||
assert.typeOf(str, 'string'); | ||
len = len || 100; | ||
return str && str.length > len ? str.substr(0, len) + '...' : str; | ||
}; |
{ | ||
"name": "memcache-plus", | ||
"version": "0.2.12", | ||
"version": "0.2.13", | ||
"description": "Better memcache for node", | ||
@@ -44,5 +44,5 @@ "main": "index.js", | ||
"chai": "^3.5.0", | ||
"collections": "^5.0.4", | ||
"debug": "^2.2.0", | ||
"hashring": "^3.2.0", | ||
"immutable": "^3.8.1", | ||
"lodash": "^4.14.0", | ||
@@ -49,0 +49,0 @@ "ramda": "^0.21.0" |
@@ -429,3 +429,3 @@ require('chai').should(); | ||
return cache.set(key, val1) | ||
@@ -644,2 +644,14 @@ .then(function() { | ||
// @todo these are placeholders for now until I can figure out a good way | ||
// to adequeately test these. | ||
describe('Client buffer', function() { | ||
it('works'); | ||
it('can be flushed'); | ||
}); | ||
describe('Connection buffer', function() { | ||
it('works'); | ||
it('can be flushed'); | ||
}); | ||
describe('Helpers', function() { | ||
@@ -815,2 +827,166 @@ describe('splitHost()', function() { | ||
describe('flush', function() { | ||
var cache; | ||
before(function() { | ||
cache = new Client(); | ||
}); | ||
it('exists', function() { | ||
cache.should.have.property('flush'); | ||
}); | ||
describe('should work', function() { | ||
it('removes all data', function () { | ||
var key = getKey(), val = chance.natural(); | ||
return cache.set(key, val) | ||
.then(function() { | ||
return cache.get(key); | ||
}) | ||
.then(function(v) { | ||
expect(v).to.equal(val); | ||
return cache.flush(); | ||
}) | ||
.then(function () { | ||
return cache.get(key); | ||
}) | ||
.then(function (v) { | ||
expect(v).to.equal(null); | ||
}); | ||
}); | ||
it('removes all data after a specified seconds', function () { | ||
var key = getKey(), val = chance.natural(); | ||
return cache.set(key, val) | ||
.then(function() { | ||
return cache.get(key); | ||
}) | ||
.then(function(v) { | ||
expect(v).to.equal(val); | ||
return cache.flush(1); | ||
}) | ||
.then(function () { | ||
return cache.get(key); | ||
}) | ||
.then(function (v) { | ||
expect(v).to.equal(v); | ||
}) | ||
.delay(1001) | ||
.then(function() { | ||
return cache.get(key); | ||
}) | ||
.then(function (v) { | ||
expect(v).to.equal(null); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('add', function() { | ||
var cache; | ||
before(function() { | ||
cache = new Client(); | ||
}); | ||
it('exists', function() { | ||
cache.should.have.property('add'); | ||
}); | ||
describe('should throw an error if called', function() { | ||
it('without a key', function() { | ||
expect(function() { cache.add(); }).to.throw('Cannot add without key!'); | ||
}); | ||
it('with a key that is too long', function() { | ||
expect(function() { cache.add(chance.string({length: 251})); }).to.throw('less than 250 characters'); | ||
}); | ||
it('with a non-string key', function() { | ||
expect(function() { cache.add({blah: 'test'}); }).to.throw('not string key'); | ||
expect(function() { cache.add([1, 2]); }).to.throw('not string key'); | ||
expect(function() { cache.add(_.noop); }).to.throw('not string key'); | ||
}); | ||
}); | ||
describe('should work', function() { | ||
it('with a brand new key', function() { | ||
var key = getKey(), val = chance.natural(); | ||
return cache.add(key, val) | ||
.then(function() { | ||
return cache.get(key); | ||
}) | ||
.then(function(v) { | ||
v.should.equal(val); | ||
}); | ||
}); | ||
it('should behave properly when add over existing key', function() { | ||
var key = getKey(), val = chance.natural(); | ||
return cache.add(key, val) | ||
.then(function() { | ||
return cache.add(key, val); | ||
}) | ||
.catch(function(err) { | ||
expect(err.toString()).to.contain('it already exists'); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('replace', function() { | ||
var cache; | ||
before(function() { | ||
cache = new Client(); | ||
}); | ||
it('exists', function() { | ||
cache.should.have.property('replace'); | ||
}); | ||
describe('should throw an error if called', function() { | ||
it('without a key', function() { | ||
expect(function() { cache.replace(); }).to.throw('Cannot replace without key!'); | ||
}); | ||
it('with a key that is too long', function() { | ||
expect(function() { cache.replace(chance.string({length: 251})); }).to.throw('less than 250 characters'); | ||
}); | ||
it('with a non-string key', function() { | ||
expect(function() { cache.replace({blah: 'test'}); }).to.throw('not string key'); | ||
expect(function() { cache.replace([1, 2]); }).to.throw('not string key'); | ||
expect(function() { cache.replace(_.noop); }).to.throw('not string key'); | ||
}); | ||
}); | ||
describe('should work', function() { | ||
it('as normal', function() { | ||
var key = getKey(), val = chance.natural(), val2 = chance.natural(); | ||
return cache.set(key, val) | ||
.then(function() { | ||
return cache.replace(key, val2); | ||
}) | ||
.then(function() { | ||
return cache.get(key); | ||
}) | ||
.then(function(v) { | ||
v.should.equal(val2); | ||
}); | ||
}); | ||
it('should behave properly when replace over non-existent key', function() { | ||
var key = getKey(), val = chance.natural(); | ||
return cache.replace(key, val) | ||
.catch(function(err) { | ||
expect(err.toString()).to.contain('does not exist'); | ||
}); | ||
}); | ||
}); | ||
}); | ||
after(function() { | ||
@@ -817,0 +993,0 @@ var cache = new Client(); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
273305
31
2017
+ Addedimmutable@^3.8.1
+ Addedimmutable@3.8.2(transitive)
- Removedcollections@^5.0.4
- Removedcollections@5.1.13(transitive)
- Removedweak-map@1.0.8(transitive)