@golevelup/nestjs-rabbitmq
Version:
Badass RabbitMQ addons for NestJS
592 lines • 28 kB
JavaScript
;
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AmqpConnection = void 0;
const common_1 = require("@nestjs/common");
const amqp_connection_manager_1 = require("amqp-connection-manager");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const crypto_1 = require("crypto");
const __1 = require("..");
const errorBehaviors_1 = require("./errorBehaviors");
const handlerResponses_1 = require("./handlerResponses");
const lodash_1 = require("lodash");
const utils_1 = require("./utils");
const DIRECT_REPLY_QUEUE = 'amq.rabbitmq.reply-to';
const defaultConfig = {
name: 'default',
prefetchCount: 10,
defaultExchangeType: 'topic',
defaultRpcErrorHandler: (0, errorBehaviors_1.getHandlerForLegacyBehavior)(errorBehaviors_1.MessageHandlerErrorBehavior.REQUEUE),
defaultSubscribeErrorBehavior: errorBehaviors_1.MessageHandlerErrorBehavior.REQUEUE,
exchanges: [],
exchangeBindings: [],
queues: [],
defaultRpcTimeout: 10000,
connectionInitOptions: {
wait: true,
timeout: 5000,
reject: true,
skipConnectionFailedLogging: false,
skipDisconnectFailedLogging: false,
},
connectionManagerOptions: {},
registerHandlers: true,
enableDirectReplyTo: true,
channels: {},
handlers: {},
defaultHandler: '',
enableControllerDiscovery: false,
};
class AmqpConnection {
constructor(config) {
this.messageSubject = new rxjs_1.Subject();
this.initialized = new rxjs_1.Subject();
this._managedChannels = {};
this._channels = {};
this._consumers = {};
this.outstandingMessageProcessing = new Set();
this.config = Object.assign(Object.assign({ deserializer: (message) => JSON.parse(message.toString()), serializer: (value) => Buffer.from(JSON.stringify(value)), logger: (config === null || config === void 0 ? void 0 : config.logger) || new common_1.Logger(AmqpConnection.name) }, defaultConfig), config);
this.logger = this.config.logger;
}
get channel() {
if (!this._channel)
throw new Error('channel is not available');
return this._channel;
}
get connection() {
if (!this._connection)
throw new Error('connection is not available');
return this._connection;
}
get managedChannel() {
return this._managedChannel;
}
get managedConnection() {
return this._managedConnection;
}
get configuration() {
return this.config;
}
get channels() {
return this._channels;
}
get managedChannels() {
return this._managedChannels;
}
get connected() {
return this._managedConnection.isConnected();
}
async init() {
const options = Object.assign(Object.assign({}, defaultConfig.connectionInitOptions), this.config.connectionInitOptions);
const { skipConnectionFailedLogging, skipDisconnectFailedLogging, wait, timeout: timeoutInterval, reject, } = options;
const p = this.initCore(wait, skipConnectionFailedLogging, skipDisconnectFailedLogging);
if (!wait) {
this.logger.log(`Skipping connection health checks as 'wait' is disabled. The application will proceed without verifying a healthy RabbitMQ connection.`);
return p;
}
return (0, rxjs_1.lastValueFrom)(this.initialized.pipe((0, operators_1.take)(1), (0, operators_1.timeout)({
each: timeoutInterval,
with: () => (0, rxjs_1.throwError)(() => new Error(`Failed to connect to a RabbitMQ broker within a timeout of ${timeoutInterval}ms`)),
}), (0, operators_1.catchError)((err) => (reject ? (0, rxjs_1.throwError)(() => err) : rxjs_1.EMPTY))));
}
async initCore(wait = false, skipConnectionFailedLogging = false, skipDisconnectFailedLogging = false) {
this.logger.log(`Trying to connect to RabbitMQ broker (${this.config.name})`);
this._managedConnection = (0, amqp_connection_manager_1.connect)(Array.isArray(this.config.uri) ? this.config.uri : [this.config.uri], this.config.connectionManagerOptions);
this._managedConnection.on('connect', ({ connection }) => {
this._connection = connection;
this.logger.log(`Successfully connected to RabbitMQ broker (${this.config.name})`);
});
// Logging disconnections should only be able if consumers
// do not skip it. We may be able to merge with the `skipConnectionFailedLogging`
// option in the future.
if (!skipDisconnectFailedLogging) {
this._managedConnection.on('disconnect', ({ err }) => {
this.logger.error(`Disconnected from RabbitMQ broker (${this.config.name})`, err === null || err === void 0 ? void 0 : err.stack);
});
}
// Certain consumers might want to skip "connectionFailed" logging
// therefore this option will allow us to conditionally register this event consumption
if (!skipConnectionFailedLogging) {
this._managedConnection.on('connectFailed', ({ err }) => {
var _a, _b;
const message = `Connection Failed: Unable to establish a connection to the broker (${this.config.name}). Check the broker's availability, network connectivity, and configuration.`;
if (!wait) {
// Lower the log severity if 'wait' is disabled, as the application continues to function.
this.logger.warn(message);
if (err === null || err === void 0 ? void 0 : err.stack) {
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Stack trace: ${err.stack}`);
}
}
else {
// Log as an error if 'wait' is enabled, as this impacts the connection health.
this.logger.error(message, err === null || err === void 0 ? void 0 : err.stack);
}
});
}
const defaultChannel = {
name: AmqpConnection.name,
config: {
prefetchCount: this.config.prefetchCount,
default: true,
},
};
await Promise.all([
Promise.all(Object.keys(this.config.channels).map(async (channelName) => {
const config = this.config.channels[channelName];
// Only takes the first channel specified as default so other ones get created.
if (defaultChannel.name === AmqpConnection.name && config.default) {
defaultChannel.name = channelName;
defaultChannel.config.prefetchCount =
config.prefetchCount || this.config.prefetchCount;
return;
}
return this.setupManagedChannel(channelName, Object.assign(Object.assign({}, config), { default: false }));
})),
this.setupManagedChannel(defaultChannel.name, defaultChannel.config),
]);
}
async setupInitChannel(channel, name, config) {
this._channels[name] = channel;
await channel.prefetch(config.prefetchCount || this.config.prefetchCount);
if (config.default) {
this._channel = channel;
// Always assert exchanges & rpc queue in default channel.
await Promise.all(this.config.exchanges.map((x) => {
const { createExchangeIfNotExists = true } = x;
if (createExchangeIfNotExists) {
return channel.assertExchange(x.name, x.type || this.config.defaultExchangeType, x.options);
}
return channel.checkExchange(x.name);
}));
await Promise.all(this.config.exchangeBindings.map((exchangeBinding) => channel.bindExchange(exchangeBinding.destination, exchangeBinding.source, exchangeBinding.pattern, exchangeBinding.args)));
await this.setupQueuesWithBindings(channel, this.config.queues);
if (this.config.enableDirectReplyTo) {
await this.initDirectReplyQueue(channel);
}
this.initialized.next();
}
}
async setupQueuesWithBindings(channel, queues) {
await Promise.all(queues.map(async (configuredQueue) => {
const { name, options, bindQueueArguments } = configuredQueue, rest = __rest(configuredQueue, ["name", "options", "bindQueueArguments"]);
const queueOptions = Object.assign(Object.assign({}, options), (bindQueueArguments !== undefined && { bindQueueArguments }));
await this.setupQueue(Object.assign(Object.assign(Object.assign({}, rest), (name !== undefined && { queue: name })), { queueOptions }), channel);
}));
}
async initDirectReplyQueue(channel) {
// Set up a consumer on the Direct Reply-To queue to facilitate RPC functionality
await channel.consume(DIRECT_REPLY_QUEUE, (msg) => {
var _a, _b, _c;
if (msg == null) {
return;
}
// Check that the Buffer has content, before trying to parse it
const message = msg.content.length > 0
? this.config.deserializer(msg.content, msg)
: undefined;
const correlationMessage = {
correlationId: msg.properties.correlationId.toString(),
requestId: (_c = (_b = (_a = msg.properties) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b['X-Request-ID']) === null || _c === void 0 ? void 0 : _c.toString(),
message: message,
};
this.messageSubject.next(correlationMessage);
}, {
noAck: true,
});
}
async request(requestOptions) {
var _a;
const correlationId = requestOptions.correlationId || (0, crypto_1.randomUUID)();
const requestId = (_a = requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.headers) === null || _a === void 0 ? void 0 : _a['X-Request-ID'];
const timeout = requestOptions.timeout || this.config.defaultRpcTimeout;
const payload = requestOptions.payload || {};
const response$ = this.messageSubject.pipe((0, operators_1.filter)((x) => requestId
? x.correlationId === correlationId && x.requestId === requestId
: x.correlationId === correlationId), (0, operators_1.map)((x) => x.message), (0, operators_1.first)());
const timeout$ = (0, rxjs_1.interval)(timeout).pipe((0, operators_1.first)(), (0, operators_1.map)(() => {
throw new Error(`Failed to receive response within timeout of ${timeout}ms for exchange "${requestOptions.exchange}" and routing key "${requestOptions.routingKey}"`);
}));
const result = (0, rxjs_1.lastValueFrom)((0, rxjs_1.race)(response$, timeout$));
await this.publish(requestOptions.exchange, requestOptions.routingKey, payload, Object.assign(Object.assign({}, requestOptions.publishOptions), { replyTo: DIRECT_REPLY_QUEUE, correlationId, headers: requestOptions.headers, expiration: requestOptions.expiration }));
return result;
}
async createSubscriber(handler, msgOptions, originalHandlerName, consumeOptions) {
return this.consumerFactory(msgOptions, (channel) => this.setupSubscriberChannel(handler, msgOptions, channel, originalHandlerName, consumeOptions));
}
async createBatchSubscriber(handler, msgOptions, consumeOptions) {
return this.consumerFactory(msgOptions, (channel) => this.setupBatchSubscriberChannel(handler, msgOptions, channel, consumeOptions));
}
async consumerFactory(msgOptions, setupFunction) {
return new Promise((res) => {
var _a;
this.selectManagedChannel((_a = msgOptions === null || msgOptions === void 0 ? void 0 : msgOptions.queueOptions) === null || _a === void 0 ? void 0 : _a.channel).addSetup(async (channel) => {
const consumerTag = await setupFunction(channel);
res({ consumerTag });
});
});
}
/**
* Wrap a consumer with logic that tracks the outstanding message processing to
* be able to wait for them on shutdown.
*/
wrapConsumer(consumer) {
return (msg) => {
const messageProcessingPromise = Promise.resolve(consumer(msg));
this.outstandingMessageProcessing.add(messageProcessingPromise);
messageProcessingPromise.finally(() => this.outstandingMessageProcessing.delete(messageProcessingPromise));
};
}
async setupSubscriberChannel(handler, msgOptions, channel, originalHandlerName = 'unknown', consumeOptions) {
const queue = await this.setupQueue(msgOptions, channel);
const { consumerTag } = await channel.consume(queue, this.wrapConsumer(async (msg) => {
try {
if ((0, lodash_1.isNull)(msg)) {
throw new Error('Received null message');
}
const result = this.deserializeMessage(msg, msgOptions);
const response = await handler(result.message, msg, result.headers);
if (response instanceof handlerResponses_1.Nack) {
channel.nack(msg, false, response.requeue);
return;
}
// developers should be responsible to avoid subscribers that return therefore
// the request will be acknowledged
if (response) {
this.logger.warn(`Received response: [${this.config.serializer(response)}] from subscribe handler [${originalHandlerName}]. Subscribe handlers should only return void`);
}
channel.ack(msg);
}
catch (e) {
if ((0, lodash_1.isNull)(msg)) {
return;
}
else {
const errorHandler = msgOptions.errorHandler ||
(0, errorBehaviors_1.getHandlerForLegacyBehavior)(msgOptions.errorBehavior ||
this.config.defaultSubscribeErrorBehavior);
await errorHandler(channel, msg, e);
}
}
}), consumeOptions);
this.registerConsumerForQueue({
type: 'subscribe',
consumerTag,
handler,
msgOptions,
channel,
});
return consumerTag;
}
async setupBatchSubscriberChannel(handler, msgOptions, channel, consumeOptions) {
var _a, _b;
let batchSize = (_a = msgOptions.batchOptions) === null || _a === void 0 ? void 0 : _a.size;
let batchTimeout = (_b = msgOptions.batchOptions) === null || _b === void 0 ? void 0 : _b.timeout;
let batchMsgs = [];
let batchTimer;
let inflightBatchHandler;
// Normalize batch values but warn consumer about this adjusts
if (!batchSize || batchSize < 2) {
this.logger.warn(`batch size too low/not defined, received: ${batchSize}. Adjusting to 10`);
batchSize = 10;
}
if (!batchTimeout || batchTimeout < 1) {
this.logger.warn(`batch timeout too low/not defined, received: ${batchTimeout}. Setting timeout to 200ms`);
batchTimeout = 200;
}
const queue = await this.setupQueue(msgOptions, channel);
const { consumerTag } = await channel.consume(queue, this.wrapConsumer(async (msg) => {
if ((0, lodash_1.isNull)(msg)) {
return;
}
batchMsgs.push(msg);
if (batchMsgs.length === 1) {
// Wrapped in a Promise to ensure outstanding message logic is aware.
await new Promise((resolve) => {
const batchHandler = async () => {
const batchMsgsToProcess = batchMsgs;
batchMsgs = [];
await this.handleBatchedMessages(handler, msgOptions, channel, batchMsgsToProcess);
resolve();
};
batchTimer = setTimeout(batchHandler, batchTimeout);
inflightBatchHandler = batchHandler;
});
}
else if (batchMsgs.length === batchSize) {
clearTimeout(batchTimer);
await inflightBatchHandler();
}
else {
batchTimer.refresh();
}
}), consumeOptions);
this.registerConsumerForQueue({
type: 'subscribe-batch',
consumerTag,
handler,
msgOptions,
channel,
});
return consumerTag;
}
async handleBatchedMessages(handler, msgOptions, channel, batchMsgs) {
var _a;
try {
const messages = [];
const headers = [];
for (const msg of batchMsgs) {
const result = this.deserializeMessage(msg, msgOptions);
messages.push(result.message);
headers.push(result.headers);
}
const response = await handler(messages, batchMsgs, headers);
if (response instanceof handlerResponses_1.Nack) {
for (const msg of batchMsgs) {
channel.nack(msg, false, response.requeue);
}
return;
}
for (const msg of batchMsgs) {
channel.ack(msg);
}
}
catch (e) {
const batchErrorHandler = (_a = msgOptions.batchOptions) === null || _a === void 0 ? void 0 : _a.errorHandler;
const errorHandler = msgOptions.errorHandler;
const defaultErrorHandler = (0, errorBehaviors_1.getHandlerForLegacyBehavior)(msgOptions.errorBehavior || this.config.defaultSubscribeErrorBehavior);
if (batchErrorHandler) {
await batchErrorHandler(channel, batchMsgs, e);
}
else if (errorHandler) {
for (const msg of batchMsgs) {
await errorHandler(channel, msg, e);
}
}
else {
await defaultErrorHandler(channel, batchMsgs, e);
}
}
}
async createRpc(handler, rpcOptions) {
return this.consumerFactory(rpcOptions, (channel) => this.setupRpcChannel(handler, rpcOptions, channel));
}
async setupRpcChannel(handler, rpcOptions, channel) {
var _a;
const queue = await this.setupQueue(rpcOptions, channel);
const { consumerTag } = await channel.consume(queue, this.wrapConsumer(async (msg) => {
var _a;
try {
if (msg == null) {
throw new Error('Received null message');
}
if (!(0, utils_1.matchesRoutingKey)(msg.fields.routingKey, rpcOptions.routingKey)) {
channel.nack(msg, false, false);
this.logger.error('Received message with invalid routing key: ' +
msg.fields.routingKey);
return;
}
const result = this.deserializeMessage(msg, rpcOptions);
const response = await handler(result.message, msg, result.headers);
if (response instanceof handlerResponses_1.Nack) {
channel.nack(msg, false, response.requeue);
return;
}
const { replyTo, correlationId, expiration, headers } = msg.properties;
if (replyTo) {
await this.publish('', replyTo, response, {
correlationId,
expiration,
headers,
persistent: (_a = rpcOptions.usePersistentReplyTo) !== null && _a !== void 0 ? _a : false,
});
}
channel.ack(msg);
}
catch (e) {
if (msg == null) {
return;
}
else {
const errorHandler = rpcOptions.errorHandler ||
this.config.defaultRpcErrorHandler ||
(0, errorBehaviors_1.getHandlerForLegacyBehavior)(rpcOptions.errorBehavior ||
this.config.defaultSubscribeErrorBehavior);
await errorHandler(channel, msg, e);
}
}
}), (_a = rpcOptions === null || rpcOptions === void 0 ? void 0 : rpcOptions.queueOptions) === null || _a === void 0 ? void 0 : _a.consumerOptions);
this.registerConsumerForQueue({
type: 'rpc',
consumerTag,
handler,
msgOptions: rpcOptions,
channel,
});
return consumerTag;
}
publish(exchange, routingKey, message, options) {
let buffer;
if (message instanceof Buffer) {
buffer = message;
}
else if (message instanceof Uint8Array) {
buffer = Buffer.from(message);
}
else if (message != null) {
buffer = this.config.serializer(message);
}
else {
buffer = Buffer.alloc(0);
}
return this._managedChannel.publish(exchange, routingKey, buffer, options);
}
deserializeMessage(msg, options) {
let message = undefined;
let headers = undefined;
const deserializer = options.deserializer || this.config.deserializer;
if (msg.content) {
if (options.allowNonJsonMessages) {
try {
message = deserializer(msg.content, msg);
}
catch (_a) {
// Pass raw message since flag `allowNonJsonMessages` is set
// Casting to `any` first as T doesn't have a type
message = msg.content.toString();
}
}
else {
message = deserializer(msg.content, msg);
}
}
if (msg.properties && msg.properties.headers) {
headers = msg.properties.headers;
}
return { message, headers };
}
async setupQueue(subscriptionOptions, channel) {
const { exchange, routingKey, createQueueIfNotExists = true, assertQueueErrorHandler = __1.defaultAssertQueueErrorHandler, queueOptions, queue: queueName = '', } = subscriptionOptions;
let actualQueue;
if (createQueueIfNotExists) {
try {
const { queue } = await channel.assertQueue(queueName, queueOptions);
actualQueue = queue;
}
catch (error) {
actualQueue = await assertQueueErrorHandler(channel, queueName, queueOptions, error);
}
}
else {
const { queue } = await channel.checkQueue(subscriptionOptions.queue || '');
actualQueue = queue;
}
let bindQueueArguments;
if (queueOptions) {
bindQueueArguments = queueOptions.bindQueueArguments;
}
const routingKeys = Array.isArray(routingKey) ? routingKey : [routingKey];
if (exchange && routingKeys) {
await Promise.all(routingKeys.map((routingKey) => {
if (routingKey != null) {
return channel.bindQueue(actualQueue, exchange, routingKey, bindQueueArguments);
}
}));
}
return actualQueue;
}
setupManagedChannel(name, config) {
const channel = this._managedConnection.createChannel({
name,
});
this._managedChannels[name] = channel;
if (config.default) {
this._managedChannel = channel;
}
channel.on('connect', () => this.logger.log(`Successfully connected a RabbitMQ channel "${name}"`));
channel.on('error', (err, { name }) => this.logger.error(`Failed to setup a RabbitMQ channel - name: ${name} / error: ${err.message} ${err.stack}`));
channel.on('close', () => this.logger.log(`Successfully closed a RabbitMQ channel "${name}"`));
return channel.addSetup((c) => this.setupInitChannel(c, name, config));
}
/**
* Selects managed channel based on name, if not found uses default.
* @param name name of the channel
* @returns channel wrapper
*/
selectManagedChannel(name) {
if (!name)
return this._managedChannel;
const channel = this._managedChannels[name];
if (!channel) {
this.logger.warn(`Channel "${name}" does not exist, using default channel: ${this._managedChannel.name}.`);
return this._managedChannel;
}
return channel;
}
registerConsumerForQueue(consumer) {
this._consumers[consumer.consumerTag] = consumer;
}
unregisterConsumerForQueue(consumerTag) {
delete this._consumers[consumerTag];
}
getConsumer(consumerTag) {
return this._consumers[consumerTag];
}
get consumerTags() {
return Object.keys(this._consumers);
}
async cancelConsumer(consumerTag) {
const consumer = this.getConsumer(consumerTag);
if (consumer && consumer.channel) {
this.logger.log(`Canceling consumer with tag: ${consumerTag}`);
await consumer.channel.cancel(consumerTag);
}
}
async resumeConsumer(consumerTag) {
const consumer = this.getConsumer(consumerTag);
if (!consumer) {
return null;
}
let newConsumerTag;
if (consumer.type === 'rpc') {
newConsumerTag = await this.setupRpcChannel(consumer.handler, consumer.msgOptions, consumer.channel);
}
else if (consumer.type === 'subscribe') {
newConsumerTag = await this.setupSubscriberChannel(consumer.handler, consumer.msgOptions, consumer.channel);
}
else if (consumer.type === 'subscribe-batch') {
newConsumerTag = await this.setupBatchSubscriberChannel(consumer.handler, consumer.msgOptions, consumer.channel);
}
else {
throw new Error(`Unable to resume consumer tag ${consumerTag}. Unexpected consumer type ${consumer.type}.`);
}
// A new consumerTag was created, remove old
this.unregisterConsumerForQueue(consumerTag);
return newConsumerTag;
}
async close() {
const managedChannels = Object.values(this._managedChannels);
// First cancel all consumers so they stop getting new messages
await Promise.all(managedChannels.map((channel) => channel.cancelAll()));
// Wait for all the outstanding messages to be processed
if (this.outstandingMessageProcessing.size) {
this.logger.log(`Waiting for outstanding consumers, outstanding message count: ${this.outstandingMessageProcessing.size}`);
}
await Promise.all(this.outstandingMessageProcessing);
// Close all channels
await Promise.all(managedChannels.map((channel) => channel.close()));
await this.managedConnection.close();
}
}
exports.AmqpConnection = AmqpConnection;
//# sourceMappingURL=connection.js.map