UNPKG

peanar

Version:

A job queue for Node.js based on RabbitMQ

261 lines (260 loc) 9.02 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)('peanar:broker'); const amqplib_1 = __importDefault(require("amqplib")); const pool_1 = require("./pool"); const exceptions_1 = require("../exceptions"); const consumer_1 = __importDefault(require("./consumer")); function timeout(ms) { return new Promise(res => { setTimeout(res, ms); }); } /** * Peanar's broker adapter */ class NodeAmqpBroker { config; conn; _connectPromise; _channelConsumers = new Map(); pool; constructor(config) { this.config = config; } async _connectAmqp(retry = 1) { debug(`_connectAmqp(${retry})`); try { const c = this.config || {}; const conn = (this.conn = await amqplib_1.default.connect({ hostname: c.connection ? c.connection.host : 'localhost', port: c.connection ? c.connection.port : 5672, username: c.connection ? c.connection.username : 'guest', password: c.connection ? c.connection.password : 'guest', vhost: c.connection ? c.connection.vhost : '/' })); return conn; } catch (ex) { if (ex.code === 'ECONNREFUSED') { await timeout(700 * retry); return this._connectAmqp(retry + 1); } else { console.error(ex); throw ex; } } } /** * Initializes adapter connection and channel */ connect = async () => { if (this._connectPromise) return this._connectPromise; const doConnect = async () => { debug('doConnect()'); const conn = await this._connectAmqp(); this.pool = new pool_1.ChannelPool(conn, this.config.poolSize, this.config.prefetch); await this.pool.open(); if (this._channelConsumers.size > 0) { await this.resurrectAllConsumers(); } this.pool.on('channelLost', (ch) => { this.pauseConsumersOnChannel(ch); }); this.pool.on('channelReplaced', (ch, newCh) => { this.rewireConsumersOnChannel(ch, newCh).catch(ex => { console.error(ex); }); }); conn.on('error', ex => { debug(`AMQP connection error ${ex.code}!`); debug(`Original error message: ${ex.message}`); }); conn.once('close', (err) => { if (err) { debug(err.message); } else { debug('AMQP connection closed.'); } this._connectPromise = undefined; if (err && err.code >= 300) { this.connect(); } else { this.pool.close(); this.pool = undefined; } }); return conn; }; return (this._connectPromise = doConnect()); }; ready() { if (!this._connectPromise) { throw new exceptions_1.PeanarAdapterError('Not connected!'); } return this._connectPromise; } async shutdown() { debug('shutdown()'); if (this.pool) { await this.pool.close(); this.pool = undefined; debug('pool closed.'); } if (this.conn) { this.conn.off('close', this.connect); await this.conn.close(); this._connectPromise = undefined; this._channelConsumers.clear(); this.conn = undefined; debug('connection closed.'); } } async queues(queues) { await this.connect(); if (!this.pool) throw new exceptions_1.PeanarAdapterError('Not connected!'); return await Promise.all(this.pool.mapOver(queues, async (ch, queue) => { return ch.assertQueue(queue.name, { durable: queue.durable, autoDelete: queue.auto_delete, exclusive: queue.exclusive, ...queue.arguments }); })); } async exchanges(exchanges) { await this.connect(); if (!this.pool) throw new exceptions_1.PeanarAdapterError('Not connected!'); return await Promise.all(this.pool.mapOver(exchanges, async (ch, exchange) => { return ch.assertExchange(exchange.name, exchange.type, { durable: exchange.durable, ...exchange.arguments }); })); } async bindings(bindings) { await this.connect(); if (!this.pool) throw new exceptions_1.PeanarAdapterError('Not connected!'); return await Promise.all(this.pool.mapOver(bindings, async (ch, binding) => { return ch.bindQueue(binding.queue, binding.exchange, binding.routing_key); })); } async resurrectAllConsumers() { await Promise.all(this.pool.mapOver([...this._channelConsumers.keys()], (newCh, oldCh) => this.rewireConsumersOnChannel(oldCh, newCh))); } pauseConsumersOnChannel(ch) { const set = this._channelConsumers.get(ch); if (!set || set.size < 1) return; for (const consumer of set) { consumer.pause(); } } async rewireConsumersOnChannel(ch, newCh) { const set = this._channelConsumers.get(ch); if (!set || set.size < 1) return; for (const consumer of set) { const res = await newCh.consume(consumer.queue, (msg) => { if (msg && consumer) { consumer.handleDelivery(msg); } }); consumer.tag = res.consumerTag; consumer.channel = newCh; consumer.resume(); } this._channelConsumers.delete(ch); this._channelConsumers.set(newCh, set); } async _startConsumer(ch, queue) { let consumer = new consumer_1.default(ch, queue); return await ch.consume(queue, (msg) => { if (msg) { consumer.handleDelivery(msg); } }).then((res) => { consumer.tag = res.consumerTag; if (!this._channelConsumers.has(ch)) { this._channelConsumers.set(ch, new Set([consumer])); } else { const set = this._channelConsumers.get(ch); set.add(consumer); } return consumer; }); } consume(queue) { if (!this.pool) throw new exceptions_1.PeanarAdapterError('Not connected!'); return this.pool.acquireAndRun(async (ch) => this._startConsumer(ch, queue)); } consumeOver(queues) { if (!this.pool) throw new exceptions_1.PeanarAdapterError('Not connected!'); return this.pool.mapOver(queues, async (ch, queue) => { return { queue, channel: ch, consumer: await this._startConsumer(ch, queue) }; }); } /** * This method will always try to make a connection * unless `this.pool` is empty. Call with care. */ async publish(message) { await this.connect(); const _doAcquire = async () => { if (!this.pool) throw new exceptions_1.PeanarAdapterError('Not connected!'); try { return await this.pool.acquire(); } catch (ex) { await this.connect(); return _doAcquire(); } }; const _doPublish = async () => { const { channel, release } = await _doAcquire(); debug(`publish to channel`); try { if (channel.publish(message.exchange || '', message.routing_key, Buffer.from(JSON.stringify(message.body)), { contentType: 'application/json', mandatory: message.mandatory, persistent: true, priority: message.properties?.priority })) { release(); return true; } else { channel.once('drain', release); return false; } } catch (ex) { if (ex.message === 'Channel closed') { return _doPublish(); } throw ex; } }; return _doPublish(); } } exports.default = NodeAmqpBroker;