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.

350 lines
11 KiB

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const commands_1 = require("@ioredis/commands");
const calculateSlot = require("cluster-key-slot");
const standard_as_callback_1 = require("standard-as-callback");
const utils_1 = require("./utils");
/**
* Command instance
*
* It's rare that you need to create a Command instance yourself.
*
* ```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);
* });
* ```
*/
class Command {
/**
* Creates an instance of Command.
* @param name Command name
* @param args An array of command arguments
* @param options
* @param callback The callback that handles the response.
* If omit, the response will be handled via Promise
*/
constructor(name, args = [], options = {}, callback) {
this.name = name;
this.inTransaction = false;
this.isResolved = false;
this.transformed = false;
this.replyEncoding = options.replyEncoding;
this.errorStack = options.errorStack;
this.args = args.flat();
this.callback = callback;
this.initPromise();
if (options.keyPrefix) {
// @ts-expect-error
const isBufferKeyPrefix = options.keyPrefix instanceof Buffer;
// @ts-expect-error
let keyPrefixBuffer = isBufferKeyPrefix
? options.keyPrefix
: null;
this._iterateKeys((key) => {
if (key instanceof Buffer) {
if (keyPrefixBuffer === null) {
keyPrefixBuffer = Buffer.from(options.keyPrefix);
}
return Buffer.concat([keyPrefixBuffer, key]);
}
else if (isBufferKeyPrefix) {
// @ts-expect-error
return Buffer.concat([options.keyPrefix, Buffer.from(String(key))]);
}
return options.keyPrefix + key;
});
}
if (options.readOnly) {
this.isReadOnly = true;
}
}
/**
* Check whether the command has the flag
*/
static checkFlag(flagName, commandName) {
return !!this.getFlagMap()[flagName][commandName];
}
static setArgumentTransformer(name, func) {
this._transformer.argument[name] = func;
}
static setReplyTransformer(name, func) {
this._transformer.reply[name] = func;
}
static getFlagMap() {
if (!this.flagMap) {
this.flagMap = Object.keys(Command.FLAGS).reduce((map, flagName) => {
map[flagName] = {};
Command.FLAGS[flagName].forEach((commandName) => {
map[flagName][commandName] = true;
});
return map;
}, {});
}
return this.flagMap;
}
getSlot() {
if (typeof this.slot === "undefined") {
const key = this.getKeys()[0];
this.slot = key == null ? null : calculateSlot(key);
}
return this.slot;
}
getKeys() {
return this._iterateKeys();
}
/**
* Convert command to writable buffer or string
*/
toWritable(_socket) {
let result;
const commandStr = "*" +
(this.args.length + 1) +
"\r\n$" +
Buffer.byteLength(this.name) +
"\r\n" +
this.name +
"\r\n";
if (this.bufferMode) {
const buffers = new MixedBuffers();
buffers.push(commandStr);
for (let i = 0; i < this.args.length; ++i) {
const arg = this.args[i];
if (arg instanceof Buffer) {
if (arg.length === 0) {
buffers.push("$0\r\n\r\n");
}
else {
buffers.push("$" + arg.length + "\r\n");
buffers.push(arg);
buffers.push("\r\n");
}
}
else {
buffers.push("$" +
Buffer.byteLength(arg) +
"\r\n" +
arg +
"\r\n");
}
}
result = buffers.toBuffer();
}
else {
result = commandStr;
for (let i = 0; i < this.args.length; ++i) {
const arg = this.args[i];
result +=
"$" +
Buffer.byteLength(arg) +
"\r\n" +
arg +
"\r\n";
}
}
return result;
}
stringifyArguments() {
for (let i = 0; i < this.args.length; ++i) {
const arg = this.args[i];
if (typeof arg === "string") {
// buffers and strings don't need any transformation
}
else if (arg instanceof Buffer) {
this.bufferMode = true;
}
else {
this.args[i] = (0, utils_1.toArg)(arg);
}
}
}
/**
* Convert buffer/buffer[] to string/string[],
* and apply reply transformer.
*/
transformReply(result) {
if (this.replyEncoding) {
result = (0, utils_1.convertBufferToString)(result, this.replyEncoding);
}
const transformer = Command._transformer.reply[this.name];
if (transformer) {
result = transformer(result);
}
return result;
}
/**
* Set the wait time before terminating the attempt to execute a command
* and generating an error.
*/
setTimeout(ms) {
if (!this._commandTimeoutTimer) {
this._commandTimeoutTimer = setTimeout(() => {
if (!this.isResolved) {
this.reject(new Error("Command timed out"));
}
}, ms);
}
}
initPromise() {
const promise = new Promise((resolve, reject) => {
if (!this.transformed) {
this.transformed = true;
const 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 = (err) => {
reject((0, utils_1.optimizeErrorStack)(err, this.errorStack.stack, __dirname));
};
}
else {
this.reject = reject;
}
});
this.promise = (0, standard_as_callback_1.default)(promise, this.callback);
}
/**
* Iterate through the command arguments that are considered keys.
*/
_iterateKeys(transform = (key) => key) {
if (typeof this.keys === "undefined") {
this.keys = [];
if ((0, commands_1.exists)(this.name)) {
// @ts-expect-error
const keyIndexes = (0, commands_1.getKeyIndexes)(this.name, this.args);
for (const index of keyIndexes) {
this.args[index] = transform(this.args[index]);
this.keys.push(this.args[index]);
}
}
}
return this.keys;
}
/**
* Convert the value from buffer to the target encoding.
*/
_convertValue(resolve) {
return (value) => {
try {
const existingTimer = this._commandTimeoutTimer;
if (existingTimer) {
clearTimeout(existingTimer);
delete this._commandTimeoutTimer;
}
resolve(this.transformReply(value));
this.isResolved = true;
}
catch (err) {
this.reject(err);
}
return this.promise;
};
}
}
exports.default = Command;
Command.FLAGS = {
VALID_IN_SUBSCRIBER_MODE: [
"subscribe",
"psubscribe",
"unsubscribe",
"punsubscribe",
"ssubscribe",
"sunsubscribe",
"ping",
"quit",
],
VALID_IN_MONITOR_MODE: ["monitor", "auth"],
ENTER_SUBSCRIBER_MODE: ["subscribe", "psubscribe", "ssubscribe"],
EXIT_SUBSCRIBER_MODE: ["unsubscribe", "punsubscribe", "sunsubscribe"],
WILL_DISCONNECT: ["quit"],
};
Command._transformer = {
argument: {},
reply: {},
};
const msetArgumentTransformer = function (args) {
if (args.length === 1) {
if (args[0] instanceof Map) {
return (0, utils_1.convertMapToArray)(args[0]);
}
if (typeof args[0] === "object" && args[0] !== null) {
return (0, utils_1.convertObjectToArray)(args[0]);
}
}
return args;
};
const hsetArgumentTransformer = function (args) {
if (args.length === 2) {
if (args[1] instanceof Map) {
return [args[0]].concat((0, utils_1.convertMapToArray)(args[1]));
}
if (typeof args[1] === "object" && args[1] !== null) {
return [args[0]].concat((0, utils_1.convertObjectToArray)(args[1]));
}
}
return args;
};
Command.setArgumentTransformer("mset", msetArgumentTransformer);
Command.setArgumentTransformer("msetnx", msetArgumentTransformer);
Command.setArgumentTransformer("hset", hsetArgumentTransformer);
Command.setArgumentTransformer("hmset", hsetArgumentTransformer);
Command.setReplyTransformer("hgetall", function (result) {
if (Array.isArray(result)) {
const obj = {};
for (let i = 0; i < result.length; i += 2) {
const key = result[i];
const value = result[i + 1];
if (key in obj) {
// can only be truthy if the property is special somehow, like '__proto__' or 'constructor'
// https://github.com/luin/ioredis/issues/1267
Object.defineProperty(obj, key, {
value,
configurable: true,
enumerable: true,
writable: true,
});
}
else {
obj[key] = value;
}
}
return obj;
}
return result;
});
class MixedBuffers {
constructor() {
this.length = 0;
this.items = [];
}
push(x) {
this.length += Buffer.byteLength(x);
this.items.push(x);
}
toBuffer() {
const result = Buffer.allocUnsafe(this.length);
let offset = 0;
for (const item of this.items) {
const length = Buffer.byteLength(item);
Buffer.isBuffer(item)
? item.copy(result, offset)
: result.write(item, offset, length);
offset += length;
}
return result;
}
}