broker-lib
Version:
Multi-Broker Message Bus with Multi-Topic Support
205 lines • 8.25 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KafkaBroker = void 0;
const kafkajs_1 = require("kafkajs");
const events_1 = require("events");
class KafkaBroker extends events_1.EventEmitter {
constructor(config) {
super();
this.connected = false;
this.producerConnected = false;
this.consumerConnected = false;
this.consumerRunning = false;
this.subscribedTopics = new Set();
const kafkaConfig = {
clientId: config.clientId,
brokers: config.brokers,
// Optionally, you can set logLevel here for debugging
// logLevel: logLevel.INFO,
};
if (config.ssl) {
kafkaConfig.ssl = config.ssl;
}
if (config.sasl) {
if (config.sasl.mechanism === 'plain') {
kafkaConfig.sasl = {
mechanism: 'plain',
username: config.sasl.username,
password: config.sasl.password,
};
}
else if (config.sasl.mechanism === 'scram-sha-256') {
kafkaConfig.sasl = {
mechanism: 'scram-sha-256',
username: config.sasl.username,
password: config.sasl.password,
};
}
else if (config.sasl.mechanism === 'scram-sha-512') {
kafkaConfig.sasl = {
mechanism: 'scram-sha-512',
username: config.sasl.username,
password: config.sasl.password,
};
}
}
this.kafka = new kafkajs_1.Kafka(kafkaConfig);
this.producer = this.kafka.producer();
this.consumer = this.kafka.consumer({ groupId: config.groupId });
}
async connect() {
try {
// Connect producer
if (!this.producerConnected) {
await this.producer.connect();
this.producerConnected = true;
}
// Connect consumer
if (!this.consumerConnected) {
await this.consumer.connect();
this.consumerConnected = true;
}
this.connected = true;
this.emit('connect');
}
catch (error) {
this.connected = false;
this.producerConnected = false;
this.consumerConnected = false;
this.emit('error', error);
throw new Error(`Failed to connect to Kafka: ${error instanceof Error ? error.message : String(error)}`);
}
}
async publish(topic, message, options) {
try {
if (!this.connected) {
throw new Error('Kafka broker is not connected. Call connect() first.');
}
const messageObj = {
value: typeof message === 'string' ? Buffer.from(message) : message,
};
if (options?.key) {
messageObj.key = options.key;
}
if (options?.headers) {
messageObj.headers = options.headers;
}
if (options?.timestamp) {
messageObj.timestamp = options.timestamp.getTime().toString();
}
if (options?.partition !== undefined) {
messageObj.partition = options.partition;
}
await this.producer.send({
topic,
messages: [messageObj],
});
}
catch (error) {
// Check if it's a connection-related error
if (error instanceof Error && (error.message.includes('ECONNRESET') ||
error.message.includes('ENOTFOUND') ||
error.message.includes('ETIMEDOUT') ||
error.message.includes('Connection closed'))) {
this.connected = false;
this.producerConnected = false;
this.consumerConnected = false;
this.consumerRunning = false;
this.emit('disconnect');
}
this.emit('error', error);
throw new Error(`Failed to publish message to topic ${topic}: ${error instanceof Error ? error.message : String(error)}`);
}
}
async subscribe(topics, handler, options) {
try {
if (!this.connected) {
throw new Error('Kafka broker is not connected. Call connect() first.');
}
// Store the message handler
this.messageHandler = handler;
// Subscribe to new topics
const newTopics = topics.filter(topic => !this.subscribedTopics.has(topic));
for (const topic of newTopics) {
await this.consumer.subscribe({
topic,
fromBeginning: options?.fromBeginning ?? true
});
this.subscribedTopics.add(topic);
}
// Start the consumer if it's not already running
if (!this.consumerRunning) {
await this.consumer.run({
eachMessage: async ({ topic, message }) => {
try {
if (this.messageHandler) {
await this.messageHandler(topic, message.value);
}
}
catch (error) {
console.error(`Error handling message from topic ${topic}:`, error);
this.emit('error', error);
}
},
eachBatch: async ({ batch, resolveOffset, heartbeat, isRunning, isStale }) => {
try {
for (const message of batch.messages) {
if (!isRunning() || isStale())
break;
if (this.messageHandler) {
await this.messageHandler(batch.topic, message.value);
}
resolveOffset(message.offset);
}
await heartbeat();
}
catch (error) {
console.error(`Error handling batch from topic ${batch.topic}:`, error);
this.emit('error', error);
}
},
});
this.consumerRunning = true;
}
}
catch (error) {
// Check if it's a connection-related error
if (error instanceof Error && (error.message.includes('ECONNRESET') ||
error.message.includes('ENOTFOUND') ||
error.message.includes('ETIMEDOUT') ||
error.message.includes('Connection closed'))) {
this.connected = false;
this.producerConnected = false;
this.consumerConnected = false;
this.consumerRunning = false;
this.emit('disconnect');
}
this.emit('error', error);
throw new Error(`Failed to subscribe to topics ${topics.join(', ')}: ${error instanceof Error ? error.message : String(error)}`);
}
}
async disconnect() {
try {
if (this.connected) {
await this.consumer.disconnect();
await this.producer.disconnect();
this.connected = false;
this.producerConnected = false;
this.consumerConnected = false;
this.consumerRunning = false;
this.subscribedTopics.clear();
this.messageHandler = undefined;
this.emit('disconnect');
}
}
catch (error) {
this.emit('error', error);
throw new Error(`Failed to disconnect: ${error instanceof Error ? error.message : String(error)}`);
}
}
isConnected() {
return this.connected;
}
}
exports.KafkaBroker = KafkaBroker;
//# sourceMappingURL=kafka.js.map