@imqueue/core
Version:
Simple JSON-based messaging queue for inter service communication
436 lines • 15.1 kB
JavaScript
"use strict";
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClusteredRedisQueue = void 0;
/*!
* Clustered messaging queue over Redis implementation
*
* 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 events_1 = require("events");
const _1 = require(".");
/**
* Class ClusteredRedisQueue
* Implements the possibility to scale queues horizontally between several
* redis instances.
*/
class ClusteredRedisQueue {
/**
* Class constructor
*
* @constructor
* @param {string} name
* @param {Partial<IMQOptions>} options
* @param {IMQMode} [mode]
*/
constructor(name, options, mode = _1.IMQMode.BOTH) {
var _a, _b;
this.name = name;
/**
* RedisQueue instances collection
*
* @type {RedisQueue[]}
*/
this.imqs = [];
/**
* Cluster servers option definitions
*
* @type {IMessageQueueConnection[]}
*/
// tslint:disable-next-line:completed-docs
this.servers = [];
/**
* Current queue index (round-robin)
*
* @type {number}
*/
this.currentQueue = 0;
// noinspection TypeScriptFieldCanBeMadeReadonly
/**
* Total length of RedisQueue instances
*
* @type {number}
*/
this.queueLength = 0;
this.state = {
started: false,
subscription: null,
};
this.initializedClusters = [];
this.templateEmitter = new events_1.EventEmitter();
this.clusterEmitter = new events_1.EventEmitter();
this.options = (0, _1.buildOptions)(_1.DEFAULT_IMQ_OPTIONS, options);
// istanbul ignore next
this.logger = this.options.logger || console;
if (!this.options.cluster && !((_a = this.options.clusterManagers) === null || _a === void 0 ? void 0 : _a.length)) {
throw new TypeError('ClusteredRedisQueue: cluster ' +
'configuration is missing!');
}
this.mqOptions = Object.assign({}, this.options);
const cluster = [...this.mqOptions.cluster || []];
delete this.mqOptions.cluster;
for (const server of cluster) {
this.addServerWithQueueInitializing(server, false);
}
if ((_b = this.options.clusterManagers) === null || _b === void 0 ? void 0 : _b.length) {
for (const manager of this.options.clusterManagers) {
this.initializedClusters.push(manager.init({
add: this.addServer.bind(this),
remove: this.removeServer.bind(this),
find: this.findServer.bind(this),
}));
}
}
}
/**
* Starts the messaging queue.
* Supposed to be an async function.
*
* @returns {Promise<ClusteredRedisQueue>}
*/
async start() {
this.state.started = true;
return await this.batch('start', 'Starting clustered redis message queue...');
}
/**
* Stops the queue (should stop handling queue messages).
* Supposed to be an async function.
*
* @returns {Promise<ClusteredRedisQueue>}
*/
async stop() {
this.state.started = false;
return await this.batch('stop', 'Stopping clustered redis message queue...');
}
/**
* Sends a message to given queue name with the given data.
* Supposed to be an async function.
*
* @param {string} toQueue - queue name to which message should be sent to
* @param {JsonObject} message - message data
* @param {number} [delay] - if specified, a message will be handled in the
* target queue after a specified period of time in milliseconds.
* @param {(err: Error) => void} [errorHandler] - callback called only when
* internal error occurs during message send execution.
* @returns {Promise<string>} - message identifier
*/
async send(toQueue, message, delay, errorHandler) {
if (!this.queueLength) {
return await new Promise(resolve => this.clusterEmitter.once('initialized', async ({ imq }) => {
resolve(await imq.send(toQueue, message, delay, errorHandler));
}));
}
if (this.currentQueue >= this.queueLength) {
this.currentQueue = 0;
}
const imq = this.imqs[this.currentQueue];
const id = await imq.send(toQueue, message, delay, errorHandler);
this.currentQueue++;
return id;
}
/**
* Safely destroys the current queue, unregistered all set event
* listeners and connections.
* Supposed to be an async function.
*
* @returns {Promise<void>}
*/
async destroy() {
var _a, e_1, _b, _c, _d, e_2, _e, _f;
var _g;
this.state.started = false;
await this.batch('destroy', 'Destroying clustered redis message queue...');
if (!((_g = this.options.clusterManagers) === null || _g === void 0 ? void 0 : _g.length)) {
return;
}
try {
for (var _h = true, _j = __asyncValues(this.options.clusterManagers), _k; _k = await _j.next(), _a = _k.done, !_a; _h = true) {
_c = _k.value;
_h = false;
const manager = _c;
try {
for (var _l = true, _m = (e_2 = void 0, __asyncValues(this.initializedClusters)), _o; _o = await _m.next(), _d = _o.done, !_d; _l = true) {
_f = _o.value;
_l = false;
const cluster = _f;
await manager.remove(cluster);
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (!_l && !_d && (_e = _m.return)) await _e.call(_m);
}
finally { if (e_2) throw e_2.error; }
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_h && !_a && (_b = _j.return)) await _b.call(_j);
}
finally { if (e_1) throw e_1.error; }
}
}
// noinspection JSUnusedGlobalSymbols
/**
* Clears queue data in queue host application.
* Supposed to be an async function.
*
* @returns {Promise<ClusteredRedisQueue>}
*/
async clear() {
return await this.batch('clear', 'Clearing clustered redis message queue...');
}
/**
* Batch imq action processing on all registered imqs at once
*
* @access private
* @param {string} action
* @param {string} message
* @return {Promise<this>}
*/
async batch(action, message) {
this.logger.log(message);
const promises = [];
for (const imq of this.imqs) {
promises.push(imq[action]());
}
await Promise.all(promises);
return this;
}
/* tslint:disable */
// EventEmitter interface
// istanbul ignore next
on(...args) {
for (const imq of this.eventEmitters()) {
imq.on.apply(imq, args);
}
return this;
}
// istanbul ignore next
// noinspection JSUnusedGlobalSymbols
off(...args) {
for (const imq of this.eventEmitters()) {
imq.off.apply(imq, args);
}
return this;
}
// istanbul ignore next
once(...args) {
for (const imq of this.eventEmitters()) {
imq.once.apply(imq, args);
}
return this;
}
// istanbul ignore next
addListener(...args) {
for (const imq of this.eventEmitters()) {
imq.addListener.apply(imq, args);
}
return this;
}
// istanbul ignore next
removeListener(...args) {
for (const imq of this.eventEmitters()) {
imq.removeListener.apply(imq, args);
}
return this;
}
// istanbul ignore next
removeAllListeners(...args) {
for (const imq of this.eventEmitters()) {
imq.removeAllListeners.apply(imq, args);
}
return this;
}
// istanbul ignore next
prependListener(...args) {
for (const imq of this.eventEmitters()) {
imq.prependListener.apply(imq, args);
}
return this;
}
// istanbul ignore next
prependOnceListener(...args) {
for (const imq of this.eventEmitters()) {
imq.prependOnceListener.apply(imq, args);
}
return this;
}
// istanbul ignore next
setMaxListeners(...args) {
for (const imq of this.eventEmitters()) {
imq.setMaxListeners.apply(imq, args);
}
return this;
}
// istanbul ignore next
listeners(...args) {
let listeners = [];
for (const imq of this.eventEmitters()) {
listeners = listeners.concat(imq.listeners.apply(imq, args));
}
return listeners;
}
// istanbul ignore next
rawListeners(...args) {
let rawListeners = [];
for (const imq of this.eventEmitters()) {
rawListeners = rawListeners.concat(imq.rawListeners.apply(imq, args));
}
return rawListeners;
}
// istanbul ignore next
getMaxListeners() {
return this.templateEmitter.getMaxListeners();
}
// istanbul ignore next
emit(...args) {
for (const imq of this.eventEmitters()) {
imq.emit.apply(imq, args);
}
return true;
}
// istanbul ignore next
eventNames(...args) {
return this.templateEmitter.eventNames.apply(this.imqs[0], args);
}
// istanbul ignore next
listenerCount(...args) {
return this.templateEmitter.listenerCount.apply(this.imqs[0], args);
}
// istanbul ignore next
async publish(data, toName) {
const promises = [];
for (const imq of this.imqs) {
promises.push(imq.publish(data, toName));
}
await Promise.all(promises);
}
// istanbul ignore next
async subscribe(channel, handler) {
this.state.subscription = { channel, handler };
const promises = [];
for (const imq of this.imqs) {
promises.push(imq.subscribe(channel, handler));
}
await Promise.all(promises);
}
// istanbul ignore next
async unsubscribe() {
this.state.subscription = null;
const promises = [];
for (const imq of this.imqs) {
promises.push(imq.unsubscribe());
}
await Promise.all(promises);
}
/**
* Adds new servers to the cluster
*
* @param {IServerInput} server
* @returns {void}
*/
addServer(server) {
return this.addServerWithQueueInitializing(server, true);
}
/**
* Removes server from the cluster
*
* @param {IServerInput} server
* @returns {void}
*/
removeServer(server) {
const remove = this.findServer(server, true);
if (!remove) {
return;
}
if (remove.imq) {
this.imqs = this.imqs.filter(imq => remove.imq !== imq);
remove.imq.destroy().catch();
}
this.clusterEmitter.emit('remove', {
server: remove,
imq: remove.imq,
});
this.queueLength = this.imqs.length;
this.servers = this.servers.filter(existing => !ClusteredRedisQueue.matchServers(existing, server));
}
addServerWithQueueInitializing(server, initializeQueue = true) {
const newServer = {
id: server.id,
host: server.host,
port: server.port,
};
const opts = Object.assign(Object.assign({}, this.mqOptions), newServer);
const imq = new _1.RedisQueue(this.name, opts);
if (initializeQueue) {
this.initializeQueue(imq).then(() => {
this.clusterEmitter.emit('initialized', {
server: newServer,
imq,
});
});
}
newServer.imq = imq;
this.imqs.push(imq);
this.servers.push(newServer);
this.clusterEmitter.emit('add', { server: newServer, imq });
this.queueLength = this.imqs.length;
}
eventEmitters() {
return [...this.imqs, this.templateEmitter];
}
async initializeQueue(imq) {
(0, _1.copyEventEmitter)(this.templateEmitter, imq);
if (this.state.started) {
await imq.start();
}
if (this.state.subscription) {
await imq.subscribe(this.state.subscription.channel, this.state.subscription.handler);
}
}
findServer(server, strict = false) {
return this.servers.find(existing => ClusteredRedisQueue.matchServers(existing, server, strict));
}
static matchServers(source, target, strict = false) {
const sameAddress = target.host === source.host
&& target.port === source.port;
if (!target.id && !source.id) {
return sameAddress;
}
const sameId = target.id === source.id;
if (strict) {
return sameId && sameAddress;
}
return sameId || sameAddress;
}
}
exports.ClusteredRedisQueue = ClusteredRedisQueue;
//# sourceMappingURL=ClusteredRedisQueue.js.map