@bgaldino/nestjs-rabbitmq
Version:
A different way of configuring your RabbitMQ
210 lines • 10.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RabbitMQConsumer = void 0;
const common_1 = require("@nestjs/common");
const faster_stable_stringify_1 = __importDefault(require("faster-stable-stringify"));
const amqp_connection_manager_1 = require("./amqp-connection-manager");
const helper_1 = require("./helper");
class RabbitMQConsumer {
constructor(connection, options, publishChannelWrapper) {
var _a, _b, _c;
this.defaultConsumerOptions = {
autoAck: true,
durable: true,
prefetch: 10,
autoDelete: false,
retryStrategy: {
enabled: true,
maxAttempts: 5,
delay: () => 5000,
},
deadLetterStrategy: {
callback: async () => true,
suffix: ".dlq",
},
};
this.connection = connection;
this.options = options;
this.delayExchange = `${this.options.delayExchangeName}.delay`;
this.publishChannel = publishChannelWrapper;
this.logType =
(_a = process.env.RABBITMQ_LOG_TYPE) !== null && _a !== void 0 ? _a : this.options.extraOptions.logType;
this.logger =
(_c = (_b = options === null || options === void 0 ? void 0 : options.extraOptions) === null || _b === void 0 ? void 0 : _b.loggerInstance) !== null && _c !== void 0 ? _c : new common_1.Logger(RabbitMQConsumer.name);
}
async createConsumer(consumer, messageHandler) {
consumer = (0, helper_1.merge)(this.defaultConsumerOptions, consumer);
const consumerChannel = this.connection.createChannel({
confirm: true,
name: consumer.queue,
setup: (channel) => {
var _a, _b;
return Promise.all([
channel.prefetch(consumer.prefetch),
channel.assertQueue(consumer.queue, {
durable: consumer.durable,
autoDelete: consumer.autoDelete,
deadLetterRoutingKey: `${consumer.queue}${(_b = (_a = consumer.deadLetterStrategy) === null || _a === void 0 ? void 0 : _a.suffix) !== null && _b !== void 0 ? _b : ".dlq"}`,
deadLetterExchange: "",
}),
new Promise((resolve) => {
if (typeof consumer.routingKey === "object") {
for (const rk of consumer.routingKey) {
channel.bindQueue(consumer.queue, consumer.exchangeName, rk);
}
}
else {
channel.bindQueue(consumer.queue, consumer.exchangeName, consumer.routingKey);
}
resolve(true);
}),
this.attachRetryAndDLQ(channel, consumer),
channel.consume(consumer.queue, async (message) => {
await this.processConsumerMessage(message, channel, consumer, messageHandler);
}),
]);
},
});
return consumerChannel;
}
async processConsumerMessage(message, channel, consumer, callback) {
let hasErrors = null;
let hasRetried = false;
const start = process.hrtime.bigint();
try {
await callback((0, helper_1.tryParseJson)(message.content.toString("utf8")), {
message,
channel,
queue: consumer.queue,
});
}
catch (e) {
hasErrors = e;
hasRetried = await this.processRetry(consumer, message);
}
finally {
if (["consumer", "all"].includes(this.logType) || hasErrors)
this.inspectConsumer({
binding: {
queue: consumer.queue,
routingKey: message.fields.routingKey,
exchange: consumer.exchangeName,
},
consumeMessage: message,
error: hasErrors,
elapsedTime: process.hrtime.bigint() - start,
isDead: hasErrors && !hasRetried,
});
this.ackMessage(channel, message, consumer, hasErrors, hasRetried);
}
}
async attachRetryAndDLQ(channel, consumer) {
var _a, _b, _c;
const deadletterQueue = `${consumer.queue}${(_b = (_a = consumer.deadLetterStrategy) === null || _a === void 0 ? void 0 : _a.suffix) !== null && _b !== void 0 ? _b : ".dlq"}`;
await channel.assertQueue(deadletterQueue, { durable: true });
if (((_c = consumer === null || consumer === void 0 ? void 0 : consumer.retryStrategy) === null || _c === void 0 ? void 0 : _c.enabled) == false) {
await channel.unbindQueue(consumer.queue, this.delayExchange, consumer.queue);
}
else {
await channel.assertExchange(this.delayExchange, "x-delayed-message", {
durable: true,
arguments: { "x-delayed-type": "direct" },
});
await channel.bindQueue(consumer.queue, this.delayExchange, consumer.queue);
}
}
async processRetry(consumer, message) {
var _a, _b, _c;
let isPublished = false;
if (consumer.retryStrategy === undefined ||
consumer.retryStrategy.enabled === undefined ||
(consumer === null || consumer === void 0 ? void 0 : consumer.retryStrategy.enabled)) {
const retryCount = (_c = (_b = (_a = message.properties) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b["x-retries-count"]) !== null && _c !== void 0 ? _c : 0;
const maxRetry = consumer.retryStrategy.maxAttempts;
if (retryCount < maxRetry) {
const retryDelay = consumer.retryStrategy.delay(retryCount);
try {
isPublished = await this.publishChannel.publish(this.delayExchange, consumer.queue, (0, faster_stable_stringify_1.default)((0, helper_1.tryParseJson)(message.content.toString("utf8"))), {
headers: Object.assign(Object.assign({}, message.properties.headers), { "x-retries-count": retryCount + 1, "x-delay": retryDelay }),
deliveryMode: 2, //persistent message
persistent: true,
});
}
catch (e) {
this.logger.error({ message: "could_not_retry", error: e });
isPublished = false;
}
}
}
return isPublished;
}
inspectConsumer(args) {
var _a, _b;
const { binding, consumeMessage, data, error } = args;
const { exchange, routingKey, queue } = binding;
const { content, fields, properties } = consumeMessage;
const message = `[AMQP] [CONSUMER] [${exchange}] [${routingKey}] [${queue}]`;
const logLevel = error ? "error" : "log";
const logData = {
logLevel,
type: "consumer",
duration: args.elapsedTime.toString(),
correlationId: (_b = (_a = args === null || args === void 0 ? void 0 : args.consumeMessage) === null || _a === void 0 ? void 0 : _a.properties) === null || _b === void 0 ? void 0 : _b.correlationId,
binding,
title: message,
isDead: args.isDead,
consumedMessage: {
fields,
properties,
content: data !== null && data !== void 0 ? data : (0, helper_1.tryParseJson)(content.toString("utf8")),
},
};
if (error) {
logData["error"] = {
stack: error === null || error === void 0 ? void 0 : error.stack,
message: error === null || error === void 0 ? void 0 : error.message,
name: error === null || error === void 0 ? void 0 : error.name,
};
}
this.logger[logLevel](logData);
}
async ackMessage(channel, message, consumer, hasErrors, hasRetried) {
var _a, _b, _c;
if (!amqp_connection_manager_1.AMQPConnectionManager.consumerConn.isConnected()) {
this.logger.error("Could not acknowledge message, Connection is offline");
return;
}
if ((!hasErrors && (consumer === null || consumer === void 0 ? void 0 : consumer.autoAck)) || (hasErrors && hasRetried)) {
channel.ack(message);
}
else if (hasErrors && !hasRetried) {
let shouldNack = true;
try {
shouldNack =
(_c = (await ((_b = (_a = consumer.deadLetterStrategy) === null || _a === void 0 ? void 0 : _a.callback) === null || _b === void 0 ? void 0 : _b.call(_a, message.content.toString("utf8"))))) !== null && _c !== void 0 ? _c : true;
}
catch (e) {
this.logger.error({
type: "consumer",
title: `[AMQP] [DEADLETTER] ${message.fields.exchange} ${message.fields.routingKey} ${message.fields} ${consumer.queue}`,
error: {
stack: e === null || e === void 0 ? void 0 : e.stack,
message: e === null || e === void 0 ? void 0 : e.message,
name: e === null || e === void 0 ? void 0 : e.name,
},
});
}
if (shouldNack) {
channel.nack(message, false, false);
}
else {
channel.ack(message);
}
}
}
}
exports.RabbitMQConsumer = RabbitMQConsumer;
//# sourceMappingURL=rabbitmq-consumers.js.map