coffee-core
Version:
Coffee IT API core library
140 lines (119 loc) • 3.93 kB
text/typescript
import { Channel, ChannelModel, connect } from 'amqplib';
import { EventPublisher } from './event-publisher.interface';
import { Logger } from '@nestjs/common';
import { BrokerConfigInterface } from '../subscriber/broker-config.interface';
// Inspired by: https://docs.nestjs.com/fundamentals/custom-providers and https://www.rabbitmq.com/tutorials/tutorial-three-javascript.html
export class RabbitMQPublisher implements EventPublisher {
private readonly logger = new Logger(RabbitMQPublisher.name);
private channel: Channel;
private server: ChannelModel;
constructor(private readonly config: BrokerConfigInterface) {}
public async publishRetry<T>(
message: T,
routingKey: string,
maxRetries = 10,
logEvent = true,
): Promise<void> {
await this.publishAndRetry(message, routingKey, 0, maxRetries, logEvent);
}
private async publishAndRetry<T>(
message: T,
routingKey: string,
retries: number,
maxRetries: number,
logEvent: boolean,
): Promise<void> {
try {
await this.publish(message, routingKey, logEvent);
} catch (e) {
const timeOut = setTimeout(
async () => {
const newRetries = retries + 1;
this.logger.error(
`Got error when publishing to ${routingKey}. Retrying for the ${newRetries}th time. ${e}`,
);
try {
await this.publish(message, routingKey, logEvent);
} catch (err) {
await this.publishRetry(message, routingKey, newRetries, logEvent);
}
},
this.config.PUBLISH_RETRY_TIMEOUT_MS * 2 ** retries,
);
if (retries >= maxRetries) {
clearTimeout(timeOut);
Logger.error(
`Stopped retrying to publish to ${routingKey} after ${retries} times.`,
);
}
}
}
public async publish<T>(
message: T,
routingKey: string,
logEvent = true,
): Promise<void> {
const channel = await this.getChannel();
const createdExchange = await channel.assertExchange(
this.config.EXCHANGE_NAME,
this.config.EXCHANGE_TYPE,
{ durable: this.config.DURABLE_EXCHANGE },
);
const byteMessage = Buffer.from(JSON.stringify(message));
const options = { persistent: this.config.PERSISTENT_MESSAGES };
channel.publish(createdExchange.exchange, routingKey, byteMessage, options);
if (logEvent) {
this.logger.debug(
`Published event to ${routingKey} with payload ${JSON.stringify(message, null, 2)}`,
);
}
}
public async close(): Promise<void> {
if (this.channel) {
await this.channel.close();
}
}
public async hasActiveConnection(): Promise<boolean> {
try {
const server = await this.getServer();
const channel = await this.getChannel();
return server !== undefined && channel !== undefined;
} catch (err) {
return false;
}
}
private async getChannel(): Promise<Channel> {
if (this.channel) {
return this.channel;
}
const server = await this.getServer();
this.channel = await server.createChannel();
this.channel.on('close', () => {
this.logger.debug('Channel closed');
this.channel = undefined;
});
this.channel.on('err', () => {
this.logger.debug('Channel got an error');
this.channel = undefined;
});
return this.channel;
}
private async getServer(): Promise<ChannelModel> {
if (this.server) {
return this.server;
}
this.server = await this.connect();
this.server.on('close', () => {
this.server = undefined;
this.logger.debug('RabbitMQ server closed');
});
this.server.on('err', () => {
this.logger.debug('RabbitMQ server got an error');
this.server = undefined;
});
return this.server;
}
private async connect(): Promise<ChannelModel> {
return connect(this.config.AMQP_CONNECTION_URI);
}
}