UNPKG

@rxap/nest-amqp

Version:
224 lines (223 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 { options; logger; connection$; client = null; channel = null; responseEmitter; constructor(options, logger = new logger_service_1.Logger(microservices_1.ClientProxy.name)) { super(); this.options = options; this.logger = logger; this.initializeSerializer(options); this.initializeDeserializer(options); } isConnected() { return this.client && this.client.isConnected(); } close() { this.channel?.close(); 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;