bb-inspired
Version:
Core library for BB-inspired NestJS backend
289 lines • 12.8 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var RabbitMQService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RabbitMQService = void 0;
const common_1 = require("@nestjs/common");
const amqplib_1 = require("amqplib");
const uuid_1 = require("uuid");
const logger_1 = require("../../utils/logger");
let RabbitMQService = RabbitMQService_1 = class RabbitMQService {
constructor(options) {
this.options = options;
this.logger = new logger_1.AppLogger(RabbitMQService_1.name);
this.handlers = new Map();
this.isConnected = false;
this.connectionRetries = 0;
this.maxRetries = 5;
}
getConnectionStatus() {
return this.isConnected;
}
async onModuleInit() {
try {
await this.connect();
}
catch (error) {
this.logger.error(`Failed to connect to RabbitMQ: ${error.message}`, error.stack);
}
}
async onModuleDestroy() {
await this.disconnect();
}
async connect() {
var _a;
try {
this.connection = await (0, amqplib_1.connect)(this.options.url, this.options.connectionOptions);
this.connection.on('error', this.handleConnectionError.bind(this));
this.connection.on('close', this.handleConnectionClose.bind(this));
this.channel = await this.connection.createChannel();
if ((_a = this.options.consumer) === null || _a === void 0 ? void 0 : _a.prefetch) {
await this.channel.prefetch(this.options.consumer.prefetch);
}
if (this.options.defaultExchange) {
await this.createExchange(this.options.defaultExchange, 'topic', { durable: true });
}
if (this.options.deadLetterExchange) {
await this.createExchange(this.options.deadLetterExchange, 'fanout', { durable: true });
await this.createQueue(this.options.deadLetterQueue || 'dead-letter-queue', {
durable: true,
arguments: {
'x-message-ttl': 1000 * 60 * 60 * 24
}
});
await this.bindQueue(this.options.deadLetterQueue || 'dead-letter-queue', this.options.deadLetterExchange, '#');
}
this.isConnected = true;
this.connectionRetries = 0;
this.logger.log('Connected to RabbitMQ server');
await this.restoreSubscriptions();
}
catch (error) {
this.connectionRetries++;
if (this.connectionRetries <= this.maxRetries) {
this.logger.warn(`RabbitMQ connection attempt ${this.connectionRetries} failed, retrying in ${this.connectionRetries * 2} seconds...`);
setTimeout(() => this.connect(), this.connectionRetries * 2000);
}
else {
this.logger.error(`Failed to connect to RabbitMQ after ${this.maxRetries} attempts: ${error.message}`, error.stack);
throw error;
}
}
}
async disconnect() {
try {
if (this.channel) {
await this.channel.close();
}
if (this.connection) {
await this.connection.close();
}
this.isConnected = false;
this.logger.log('Disconnected from RabbitMQ server');
}
catch (error) {
this.logger.error(`Error disconnecting from RabbitMQ: ${error.message}`, error.stack);
}
}
async createExchange(name, type = 'topic', options = { durable: true }) {
if (!this.isConnected) {
throw new Error('Not connected to RabbitMQ');
}
await this.channel.assertExchange(name, type, options);
this.logger.verbose(`Created exchange ${name} of type ${type}`);
}
async createQueue(name, options = { durable: true }) {
if (!this.isConnected) {
throw new Error('Not connected to RabbitMQ');
}
await this.channel.assertQueue(name, options);
this.logger.verbose(`Created queue ${name}`);
}
async bindQueue(queue, exchange, routingKey) {
if (!this.isConnected) {
throw new Error('Not connected to RabbitMQ');
}
await this.channel.bindQueue(queue, exchange, routingKey);
this.logger.verbose(`Bound queue ${queue} to exchange ${exchange} with routing key ${routingKey}`);
}
async publish(routingKey, message) {
await this.publishWithOptions(routingKey, message, {});
}
async publishToMany(routingKeys, message) {
var _a, _b;
if (!this.isConnected) {
throw new Error('Not connected to RabbitMQ');
}
const completeMessage = {
id: (0, uuid_1.v4)(),
timestamp: new Date(),
...message,
};
const content = Buffer.from(JSON.stringify(completeMessage));
const exchange = this.options.defaultExchange || '';
for (const routingKey of routingKeys) {
await this.channel.publish(exchange, routingKey, content, {
persistent: ((_a = this.options.producer) === null || _a === void 0 ? void 0 : _a.persistent) !== false,
mandatory: ((_b = this.options.producer) === null || _b === void 0 ? void 0 : _b.mandatory) === true,
});
}
this.logger.verbose(`Published message to ${routingKeys.length} routing keys: ${routingKeys.join(', ')}`, { metadata: { type: message.type } });
}
async publishWithOptions(routingKey, message, options) {
var _a, _b;
if (!this.isConnected) {
throw new Error('Not connected to RabbitMQ');
}
const completeMessage = {
id: (0, uuid_1.v4)(),
timestamp: new Date(),
...message,
};
const content = Buffer.from(JSON.stringify(completeMessage));
const exchange = this.options.defaultExchange || '';
await this.channel.publish(exchange, routingKey, content, {
persistent: ((_a = this.options.producer) === null || _a === void 0 ? void 0 : _a.persistent) !== false,
mandatory: ((_b = this.options.producer) === null || _b === void 0 ? void 0 : _b.mandatory) === true,
...options,
});
this.logger.verbose(`Published message to ${exchange || 'default exchange'}:${routingKey}`, { metadata: { type: message.type, id: completeMessage.id } });
}
async subscribe(routingKey, handler) {
var _a;
if (!this.isConnected) {
throw new Error('Not connected to RabbitMQ');
}
if (!this.handlers.has(routingKey)) {
this.handlers.set(routingKey, new Set());
}
this.handlers.get(routingKey).add(handler);
const queueName = `${this.options.defaultQueue || 'app'}.${handler.messageType}`;
await this.createQueue(queueName, {
durable: true,
arguments: this.options.deadLetterExchange
? { 'x-dead-letter-exchange': this.options.deadLetterExchange }
: undefined,
});
await this.bindQueue(queueName, this.options.defaultExchange || '', routingKey);
await this.channel.consume(queueName, this.createMessageHandler(handler, queueName), { noAck: ((_a = this.options.consumer) === null || _a === void 0 ? void 0 : _a.noAck) === true });
this.logger.verbose(`Subscribed to ${routingKey} with handler for ${handler.messageType}`, { metadata: { queue: queueName } });
}
async unsubscribe(routingKey, handler) {
if (!this.handlers.has(routingKey)) {
return;
}
if (handler) {
this.handlers.get(routingKey).delete(handler);
if (this.handlers.get(routingKey).size === 0) {
this.handlers.delete(routingKey);
}
}
else {
this.handlers.delete(routingKey);
}
this.logger.verbose(`Unsubscribed from ${routingKey}`);
}
createMessageHandler(handler, queueName) {
return async (msg) => {
var _a, _b;
if (!msg) {
return;
}
try {
const content = msg.content.toString();
const message = JSON.parse(content);
if (message.type !== handler.messageType) {
this.channel.ack(msg);
return;
}
if (handler.filter && !handler.filter(message)) {
this.channel.ack(msg);
return;
}
await handler.handle(message);
if (!((_a = this.options.consumer) === null || _a === void 0 ? void 0 : _a.noAck)) {
this.channel.ack(msg);
}
this.logger.verbose(`Successfully processed message ${message.id} of type ${message.type}`, { metadata: { queue: queueName } });
}
catch (error) {
this.logger.error(`Error processing message: ${error.message}`, error.stack, { metadata: { queue: queueName } });
if (!((_b = this.options.consumer) === null || _b === void 0 ? void 0 : _b.noAck)) {
const requeue = this.shouldRequeueMessage(msg);
this.channel.nack(msg, false, requeue);
if (!requeue) {
this.logger.warn(`Message exceeded retry limit and was sent to dead letter queue`, { metadata: { queue: queueName } });
}
}
}
};
}
shouldRequeueMessage(msg) {
if (!this.options.retryOptions || !this.options.retryOptions.maxRetries) {
return false;
}
const headers = msg.properties.headers || {};
const deaths = headers['x-death'] || [];
const deathCount = deaths.length;
return deathCount < this.options.retryOptions.maxRetries;
}
handleConnectionError(error) {
this.isConnected = false;
this.logger.error(`RabbitMQ connection error: ${error.message}`, error.stack);
}
handleConnectionClose() {
this.isConnected = false;
this.logger.warn('RabbitMQ connection closed, attempting to reconnect...');
setTimeout(() => this.connect(), 5000);
}
async restoreSubscriptions() {
for (const [routingKey, handlers] of this.handlers.entries()) {
for (const handler of handlers) {
try {
await this.subscribe(routingKey, handler);
}
catch (error) {
this.logger.error(`Failed to restore subscription for ${routingKey}: ${error.message}`, error.stack);
}
}
}
}
async pause() {
if (this.isConnected && this.channel) {
await this.channel.close();
this.logger.log('Paused message consumption');
}
}
async resume() {
var _a;
if (this.isConnected && this.connection) {
this.channel = await this.connection.createChannel();
if ((_a = this.options.consumer) === null || _a === void 0 ? void 0 : _a.prefetch) {
await this.channel.prefetch(this.options.consumer.prefetch);
}
await this.restoreSubscriptions();
this.logger.log('Resumed message consumption');
}
else {
await this.connect();
}
}
};
exports.RabbitMQService = RabbitMQService;
exports.RabbitMQService = RabbitMQService = RabbitMQService_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Inject)('MESSAGING_OPTIONS')),
__metadata("design:paramtypes", [Object])
], RabbitMQService);
//# sourceMappingURL=rabbitmq.service.js.map