UNPKG

@gabliam/amqp

Version:
384 lines (383 loc) 14.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AmqpConnection = void 0; const tslib_1 = require("tslib"); const core_1 = require("@gabliam/core"); const log4js_1 = require("@gabliam/log4js"); const amqp_connection_manager_1 = require("amqp-connection-manager"); const bluebird_1 = tslib_1.__importDefault(require("bluebird")); const lodash_1 = tslib_1.__importDefault(require("lodash")); const uuid_1 = require("uuid"); const zlib_1 = require("zlib"); const errors_1 = require("./errors"); var ConnectionState; (function (ConnectionState) { ConnectionState[ConnectionState["stopped"] = 0] = "stopped"; ConnectionState[ConnectionState["running"] = 1] = "running"; ConnectionState[ConnectionState["starting"] = 2] = "starting"; ConnectionState[ConnectionState["stopping"] = 3] = "stopping"; })(ConnectionState || (ConnectionState = {})); /** * Amqp Connection */ class AmqpConnection { constructor(indexConfig, name, url, undefinedValue, queues, valueExtractor, gzipEnabled) { this.indexConfig = indexConfig; this.name = name; this.url = url; this.undefinedValue = undefinedValue; this.queues = queues; this.valueExtractor = valueExtractor; this.gzipEnabled = gzipEnabled; this.logger = log4js_1.log4js.getLogger(AmqpConnection.name); this.state = ConnectionState.stopped; this.consumerList = []; this.extractArgs = {}; } /** * Start the connection */ async start() { if (this.state !== ConnectionState.stopped) { return; } this.state = ConnectionState.starting; this.connection = (0, amqp_connection_manager_1.connect)([this.url]); this.channel = this.connection.createChannel({ setup: async (channel) => { for (const queue of this.queues) { // eslint-disable-next-line no-await-in-loop await channel.assertQueue(queue.queueName, queue.queueOptions); } for (const { queueName, handler, options } of this.consumerList) { // eslint-disable-next-line no-await-in-loop await channel.consume(queueName, handler, options); } }, }); await new Promise((resolve, reject) => { const onConnectFailed = (err) => { if (lodash_1.default.get(err, 'err.errno', undefined) === 'ENOTFOUND' || lodash_1.default.get(err, 'err.code', undefined) === 'ENOTFOUND') { this.channel.removeAllListeners('connect'); this.channel.removeAllListeners('error'); reject(err.err); } else { /* istanbul ignore next */ this.logger.error(`Amqp error %O`, err); } }; this.connection.once('connectFailed', onConnectFailed); this.state = ConnectionState.running; const isConnect = () => { this.connection.removeListener('connectFailed', onConnectFailed); resolve(); }; this.channel.once('connect', isConnect); this.channel.once('error', isConnect); }); } /** * Add a consumer for a queue */ addConsume(queue, handler, options) { const queueName = this.getQueueName(queue); if (!this.queueExist(queueName)) { throw new errors_1.AmqpQueueDoesntExistError(queueName); } this.consumerList.push({ queueName, handler, options }); } /** * contrust consumer with controller instance and HandlerMetadata */ constructAndAddConsume(propKey, handlerMetadata, controller) { let consumeHandler; if (handlerMetadata.type === 'Listener') { consumeHandler = this.constructListener(propKey, controller); } else { consumeHandler = this.constructConsumer(propKey, handlerMetadata, controller); } this.addConsume(handlerMetadata.queue, consumeHandler, handlerMetadata.consumeOptions); } /** * Send a content to a queue. * Content can be undefined */ async sendToQueue(queue, content, options) { const queueName = this.getQueueName(queue); const channel = this.getChannel(); await channel.sendToQueue(queueName, await this.contentToBuffer(content), Object.assign({ contentEncoding: this.gzipEnabled ? 'gzip' : undefined, contentType: 'application/json' }, options)); } /** * Send a content to a queue and Ack the message * Content can be undefined */ async sendToQueueAck(queue, content, msg, options) { const queueName = this.getQueueName(queue); const channel = this.getChannel(); if (channel === null) { /* istanbul ignore next */ throw new errors_1.AmqpConnectionError(); } await channel.sendToQueue(queueName, await this.contentToBuffer(content), Object.assign({ contentEncoding: this.gzipEnabled ? 'gzip' : undefined, contentType: 'application/json' }, options)); await this.channel.ack(msg); } /** * Basic RPC pattern with conversion. * Send a Javascrip object converted to a message to a queue and attempt to receive a response, converting that to a Java object. * Implementations will normally set the reply-to header to an exclusive queue and wait up for some time limited by a timeout. */ async sendAndReceive(queue, content, options = {}, timeout = 5000) { let onTimeout = false; let chan; let replyTo; let promise = new bluebird_1.default((resolve, reject) => { const queueName = this.getQueueName(queue); if (!options.correlationId) { // eslint-disable-next-line no-param-reassign options.correlationId = (0, uuid_1.v4)(); } const correlationId = options.correlationId; if (!options.replyTo) { // eslint-disable-next-line no-param-reassign options.replyTo = `amqpSendAndReceive${(0, uuid_1.v4)()}`; } if (!options.expiration) { // eslint-disable-next-line no-param-reassign options.expiration = `${timeout}`; } replyTo = options.replyTo; chan = this.getChannel(); // create new Queue for get the response chan .assertQueue(replyTo, { exclusive: false, autoDelete: true, durable: false, }) .then(() => chan.consume(replyTo, async (msg) => { if (msg === null) { /* istanbul ignore next */ reject(new errors_1.AmqpMessageIsNullError()); return; } if (!onTimeout && msg.properties.correlationId === correlationId) { resolve(await this.parseContent(msg)); } chan.ack(msg); try { await chan.deleteQueue(replyTo); // eslint-disable-next-line no-empty } catch (_a) { } })) .then(async () => { chan.sendToQueue(queueName, await this.contentToBuffer(content), Object.assign({ contentEncoding: this.gzipEnabled ? 'gzip' : undefined, contentType: 'application/json' }, options)); }) // catch when error amqp (untestable) .catch( // prettier-ignore /* istanbul ignore next */ async (err) => { reject(err); if (chan) { try { await chan.deleteQueue(replyTo); // eslint-disable-next-line no-empty } catch (_a) { } } }); }); if (timeout) { promise = promise .timeout(timeout) .catch(bluebird_1.default.TimeoutError, async (e) => { onTimeout = true; if (chan) { try { await chan.deleteQueue(replyTo); // eslint-disable-next-line no-empty } catch (_a) { } } throw new errors_1.AmqpTimeoutError(e.message); }); } return promise; } /** * Stop the connection */ async stop() { if (this.state !== ConnectionState.running) { return; } this.state = ConnectionState.stopping; try { this.channel.removeAllListeners('connect'); this.channel.removeAllListeners('error'); await this.connection.close(); // eslint-disable-next-line no-empty } catch (_a) { } this.state = ConnectionState.stopped; } /** * Test if queue exist */ queueExist(queueName) { for (const queue of this.queues) { if (queue.queueName === queueName) { return true; } } return false; } /** * Get the real queueName * * Search if the queueName is the index of the map of queues => return queueName * Search if the queueName is a key value => return the value * else return the queue passed on parameter */ getQueueName(queueName) { const defaultValue = this.valueExtractor(`"${queueName}"`, queueName); if (this.valueExtractor(`application.amqp[0] ? true : false`, false)) { return this.valueExtractor(`application.amqp[${this.indexConfig}].queues['${queueName}'].queueName`, defaultValue); } return this.valueExtractor(`application.amqp.queues['${queueName}'].queueName`, defaultValue); } /** * Convert content to buffer for send in queue */ async contentToBuffer(content) { let data; if (content === undefined) { data = this.undefinedValue; } else if (content instanceof Buffer) { data = content; } else if (typeof content === 'string') { data = content; } else if (content instanceof Error) { data = JSON.stringify(content, Object.getOwnPropertyNames(content)); } else { data = JSON.stringify(content); } if (this.gzipEnabled) { return new Promise((resolve) => { (0, zlib_1.gzip)(Buffer.from(data), (err, res) => { if (err) { resolve(Buffer.from('')); } else { resolve(res); } }); }); } return Promise.resolve(Buffer.from(data)); } /** * Parse content in message */ async parseContent(msg) { let data; if (this.gzipEnabled) { data = await new Promise((resolve) => { (0, zlib_1.gunzip)(msg.content, (err, res) => { if (err) { resolve(msg.content.toString()); } else { resolve(res.toString()); } }); }); } else { data = msg.content.toString(); } try { data = JSON.parse(data); // eslint-disable-next-line no-empty } catch (_a) { } if (data === this.undefinedValue) { return undefined; } return data; } constructListener(propKey, controller) { return async (msg) => { /* istanbul ignore next */ if (msg === null) { return; } const extractArgs = this.getExtractArgs(propKey, controller); const args = await extractArgs(msg); await (0, core_1.toPromise)(controller[propKey](...args)); await this.channel.ack(msg); }; } constructConsumer(propKey, handlerMetadata, controller) { return async (msg) => { if (msg === null) { /* istanbul ignore next */ return; } // catch when error amqp (untestable) /* istanbul ignore next */ if (msg.properties.replyTo === undefined) { throw new errors_1.AmqpReplytoIsMissingError(); } const extractArgs = this.getExtractArgs(propKey, controller); const args = await extractArgs(msg); let response; let sendOptions; try { response = await (0, core_1.toPromise)(controller[propKey](...args)); sendOptions = handlerMetadata.sendOptions || {}; } catch (err) { response = err; sendOptions = handlerMetadata.sendOptionsError || {}; } this.sendToQueueAck(msg.properties.replyTo, response, msg, Object.assign({ correlationId: msg.properties.correlationId, contentEncoding: this.gzipEnabled ? 'gzip' : undefined, contentType: 'application/json' }, sendOptions)); }; } getExtractArgs(propKey, controller) { const k = `${controller.constructor.name}#${propKey}`; if (this.extractArgs[k]) { return this.extractArgs[k]; } const params = core_1.reflection.parameters(controller.constructor, propKey); if (params.length === 0) { // eslint-disable-next-line no-return-assign return (this.extractArgs[k] = async (msg) => [ await this.parseContent(msg), ]); } const parameters = params.map((meta) => meta.slice(-1)[0]); // eslint-disable-next-line no-return-assign return (this.extractArgs[k] = async (msg) => { const content = await this.parseContent(msg); return parameters.map((p) => p.handler(p.args, msg, content)); }); } getChannel() { // eslint-disable-next-line no-underscore-dangle if (this.channel._channel === null) { throw new errors_1.AmqpConnectionError(); } // eslint-disable-next-line no-underscore-dangle return this.channel._channel; } } exports.AmqpConnection = AmqpConnection;