UNPKG

@imqueue/core

Version:

Simple JSON-based messaging queue for inter service communication

436 lines 15.1 kB
"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