@gabliam/amqp
Version:
amqp plugin for gabliam
384 lines (383 loc) • 14.7 kB
JavaScript
"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;