@imqueue/core
Version:
Simple JSON-based messaging queue for inter service communication
1,091 lines • 34.6 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RedisQueue = exports.IMQ_SHUTDOWN_TIMEOUT = exports.DEFAULT_IMQ_OPTIONS = void 0;
exports.sha1 = sha1;
exports.intrand = intrand;
exports.pack = pack;
exports.unpack = unpack;
/*!
* Fast messaging queue over Redis
*
* I'm Queue Software Project
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* If you want to use this code in a closed source (commercial) project, you can
* purchase a proprietary commercial license. Please contact us at
* <support@imqueue.com> to get commercial licensing options.
*/
const crypto = require("crypto");
const events_1 = require("events");
const os = require("os");
const zlib_1 = require("zlib");
const _1 = require(".");
const redis_1 = require("./redis");
const RX_CLIENT_NAME = /name=(\S+)/g;
const RX_CLIENT_TEST = /:(reader|writer|watcher)/;
const RX_CLIENT_CLEAN = /:(reader|writer|watcher).*$/;
exports.DEFAULT_IMQ_OPTIONS = {
host: 'localhost',
port: 6379,
cleanup: false,
cleanupFilter: '*',
logger: console,
prefix: 'imq',
safeDelivery: false,
safeDeliveryTtl: 5000,
useGzip: false,
watcherCheckDelay: 5000,
};
exports.IMQ_SHUTDOWN_TIMEOUT = +(process.env.IMQ_SHUTDOWN_TIMEOUT || 1000);
/**
* Returns SHA1 hash sum of the given string
*
* @param {string} str
* @returns {string}
*/
function sha1(str) {
const sha = crypto.createHash('sha1');
sha.update(str);
return sha.digest('hex');
}
/**
* Returns random integer between given min and max
*
* @param {number} min
* @param {number} max
* @returns {number}
*/
function intrand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Compress given data and returns binary string
*
* @param {any} data
* @returns {string}
*/
// istanbul ignore next
function pack(data) {
return (0, zlib_1.gzipSync)(JSON.stringify(data)).toString('binary');
}
/**
* Decompress binary string and returns plain data
*
* @param {string} data
* @returns {any}
*/
// istanbul ignore next
function unpack(data) {
return JSON.parse((0, zlib_1.gunzipSync)(Buffer.from(data, 'binary')).toString());
}
/**
* Class RedisQueue
* Implements simple messaging queue over redis.
*/
class RedisQueue extends events_1.EventEmitter {
/**
* @constructor
* @param {string} name
* @param {IMQOptions} [options]
* @param {IMQMode} [mode]
*/
constructor(name, options, mode = _1.IMQMode.BOTH) {
super();
this.name = name;
this.mode = mode;
/**
* Init state for this queue instance
*
* @type {boolean}
*/
this.initialized = false;
/**
* Signals if the queue was destroyed
*
* @type {boolean}
*/
this.destroyed = false;
/**
* True if the current instance owns a watcher connection, false otherwise
*
* @type {boolean}
*/
this.watchOwner = false;
/**
* Signals initialization state
*
* @type {boolean}
*/
this.signalsInitialized = false;
/**
* LUA scripts for redis
*
* @type {{moveDelayed: {code: string}}}
*/
// tslint:disable-next-line:completed-docs
this.scripts = {
moveDelayed: {
code: 'local messages = redis.call(' +
'"zrangebyscore", KEYS[1], "-inf", ARGV[1]) ' +
'local count = table.getn(messages) ' +
'local message ' +
'local i = 1 ' +
'if count > 0 then ' +
'while messages[i] do ' +
'redis.call("lpush", KEYS[2], messages[i]) ' +
'i = i + 1 ' +
'end ' +
'redis.call("zremrangebyscore", KEYS[1], ' +
'"-inf", ARGV[1]) ' +
'end ' +
'return count',
},
};
this.options = (0, _1.buildOptions)(exports.DEFAULT_IMQ_OPTIONS, options);
/* tslint:disable */
this.pack = this.options.useGzip ? pack : JSON.stringify;
this.unpack = this.options.useGzip ? unpack : JSON.parse;
/* tslint:enable */
this.redisKey = `${this.options.host}:${this.options.port}`;
}
/**
* Creates a subscription channel over redis and sets up channel
* data read handler
*
* @param {string} channel
* @param {(data: JsonObject) => any} handler
* @return {Promise<void>}
*/
async subscribe(channel, handler) {
// istanbul ignore next
if (!channel) {
throw new TypeError(`${channel}: No subscription channel name provided!`);
}
// istanbul ignore next
if (this.subscriptionName && this.subscriptionName !== channel) {
throw new TypeError(`Invalid channel name provided: expected "${this.subscriptionName}", but "${channel}" given instead!`);
}
else if (!this.subscriptionName) {
this.subscriptionName = channel;
}
const fcn = `${this.options.prefix}:${this.subscriptionName}`;
const chan = await this.connect('subscription', this.options);
await chan.subscribe(fcn);
// istanbul ignore next
chan.on('message', (ch, message) => {
if (ch === fcn && typeof handler === 'function') {
handler(JSON.parse(message));
}
});
}
/**
* Closes subscription channel
*
* @return {Promise<void>}
*/
async unsubscribe() {
if (this.subscription) {
if (this.subscriptionName) {
this.subscription.unsubscribe(`${this.options.prefix}:${this.subscriptionName}`);
}
this.subscription.removeAllListeners();
this.subscription.disconnect(false);
this.subscription.quit();
}
this.subscriptionName = undefined;
this.subscription = undefined;
}
/**
* Publishes a message to this queue subscription channel for currently
* subscribed clients.
*
* If toName specified will publish to PubSub with a different name. This
* can be used to implement broadcasting some messages to other subscribers
* on other PubSub channels.
*
* @param {string} [toName]
* @param {JsonObject} data
*/
async publish(data, toName) {
if (!this.writer) {
throw new TypeError('Writer is not connected!');
}
await this.writer.publish(`${this.options.prefix}:${toName || this.name}`, JSON.stringify(data));
}
/**
* Initializes and starts current queue routines
*
* @returns {Promise<RedisQueue>}
*/
async start() {
if (!this.name) {
throw new TypeError(`${this.name}: No queue name provided!`);
}
if (this.initialized) {
return this;
}
this.destroyed = false;
const connPromises = [];
// istanbul ignore next
if (!this.reader && this.isWorker()) {
connPromises.push(this.connect('reader', this.options));
}
if (!this.writer) {
connPromises.push(this.connect('writer', this.options));
}
await Promise.all(connPromises);
if (!this.signalsInitialized) {
// istanbul ignore next
const free = async () => {
let exitCode = 0;
setTimeout(() => process.exit(exitCode), exports.IMQ_SHUTDOWN_TIMEOUT);
try {
if (this.watchOwner) {
await this.unlock();
}
}
catch (err) {
this.logger.error(err);
exitCode = 1;
}
};
process.on('SIGTERM', free);
process.on('SIGINT', free);
process.on('SIGABRT', free);
this.signalsInitialized = true;
}
await this.initWatcher();
this.initialized = true;
return this;
}
/**
* Sends a given message to a given queue (by name)
*
* @param {string} toQueue
* @param {JsonObject} message
* @param {number} [delay]
* @param {(err: Error) => void} [errorHandler]
* @returns {Promise<RedisQueue>}
*/
async send(toQueue, message, delay, errorHandler) {
if (!this.isPublisher()) {
throw new TypeError('IMQ: Unable to publish in WORKER only mode!');
}
// istanbul ignore next
if (!this.writer) {
await this.start();
}
if (!this.writer) {
throw new TypeError('IMQ: unable to initialize queue!');
}
const id = (0, _1.uuid)();
const data = { id, message, from: this.name };
const key = `${this.options.prefix}:${toQueue}`;
const packet = this.pack(data);
const cb = (error) => {
// istanbul ignore next
if (error && errorHandler) {
errorHandler(error);
}
};
if (delay) {
this.writer.zadd(`${key}:delayed`, Date.now() + delay, packet, (err) => {
// istanbul ignore next
if (err) {
cb(err);
return;
}
this.writer.set(`${key}:${id}:ttl`, '', 'PX', delay, 'NX', cb);
});
}
else {
this.writer.lpush(key, packet, cb);
}
return id;
}
/**
* Stops current queue routines
*
* @returns {Promise<RedisQueue>}
*/
async stop() {
if (this.reader) {
this.reader.removeAllListeners();
this.reader.quit();
this.reader.disconnect(false);
delete this.reader;
}
this.initialized = false;
return this;
}
/**
* Gracefully destroys this queue
*
* @returns {Promise<void>}
*/
async destroy() {
this.destroyed = true;
this.removeAllListeners();
this.cleanSafeCheckInterval();
this.destroyWatcher();
await this.stop();
await this.clear();
this.destroyWriter();
await this.unsubscribe();
}
/**
* Clears queue data in redis;
*
* @returns {Promise<void>}
*/
async clear() {
if (!this.writer) {
return this;
}
try {
await Promise.all([
this.writer.del(this.key),
this.writer.del(`${this.key}:delayed`),
]);
}
catch (err) {
// istanbul ignore next
if (this.initialized) {
this.logger.error(`${context.name}: error clearing the redis queue host ${this.redisKey} on writer, pid ${process.pid}:`, err);
}
}
return this;
}
/**
* Returns true if publisher mode is enabled on this queue, false otherwise.
*
* @return {boolean}
*/
isPublisher() {
return this.mode === _1.IMQMode.BOTH || this.mode === _1.IMQMode.PUBLISHER;
}
/**
* Returns true if worker mode is enabled on this queue, false otherwise.
*
* @return {boolean}
*/
isWorker() {
return this.mode === _1.IMQMode.BOTH || this.mode === _1.IMQMode.WORKER;
}
// noinspection JSMethodCanBeStatic
/**
* Writer connection associated with this queue instance
*
* @type {IRedisClient}
*/
get writer() {
return RedisQueue.writers[this.redisKey];
}
// noinspection JSUnusedLocalSymbols
/**
* Writer connection setter.
*
* @param {IRedisClient} conn
*/
// noinspection JSUnusedLocalSymbols,JSUnusedLocalSymbols
set writer(conn) {
RedisQueue.writers[this.redisKey] = conn;
}
/**
* Watcher connection instance associated with this queue instance
*
* @type {IRedisClient}
*/
get watcher() {
return RedisQueue.watchers[this.redisKey];
}
// noinspection JSUnusedLocalSymbols
/**
* Watcher setter sets the watcher connection property for this
* queue instance
*
* @param {IRedisClient} conn
*/
// noinspection JSUnusedLocalSymbols
set watcher(conn) {
RedisQueue.watchers[this.redisKey] = conn;
}
/**
* Logger instance associated with the current queue instance
* @type {ILogger}
*/
get logger() {
// istanbul ignore next
return this.options.logger || console;
}
/**
* Return a lock key for watcher connection
*
* @access private
* @returns {string}
*/
get lockKey() {
return `${this.options.prefix}:watch:lock`;
}
/**
* Returns current queue key
*
* @access private
* @returns {string}
*/
get key() {
return `${this.options.prefix}:${this.name}`;
}
/**
* Destroys watcher channel
*
* @access private
*/
destroyWatcher() {
if (this.watcher) {
this.watcher.removeAllListeners();
this.watcher.quit();
this.watcher.disconnect(false);
delete RedisQueue.watchers[this.redisKey];
}
}
/**
* Destroys writer channel
*
* @access private
*/
destroyWriter() {
if (this.writer) {
this.writer.removeAllListeners();
this.writer.quit();
this.writer.disconnect(false);
delete RedisQueue.writers[this.redisKey];
}
}
/**
* Establishes a given connection channel by its name
*
* @access private
* @param {RedisConnectionChannel} channel
* @param {IMQOptions} options
* @param {any} context
* @returns {Promise<IRedisClient>}
*/
async connect(channel, options, context = this) {
// istanbul ignore next
if (context[channel]) {
return context[channel];
}
return new Promise((resolve, reject) => {
const redis = new redis_1.default({
// istanbul ignore next
port: options.port || 6379,
// istanbul ignore next
host: options.host || 'localhost',
// istanbul ignore next
username: options.username,
// istanbul ignore next
password: options.password,
connectionName: this.getChannelName(context.name, options.prefix || '', channel),
retryStrategy: this.retryStrategy(context),
autoResubscribe: true,
});
context[channel] = (0, _1.makeRedisSafe)(redis);
context[channel].__imq = true;
redis.setMaxListeners(10000);
redis.on('ready', this.onReadyHandler(context, channel, resolve));
redis.on('error', this.onErrorHandler(context, channel, reject));
redis.on('end', this.onCloseHandler(context, channel));
});
}
// istanbul ignore next
/**
* Builds and returns redis reconnection strategy
*
* @param {RedisQueue} context
* @returns {() => (number | void | null)}
* @private
*/
retryStrategy(context) {
return () => {
if (context.destroyed) {
return null;
}
return 200;
};
}
/**
* Builds and returns connection ready state handler
*
* @access private
* @param {RedisQueue} context
* @param {RedisConnectionChannel} channel
* @param {(...args: any[]) => void} resolve
* @return {() => Promise<void>}
*/
onReadyHandler(context, channel, resolve) {
return (async () => {
this.logger.info('%s: %s channel connected, host %s, pid %s', context.name, channel, this.redisKey, process.pid);
switch (channel) {
case 'reader':
this.read();
break;
case 'writer':
await this.processDelayed(this.key);
break;
case 'watcher':
await this.initWatcher();
break;
}
resolve(context[channel]);
});
}
// noinspection JSMethodCanBeStatic
/**
* Generates channel name
*
* @param {string} contextName
* @param {string} prefix
* @param {RedisConnectionChannel} name
* @return {string}
*/
getChannelName(contextName, prefix, name) {
const uniqueSuffix = `pid:${process.pid}:host:${os.hostname()}`;
return `${prefix}:${contextName}:${name}:${uniqueSuffix}`;
}
/**
* Builds and returns connection error handler
*
* @access private
* @param {RedisQueue} context
* @param {RedisConnectionChannel} channel
* @param {(...args: any[]) => void} reject
* @return {(err: Error) => void}
*/
onErrorHandler(context, channel, reject) {
// istanbul ignore next
return ((err) => {
if (this.destroyed) {
return;
}
this.logger.error(`${context.name}: error connecting redis host ${this.redisKey} on ${channel}, pid ${process.pid}:`, err);
if (!this.initialized) {
this.initialized = false;
reject(err);
}
});
}
/**
* Builds and returns redis connection close handler
*
* @access private
* @param {RedisQueue} context
* @param {RedisConnectionChannel} channel
* @return {(...args: any[]) => any}
*/
onCloseHandler(context, channel) {
// istanbul ignore next
return (() => {
this.initialized = false;
this.logger.warn('%s: redis connection %s closed on host %s, pid %s!', context.name, channel, this.redisKey, process.pid);
});
}
/**
* Processes given redis-queue message
*
* @access private
* @param {[any, any]} msg
* @returns {RedisQueue}
*/
process(msg) {
const [queue, data] = msg;
// istanbul ignore next
if (!queue || queue !== this.key) {
return this;
}
try {
const { id, message, from } = this.unpack(data);
this.emit('message', message, id, from);
}
catch (err) {
// istanbul ignore next
this.emitError('OnMessage', 'process error - message is invalid', err);
}
return this;
}
/**
* Returns the number of established watcher connections on redis
*
* @access private
* @returns {Promise<number>}
*/
// istanbul ignore next
async watcherCount() {
if (!this.writer) {
return 0;
}
const rx = new RegExp(`\\bname=${this.options.prefix}:[\\S]+?:watcher:`);
const list = await this.writer.client('LIST');
return list.split(/\r?\n/).filter(client => rx.test(client)).length;
}
/**
* Processes delayed a message by its given redis key
*
* @access private
* @param {string} key
* @returns {Promise<void>}
*/
async processDelayed(key) {
try {
if (this.scripts.moveDelayed.checksum) {
await this.writer.evalsha(this.scripts.moveDelayed.checksum, 2, `${key}:delayed`, key, Date.now());
}
}
catch (err) {
this.emitError('OnProcessDelayed', 'error processing delayed queue', err);
}
}
// istanbul ignore next
/**
* Watch routine
*
* @access private
* @return {Promise<void>}
*/
async processWatch() {
const now = Date.now();
let cursor = '0';
while (true) {
try {
const data = await this.writer.scan(cursor, 'MATCH', `${this.options.prefix}:*:worker:*`, 'COUNT', '1000');
cursor = data.shift();
const keys = data.shift() || [];
await this.processKeys(keys, now);
if (cursor === '0') {
return;
}
}
catch (err) {
this.emitError('OnSafeDelivery', 'safe queue message delivery problem', err);
this.cleanSafeCheckInterval();
return;
}
}
}
// istanbul ignore next
/**
* Process given keys from a message queue
*
* @access private
* @param {string[]} keys
* @param {number} now
* @return {Promise<void>}
*/
async processKeys(keys, now) {
if (!keys.length) {
return;
}
for (const key of keys) {
const kp = key.split(':');
if (Number(kp.pop()) < now) {
continue;
}
await this.writer.rpoplpush(key, `${kp.shift()}:${kp.shift()}`);
}
}
// istanbul ignore next
/**
* Watch message processor
*
* @access private
* @param {...any[]} args
* @return {Promise<void>}
*/
async onWatchMessage(...args) {
try {
const key = (args.pop() || '').split(':');
if (key.pop() !== 'ttl') {
return;
}
key.pop(); // msg id
await this.processDelayed(key.join(':'));
}
catch (err) {
this.emitError('OnWatch', 'watch error', err);
}
}
// istanbul ignore next
/**
* Clears safe check interval
*
* @access private
*/
cleanSafeCheckInterval() {
if (this.safeCheckInterval) {
clearInterval(this.safeCheckInterval);
delete this.safeCheckInterval;
}
}
/**
* Setups watch a process on delayed messages
*
* @access private
* @returns {RedisQueue}
*/
// istanbul ignore next
watch() {
if (!this.watcher || this.watcher.__ready__) {
return this;
}
try {
this.writer.config('SET', 'notify-keyspace-events', 'Ex');
}
catch (err) {
this.emitError('OnConfig', 'events config error', err);
}
this.watcher.on('pmessage', this.onWatchMessage.bind(this));
this.watcher.psubscribe('__keyevent@0__:expired', `${this.options.prefix}:delayed:*`);
// watch for expired unhandled safe queues
if (!this.safeCheckInterval) {
// tslint:disable-next-line:triple-equals no-null-keyword
if (this.options.safeDeliveryTtl != null) {
this.safeCheckInterval = setInterval(async () => {
if (!this.writer) {
this.cleanSafeCheckInterval();
return;
}
if (this.options.safeDelivery) {
await this.processWatch();
}
await this.processCleanup();
}, this.options.safeDeliveryTtl);
}
}
this.watcher.__ready__ = true;
return this;
}
/**
* Cleans up orphaned keys from redis
*
* @access private
* @returns {Promise<RedisQueue | undefined>}
*/
async processCleanup() {
try {
if (!this.options.cleanup) {
return;
}
const filter = new RegExp(this.options.prefix + ':' +
(this.options.cleanupFilter || '*').replace(/\*/g, '.*'), 'i');
const clients = await this.writer.client('LIST');
const connectedKeys = (clients.match(RX_CLIENT_NAME) || [])
.filter((name) => RX_CLIENT_TEST.test(name) && filter.test(name))
.map((name) => name
.replace(/^name=/, '')
.replace(RX_CLIENT_CLEAN, ''))
.filter((name, i, a) => a.indexOf(name) === i);
const keysToRemove = [];
let cursor = '0';
while (true) {
const data = await this.writer.scan(cursor, 'MATCH', `${this.options.prefix}:${this.options.cleanupFilter || '*'}`, 'COUNT', '1000');
cursor = data.shift();
const keys = data.shift() || [];
keysToRemove.push(...keys.filter(key => key !== this.lockKey &&
connectedKeys.every(connectedKey => key.indexOf(connectedKey) === -1)));
if (cursor === '0') {
break;
}
}
if (keysToRemove.length) {
await this.writer.del(...keysToRemove);
}
}
catch (err) {
this.logger.warn('Clean-up error occurred:', err);
}
return this;
}
// noinspection JSUnusedLocalSymbols
/**
* Unreliable but fast way of message handling by the queue
*/
async readUnsafe() {
try {
const key = this.key;
while (true) {
if (!this.reader) {
break;
}
try {
const msg = await this.reader.brpop(key, 0);
if (msg) {
this.process(msg);
}
}
catch (err) {
// istanbul ignore next
if (err.message.match(/Stream connection ended/)) {
break;
}
// istanbul ignore next
// noinspection ExceptionCaughtLocallyJS
throw err;
}
}
}
catch (err) {
// istanbul ignore next
this.emitError('OnReadUnsafe', 'unsafe reader failed', err);
}
}
// noinspection JSUnusedLocalSymbols
/**
* Reliable but slow method of message handling by message queue
*/
async readSafe() {
try {
const key = this.key;
while (true) {
const expire = Date.now() +
Number(this.options.safeDeliveryTtl);
const workerKey = `${key}:worker:${(0, _1.uuid)()}:${expire}`;
if (!this.reader || !this.writer) {
break;
}
try {
await this.reader.brpoplpush(this.key, workerKey, 0);
}
catch (err) {
// istanbul ignore next
break;
}
const msgArr = await this.writer.lrange(workerKey, -1, 1);
if (msgArr.length !== 1) {
// noinspection ExceptionCaughtLocallyJS
throw new Error('Wrong messages count');
}
const msg = msgArr[0];
this.process([key, msg]);
this.writer.del(workerKey);
}
}
catch (err) {
// istanbul ignore next
this.emitError('OnReadSafe', 'safe reader failed', err);
}
}
/**
* Initializes a read process on the redis message queue
*
* @returns {RedisQueue}
*/
read() {
// istanbul ignore next
if (!this.reader) {
this.logger.error(`${this.name}: reader connection is not initialized, pid ${process.pid} on redis host ${this.redisKey}!`);
return this;
}
const readMethod = this.options.safeDelivery
? 'readSafe'
: 'readUnsafe';
process.nextTick(this[readMethod].bind(this));
return this;
}
/**
* Checks if the watcher connection is locked
*
* @access private
* @returns {Promise<boolean>}
*/
async isLocked() {
if (this.writer) {
return Boolean(Number(await this.writer.exists(this.lockKey)));
}
return false;
}
/**
* Locks watcher connection
*
* @access private
* @returns {Promise<boolean>}
*/
async lock() {
if (this.writer) {
return Boolean(Number(await this.writer.setnx(this.lockKey, '')));
}
return false;
}
/**
* Unlocks watcher connection
*
* @access private
* @returns {Promise<boolean>}
*/
async unlock() {
if (this.writer) {
return Boolean(Number(await this.writer.del(this.lockKey)));
}
return false;
}
// istanbul ignore next
/**
* Emits error
*
* @access private
* @param {string} eventName
* @param {string} message
* @param {Error} err
*/
emitError(eventName, message, err) {
this.emit('error', err, eventName);
this.logger.error(`${this.name}: ${message}, pid ${process.pid} on redis host ${this.redisKey}:`, err);
}
/**
* Acquires an owner for watcher connection to this instance of the queue
*
* @returns {Promise<void>}
*/
// istanbul ignore next
async ownWatch() {
const owned = await this.lock();
if (owned) {
for (const script of Object.keys(this.scripts)) {
try {
const checksum = sha1(this.scripts[script].code);
this.scripts[script].checksum = checksum;
const scriptExists = await this.writer.script('EXISTS', checksum);
const loaded = (scriptExists || []).shift();
if (!loaded) {
await this.writer.script('LOAD', this.scripts[script].code);
}
}
catch (err) {
this.emitError('OnScriptLoad', 'script load error', err);
}
}
this.watchOwner = true;
await this.connect('watcher', this.options);
this.watch();
}
}
// istanbul ignore next
/**
* This method returns watcher lock resolver function
*
* @access private
* @param {(...args: any[]) => void} resolve
* @param {(...args: any[]) => void} reject
* @return {() => Promise<any>}
*/
watchLockResolver(resolve, reject) {
return (async () => {
try {
const noWatcher = !await this.watcherCount();
if (await this.isLocked() && noWatcher) {
await this.unlock();
await this.ownWatch();
}
resolve();
}
catch (err) {
reject(err);
}
});
}
/**
* Initializes a single watcher connection across all queues with the same
* prefix.
*
* @returns {Promise<void>}
*/
// istanbul ignore next
async initWatcher() {
return new Promise(async (resolve, reject) => {
try {
if (!await this.watcherCount()) {
await this.ownWatch();
if (this.watchOwner && this.watcher) {
resolve();
}
else {
// check for possible deadlock to resolve
setTimeout(this.watchLockResolver(resolve, reject), intrand(1, 50));
}
}
else {
resolve();
}
}
catch (err) {
this.logger.error(`${this.name}: error initializing watcher, pid ${process.pid} on redis host ${this.redisKey}`, err);
reject(err);
}
});
}
}
exports.RedisQueue = RedisQueue;
/**
* Writer connections collection
*
* @type {{}}
*/
RedisQueue.writers = {};
/**
* Watcher connections collection
*
* @type {{}}
*/
RedisQueue.watchers = {};
__decorate([
(0, _1.profile)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], RedisQueue.prototype, "stop", null);
__decorate([
(0, _1.profile)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], RedisQueue.prototype, "destroy", null);
__decorate([
(0, _1.profile)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], RedisQueue.prototype, "clear", null);
__decorate([
(0, _1.profile)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], RedisQueue.prototype, "destroyWatcher", null);
__decorate([
(0, _1.profile)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], RedisQueue.prototype, "destroyWriter", null);
//# sourceMappingURL=RedisQueue.js.map