Command.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  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. const argumentParsers_1 = require("./utils/argumentParsers");
  8. /**
  9. * Command instance
  10. *
  11. * It's rare that you need to create a Command instance yourself.
  12. *
  13. * ```js
  14. * var infoCommand = new Command('info', null, function (err, result) {
  15. * console.log('result', result);
  16. * });
  17. *
  18. * redis.sendCommand(infoCommand);
  19. *
  20. * // When no callback provided, Command instance will have a `promise` property,
  21. * // which will resolve/reject with the result of the command.
  22. * var getCommand = new Command('get', ['foo']);
  23. * getCommand.promise.then(function (result) {
  24. * console.log('result', result);
  25. * });
  26. * ```
  27. */
  28. class Command {
  29. /**
  30. * Creates an instance of Command.
  31. * @param name Command name
  32. * @param args An array of command arguments
  33. * @param options
  34. * @param callback The callback that handles the response.
  35. * If omit, the response will be handled via Promise
  36. */
  37. constructor(name, args = [], options = {}, callback) {
  38. this.name = name;
  39. this.inTransaction = false;
  40. this.isResolved = false;
  41. this.transformed = false;
  42. this.replyEncoding = options.replyEncoding;
  43. this.errorStack = options.errorStack;
  44. this.args = args.flat();
  45. this.callback = callback;
  46. this.initPromise();
  47. if (options.keyPrefix) {
  48. // @ts-expect-error
  49. const isBufferKeyPrefix = options.keyPrefix instanceof Buffer;
  50. // @ts-expect-error
  51. let keyPrefixBuffer = isBufferKeyPrefix
  52. ? options.keyPrefix
  53. : null;
  54. this._iterateKeys((key) => {
  55. if (key instanceof Buffer) {
  56. if (keyPrefixBuffer === null) {
  57. keyPrefixBuffer = Buffer.from(options.keyPrefix);
  58. }
  59. return Buffer.concat([keyPrefixBuffer, key]);
  60. }
  61. else if (isBufferKeyPrefix) {
  62. // @ts-expect-error
  63. return Buffer.concat([options.keyPrefix, Buffer.from(String(key))]);
  64. }
  65. return options.keyPrefix + key;
  66. });
  67. }
  68. if (options.readOnly) {
  69. this.isReadOnly = true;
  70. }
  71. }
  72. /**
  73. * Check whether the command has the flag
  74. */
  75. static checkFlag(flagName, commandName) {
  76. commandName = commandName.toLowerCase();
  77. return !!this.getFlagMap()[flagName][commandName];
  78. }
  79. static setArgumentTransformer(name, func) {
  80. this._transformer.argument[name] = func;
  81. }
  82. static setReplyTransformer(name, func) {
  83. this._transformer.reply[name] = func;
  84. }
  85. static getFlagMap() {
  86. if (!this.flagMap) {
  87. this.flagMap = Object.keys(Command.FLAGS).reduce((map, flagName) => {
  88. map[flagName] = {};
  89. Command.FLAGS[flagName].forEach((commandName) => {
  90. map[flagName][commandName] = true;
  91. });
  92. return map;
  93. }, {});
  94. }
  95. return this.flagMap;
  96. }
  97. getSlot() {
  98. if (typeof this.slot === "undefined") {
  99. const key = this.getKeys()[0];
  100. this.slot = key == null ? null : calculateSlot(key);
  101. }
  102. return this.slot;
  103. }
  104. getKeys() {
  105. return this._iterateKeys();
  106. }
  107. /**
  108. * Convert command to writable buffer or string
  109. */
  110. toWritable(_socket) {
  111. let result;
  112. const commandStr = "*" +
  113. (this.args.length + 1) +
  114. "\r\n$" +
  115. Buffer.byteLength(this.name) +
  116. "\r\n" +
  117. this.name +
  118. "\r\n";
  119. if (this.bufferMode) {
  120. const buffers = new MixedBuffers();
  121. buffers.push(commandStr);
  122. for (let i = 0; i < this.args.length; ++i) {
  123. const arg = this.args[i];
  124. if (arg instanceof Buffer) {
  125. if (arg.length === 0) {
  126. buffers.push("$0\r\n\r\n");
  127. }
  128. else {
  129. buffers.push("$" + arg.length + "\r\n");
  130. buffers.push(arg);
  131. buffers.push("\r\n");
  132. }
  133. }
  134. else {
  135. buffers.push("$" +
  136. Buffer.byteLength(arg) +
  137. "\r\n" +
  138. arg +
  139. "\r\n");
  140. }
  141. }
  142. result = buffers.toBuffer();
  143. }
  144. else {
  145. result = commandStr;
  146. for (let i = 0; i < this.args.length; ++i) {
  147. const arg = this.args[i];
  148. result +=
  149. "$" +
  150. Buffer.byteLength(arg) +
  151. "\r\n" +
  152. arg +
  153. "\r\n";
  154. }
  155. }
  156. return result;
  157. }
  158. stringifyArguments() {
  159. for (let i = 0; i < this.args.length; ++i) {
  160. const arg = this.args[i];
  161. if (typeof arg === "string") {
  162. // buffers and strings don't need any transformation
  163. }
  164. else if (arg instanceof Buffer) {
  165. this.bufferMode = true;
  166. }
  167. else {
  168. this.args[i] = (0, utils_1.toArg)(arg);
  169. }
  170. }
  171. }
  172. /**
  173. * Convert buffer/buffer[] to string/string[],
  174. * and apply reply transformer.
  175. */
  176. transformReply(result) {
  177. if (this.replyEncoding) {
  178. result = (0, utils_1.convertBufferToString)(result, this.replyEncoding);
  179. }
  180. const transformer = Command._transformer.reply[this.name];
  181. if (transformer) {
  182. result = transformer(result);
  183. }
  184. return result;
  185. }
  186. /**
  187. * Set the wait time before terminating the attempt to execute a command
  188. * and generating an error.
  189. */
  190. setTimeout(ms) {
  191. if (!this._commandTimeoutTimer) {
  192. this._commandTimeoutTimer = setTimeout(() => {
  193. if (!this.isResolved) {
  194. this.reject(new Error("Command timed out"));
  195. }
  196. }, ms);
  197. }
  198. }
  199. /**
  200. * Set a timeout for blocking commands.
  201. * When the timeout expires, the command resolves with null (matching Redis behavior).
  202. * This handles the case of undetectable network failures (e.g., docker network disconnect)
  203. * where the TCP connection becomes a zombie and no close event fires.
  204. */
  205. setBlockingTimeout(ms) {
  206. if (ms <= 0) {
  207. return;
  208. }
  209. // Clear existing timer if any (can happen when command moves from offline to command queue)
  210. if (this._blockingTimeoutTimer) {
  211. clearTimeout(this._blockingTimeoutTimer);
  212. this._blockingTimeoutTimer = undefined;
  213. }
  214. const now = Date.now();
  215. // First call: establish absolute deadline
  216. if (this._blockingDeadline === undefined) {
  217. this._blockingDeadline = now + ms;
  218. }
  219. // Check if we've already exceeded the deadline
  220. const remaining = this._blockingDeadline - now;
  221. if (remaining <= 0) {
  222. // Resolve with null to indicate timeout (same as Redis behavior)
  223. this.resolve(null);
  224. return;
  225. }
  226. this._blockingTimeoutTimer = setTimeout(() => {
  227. if (this.isResolved) {
  228. this._blockingTimeoutTimer = undefined;
  229. return;
  230. }
  231. this._blockingTimeoutTimer = undefined;
  232. // Timeout expired - resolve with null (same as Redis behavior when blocking command times out)
  233. this.resolve(null);
  234. }, remaining);
  235. }
  236. /**
  237. * Extract the blocking timeout from the command arguments.
  238. *
  239. * @returns The timeout in seconds, null for indefinite blocking (timeout of 0),
  240. * or undefined if this is not a blocking command
  241. */
  242. extractBlockingTimeout() {
  243. const args = this.args;
  244. if (!args || args.length === 0) {
  245. return undefined;
  246. }
  247. const name = this.name.toLowerCase();
  248. if (Command.checkFlag("LAST_ARG_TIMEOUT_COMMANDS", name)) {
  249. return (0, argumentParsers_1.parseSecondsArgument)(args[args.length - 1]);
  250. }
  251. if (Command.checkFlag("FIRST_ARG_TIMEOUT_COMMANDS", name)) {
  252. return (0, argumentParsers_1.parseSecondsArgument)(args[0]);
  253. }
  254. if (Command.checkFlag("BLOCK_OPTION_COMMANDS", name)) {
  255. return (0, argumentParsers_1.parseBlockOption)(args);
  256. }
  257. return undefined;
  258. }
  259. /**
  260. * Clear the command and blocking timers
  261. */
  262. _clearTimers() {
  263. const existingTimer = this._commandTimeoutTimer;
  264. if (existingTimer) {
  265. clearTimeout(existingTimer);
  266. delete this._commandTimeoutTimer;
  267. }
  268. const blockingTimer = this._blockingTimeoutTimer;
  269. if (blockingTimer) {
  270. clearTimeout(blockingTimer);
  271. delete this._blockingTimeoutTimer;
  272. }
  273. }
  274. initPromise() {
  275. const promise = new Promise((resolve, reject) => {
  276. if (!this.transformed) {
  277. this.transformed = true;
  278. const transformer = Command._transformer.argument[this.name];
  279. if (transformer) {
  280. this.args = transformer(this.args);
  281. }
  282. this.stringifyArguments();
  283. }
  284. this.resolve = this._convertValue(resolve);
  285. this.reject = (err) => {
  286. this._clearTimers();
  287. if (this.errorStack) {
  288. reject((0, utils_1.optimizeErrorStack)(err, this.errorStack.stack, __dirname));
  289. }
  290. else {
  291. reject(err);
  292. }
  293. };
  294. });
  295. this.promise = (0, standard_as_callback_1.default)(promise, this.callback);
  296. }
  297. /**
  298. * Iterate through the command arguments that are considered keys.
  299. */
  300. _iterateKeys(transform = (key) => key) {
  301. if (typeof this.keys === "undefined") {
  302. this.keys = [];
  303. if ((0, commands_1.exists)(this.name, { caseInsensitive: true })) {
  304. // @ts-expect-error
  305. const keyIndexes = (0, commands_1.getKeyIndexes)(this.name, this.args, {
  306. nameCaseInsensitive: true,
  307. });
  308. for (const index of keyIndexes) {
  309. this.args[index] = transform(this.args[index]);
  310. this.keys.push(this.args[index]);
  311. }
  312. }
  313. }
  314. return this.keys;
  315. }
  316. /**
  317. * Convert the value from buffer to the target encoding.
  318. */
  319. _convertValue(resolve) {
  320. return (value) => {
  321. try {
  322. this._clearTimers();
  323. resolve(this.transformReply(value));
  324. this.isResolved = true;
  325. }
  326. catch (err) {
  327. this.reject(err);
  328. }
  329. return this.promise;
  330. };
  331. }
  332. }
  333. exports.default = Command;
  334. Command.FLAGS = {
  335. VALID_IN_SUBSCRIBER_MODE: [
  336. "subscribe",
  337. "psubscribe",
  338. "unsubscribe",
  339. "punsubscribe",
  340. "ssubscribe",
  341. "sunsubscribe",
  342. "ping",
  343. "quit",
  344. ],
  345. VALID_IN_MONITOR_MODE: ["monitor", "auth"],
  346. ENTER_SUBSCRIBER_MODE: ["subscribe", "psubscribe", "ssubscribe"],
  347. EXIT_SUBSCRIBER_MODE: ["unsubscribe", "punsubscribe", "sunsubscribe"],
  348. WILL_DISCONNECT: ["quit"],
  349. HANDSHAKE_COMMANDS: ["auth", "select", "client", "readonly", "info"],
  350. IGNORE_RECONNECT_ON_ERROR: ["client"],
  351. BLOCKING_COMMANDS: [
  352. "blpop",
  353. "brpop",
  354. "brpoplpush",
  355. "blmove",
  356. "bzpopmin",
  357. "bzpopmax",
  358. "bzmpop",
  359. "blmpop",
  360. "xread",
  361. "xreadgroup",
  362. ],
  363. LAST_ARG_TIMEOUT_COMMANDS: [
  364. "blpop",
  365. "brpop",
  366. "brpoplpush",
  367. "blmove",
  368. "bzpopmin",
  369. "bzpopmax",
  370. ],
  371. FIRST_ARG_TIMEOUT_COMMANDS: ["bzmpop", "blmpop"],
  372. BLOCK_OPTION_COMMANDS: ["xread", "xreadgroup"],
  373. };
  374. Command._transformer = {
  375. argument: {},
  376. reply: {},
  377. };
  378. const msetArgumentTransformer = function (args) {
  379. if (args.length === 1) {
  380. if (args[0] instanceof Map) {
  381. return (0, utils_1.convertMapToArray)(args[0]);
  382. }
  383. if (typeof args[0] === "object" && args[0] !== null) {
  384. return (0, utils_1.convertObjectToArray)(args[0]);
  385. }
  386. }
  387. return args;
  388. };
  389. const hsetArgumentTransformer = function (args) {
  390. if (args.length === 2) {
  391. if (args[1] instanceof Map) {
  392. return [args[0]].concat((0, utils_1.convertMapToArray)(args[1]));
  393. }
  394. if (typeof args[1] === "object" && args[1] !== null) {
  395. return [args[0]].concat((0, utils_1.convertObjectToArray)(args[1]));
  396. }
  397. }
  398. return args;
  399. };
  400. Command.setArgumentTransformer("mset", msetArgumentTransformer);
  401. Command.setArgumentTransformer("msetnx", msetArgumentTransformer);
  402. Command.setArgumentTransformer("hset", hsetArgumentTransformer);
  403. Command.setArgumentTransformer("hmset", hsetArgumentTransformer);
  404. Command.setReplyTransformer("hgetall", function (result) {
  405. if (Array.isArray(result)) {
  406. const obj = {};
  407. for (let i = 0; i < result.length; i += 2) {
  408. const key = result[i];
  409. const value = result[i + 1];
  410. if (key in obj) {
  411. // can only be truthy if the property is special somehow, like '__proto__' or 'constructor'
  412. // https://github.com/luin/ioredis/issues/1267
  413. Object.defineProperty(obj, key, {
  414. value,
  415. configurable: true,
  416. enumerable: true,
  417. writable: true,
  418. });
  419. }
  420. else {
  421. obj[key] = value;
  422. }
  423. }
  424. return obj;
  425. }
  426. return result;
  427. });
  428. class MixedBuffers {
  429. constructor() {
  430. this.length = 0;
  431. this.items = [];
  432. }
  433. push(x) {
  434. this.length += Buffer.byteLength(x);
  435. this.items.push(x);
  436. }
  437. toBuffer() {
  438. const result = Buffer.allocUnsafe(this.length);
  439. let offset = 0;
  440. for (const item of this.items) {
  441. const length = Buffer.byteLength(item);
  442. Buffer.isBuffer(item)
  443. ? item.copy(result, offset)
  444. : result.write(item, offset, length);
  445. offset += length;
  446. }
  447. return result;
  448. }
  449. }