UNPKG

bb-inspired

Version:

Core library for BB-inspired NestJS backend

289 lines 12.8 kB
"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