UNPKG

@rxap/nest-rabbitmq

Version:

This package provides a NestJS module for integrating with RabbitMQ using exchanges. It offers a client and server implementation for message queuing and supports features like health checks and error serialization. It simplifies the process of setting up

221 lines (220 loc) 9.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClientRMQExchange = exports.REPLY_QUEUE = void 0; const logger_service_1 = require("@nestjs/common/services/logger.service"); const random_string_generator_util_1 = require("@nestjs/common/utils/random-string-generator.util"); const shared_utils_1 = require("@nestjs/common/utils/shared.utils"); const microservices_1 = require("@nestjs/microservices"); const constants_1 = require("@nestjs/microservices/constants"); const serializers_1 = require("@nestjs/microservices/serializers"); const amqp_connection_manager_1 = require("amqp-connection-manager"); const events_1 = require("events"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const incoming_response_deserializer_1 = require("./incoming-response.deserializer"); exports.REPLY_QUEUE = 'amq.rabbitmq.reply-to'; class ClientRMQExchange extends microservices_1.ClientProxy { constructor(options, logger = new logger_service_1.Logger(microservices_1.ClientProxy.name)) { super(); this.options = options; this.logger = logger; this.client = null; this.channel = null; this.initializeSerializer(options); this.initializeDeserializer(options); } isConnected() { return this.client && this.client.isConnected(); } close() { this.channel && this.channel.close(); this.client && this.client.close(); this.channel = null; this.client = null; } connect() { if (this.client) { return this.convertConnectionToPromise(); } this.client = this.createClient(); this.handleError(this.client); this.handleDisconnectError(this.client); this.responseEmitter = new events_1.EventEmitter(); this.responseEmitter.setMaxListeners(0); const connect$ = this.connect$(this.client); const withDisconnect$ = this.mergeDisconnectEvent(this.client, connect$).pipe((0, rxjs_1.tap)(() => this.createChannel())); const withReconnect$ = (0, rxjs_1.fromEvent)(this.client, constants_1.CONNECT_EVENT).pipe((0, rxjs_1.tap)(() => this.logger.log('Connected to RMQ', 'ClientRMQExchange')), (0, operators_1.skip)(1)); const source$ = (0, rxjs_1.merge)(withDisconnect$, withReconnect$); this.connection$ = new rxjs_1.ReplaySubject(1); source$.subscribe({ next: data => { this.connection$.next(data.connection); }, }); return this.convertConnectionToPromise(); } createChannel() { return new Promise(resolve => { this.channel = this.client.createChannel({ json: false, setup: (channel) => this.setupExchange(channel, resolve), }); }); } createClient() { const socketOptions = this.getOptionsProp(this.options, 'socketOptions'); return (0, amqp_connection_manager_1.connect)(this.options.urls, socketOptions); } mergeDisconnectEvent(instance, source$) { const eventToError = (eventType) => (0, rxjs_1.fromEvent)(instance, eventType).pipe((0, operators_1.map)((err) => { this.logger.error(`Error occurred for event type "${eventType}": ${err.message}`, err.stack, 'ClientRMQExchange'); throw err; })); const disconnect$ = eventToError(constants_1.DISCONNECT_EVENT); const urls = this.getOptionsProp(this.options, 'urls', []) ?? []; const connectFailed$ = eventToError(constants_1.CONNECT_FAILED_EVENT).pipe((0, operators_1.retryWhen)(e => e.pipe((0, operators_1.scan)((errorCount, error) => { if (urls.indexOf(error.url) >= urls.length - 1) { throw error; } return errorCount + 1; }, 0)))); // If we ever decide to propagate all disconnect errors & re-emit them through // the "connection" stream then comment out "first()" operator. return (0, rxjs_1.merge)(source$, disconnect$, connectFailed$).pipe((0, operators_1.first)()); } async convertConnectionToPromise() { // try { return await (0, rxjs_1.firstValueFrom)(this.connection$); // } catch (err) { // if (err instanceof EmptyError) { // return; // } // throw err; // } } async setupExchange(channel, resolve) { this.logger.verbose(`Setting up exchange '${this.options.exchange.name}'`, 'ClientRMQExchange'); if (!this.options.noAssert) { await channel.assertExchange(this.options.exchange.name, this.options.exchange.type, this.options.exchange.options); } await this.consumeChannel(channel); resolve(); } async consumeChannel(channel) { const noAck = this.getOptionsProp(this.options, 'noAck', constants_1.RQM_DEFAULT_NOACK); await channel.consume(this.options.replyQueue ?? exports.REPLY_QUEUE, (msg) => { if (msg) { this.responseEmitter.emit(msg.properties.correlationId, msg); } else { this.logger.warn(`Message is empty from RMQ queue ${this.options.replyQueue ?? exports.REPLY_QUEUE}`, 'ClientRMQExchange'); } }, { noAck, }); } handleError(client) { client.addListener(constants_1.ERROR_EVENT, (err) => this.logger.error(err, undefined, 'ClientRMQExchange')); } handleDisconnectError(client) { client.addListener(constants_1.DISCONNECT_EVENT, (err) => { this.logger.error(constants_1.DISCONNECTED_RMQ_MESSAGE, undefined, 'ClientRMQExchange'); this.logger.error(err, undefined, 'ClientRMQExchange'); }); } async handleMessage(packet, optionsOrCallback, callback) { this.logger.verbose('Received message: %JSON', packet, 'ClientRMQExchange'); let options = undefined; if ((0, shared_utils_1.isFunction)(options)) { callback = options; } else { options = optionsOrCallback; } if (!callback) { throw new Error('No callback provided'); } const { err, response, isDisposed, } = await this.deserializer.deserialize(packet, options); // this.logger.verbose('Deserialized response: %JSON', response, 'ClientRMQExchange'); if (err) { this.logger.verbose('Deserialized error: %JSON', err, 'ClientRMQExchange'); } // this.logger.verbose('Deserialized isDisposed: %JSON', isDisposed, 'ClientRMQExchange'); if (isDisposed || err) { callback({ err, response, isDisposed: true, }); } callback({ err, response, }); } publish(message, callback) { this.logger.verbose('Publishing message: %JSON', message, 'ClientRMQExchange'); try { const correlationId = (0, random_string_generator_util_1.randomStringGenerator)(); const listener = ({ content, fields, properties, }) => this.handleMessage(this.parseMessageContent(content), { fields, properties, }, callback); Object.assign(message, { id: correlationId }); const serializedPacket = this.serializer.serialize(message); const options = serializedPacket.options; delete serializedPacket.options; this.responseEmitter.on(correlationId, listener); this.channel .publish(this.options.exchange.name, serializedPacket.pattern, Buffer.from(JSON.stringify(serializedPacket.data)), { replyTo: this.options.replyQueue ?? exports.REPLY_QUEUE, persistent: this.options.persistent, ...options, headers: this.mergeHeaders(options?.headers), correlationId, }) .catch(err => callback({ err })); return () => this.responseEmitter.removeListener(correlationId, listener); } catch (err) { callback({ err }); } return () => void 0; } dispatchEvent(packet) { this.logger.verbose('Dispatching event: %JSON', packet, 'ClientRMQExchange'); const serializedPacket = this.serializer.serialize(packet); const options = serializedPacket.options; delete serializedPacket.options; return new Promise((resolve, reject) => this.channel.publish(this.options.exchange.name, serializedPacket.pattern, Buffer.from(JSON.stringify(serializedPacket.data)), { persistent: this.options.persistent, ...options, headers: this.mergeHeaders(options?.headers), }, (err) => (err ? reject(err) : resolve()))); } initializeSerializer(options) { this.serializer = options.serializer ?? new serializers_1.RmqRecordSerializer(); } initializeDeserializer(options) { this.deserializer = options.deserializer ?? new incoming_response_deserializer_1.IncomingResponseDeserializer(); } mergeHeaders(requestHeaders) { if (!requestHeaders && !this.options?.headers) { return undefined; } return { ...this.options?.headers, ...requestHeaders, }; } parseMessageContent(content) { const rawContent = content.toString(); try { return JSON.parse(rawContent); } catch { return rawContent; } } } exports.ClientRMQExchange = ClientRMQExchange;