@goparrot/pubsub-event-bus
Version:
NestJS EventBus extension for RabbitMQ PubSub
152 lines • 6.92 kB
JavaScript
import { __decorate, __metadata, __param } from "tslib";
import { Inject, Injectable } from '@nestjs/common';
import { chain } from 'lodash';
import { AutoAckEnum, RetryStrategyEnum } from '../interface';
import { HandlerBound } from '../lifecycle-event';
import { CQRS_PREPARE_HANDLER_STRATEGIES, CQRS_RETRY_STRATEGIES } from '../provider';
import { appInTestingMode, toEventName, toSnakeCase } from '../utils';
import { CQRS_BINDING_QUEUE_CONFIG, CQRS_MODULE_CONSUMER_OPTIONS, CQRS_RETRY_OPTIONS } from '../utils/configuration';
import { EventBus } from './EventBus';
import { PubsubManager } from './PubsubManager';
let Consumer = class Consumer extends PubsubManager {
constructor(eventBus, retryStrategies, options, rootRetryOptions, bindingQueueOptions, prepareHandlerStrategies) {
super();
this.eventBus = eventBus;
this.retryStrategies = retryStrategies;
this.options = options;
this.rootRetryOptions = rootRetryOptions;
this.bindingQueueOptions = bindingQueueOptions;
this.prepareHandlerStrategies = prepareHandlerStrategies;
this.exchanges = new Set();
}
async setupChannel(channel) {
if (this.options.prefetchPerConsumer) {
await channel.prefetch(this.options.prefetchPerConsumer, false);
}
if (this.options.prefetchPerChannel) {
await channel.prefetch(this.options.prefetchPerChannel, true);
}
}
async consume(handlerWrapper, onMessage) {
if (appInTestingMode()) {
return;
}
const { handler, eventWrappers, options, queue } = handlerWrapper;
this.initConnectionIfRequired();
this.initChannelIfRequired();
const handlerExchanges = eventWrappers.map((event) => event.options.exchange);
const exchangesToAssert = chain(handlerExchanges)
.filter((exchange) => !this.exchanges.has(exchange))
.uniq()
.value();
exchangesToAssert.forEach((exchange) => this.exchanges.add(exchange));
const bindingPatterns = eventWrappers.map((event) => this.extractBindingPattern(event));
await this.channelWrapper.addSetup(async (channel) => {
await Promise.all([
...exchangesToAssert.map(async (exchange) => channel.assertExchange(exchange, 'topic', this.assertExchangeOptions)),
channel.assertQueue(queue, this.bindingOptions(options.bindingQueueOptions)),
...this.bindEvents(channel, queue, eventWrappers),
channel.consume(queue, (msg) => {
try {
onMessage(msg);
}
catch (e) {
this.logger().warn(`Message execution/acknowledge error: [${e.message}]`);
}
}),
]);
this.logger().log(`Listening for "${bindingPatterns.join(', ')}" events from [${handlerExchanges.join(', ')} <- ${queue}]`);
await this.eventBus.publish(new HandlerBound(handler.name));
});
}
extractBindingPattern(mappedEvent) {
const { event, options: { customBindingPattern, customRoutingKey }, } = mappedEvent;
return customBindingPattern ?? customRoutingKey ?? toEventName(event.name);
}
configureAutoAck(wrapper) {
this.prepareHandlerStrategies[wrapper.options.autoAck ?? AutoAckEnum.ALWAYS_ACK].process(wrapper, this);
}
addHandleCatch(handlerWrapper) {
const { handler } = handlerWrapper;
const originalMethod = handler.prototype.handle;
const logger = this.logger();
Reflect.defineProperty(handler.prototype, 'handle', {
...Reflect.getOwnPropertyDescriptor(handler.prototype, 'handle'),
async value(event) {
try {
await originalMethod.apply(this, [event]);
}
catch (error) {
logger.error(JSON.stringify({
error,
event,
}), error instanceof Error ? error.stack : undefined, handler.name);
}
},
});
}
ack(message) {
if (appInTestingMode()) {
return;
}
this.channelWrapper.ack(message);
}
nack(message) {
if (appInTestingMode()) {
return;
}
this.channelWrapper.nack(message);
}
async publish(exchange, routingKey, content, options) {
await this.channelWrapper.publish(exchange, routingKey, content, options);
}
async configureRetryInfrastructure(wrappers) {
const wrappersWithRetryStrategy = wrappers.filter((wrapper) => wrapper.options.autoAck === AutoAckEnum.AUTO_RETRY);
const maxRetryAttempts = Math.max(this.rootRetryOptions.maxRetryAttempts, ...wrappersWithRetryStrategy.map((wrapper) => wrapper.options.retryOptions?.maxRetryAttempts ?? 0));
if (!maxRetryAttempts) {
this.logger().debug?.('Retry infrastructure configuration skipped because no handlers with auto retry enabled found');
return;
}
if (maxRetryAttempts < 0) {
throw new Error(`Invalid max retry count value (${maxRetryAttempts})`);
}
if (maxRetryAttempts > 100) {
throw new Error(`Too great value for max retry count (${maxRetryAttempts})`);
}
await Promise.all(chain(wrappersWithRetryStrategy)
.groupBy((wrapper) => wrapper.options.retryOptions?.strategy ?? RetryStrategyEnum.DEAD_LETTER_TTL)
.entries()
.map(async ([strategy, wrappers]) => this.retryStrategies[strategy].setupInfrastructure(this.channelWrapper, wrappers))
.value());
}
queue(handlerWrapper) {
const pckName = process.env.npm_package_name.split('/').pop();
const platform = pckName.replace(/[_-]/gi, '.');
return [platform, toSnakeCase(handlerWrapper.handler.name)].join(':');
}
bindingOptions(extra = {}) {
return {
...this.consumerConfiguration(),
...extra,
};
}
consumerConfiguration() {
return this.bindingQueueOptions;
}
bindEvents(channel, queueName, eventWrappers) {
return eventWrappers.map(async (event) => {
return channel.bindQueue(queueName, event.options.exchange, this.extractBindingPattern(event));
});
}
};
Consumer = __decorate([
Injectable(),
__param(1, Inject(CQRS_RETRY_STRATEGIES)),
__param(2, Inject(CQRS_MODULE_CONSUMER_OPTIONS)),
__param(3, Inject(CQRS_RETRY_OPTIONS)),
__param(4, Inject(CQRS_BINDING_QUEUE_CONFIG)),
__param(5, Inject(CQRS_PREPARE_HANDLER_STRATEGIES)),
__metadata("design:paramtypes", [EventBus, Object, Object, Object, Object, Object])
], Consumer);
export { Consumer };
//# sourceMappingURL=Consumer.js.map