UNPKG

ioredis

Version:

A robust, performance-focused and full-featured Redis client for Node.js.

369 lines (368 loc) 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.noop = exports.isArguments = exports.defaults = exports.Debug = exports.getPackageMeta = exports.zipMap = exports.CONNECTION_CLOSED_ERROR_MSG = exports.shuffle = exports.sample = exports.resolveTLSProfile = exports.parseURL = exports.optimizeErrorStack = exports.toArg = exports.convertMapToArray = exports.convertObjectToArray = exports.timeout = exports.packObject = exports.isInt = exports.wrapMultiResult = exports.convertBufferToString = void 0; const fs_1 = require("fs"); const path_1 = require("path"); const lodash_1 = require("./lodash"); Object.defineProperty(exports, "defaults", { enumerable: true, get: function () { return lodash_1.defaults; } }); Object.defineProperty(exports, "isArguments", { enumerable: true, get: function () { return lodash_1.isArguments; } }); Object.defineProperty(exports, "noop", { enumerable: true, get: function () { return lodash_1.noop; } }); const debug_1 = require("./debug"); exports.Debug = debug_1.default; const TLSProfiles_1 = require("../constants/TLSProfiles"); /** * Convert a buffer to string, supports buffer array * * @example * ```js * const input = [Buffer.from('foo'), [Buffer.from('bar')]] * const res = convertBufferToString(input, 'utf8') * expect(res).to.eql(['foo', ['bar']]) * ``` */ function convertBufferToString(value, encoding) { if (value instanceof Buffer) { return value.toString(encoding); } if (Array.isArray(value)) { const length = value.length; const res = Array(length); for (let i = 0; i < length; ++i) { res[i] = value[i] instanceof Buffer && encoding === "utf8" ? value[i].toString() : convertBufferToString(value[i], encoding); } return res; } return value; } exports.convertBufferToString = convertBufferToString; /** * Convert a list of results to node-style * * @example * ```js * const input = ['a', 'b', new Error('c'), 'd'] * const output = exports.wrapMultiResult(input) * expect(output).to.eql([[null, 'a'], [null, 'b'], [new Error('c')], [null, 'd']) * ``` */ function wrapMultiResult(arr) { // When using WATCH/EXEC transactions, the EXEC will return // a null instead of an array if (!arr) { return null; } const result = []; const length = arr.length; for (let i = 0; i < length; ++i) { const item = arr[i]; if (item instanceof Error) { result.push([item]); } else { result.push([null, item]); } } return result; } exports.wrapMultiResult = wrapMultiResult; /** * Detect if the argument is a int * @example * ```js * > isInt('123') * true * > isInt('123.3') * false * > isInt('1x') * false * > isInt(123) * true * > isInt(true) * false * ``` */ function isInt(value) { const x = parseFloat(value); return !isNaN(value) && (x | 0) === x; } exports.isInt = isInt; /** * Pack an array to an Object * * @example * ```js * > packObject(['a', 'b', 'c', 'd']) * { a: 'b', c: 'd' } * ``` */ function packObject(array) { const result = {}; const length = array.length; for (let i = 1; i < length; i += 2) { result[array[i - 1]] = array[i]; } return result; } exports.packObject = packObject; /** * Return a callback with timeout */ function timeout(callback, timeout) { let timer = null; const run = function () { if (timer) { clearTimeout(timer); timer = null; callback.apply(this, arguments); } }; timer = setTimeout(run, timeout, new Error("timeout")); return run; } exports.timeout = timeout; /** * Convert an object to an array * @example * ```js * > convertObjectToArray({ a: '1' }) * ['a', '1'] * ``` */ function convertObjectToArray(obj) { const result = []; const keys = Object.keys(obj); // Object.entries requires node 7+ for (let i = 0, l = keys.length; i < l; i++) { result.push(keys[i], obj[keys[i]]); } return result; } exports.convertObjectToArray = convertObjectToArray; /** * Convert a map to an array * @example * ```js * > convertMapToArray(new Map([[1, '2']])) * [1, '2'] * ``` */ function convertMapToArray(map) { const result = []; let pos = 0; map.forEach(function (value, key) { result[pos] = key; result[pos + 1] = value; pos += 2; }); return result; } exports.convertMapToArray = convertMapToArray; /** * Convert a non-string arg to a string */ function toArg(arg) { if (arg === null || typeof arg === "undefined") { return ""; } return String(arg); } exports.toArg = toArg; /** * Optimize error stack * * @param error actually error * @param friendlyStack the stack that more meaningful * @param filterPath only show stacks with the specified path */ function optimizeErrorStack(error, friendlyStack, filterPath) { const stacks = friendlyStack.split("\n"); let lines = ""; let i; for (i = 1; i < stacks.length; ++i) { if (stacks[i].indexOf(filterPath) === -1) { break; } } for (let j = i; j < stacks.length; ++j) { lines += "\n" + stacks[j]; } if (error.stack) { const pos = error.stack.indexOf("\n"); error.stack = error.stack.slice(0, pos) + lines; } return error; } exports.optimizeErrorStack = optimizeErrorStack; /** * Parse the redis protocol url */ function parseURL(url) { if (isInt(url)) { return { port: url }; } // Re-assert string type: isInt's type guard (value is string) incorrectly // narrows url to 'never' in the false branch since url is already string. const rawUrl = url; // Determine if the URL has a known protocol (redis:// or rediss://) const hasProtocol = /^rediss?:\/\//i.test(rawUrl); // Unix socket paths (starting with "/") are not valid URLs — handle separately. // Preserve query params (e.g., /tmp/redis.sock?db=2) if (rawUrl[0] === "/") { const qIdx = rawUrl.indexOf("?"); const result = { path: qIdx === -1 ? rawUrl : rawUrl.slice(0, qIdx), }; if (qIdx !== -1) { const options = {}; const params = new URLSearchParams(rawUrl.slice(qIdx + 1)); params.forEach((value, key) => { options[key] = parseURLQueryItem(key, value); }); (0, lodash_1.defaults)(result, options); } return result; } let parsed; if (hasProtocol) { parsed = new URL(rawUrl); } else { // Prepend a dummy protocol so new URL() can parse "host:port?query" correctly parsed = new URL("redis://" + rawUrl); } // Convert searchParams to a plain object (equivalent to url.parse(url, true).query) const options = {}; parsed.searchParams.forEach((value, key) => { options[key] = parseURLQueryItem(key, value); }); const result = {}; // Extract auth — WHATWG URL percent-encodes special chars, so decode them. // Check both username AND password: redis://:password@host has empty username but valid password. if (parsed.username || parsed.password) { result.username = decodeURIComponent(parsed.username); result.password = decodeURIComponent(parsed.password); } if (parsed.pathname && parsed.pathname !== "/") { if (hasProtocol) { // For redis:// and rediss://, the path after / is the db number if (parsed.pathname.length > 1) { result.db = parsed.pathname.slice(1); } } else { result.path = parsed.pathname; } } if (parsed.hostname) { // WHATWG URL keeps brackets around IPv6 addresses (e.g., "[::1]"). // Strip them to match the old url.parse() behavior. result.host = parsed.hostname.replace(/^\[|\]$/g, ""); } if (parsed.port) { result.port = parsed.port; } (0, lodash_1.defaults)(result, options); return result; } exports.parseURL = parseURL; function parseURLQueryItem(key, value) { if (key === "family") { const intFamily = Number.parseInt(value, 10); if (!Number.isNaN(intFamily)) { return intFamily; } } return value; } /** * Resolve TLS profile shortcut in connection options */ function resolveTLSProfile(options) { let tls = options === null || options === void 0 ? void 0 : options.tls; if (typeof tls === "string") tls = { profile: tls }; const profile = TLSProfiles_1.default[tls === null || tls === void 0 ? void 0 : tls.profile]; if (profile) { tls = Object.assign({}, profile, tls); delete tls.profile; options = Object.assign({}, options, { tls }); } return options; } exports.resolveTLSProfile = resolveTLSProfile; /** * Get a random element from `array` */ function sample(array, from = 0) { const length = array.length; if (from >= length) { return null; } return array[from + Math.floor(Math.random() * (length - from))]; } exports.sample = sample; /** * Shuffle the array using the Fisher-Yates Shuffle. * This method will mutate the original array. */ function shuffle(array) { let counter = array.length; // While there are elements in the array while (counter > 0) { // Pick a random index const index = Math.floor(Math.random() * counter); // Decrease counter by 1 counter--; // And swap the last element with it [array[counter], array[index]] = [array[index], array[counter]]; } return array; } exports.shuffle = shuffle; /** * Error message for connection being disconnected */ exports.CONNECTION_CLOSED_ERROR_MSG = "Connection is closed."; function zipMap(keys, values) { const map = new Map(); keys.forEach((key, index) => { map.set(key, values[index]); }); return map; } exports.zipMap = zipMap; /** * Memoized package metadata to avoid repeated file system reads. * * @internal */ let cachedPackageMeta = null; /** * Retrieves cached package metadata from package.json. * * @internal * @returns {Promise<{version: string} | null>} Package metadata or null if unavailable */ async function getPackageMeta() { if (cachedPackageMeta) { return cachedPackageMeta; } try { const filePath = (0, path_1.resolve)(__dirname, "..", "..", "package.json"); const data = await fs_1.promises.readFile(filePath, "utf8"); const parsed = JSON.parse(data); cachedPackageMeta = { version: parsed.version, }; return cachedPackageMeta; } catch (err) { cachedPackageMeta = { version: "error-fetching-version", }; return cachedPackageMeta; } } exports.getPackageMeta = getPackageMeta;