Command.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const commands_1 = require("@ioredis/commands");
  4. const calculateSlot = require("cluster-key-slot");
  5. const standard_as_callback_1 = require("standard-as-callback");
  6. const utils_1 = require("./utils");
  7. /**
  8. * Command instance
  9. *
  10. * It's rare that you need to create a Command instance yourself.
  11. *
  12. * ```js
  13. * var infoCommand = new Command('info', null, function (err, result) {
  14. * console.log('result', result);
  15. * });
  16. *
  17. * redis.sendCommand(infoCommand);
  18. *
  19. * // When no callback provided, Command instance will have a `promise` property,
  20. * // which will resolve/reject with the result of the command.
  21. * var getCommand = new Command('get', ['foo']);
  22. * getCommand.promise.then(function (result) {
  23. * console.log('result', result);
  24. * });
  25. * ```
  26. */
  27. class Command {
  28. /**
  29. * Creates an instance of Command.
  30. * @param name Command name
  31. * @param args An array of command arguments
  32. * @param options
  33. * @param callback The callback that handles the response.
  34. * If omit, the response will be handled via Promise
  35. */
  36. constructor(name, args = [], options = {}, callback) {
  37. this.name = name;
  38. this.inTransaction = false;
  39. this.isResolved = false;
  40. this.transformed = false;
  41. this.replyEncoding = options.replyEncoding;
  42. this.errorStack = options.errorStack;
  43. this.args = args.flat();
  44. this.callback = callback;
  45. this.initPromise();
  46. if (options.keyPrefix) {
  47. // @ts-expect-error
  48. const isBufferKeyPrefix = options.keyPrefix instanceof Buffer;
  49. // @ts-expect-error
  50. let keyPrefixBuffer = isBufferKeyPrefix
  51. ? options.keyPrefix
  52. : null;
  53. this._iterateKeys((key) => {
  54. if (key instanceof Buffer) {
  55. if (keyPrefixBuffer === null) {
  56. keyPrefixBuffer = Buffer.from(options.keyPrefix);
  57. }
  58. return Buffer.concat([keyPrefixBuffer, key]);
  59. }
  60. else if (isBufferKeyPrefix) {
  61. // @ts-expect-error
  62. return Buffer.concat([options.keyPrefix, Buffer.from(String(key))]);
  63. }
  64. return options.keyPrefix + key;
  65. });
  66. }
  67. if (options.readOnly) {
  68. this.isReadOnly = true;
  69. }
  70. }
  71. /**
  72. * Check whether the command has the flag
  73. */
  74. static checkFlag(flagName, commandName) {
  75. return !!this.getFlagMap()[flagName][commandName];
  76. }
  77. static setArgumentTransformer(name, func) {
  78. this._transformer.argument[name] = func;
  79. }
  80. static setReplyTransformer(name, func) {
  81. this._transformer.reply[name] = func;
  82. }
  83. static getFlagMap() {
  84. if (!this.flagMap) {
  85. this.flagMap = Object.keys(Command.FLAGS).reduce((map, flagName) => {
  86. map[flagName] = {};
  87. Command.FLAGS[flagName].forEach((commandName) => {
  88. map[flagName][commandName] = true;
  89. });
  90. return map;
  91. }, {});
  92. }
  93. return this.flagMap;
  94. }
  95. getSlot() {
  96. if (typeof this.slot === "undefined") {
  97. const key = this.getKeys()[0];
  98. this.slot = key == null ? null : calculateSlot(key);
  99. }
  100. return this.slot;
  101. }
  102. getKeys() {
  103. return this._iterateKeys();
  104. }
  105. /**
  106. * Convert command to writable buffer or string
  107. */
  108. toWritable(_socket) {
  109. let result;
  110. const commandStr = "*" +
  111. (this.args.length + 1) +
  112. "\r\n$" +
  113. Buffer.byteLength(this.name) +
  114. "\r\n" +
  115. this.name +
  116. "\r\n";
  117. if (this.bufferMode) {
  118. const buffers = new MixedBuffers();
  119. buffers.push(commandStr);
  120. for (let i = 0; i < this.args.length; ++i) {
  121. const arg = this.args[i];
  122. if (arg instanceof Buffer) {
  123. if (arg.length === 0) {
  124. buffers.push("$0\r\n\r\n");
  125. }
  126. else {
  127. buffers.push("$" + arg.length + "\r\n");
  128. buffers.push(arg);
  129. buffers.push("\r\n");
  130. }
  131. }
  132. else {
  133. buffers.push("$" +
  134. Buffer.byteLength(arg) +
  135. "\r\n" +
  136. arg +
  137. "\r\n");
  138. }
  139. }
  140. result = buffers.toBuffer();
  141. }
  142. else {
  143. result = commandStr;
  144. for (let i = 0; i < this.args.length; ++i) {
  145. const arg = this.args[i];
  146. result +=
  147. "$" +
  148. Buffer.byteLength(arg) +
  149. "\r\n" +
  150. arg +
  151. "\r\n";
  152. }
  153. }
  154. return result;
  155. }
  156. stringifyArguments() {
  157. for (let i = 0; i < this.args.length; ++i) {
  158. const arg = this.args[i];
  159. if (typeof arg === "string") {
  160. // buffers and strings don't need any transformation
  161. }
  162. else if (arg instanceof Buffer) {
  163. this.bufferMode = true;
  164. }
  165. else {
  166. this.args[i] = (0, utils_1.toArg)(arg);
  167. }
  168. }
  169. }
  170. /**
  171. * Convert buffer/buffer[] to string/string[],
  172. * and apply reply transformer.
  173. */
  174. transformReply(result) {
  175. if (this.replyEncoding) {
  176. result = (0, utils_1.convertBufferToString)(result, this.replyEncoding);
  177. }
  178. const transformer = Command._transformer.reply[this.name];
  179. if (transformer) {
  180. result = transformer(result);
  181. }
  182. return result;
  183. }
  184. /**
  185. * Set the wait time before terminating the attempt to execute a command
  186. * and generating an error.
  187. */
  188. setTimeout(ms) {
  189. if (!this._commandTimeoutTimer) {
  190. this._commandTimeoutTimer = setTimeout(() => {
  191. if (!this.isResolved) {
  192. this.reject(new Error("Command timed out"));
  193. }
  194. }, ms);
  195. }
  196. }
  197. initPromise() {
  198. const promise = new Promise((resolve, reject) => {
  199. if (!this.transformed) {
  200. this.transformed = true;
  201. const transformer = Command._transformer.argument[this.name];
  202. if (transformer) {
  203. this.args = transformer(this.args);
  204. }
  205. this.stringifyArguments();
  206. }
  207. this.resolve = this._convertValue(resolve);
  208. if (this.errorStack) {
  209. this.reject = (err) => {
  210. reject((0, utils_1.optimizeErrorStack)(err, this.errorStack.stack, __dirname));
  211. };
  212. }
  213. else {
  214. this.reject = reject;
  215. }
  216. });
  217. this.promise = (0, standard_as_callback_1.default)(promise, this.callback);
  218. }
  219. /**
  220. * Iterate through the command arguments that are considered keys.
  221. */
  222. _iterateKeys(transform = (key) => key) {
  223. if (typeof this.keys === "undefined") {
  224. this.keys = [];
  225. if ((0, commands_1.exists)(this.name)) {
  226. // @ts-expect-error
  227. const keyIndexes = (0, commands_1.getKeyIndexes)(this.name, this.args);
  228. for (const index of keyIndexes) {
  229. this.args[index] = transform(this.args[index]);
  230. this.keys.push(this.args[index]);
  231. }
  232. }
  233. }
  234. return this.keys;
  235. }
  236. /**
  237. * Convert the value from buffer to the target encoding.
  238. */
  239. _convertValue(resolve) {
  240. return (value) => {
  241. try {
  242. const existingTimer = this._commandTimeoutTimer;
  243. if (existingTimer) {
  244. clearTimeout(existingTimer);
  245. delete this._commandTimeoutTimer;
  246. }
  247. resolve(this.transformReply(value));
  248. this.isResolved = true;
  249. }
  250. catch (err) {
  251. this.reject(err);
  252. }
  253. return this.promise;
  254. };
  255. }
  256. }
  257. exports.default = Command;
  258. Command.FLAGS = {
  259. VALID_IN_SUBSCRIBER_MODE: [
  260. "subscribe",
  261. "psubscribe",
  262. "unsubscribe",
  263. "punsubscribe",
  264. "ping",
  265. "quit",
  266. ],
  267. VALID_IN_MONITOR_MODE: ["monitor", "auth"],
  268. ENTER_SUBSCRIBER_MODE: ["subscribe", "psubscribe"],
  269. EXIT_SUBSCRIBER_MODE: ["unsubscribe", "punsubscribe"],
  270. WILL_DISCONNECT: ["quit"],
  271. };
  272. Command._transformer = {
  273. argument: {},
  274. reply: {},
  275. };
  276. const msetArgumentTransformer = function (args) {
  277. if (args.length === 1) {
  278. if (args[0] instanceof Map) {
  279. return (0, utils_1.convertMapToArray)(args[0]);
  280. }
  281. if (typeof args[0] === "object" && args[0] !== null) {
  282. return (0, utils_1.convertObjectToArray)(args[0]);
  283. }
  284. }
  285. return args;
  286. };
  287. const hsetArgumentTransformer = function (args) {
  288. if (args.length === 2) {
  289. if (args[1] instanceof Map) {
  290. return [args[0]].concat((0, utils_1.convertMapToArray)(args[1]));
  291. }
  292. if (typeof args[1] === "object" && args[1] !== null) {
  293. return [args[0]].concat((0, utils_1.convertObjectToArray)(args[1]));
  294. }
  295. }
  296. return args;
  297. };
  298. Command.setArgumentTransformer("mset", msetArgumentTransformer);
  299. Command.setArgumentTransformer("msetnx", msetArgumentTransformer);
  300. Command.setArgumentTransformer("hset", hsetArgumentTransformer);
  301. Command.setArgumentTransformer("hmset", hsetArgumentTransformer);
  302. Command.setReplyTransformer("hgetall", function (result) {
  303. if (Array.isArray(result)) {
  304. const obj = {};
  305. for (let i = 0; i < result.length; i += 2) {
  306. const key = result[i];
  307. const value = result[i + 1];
  308. if (key in obj) {
  309. // can only be truthy if the property is special somehow, like '__proto__' or 'constructor'
  310. // https://github.com/luin/ioredis/issues/1267
  311. Object.defineProperty(obj, key, {
  312. value,
  313. configurable: true,
  314. enumerable: true,
  315. writable: true,
  316. });
  317. }
  318. else {
  319. obj[key] = value;
  320. }
  321. }
  322. return obj;
  323. }
  324. return result;
  325. });
  326. class MixedBuffers {
  327. constructor() {
  328. this.length = 0;
  329. this.items = [];
  330. }
  331. push(x) {
  332. this.length += Buffer.byteLength(x);
  333. this.items.push(x);
  334. }
  335. toBuffer() {
  336. const result = Buffer.allocUnsafe(this.length);
  337. let offset = 0;
  338. for (const item of this.items) {
  339. const length = Buffer.byteLength(item);
  340. Buffer.isBuffer(item)
  341. ? item.copy(result, offset)
  342. : result.write(item, offset, length);
  343. offset += length;
  344. }
  345. return result;
  346. }
  347. }