peanar
Version:
A job queue for Node.js based on RabbitMQ
261 lines (260 loc) • 9.02 kB
JavaScript
"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;