trade360-nodejs-sdk
Version:
LSports Trade360 SDK for Node.js
178 lines • 7.25 kB
JavaScript
"use strict";
const amqplib_1 = require("amqplib");
const lodash_1 = require("lodash");
const adapters_1 = require("../../logger/adapters");
const errors_1 = require("../../entities/errors");
const _utilities_1 = require("../../utilities");
const message_consumer_1 = require("./message-consumer");
/**
* Class that represent all the abilities of rabbitmq instance
* to consume messages and handle them with all the configured
* desired handlers for entities types and process them with
* the entityHandler call-back function for the entityConstructor
* class type entity type and process them with the entityHandler
* call-back function for the entityConstructor class type entity type
* @implements IFeed interface that represent Feed implementation for
* the feed service to consume messages, process connect creation,
* attach listeners for reconnection and consumption process and
* handle them with all the configured desired handlers for entities
* types
*/
class RabbitMQFeed {
constructor(mqSettings, logger = new adapters_1.ConsoleAdapter()) {
this.mqSettings = mqSettings;
this.logger = logger;
this.requestQueue = 'ReqQueue';
this.isConnected = false;
this.stopTryReconnect = false;
this.reconnectionLock = new _utilities_1.AsyncLock();
this.isReconnecting = false;
this.requestQueue = `_${this.mqSettings.packageId}_`;
this.stopTryReconnect = false;
this.consumer = new message_consumer_1.MessageConsumer(this.logger);
}
setLogger(logger) {
this.logger = logger;
}
getLogger() {
return this.logger;
}
async start() {
await this.connect();
await this.attachListeners();
const { prefetchCount, autoAck, consumptionLatencyThreshold } = this.mqSettings;
// config and define queue prefetch
await this.channel.prefetch(prefetchCount, false);
const isAutoAck = autoAck;
const { consumerTag } = await this.channel.consume(this.requestQueue, async (msg) => {
if (!(0, lodash_1.isNil)(msg) && !(0, lodash_1.isNil)(msg.content)) {
try {
const { content, properties } = msg;
await this.consumer.handleBasicMessage(content, {
messageMqTimestamp: this.getMessageMqTimestamp(properties),
consumptionLatencyThreshold,
});
// Manually acknowledge the processed message
if (!isAutoAck)
await this.channel.ack(msg);
}
catch (err) {
if (!isAutoAck)
await this.channel.nack(msg, false, true);
throw new errors_1.ConsumptionMessageError(`Error processing message, Error: ${err}`);
}
}
}, {
noAck: isAutoAck,
});
this.consumerTag = consumerTag;
}
/**
* establish connection to rabbitmq
*/
async connect() {
if (this.isConnected && this.channel)
return;
const { hostname, port, username, password, vhost } = this.mqSettings;
try {
this.logger.info('initialize connection to RabbitMQ');
// Establish connection to RabbitMQ server
const connectionString = encodeURI(`amqp://${username}:${password}@${hostname}:${port}/${vhost}`);
this.connection = await (0, amqplib_1.connect)(connectionString);
if (!this.connection) {
throw new Error('failed initializing connection!');
}
this.logger.info(`connection initialized successfully!\nconnectionString: ${connectionString}\nListen to ${this.requestQueue} queue`);
// create channel through the connection
this.channel = await this.connection.createChannel();
this.isConnected = true;
}
catch (err) {
this.logger.error(`failed initialize connection to RabbitMQ, Error: ${err}`);
throw err;
}
}
/**
* attach listeners for desire invoked events
*/
async attachListeners() {
this.connection.on('error', await this.connectionErrorHandler.bind(this));
this.connection.on('close', await this.connectionClosedHandler.bind(this));
}
/**
* handle close event invoke for rabbitmq instance
* handle reconnection option
*/
async connectionClosedHandler() {
this.logger.debug('RabbitMQ connection was closed!');
this.isConnected = false;
const { automaticRecoveryEnabled, networkRecoveryIntervalInMs, maxRetryAttempts } = this.mqSettings;
const options = {
maxAttempts: maxRetryAttempts,
delayMs: networkRecoveryIntervalInMs,
backoffFactor: 1,
};
if (this.stopTryReconnect || this.isConnected || !automaticRecoveryEnabled) {
return;
}
// Retry establish connection after a delay
await this.reconnectionLock.acquire();
try {
if (this.isReconnecting)
return; // Already reconnecting, exit to prevent multiple attempts
this.isReconnecting = true;
return await (0, _utilities_1.withRetry)(async () => {
await this.start();
}, options, 'Retry establish connection after a delay', this.logger);
}
finally {
this.isReconnecting = false;
this.reconnectionLock.release();
}
}
/**
* handle error event invoke for rabbitmq instance
* connection error
* @param err error been thrown from the connection
* instance of rabbitmq instance connection error
* event invoke handler method call back function
* @returns void
*/
connectionErrorHandler(err) {
this.logger.error(err.message);
}
/**
* get the message timestamp from the message properties
* @param msgProperties the message properties
* @returns the message timestamp in milliseconds
*/
getMessageMqTimestamp(msgProperties) {
if ((0, lodash_1.isNil)(msgProperties))
return;
const { timestamp, headers } = msgProperties;
const timestampInMs = headers?.timestamp_in_ms;
if (!(0, lodash_1.isNil)(timestampInMs))
return timestampInMs;
if (typeof timestamp === 'number') {
// RabbitMQ timestamps are in seconds, so convert to milliseconds
return timestamp * 1000;
}
else if (timestamp instanceof Date) {
return timestamp.getTime();
}
return;
}
async stop() {
this.stopTryReconnect = true;
if (this.isConnected) {
await this.channel?.cancel(this.consumerTag);
await this.connection?.close();
}
this.logger.info('closed gracefully channel and connection to rabbitMQ!');
}
async addEntityHandler(entityHandler, entityConstructor) {
this.consumer.RegisterEntityHandler(entityHandler, entityConstructor);
}
}
module.exports = RabbitMQFeed;
//# sourceMappingURL=rabbitmq-feed.js.map