diff --git a/README.md b/README.md index d2b82a0ee2..e9c92bdac5 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ This will display: mjr:~/work/node_redis (master)$ Note that the API is entirely asynchronous. To get data back from the server, you'll need to use a callback. +From v.2.6 on the API supports camelCase and snack_case and all options / variables / events etc. can be used either way. +It is recommended to use camelCase as this is the default for the Node.js landscape. ### Promises @@ -109,8 +111,6 @@ client.get("missingkey", function(err, reply) { For a list of Redis commands, see [Redis Command Reference](http://redis.io/commands) -The commands can be specified in uppercase or lowercase for convenience. `client.get()` is the same as `client.GET()`. - Minimal parsing is done on the replies. Commands that return a integer return JavaScript Numbers, arrays return JavaScript Array. `HGETALL` returns an Object keyed by the hash keys. All strings will either be returned as string or as buffer depending on your setting. Please be aware that sending null, undefined and Boolean values will result in the value coerced to a string! diff --git a/index.js b/index.js index 9cdc46dc37..78e07e6579 100644 --- a/index.js +++ b/index.js @@ -479,13 +479,19 @@ RedisClient.prototype.send_offline_queue = function () { var retry_connection = function (self, error) { debug('Retrying connection...'); - self.emit('reconnecting', { + var reconnect_params = { delay: self.retry_delay, attempt: self.attempts, - error: error, - times_connected: self.times_connected, - total_retry_time: self.retry_totaltime - }); + error: error + }; + if (self.options.camel_case) { + reconnect_params.totalRetryTime = self.retry_totaltime; + reconnect_params.timesConnected = self.times_connected; + } else { + reconnect_params.total_retry_time = self.retry_totaltime; + reconnect_params.times_connected = self.times_connected; + } + self.emit('reconnecting', reconnect_params); self.retry_totaltime += self.retry_delay; self.attempts += 1; @@ -529,12 +535,18 @@ RedisClient.prototype.connection_gone = function (why, error) { } if (typeof this.options.retry_strategy === 'function') { - this.retry_delay = this.options.retry_strategy({ + var retry_params = { attempt: this.attempts, - error: error, - total_retry_time: this.retry_totaltime, - times_connected: this.times_connected - }); + error: error + }; + if (this.options.camel_case) { + retry_params.totalRetryTime = this.retry_totaltime; + retry_params.timesConnected = this.times_connected; + } else { + retry_params.total_retry_time = this.retry_totaltime; + retry_params.times_connected = this.times_connected; + } + this.retry_delay = this.options.retry_strategy(retry_params); if (typeof this.retry_delay !== 'number') { // Pass individual error through if (this.retry_delay instanceof Error) { @@ -902,6 +914,72 @@ RedisClient.prototype.write = function (data) { return; }; +Object.defineProperty(exports, 'debugMode', { + get: function () { + return this.debug_mode; + }, + set: function (val) { + this.debug_mode = val; + } +}); + +// Don't officially expose the command_queue directly but only the length as read only variable +Object.defineProperty(RedisClient.prototype, 'command_queue_length', { + get: function () { + return this.command_queue.length; + } +}); + +Object.defineProperty(RedisClient.prototype, 'offline_queue_length', { + get: function () { + return this.offline_queue.length; + } +}); + +// Add support for camelCase by adding read only properties to the client +// All known exposed snack_case variables are added here +Object.defineProperty(RedisClient.prototype, 'retryDelay', { + get: function () { + return this.retry_delay; + } +}); + +Object.defineProperty(RedisClient.prototype, 'retryBackoff', { + get: function () { + return this.retry_backoff; + } +}); + +Object.defineProperty(RedisClient.prototype, 'commandQueueLength', { + get: function () { + return this.command_queue.length; + } +}); + +Object.defineProperty(RedisClient.prototype, 'offlineQueueLength', { + get: function () { + return this.offline_queue.length; + } +}); + +Object.defineProperty(RedisClient.prototype, 'shouldBuffer', { + get: function () { + return this.should_buffer; + } +}); + +Object.defineProperty(RedisClient.prototype, 'connectionId', { + get: function () { + return this.connection_id; + } +}); + +Object.defineProperty(RedisClient.prototype, 'serverInfo', { + get: function () { + return this.server_info; + } +}); + exports.createClient = function () { return new RedisClient(unifyOptions.apply(null, arguments)); }; diff --git a/lib/extendedApi.js b/lib/extendedApi.js index e9182028e7..0d70bf1418 100644 --- a/lib/extendedApi.js +++ b/lib/extendedApi.js @@ -10,7 +10,7 @@ All documented and exposed API belongs in here **********************************************/ // Redirect calls to the appropriate function and use to send arbitrary / not supported commands -RedisClient.prototype.send_command = function (command, args, callback) { +RedisClient.prototype.send_command = RedisClient.prototype.sendCommand = function (command, args, callback) { // Throw to fail early instead of relying in order in this case if (typeof command !== 'string') { throw new Error('Wrong input type "' + (command !== null && command !== undefined ? command.constructor.name : command) + '" for command name'); diff --git a/lib/multi.js b/lib/multi.js index ca7afda481..eaa6dd618e 100644 --- a/lib/multi.js +++ b/lib/multi.js @@ -70,7 +70,7 @@ function pipeline_transaction_command (self, command, args, index, cb) { }); } -Multi.prototype.exec_atomic = function exec_atomic (callback) { +Multi.prototype.exec_atomic = Multi.prototype.EXEC_ATOMIC = Multi.prototype.execAtomic = function exec_atomic (callback) { if (this.queue.length < 2) { return this.exec_batch(callback); } diff --git a/lib/utils.js b/lib/utils.js index 4093b61dcb..b46b08b199 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -41,8 +41,10 @@ function print (err, reply) { } } +var camelCase; // Deep clone arbitrary objects with arrays. Can't handle cyclic structures (results in a range error) // Any attribute with a non primitive value besides object and array will be passed by reference (e.g. Buffers, Maps, Functions) +// All capital letters are going to be replaced with a lower case letter and a underscore infront of it function clone (obj) { var copy; if (Array.isArray(obj)) { @@ -57,7 +59,14 @@ function clone (obj) { var elems = Object.keys(obj); var elem; while (elem = elems.pop()) { - copy[elem] = clone(obj[elem]); + // Accept camelCase options and convert them to snack_case + var snack_case = elem.replace(/[A-Z][^A-Z]/g, '_$&').toLowerCase(); + // If camelCase is detected, pass it to the client, so all variables are going to be camelCased + // There are no deep nested options objects yet, but let's handle this future proof + if (snack_case !== elem.toLowerCase()) { + camelCase = true; + } + copy[snack_case] = clone(obj[elem]); } return copy; } @@ -65,7 +74,12 @@ function clone (obj) { } function convenienceClone (obj) { - return clone(obj) || {}; + camelCase = false; + obj = clone(obj) || {}; + if (camelCase) { + obj.camel_case = true; + } + return obj; } function callbackOrEmit (self, callback, err, res) { diff --git a/test/auth.spec.js b/test/auth.spec.js index 47fa0964a2..b9bb3c033c 100644 --- a/test/auth.spec.js +++ b/test/auth.spec.js @@ -166,8 +166,18 @@ describe('client authentication', function () { client = redis.createClient.apply(null, args); client.auth(auth); client.on('ready', function () { - if (this.times_connected === 1) { - client.stream.destroy(); + if (this.times_connected < 3) { + var interval = setInterval(function () { + if (client.commandQueueLength !== 0) { + return; + } + clearInterval(interval); + interval = null; + client.stream.destroy(); + client.set('foo', 'bar'); + client.get('foo'); // Errors would bubble + assert.strictEqual(client.offlineQueueLength, 2); + }, 1); } else { done(); } diff --git a/test/commands/info.spec.js b/test/commands/info.spec.js index 838dac21b8..3a67a1a178 100644 --- a/test/commands/info.spec.js +++ b/test/commands/info.spec.js @@ -23,16 +23,16 @@ describe("The 'info' method", function () { client.end(true); }); - it('update server_info after a info command', function (done) { + it('update serverInfo after a info command', function (done) { client.set('foo', 'bar'); client.info(); client.select(2, function () { - assert.strictEqual(client.server_info.db2, undefined); + assert.strictEqual(client.serverInfo.db2, undefined); }); client.set('foo', 'bar'); client.info(); setTimeout(function () { - assert.strictEqual(typeof client.server_info.db2, 'object'); + assert.strictEqual(typeof client.serverInfo.db2, 'object'); done(); }, 30); }); diff --git a/test/connection.spec.js b/test/connection.spec.js index 1279ec4e37..41de60b3b4 100644 --- a/test/connection.spec.js +++ b/test/connection.spec.js @@ -132,12 +132,14 @@ describe('connection tests', function () { describe('on lost connection', function () { it('emit an error after max retry attempts and do not try to reconnect afterwards', function (done) { - var max_attempts = 3; + var maxAttempts = 3; var options = { parser: parser, - max_attempts: max_attempts + maxAttempts: maxAttempts }; client = redis.createClient(options); + assert.strictEqual(client.retryBackoff, 1.7); + assert.strictEqual(client.retryDelay, 200); assert.strictEqual(Object.keys(options).length, 2); var calls = 0; @@ -152,7 +154,7 @@ describe('connection tests', function () { client.on('error', function (err) { if (/Redis connection in broken state: maximum connection attempts.*?exceeded./.test(err.message)) { process.nextTick(function () { // End is called after the error got emitted - assert.strictEqual(calls, max_attempts - 1); + assert.strictEqual(calls, maxAttempts - 1); assert.strictEqual(client.emitted_end, true); assert.strictEqual(client.connected, false); assert.strictEqual(client.ready, false); @@ -248,7 +250,7 @@ describe('connection tests', function () { }); }); - it('retry_strategy used to reconnect with individual error', function (done) { + it('retryStrategy used to reconnect with individual error', function (done) { var text = ''; var unhookIntercept = intercept(function (data) { text += data; @@ -256,8 +258,8 @@ describe('connection tests', function () { }); var end = helper.callFuncAfter(done, 2); client = redis.createClient({ - retry_strategy: function (options) { - if (options.total_retry_time > 150) { + retryStrategy: function (options) { + if (options.totalRetryTime > 150) { client.set('foo', 'bar', function (err, res) { assert.strictEqual(err.message, 'Connection timeout'); end(); @@ -267,8 +269,8 @@ describe('connection tests', function () { } return Math.min(options.attempt * 25, 200); }, - max_attempts: 5, - retry_max_delay: 123, + maxAttempts: 5, + retryMaxDelay: 123, port: 9999 }); diff --git a/test/multi.spec.js b/test/multi.spec.js index 817433db34..a969bfbbd5 100644 --- a/test/multi.spec.js +++ b/test/multi.spec.js @@ -571,7 +571,7 @@ describe("The 'multi' method", function () { test = true; }; multi.set('baz', 'binary'); - multi.exec_atomic(); + multi.EXEC_ATOMIC(); assert(test); }); diff --git a/test/node_redis.spec.js b/test/node_redis.spec.js index 060c62926c..fd9fc2c2d0 100644 --- a/test/node_redis.spec.js +++ b/test/node_redis.spec.js @@ -30,6 +30,7 @@ describe('The node_redis client', function () { it('check if all options got copied properly', function (done) { client.selected_db = 2; var client2 = client.duplicate(); + assert.strictEqual(client.connectionId + 1, client2.connection_id); assert.strictEqual(client2.selected_db, 2); assert(client.connected); assert(!client2.connected); @@ -360,7 +361,7 @@ describe('The node_redis client', function () { client.on('error', function (err) { assert.strictEqual(err.message, 'SET can\'t be processed. The connection has already been closed.'); assert.strictEqual(err.command, 'SET'); - assert.strictEqual(client.offline_queue.length, 0); + assert.strictEqual(client.offline_queue_length, 0); done(); }); setTimeout(function () { @@ -966,7 +967,7 @@ describe('The node_redis client', function () { multi.set('foo' + (i + 2), 'bar' + (i + 2)); } multi.exec(); - assert.equal(client.command_queue.length, 15); + assert.equal(client.command_queue_length, 15); helper.killConnection(client); }); diff --git a/test/utils.spec.js b/test/utils.spec.js index d77c7eb607..17aaf89a84 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -11,7 +11,7 @@ describe('utils.js', function () { it('ignore the object prototype and clone a nested array / object', function () { var obj = { a: [null, 'foo', ['bar'], { - "I'm special": true + "i'm special": true }], number: 5, fn: function noop () {} @@ -22,13 +22,28 @@ describe('utils.js', function () { assert(typeof clone.fn === 'function'); }); - it('replace faulty values with an empty object as return value', function () { + it('replace falsy values with an empty object as return value', function () { var a = utils.clone(); var b = utils.clone(null); assert.strictEqual(Object.keys(a).length, 0); assert.strictEqual(Object.keys(b).length, 0); }); + it('transform camelCase options to snack_case and add the camel_case option', function () { + var a = utils.clone({ + optionOneTwo: true, + retryStrategy: false, + nested: { + onlyContainCamelCaseOnce: true + } + }); + assert.strictEqual(Object.keys(a).length, 4); + assert.strictEqual(a.option_one_two, true); + assert.strictEqual(a.retry_strategy, false); + assert.strictEqual(a.camel_case, true); + assert.strictEqual(Object.keys(a.nested).length, 1); + }); + it('throws on circular data', function () { try { var a = {};