UNPKG

@nestjs/microservices

Version:

Nest - modern, fast, powerful node.js web framework (@microservices)

323 lines (322 loc) • 14.3 kB
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ import { createRequire } from 'module'; import { BLOCKED_RMQ_MESSAGE, CONNECTION_FAILED_MESSAGE, DISCONNECTED_RMQ_MESSAGE, NO_MESSAGE_HANDLER, RMQ_SEPARATOR, RMQ_WILDCARD_ALL, RMQ_WILDCARD_SINGLE, RMQ_DEFAULT_IS_GLOBAL_PREFETCH_COUNT, RMQ_DEFAULT_NOACK, RMQ_DEFAULT_NO_ASSERT, RMQ_DEFAULT_PREFETCH_COUNT, RMQ_DEFAULT_QUEUE, RMQ_DEFAULT_QUEUE_OPTIONS, RMQ_DEFAULT_URL, RMQ_NO_EVENT_HANDLER, RMQ_NO_MESSAGE_HANDLER, UNBLOCKED_RMQ_MESSAGE, } from '../constants.js'; import { RmqContext } from '../ctx-host/index.js'; import { Transport } from '../enums/index.js'; import { RmqRecordSerializer } from '../serializers/rmq-record.serializer.js'; import { Server } from './server.js'; import { isNil, isUndefined } from '@nestjs/common/internal'; const INFINITE_CONNECTION_ATTEMPTS = -1; /** * @publicApi */ export class ServerRMQ extends Server { options; transportId = Transport.RMQ; server = null; channel = null; connectionAttempts = 0; urls; queue; noAck; queueOptions; wildcardHandlers = new Map(); pendingEventListeners = []; constructor(options) { super(); this.options = options; this.urls = this.getOptionsProp(this.options, 'urls') || [RMQ_DEFAULT_URL]; this.queue = this.getOptionsProp(this.options, 'queue') || RMQ_DEFAULT_QUEUE; this.noAck = this.getOptionsProp(this.options, 'noAck', RMQ_DEFAULT_NOACK); this.queueOptions = this.getOptionsProp(this.options, 'queueOptions') || RMQ_DEFAULT_QUEUE_OPTIONS; this.loadPackageSynchronously('amqplib', ServerRMQ.name, () => createRequire(import.meta.url)('amqplib')); this.initializeSerializer(options); this.initializeDeserializer(options); } async listen(callback) { try { await this.start(callback); } catch (err) { callback(err); } } async close() { this.channel && (await this.channel.close()); this.server && (await this.server.close()); this.pendingEventListeners = []; } async start(callback) { this.server = await this.createClient(); let listenCallback = callback; const settleListenCallback = (...args) => { const cb = listenCallback; listenCallback = undefined; cb?.(...args); }; this.server.once("connect" /* RmqEventsMap.CONNECT */, () => { if (this.channel) { return; } this._status$.next("connected" /* RmqStatus.CONNECTED */); this.channel = this.server.createChannel({ json: false, setup: (channel) => this.setupChannel(channel, settleListenCallback), }); // The wrapper emits "error" when the setup throws, and an unhandled emit // crashes the process. A later error is only logged: the callback // settles once. this.channel.on("error" /* RmqEventsMap.ERROR */, (err) => { this.logger.error(err); const cb = listenCallback; if (!cb) { return; } listenCallback = undefined; // Otherwise the wrapper re-runs the setup on every reconnect, and the // server would start consuming after "listen()" has failed. void this.close() .catch(closeErr => this.logger.error(closeErr)) .then(() => cb(err)); }); }); const maxConnectionAttempts = this.getOptionsProp(this.options, 'maxConnectionAttempts', INFINITE_CONNECTION_ATTEMPTS); this.registerConnectListener(); this.registerDisconnectListener(); this.registerBlockedListener(); this.registerUnblockedListener(); this.pendingEventListeners.forEach(({ event, callback }) => this.server.on(event, callback)); this.pendingEventListeners = []; const connectFailedEvent = 'connectFailed'; this.server.once(connectFailedEvent, async (error) => { this._status$.next("disconnected" /* RmqStatus.DISCONNECTED */); this.logger.error(CONNECTION_FAILED_MESSAGE); if (error?.err) { this.logger.error(error.err); } const isReconnecting = !!this.channel; if (maxConnectionAttempts === INFINITE_CONNECTION_ATTEMPTS || isReconnecting) { return; } if (++this.connectionAttempts === maxConnectionAttempts) { await this.close(); settleListenCallback(error.err ?? new Error(CONNECTION_FAILED_MESSAGE)); } }); } async createClient() { const rmqPackage = await this.loadPackage('amqp-connection-manager', ServerRMQ.name, () => import('amqp-connection-manager')); const socketOptions = this.getOptionsProp(this.options, 'socketOptions'); return rmqPackage.connect(this.urls, { connectionOptions: socketOptions?.connectionOptions, heartbeatIntervalInSeconds: socketOptions?.heartbeatIntervalInSeconds, reconnectTimeInSeconds: socketOptions?.reconnectTimeInSeconds, }); } registerConnectListener() { this.server.on("connect" /* RmqEventsMap.CONNECT */, (err) => { this._status$.next("connected" /* RmqStatus.CONNECTED */); }); } registerDisconnectListener() { this.server.on("disconnect" /* RmqEventsMap.DISCONNECT */, (err) => { this._status$.next("disconnected" /* RmqStatus.DISCONNECTED */); this.logger.error(DISCONNECTED_RMQ_MESSAGE); this.logger.error(err); }); } registerBlockedListener() { this.server.on("blocked" /* RmqEventsMap.BLOCKED */, ({ reason }) => { this._status$.next("blocked" /* RmqStatus.BLOCKED */); this.logger.warn(BLOCKED_RMQ_MESSAGE(reason)); }); } registerUnblockedListener() { this.server.on("unblocked" /* RmqEventsMap.UNBLOCKED */, () => { this._status$.next("unblocked" /* RmqStatus.UNBLOCKED */); this.logger.log(UNBLOCKED_RMQ_MESSAGE); }); } async setupChannel(channel, callback) { const noAssert = this.getOptionsProp(this.options, 'noAssert') ?? this.queueOptions.noAssert ?? RMQ_DEFAULT_NO_ASSERT; let createdQueue; if (this.queue === RMQ_DEFAULT_QUEUE || !noAssert) { const { queue } = await channel.assertQueue(this.queue, this.queueOptions); createdQueue = queue; } else { createdQueue = this.queue; } const isGlobalPrefetchCount = this.getOptionsProp(this.options, 'isGlobalPrefetchCount', RMQ_DEFAULT_IS_GLOBAL_PREFETCH_COUNT); const prefetchCount = this.getOptionsProp(this.options, 'prefetchCount', RMQ_DEFAULT_PREFETCH_COUNT); if (this.options.exchange || this.options.wildcards) { // Use queue name as exchange name if exchange is not provided and "wildcards" is set to true const exchange = this.getOptionsProp(this.options, 'exchange', this.options.queue); const exchangeType = this.getOptionsProp(this.options, 'exchangeType', 'topic'); await channel.assertExchange(exchange, exchangeType, { durable: true, arguments: this.getOptionsProp(this.options, 'exchangeArguments', {}), }); if (this.options.routingKey || this.options.exchangeType === 'fanout') { await channel.bindQueue(createdQueue, exchange, this.options.exchangeType === 'fanout' ? '' : this.options.routingKey); } if (this.options.wildcards) { const routingKeys = Array.from(this.getHandlers().keys()); await Promise.all(routingKeys.map(routingKey => channel.bindQueue(createdQueue, exchange, routingKey))); // When "wildcards" is set to true, we need to initialize wildcard handlers // otherwise we would not be able to associate the incoming messages with the handlers this.initializeWildcardHandlersIfExist(); } } await channel.prefetch(prefetchCount, isGlobalPrefetchCount); await channel.consume(createdQueue, (msg) => this.handleMessage(msg, channel).catch(err => this.handleError(err)), { noAck: this.noAck, consumerTag: this.getOptionsProp(this.options, 'consumerTag', undefined), }); callback(); } async handleMessage(message, channel) { if (isNil(message)) { return; } const { content, properties } = message; const rawMessage = this.parseMessageContent(content); const packet = await this.deserializer.deserialize(rawMessage, properties); const pattern = this.getPatternAsString(packet.pattern); const rmqContext = new RmqContext([message, channel, pattern]); if (isUndefined(packet.id)) { return this.handleEvent(pattern, packet, rmqContext); } const handler = this.getHandlerByPattern(pattern); if (!handler) { if (!this.noAck) { this.logger.warn(RMQ_NO_MESSAGE_HANDLER `${pattern}`); this.channel.nack(rmqContext.getMessage(), false, false); } const status = 'error'; const noHandlerPacket = { id: packet.id, err: NO_MESSAGE_HANDLER, status, }; await this.sendMessage(noHandlerPacket, properties.replyTo, properties.correlationId, rmqContext); return; } const publish = (data) => this.sendMessage(data, properties.replyTo, properties.correlationId, rmqContext); return this.handleRequest(rmqContext, async () => this.transformToObservable(await handler(packet.data, rmqContext)), publish); } async handleEvent(pattern, packet, context) { const handler = this.getHandlerByPattern(pattern); if (!handler && !this.noAck) { this.channel.nack(context.getMessage(), false, false); return this.logger.warn(RMQ_NO_EVENT_HANDLER `${pattern}`); } return super.handleEvent(pattern, packet, context); } sendMessage(message, replyTo, correlationId, context) { const outgoingResponse = this.serializer.serialize(message); const options = outgoingResponse.options; delete outgoingResponse.options; const buffer = Buffer.from(JSON.stringify(outgoingResponse)); const sendOptions = { ...options, correlationId }; return this.channel.sendToQueue(replyTo, buffer, sendOptions); } unwrap() { if (!this.server) { throw new Error('Not initialized. Please call the "listen"/"startAllMicroservices" method before accessing the server.'); } return this.server; } on(event, callback) { if (this.server) { this.server.addListener(event, callback); } else { this.pendingEventListeners.push({ event, callback }); } } getHandlerByPattern(pattern) { if (!this.options.wildcards) { return super.getHandlerByPattern(pattern); } // Search for non-wildcard handler first const handler = super.getHandlerByPattern(pattern); if (handler) { return handler; } // Search for wildcard handler if (this.wildcardHandlers.size === 0) { return null; } for (const [wildcardPattern, handler] of this.wildcardHandlers) { if (this.matchRmqPattern(wildcardPattern, pattern)) { return handler; } } return null; } initializeSerializer(options) { this.serializer = options?.serializer ?? new RmqRecordSerializer(); } parseMessageContent(content) { try { return JSON.parse(content.toString()); } catch { return content.toString(); } } initializeWildcardHandlersIfExist() { if (this.wildcardHandlers.size !== 0) { return; } const handlers = this.getHandlers(); handlers.forEach((handler, pattern) => { if (typeof pattern !== 'string') { return; } if (pattern.includes(RMQ_WILDCARD_ALL) || pattern.includes(RMQ_WILDCARD_SINGLE)) { this.wildcardHandlers.set(pattern, handler); } }); } matchRmqPattern(pattern, routingKey) { // The broker splits an empty key into no words at all, so "#" and "#.#" // match it const patternSegments = pattern ? pattern.split(RMQ_SEPARATOR) : []; const routingKeySegments = routingKey ? routingKey.split(RMQ_SEPARATOR) : []; const routingKeySegmentsLength = routingKeySegments.length; // matched[j] is true when the pattern segments read so far match // the first j segments of the routing key let matched = [true, ...routingKeySegments.map(() => false)]; for (const currentPattern of patternSegments) { const isWildcardAll = currentPattern === RMQ_WILDCARD_ALL; const next = [isWildcardAll && matched[0]]; for (let j = 1; j <= routingKeySegmentsLength; j++) { if (isWildcardAll) { // "#" matches zero segments (matched[j]) or one more (next[j - 1]) next[j] = matched[j] || next[j - 1]; continue; } next[j] = matched[j - 1] && (currentPattern === RMQ_WILDCARD_SINGLE || currentPattern === routingKeySegments[j - 1]); } if (!next.includes(true)) { return false; } matched = next; } return matched[routingKeySegmentsLength]; } }