You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

340 lines
9.8 KiB

'use strict';
var _ = require('lodash');
var Promise = require('bluebird');
var fbuffer = require('flexbuffer');
var utils = require('./utils');
var commands = require('../commands');
/**
* Command instance
*
* It's rare that you need to create a Command instance yourself.
*
* @constructor
* @param {string} name - Command name
* @param {string[]} [args=null] - An array of command arguments
* @param {object} [options]
* @param {string} [options.replyEncoding=null] - Set the encoding of the reply,
* by default buffer will be returned.
* @param {function} [callback=null] - The callback that handles the response.
* If omit, the response will be handled via Promise.
* @example
* ```js
* var infoCommand = new Command('info', null, function (err, result) {
* console.log('result', result);
* });
*
* redis.sendCommand(infoCommand);
*
* // When no callback provided, Command instance will have a `promise` property,
* // which will resolve/reject with the result of the command.
* var getCommand = new Command('get', ['foo']);
* getCommand.promise.then(function (result) {
* console.log('result', result);
* });
* ```
*
* @see {@link Redis#sendCommand} which can send a Command instance to Redis
* @public
*/
function Command(name, args, options, callback) {
if (typeof options === 'undefined') {
options = {};
}
this.name = name;
this.replyEncoding = options.replyEncoding;
this.errorStack = options.errorStack;
this.args = args ? _.flatten(args) : [];
this.callback = callback;
this.initPromise();
var keyPrefix = options.keyPrefix;
if (keyPrefix) {
this._iterateKeys(function (key) {
return keyPrefix + key;
});
}
}
Command.prototype.initPromise = function () {
var _this = this;
this.promise = new Promise(function (resolve, reject) {
if (!_this.transformed) {
_this.transformed = true;
var transformer = Command._transformer.argument[_this.name];
if (transformer) {
_this.args = transformer(_this.args);
}
_this.stringifyArguments();
}
_this.resolve = _this._convertValue(resolve);
if (_this.errorStack) {
_this.reject = function (err) {
reject(utils.optimizeErrorStack(err, _this.errorStack, __dirname));
};
} else {
_this.reject = reject;
}
}).nodeify(this.callback);
};
Command.prototype.getSlot = function () {
if (typeof this._slot === 'undefined') {
var key = this.getKeys()[0];
if (key) {
this.slot = utils.calcSlot(key);
} else {
this.slot = null;
}
}
return this.slot;
};
Command.prototype.getKeys = function () {
return this._iterateKeys();
};
/**
* Iterate through the command arguments that are considered keys.
*
* @param {function} [transform] - The transformation that should be applied to
* each key. The transformations will persist.
* @return {string[]} The keys of the command.
* @private
*/
Command.prototype._iterateKeys = function (transform) {
if (typeof this._keys === 'undefined') {
if (typeof transform !== 'function') {
transform = function (key) {
return key;
};
}
this._keys = [];
var i, keyStart, keyStop;
var def = commands[this.name];
if (def) {
switch (this.name) {
case 'eval':
case 'evalsha':
keyStop = parseInt(this.args[1], 10) + 2;
for (i = 2; i < keyStop; ++i) {
this.args[i] = transform(this.args[i]);
this._keys.push(this.args[i]);
}
break;
case 'sort':
this.args[0] = transform(this.args[0]);
this._keys.push(this.args[0]);
for (i = 1; i < this.args.length - 1; ++i) {
if (typeof this.args[i] !== 'string') {
continue;
}
var directive = this.args[i].toUpperCase();
if (directive === 'GET') {
i += 1;
if (this.args[i] !== '#') {
this.args[i] = transform(this.args[i]);
this._keys.push(this.getKeyPart(this.args[i]));
}
} else if (directive === 'BY') {
i += 1;
this.args[i] = transform(this.args[i]);
this._keys.push(this.getKeyPart(this.args[i]));
} else if (directive === 'STORE') {
i += 1;
this.args[i] = transform(this.args[i]);
this._keys.push(this.args[i]);
}
}
break;
case 'zunionstore':
case 'zinterstore':
this.args[0] = transform(this.args[0]);
this._keys.push(this.args[0]);
keyStop = parseInt(this.args[1], 10) + 2;
for (i = 2; i < keyStop; ++i) {
this.args[i] = transform(this.args[i]);
this._keys.push(this.args[i]);
}
break;
default:
keyStart = def.keyStart - 1;
keyStop = def.keyStop > 0 ? def.keyStop : this.args.length + def.keyStop + 1;
if (keyStart >= 0 && keyStop <= this.args.length && keyStop > keyStart && def.step > 0) {
for (i = keyStart; i < keyStop; i += def.step) {
this.args[i] = transform(this.args[i]);
this._keys.push(this.args[i]);
}
}
break;
}
}
}
return this._keys;
};
Command.prototype.getKeyPart = function (key) {
var starPos = key.indexOf('*');
if (starPos === -1) {
return key;
}
var hashPos = key.indexOf('->', starPos + 1);
if (hashPos === 1) {
return key;
}
return key.slice(0, hashPos);
};
/**
* Convert command to writable buffer or string
*
* @return {string|Buffer}
* @see {@link Redis#sendCommand}
* @public
*/
Command.prototype.toWritable = function () {
var bufferMode = false;
var i;
for (i = 0; i < this.args.length; ++i) {
if (this.args[i] instanceof Buffer) {
bufferMode = true;
break;
}
}
var result, arg;
var commandStr = '*' + (this.args.length + 1) + '\r\n$' + this.name.length + '\r\n' + this.name + '\r\n';
if (bufferMode) {
var resultBuffer = new fbuffer.FlexBuffer(0);
resultBuffer.write(commandStr);
for (i = 0; i < this.args.length; ++i) {
arg = this.args[i];
if (arg instanceof Buffer) {
if (arg.length === 0) {
resultBuffer.write('$0\r\n\r\n');
} else {
resultBuffer.write('$' + arg.length + '\r\n');
resultBuffer.write(arg);
resultBuffer.write('\r\n');
}
} else {
resultBuffer.write('$' + Buffer.byteLength(arg) + '\r\n' + arg + '\r\n');
}
}
result = resultBuffer.getBuffer();
} else {
result = commandStr;
for (i = 0; i < this.args.length; ++i) {
result += '$' + Buffer.byteLength(this.args[i]) + '\r\n' + this.args[i] + '\r\n';
}
}
return result;
};
Command.prototype.stringifyArguments = function () {
for (var i = 0; i < this.args.length; ++i) {
if (!(this.args[i] instanceof Buffer) && typeof this.args[i] !== 'string') {
this.args[i] = utils.toArg(this.args[i]);
}
}
};
/**
* Convert the value from buffer to the target encoding.
*
* @param {function} resolve - The resolve function of the Promise
* @return {function} A funtion to transform and resolve a value
* @private
*/
Command.prototype._convertValue = function (resolve) {
var _this = this;
return function (value) {
// Convert buffer/buffer[] to string/string[]
var result = value;
var transformer;
try {
if (_this.replyEncoding) {
result = utils.convertBufferToString(value, _this.replyEncoding);
}
transformer = Command._transformer.reply[_this.name];
if (transformer) {
result = transformer(result);
}
resolve(result);
} catch (err) {
_this.reject(err);
}
return _this.promise;
};
};
Command.FLAGS = {
// Commands that can be processed when Redis is loading data from disk
VALID_WHEN_LOADING: ['info', 'auth', 'select', 'subscribe', 'unsubscribe', 'psubscribe',
'pubsubscribe', 'publish', 'shutdown', 'replconf', 'role', 'pubsub', 'command', 'latency'],
// Commands that can be processed when client is in the subscriber mode
VALID_IN_SUBSCRIBER_MODE: ['subscribe', 'psubscribe', 'unsubscribe', 'punsubscribe', 'ping', 'quit'],
// Commands that are valid in monitor mode
VALID_IN_MONITOR_MODE: ['monitor', 'auth'],
// Commands that will turn current connection into subscriber mode
ENTER_SUBSCRIBER_MODE: ['subscribe', 'psubscribe'],
// Commands that may make current connection quit subscriber mode
EXIT_SUBSCRIBER_MODE: ['unsubscribe', 'punsubscribe'],
// Commands that will make client disconnect from server TODO shutdown?
WILL_DISCONNECT: ['quit']
};
Command._transformer = {
argument: {},
reply: {}
};
Command.setArgumentTransformer = function (name, func) {
Command._transformer.argument[name] = func;
};
Command.setReplyTransformer = function (name, func) {
Command._transformer.reply[name] = func;
};
var msetArgumentTransformer = function (args) {
if (args.length === 1) {
if (typeof Map !== 'undefined' && args[0] instanceof Map) {
return utils.convertMapToArray(args[0]);
}
if (typeof args[0] === 'object' && args[0] !== null) {
return utils.convertObjectToArray(args[0]);
}
}
return args;
};
Command.setArgumentTransformer('mset', msetArgumentTransformer);
Command.setArgumentTransformer('msetnx', msetArgumentTransformer);
Command.setArgumentTransformer('hmset', function (args) {
if (args.length === 2) {
if (typeof Map !== 'undefined' && args[1] instanceof Map) {
return [args[0]].concat(utils.convertMapToArray(args[1]));
}
if (typeof args[1] === 'object' && args[1] !== null) {
return [args[0]].concat(utils.convertObjectToArray(args[1]));
}
}
return args;
});
Command.setReplyTransformer('hgetall', function (result) {
if (Array.isArray(result)) {
var obj = {};
for (var i = 0; i < result.length; i += 2) {
obj[result[i]] = result[i + 1];
}
return obj;
}
return result;
});
module.exports = Command;