"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RedisConnection = void 0; const tslib_1 = require("tslib"); const events_1 = require("events"); const ioredis_1 = require("ioredis"); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const utils_1 = require("ioredis/built/utils"); const utils_2 = require("../utils"); const version_1 = require("../version"); const scripts = require("../scripts"); const overrideMessage = [ 'BullMQ: WARNING! Your redis options maxRetriesPerRequest must be null', 'and will be overridden by BullMQ.', ].join(' '); const deprecationMessage = 'BullMQ: Your redis options maxRetriesPerRequest must be null.'; class RedisConnection extends events_1.EventEmitter { constructor(opts, extraOptions) { super(); this.extraOptions = extraOptions; this.capabilities = { canDoubleTimeout: false, canBlockFor1Ms: true, }; this.status = 'initializing'; this.packageVersion = version_1.version; // Set extra options defaults this.extraOptions = Object.assign({ shared: false, blocking: true, skipVersionCheck: false, skipWaitingForReady: false }, extraOptions); if (!(0, utils_2.isRedisInstance)(opts)) { this.checkBlockingOptions(overrideMessage, opts); this.opts = Object.assign({ port: 6379, host: '127.0.0.1', retryStrategy: function (times) { return Math.max(Math.min(Math.exp(times), 20000), 1000); } }, opts); if (this.extraOptions.blocking) { this.opts.maxRetriesPerRequest = null; } } else { this._client = opts; // Test if the redis instance is using keyPrefix // and if so, throw an error. if (this._client.options.keyPrefix) { throw new Error('BullMQ: ioredis does not support ioredis prefixes, use the prefix option instead.'); } if ((0, utils_2.isRedisCluster)(this._client)) { this.opts = this._client.options.redisOptions; } else { this.opts = this._client.options; } this.checkBlockingOptions(deprecationMessage, this.opts, true); } this.skipVersionCheck = (extraOptions === null || extraOptions === void 0 ? void 0 : extraOptions.skipVersionCheck) || !!(this.opts && this.opts.skipVersionCheck); this.handleClientError = (err) => { this.emit('error', err); }; this.handleClientClose = () => { this.emit('close'); }; this.handleClientReady = () => { this.emit('ready'); }; this.initializing = this.init(); this.initializing.catch(err => this.emit('error', err)); } checkBlockingOptions(msg, options, throwError = false) { if (this.extraOptions.blocking && options && options.maxRetriesPerRequest) { if (throwError) { throw new Error(msg); } else { console.error(msg); } } } /** * Waits for a redis client to be ready. * @param redis - client */ static async waitUntilReady(client) { if (client.status === 'ready') { return; } if (client.status === 'wait') { return client.connect(); } if (client.status === 'end') { throw new Error(utils_1.CONNECTION_CLOSED_ERROR_MSG); } let handleReady; let handleEnd; let handleError; try { await new Promise((resolve, reject) => { let lastError; handleError = (err) => { lastError = err; }; handleReady = () => { resolve(); }; handleEnd = () => { if (client.status !== 'end') { reject(lastError || new Error(utils_1.CONNECTION_CLOSED_ERROR_MSG)); } else { if (lastError) { reject(lastError); } else { // when custon 'end' status is set we already closed resolve(); } } }; (0, utils_2.increaseMaxListeners)(client, 3); client.once('ready', handleReady); client.on('end', handleEnd); client.once('error', handleError); }); } finally { client.removeListener('end', handleEnd); client.removeListener('error', handleError); client.removeListener('ready', handleReady); (0, utils_2.decreaseMaxListeners)(client, 3); } } get client() { return this.initializing; } loadCommands(packageVersion, providedScripts) { const finalScripts = providedScripts || scripts; for (const property in finalScripts) { // Only define the command if not already defined const commandName = `${finalScripts[property].name}:${packageVersion}`; if (!this._client[commandName]) { this._client.defineCommand(commandName, { numberOfKeys: finalScripts[property].keys, lua: finalScripts[property].content, }); } } } async init() { if (!this._client) { const _a = this.opts, { url } = _a, rest = tslib_1.__rest(_a, ["url"]); this._client = url ? new ioredis_1.default(url, rest) : new ioredis_1.default(rest); } (0, utils_2.increaseMaxListeners)(this._client, 3); this._client.on('error', this.handleClientError); // ioredis treats connection errors as a different event ('close') this._client.on('close', this.handleClientClose); this._client.on('ready', this.handleClientReady); if (!this.extraOptions.skipWaitingForReady) { await RedisConnection.waitUntilReady(this._client); } this.loadCommands(this.packageVersion); if (this._client['status'] !== 'end') { this.version = await this.getRedisVersion(); if (this.skipVersionCheck !== true && !this.closing) { if ((0, utils_2.isRedisVersionLowerThan)(this.version, RedisConnection.minimumVersion)) { throw new Error(`Redis version needs to be greater or equal than ${RedisConnection.minimumVersion} ` + `Current: ${this.version}`); } if ((0, utils_2.isRedisVersionLowerThan)(this.version, RedisConnection.recommendedMinimumVersion)) { console.warn(`It is highly recommended to use a minimum Redis version of ${RedisConnection.recommendedMinimumVersion} Current: ${this.version}`); } } this.capabilities = { canDoubleTimeout: !(0, utils_2.isRedisVersionLowerThan)(this.version, '6.0.0'), canBlockFor1Ms: !(0, utils_2.isRedisVersionLowerThan)(this.version, '7.0.8'), }; this.status = 'ready'; } return this._client; } async disconnect(wait = true) { const client = await this.client; if (client.status !== 'end') { let _resolve, _reject; if (!wait) { return client.disconnect(); } const disconnecting = new Promise((resolve, reject) => { (0, utils_2.increaseMaxListeners)(client, 2); client.once('end', resolve); client.once('error', reject); _resolve = resolve; _reject = reject; }); client.disconnect(); try { await disconnecting; } finally { (0, utils_2.decreaseMaxListeners)(client, 2); client.removeListener('end', _resolve); client.removeListener('error', _reject); } } } async reconnect() { const client = await this.client; return client.connect(); } async close(force = false) { if (!this.closing) { const status = this.status; this.status = 'closing'; this.closing = true; try { if (status === 'ready') { // Not sure if we need to wait for this await this.initializing; } if (!this.extraOptions.shared) { if (status == 'initializing' || force) { // If we have not still connected to Redis, we need to disconnect. this._client.disconnect(); } else { await this._client.quit(); } // As IORedis does not update this status properly, we do it ourselves. this._client['status'] = 'end'; } } catch (error) { if ((0, utils_2.isNotConnectionError)(error)) { throw error; } } finally { this._client.off('error', this.handleClientError); this._client.off('close', this.handleClientClose); this._client.off('ready', this.handleClientReady); (0, utils_2.decreaseMaxListeners)(this._client, 3); this.removeAllListeners(); this.status = 'closed'; } } } async getRedisVersion() { if (this.skipVersionCheck) { return RedisConnection.minimumVersion; } const doc = await this._client.info(); const redisPrefix = 'redis_version:'; const maxMemoryPolicyPrefix = 'maxmemory_policy:'; const lines = doc.split(/\r?\n/); let redisVersion; for (let i = 0; i < lines.length; i++) { if (lines[i].indexOf(maxMemoryPolicyPrefix) === 0) { const maxMemoryPolicy = lines[i].substr(maxMemoryPolicyPrefix.length); if (maxMemoryPolicy !== 'noeviction') { console.warn(`IMPORTANT! Eviction policy is ${maxMemoryPolicy}. It should be "noeviction"`); } } if (lines[i].indexOf(redisPrefix) === 0) { redisVersion = lines[i].substr(redisPrefix.length); } } return redisVersion; } get redisVersion() { return this.version; } } exports.RedisConnection = RedisConnection; RedisConnection.minimumVersion = '5.0.0'; RedisConnection.recommendedMinimumVersion = '6.2.0'; //# sourceMappingURL=redis-connection.js.map