@dugongjs/kafkajs
Version:
75 lines (74 loc) • 4.34 kB
JavaScript
import {} from "@dugongjs/core";
import * as changeCase from "change-case";
import { Kafka } from "kafkajs";
import { MessageChannelParticipantKafkaJs } from "../../common/message-broker/message-channel-participator-kafkajs.js";
/**
* MessageConsumerKafkaJs is an implementation of the IMessageConsumer interface using the KafkaJs library.
*/
export class MessageConsumerKafkaJs extends MessageChannelParticipantKafkaJs {
constructor(kafka, consumerConfig, consumerSubscribeTopics, consumerRunConfig, logger) {
super();
this.kafka = kafka;
this.consumerConfig = consumerConfig;
this.consumerSubscribeTopics = consumerSubscribeTopics;
this.consumerRunConfig = consumerRunConfig;
this.logger = logger;
this.consumers = [];
}
/**
* Generates a unique message consumer ID based on the provided origin, aggregate type, and consumer name.
* @param origin The origin or source of the messages (e.g., service name or application name).
* @param aggregateType The type of aggregate or domain entity that the consumer is interested in (e.g., "Order", "User", etc.).
* @param consumerName A descriptive name for the consumer, which can help identify its purpose (e.g., "EmailNotificationConsumer", "AnalyticsConsumer", etc.).
* @returns A unique consumer ID string in kebab-case format, combining the origin, aggregate type, and consumer name. For example: "my-service-order-email-notification-consumer".
*/
generateMessageConsumerIdForAggregate(origin, aggregateType, consumerName) {
const originKebabCase = changeCase.kebabCase(origin);
const aggregateTypeKebabCase = changeCase.kebabCase(aggregateType);
const consumerNameKebabCase = changeCase.kebabCase(consumerName);
return `${originKebabCase}-${aggregateTypeKebabCase}-${consumerNameKebabCase}`;
}
/**
* Registers a domain event message consumer for a specific channel and consumer ID, with an optional message handler.
* @param channelId The ID of the message channel (Kafka topic) to subscribe to.
* @param consumerId The unique ID for the consumer group.
* @param onMessage An optional asynchronous function that will be called for each received message. If not provided, the consumer will run without a message handler.
* @returns A promise that resolves when the consumer is successfully registered and running.
*/
async registerDomainEventMessageConsumer(channelId, consumerId, onMessage) {
const consumer = this.kafka.consumer({ groupId: consumerId, ...(this.consumerConfig ?? {}) });
await consumer.connect();
await this.subscribeWithRetry(consumer, channelId);
await consumer.run({ eachMessage: onMessage, ...(this.consumerRunConfig ?? {}) });
this.consumers.push(consumer);
}
async disconnect() {
await Promise.all(this.consumers.map(async (consumer) => consumer.disconnect()));
}
/**
* Subscribes to a Kafka topic with retry logic to handle the case where the topic might not exist yet.
* This is required when running Kafka in KRaft mode, as topic creation is asynchronous.
* @param consumer The Kafka consumer instance.
* @param topic The topic to subscribe to.
* @param retries The number of retry attempts before giving up.
* @param delayMs The delay in milliseconds between retry attempts.
* @returns A promise that resolves when the subscription is successful, or rejects if all retry attempts fail.
*/
async subscribeWithRetry(consumer, topic, retries = 10, delayMs = 300) {
for (let i = 0; i < retries; i++) {
try {
await consumer.subscribe({ topic, ...(this.consumerSubscribeTopics ?? {}) });
return;
}
catch (err) {
const isRetryable = err?.type === "UNKNOWN_TOPIC_OR_PARTITION";
if (!isRetryable) {
this.logger?.error(`Failed to subscribe to topic "${topic}": ${err.message}`);
throw err;
}
this.logger?.warn(`Topic "${topic}" does not exist. Attempt ${i + 1} of ${retries}. Retrying in ${delayMs}ms...`);
await new Promise((r) => setTimeout(r, delayMs));
}
}
}
}