kafka-pub-sub
Version:
Enterprise-grade Kafka publish/subscribe library for Node.js — producer pool, batch sending, DLQ, SSL/SASL, multi-broker, and real-world industry examples.
157 lines (142 loc) • 4.8 kB
JavaScript
const kafka = require('./config/kafka');
const validateTopic = require('./validation/validateTopic');
/**
* @typedef {Object} TopicConfig
* @property {string} topic - Topic name.
* @property {number} [numPartitions=6] - Number of partitions.
* @property {number} [replicationFactor=1] - Replication factor (set ≥ 3 in production).
* @property {Object} [configEntries] - Broker-level topic config overrides.
* @property {string} [configEntries.retention.ms] - Retention in milliseconds.
* @property {string} [configEntries.cleanup.policy] - 'delete' | 'compact'.
* @property {string} [configEntries.compression.type] - Codec for stored messages.
*/
/**
* Admin client wrapper for programmatic Kafka topic management.
*
* Typical enterprise uses:
* - CI/CD pipelines that need to create topics before deploying consumers.
* - Health checks that verify required topics exist.
* - Tenant provisioning that creates isolated topics per customer.
*
* @example
* const admin = KafkaAdmin();
*
* await admin.createTopics([
* { topic: 'order.placed', numPartitions: 12, replicationFactor: 3 },
* { topic: 'payment.transaction', numPartitions: 24, replicationFactor: 3,
* configEntries: { 'retention.ms': '604800000' } }, // 7 days
* ]);
*
* const topics = await admin.listTopics();
* console.log(topics); // ['order.placed', 'payment.transaction', …]
*
* await admin.disconnect();
*/
const KafkaAdmin = () => {
const admin = kafka.admin();
let connected = false;
const ensureConnected = async () => {
if (!connected) {
await admin.connect();
connected = true;
}
};
return {
/**
* Creates one or more topics. Silently skips topics that already exist.
*
* @param {TopicConfig[]} topics
* @param {Object} [options={}]
* @param {boolean} [options.waitForLeaders=true] - Wait for partition leaders before resolving.
* @param {number} [options.timeout=5000]
* @return {Promise<boolean>} True if at least one topic was created.
*/
async createTopics(topics, options = {}) {
await ensureConnected();
const { waitForLeaders = true, timeout = 5000 } = options;
const topicConfigs = topics.map((t) => {
validateTopic(t.topic);
const config = {
topic: t.topic,
numPartitions: t.numPartitions || 6,
replicationFactor: t.replicationFactor || 1,
};
if (t.configEntries) {
config.configEntries = Object.entries(t.configEntries).map(([name, value]) => ({
name,
value: String(value),
}));
}
return config;
});
return admin.createTopics({ topics: topicConfigs, waitForLeaders, timeout });
},
/**
* Deletes one or more topics.
*
* @param {string[]} topics - Array of topic names to delete.
* @return {Promise<void>}
*/
async deleteTopics(topics) {
await ensureConnected();
topics.forEach(validateTopic);
return admin.deleteTopics({ topics });
},
/**
* Lists all topic names in the cluster.
*
* @return {Promise<string[]>}
*/
async listTopics() {
await ensureConnected();
return admin.listTopics();
},
/**
* Fetches metadata for one or more topics including partition and leader info.
*
* @param {string[]} [topics] - If omitted, returns metadata for all topics.
* @return {Promise<Object>} KafkaJS metadata object.
*/
async getTopicMetadata(topics) {
await ensureConnected();
return admin.fetchTopicMetadata({ topics });
},
/**
* Returns the current consumer group offsets for a given topic.
* Useful for monitoring consumer lag.
*
* @param {string} groupId
* @param {string} topic
* @return {Promise<Object>}
*/
async getConsumerGroupOffsets(groupId, topic) {
await ensureConnected();
validateTopic(topic);
return admin.fetchOffsets({ groupId, topics: [topic] });
},
/**
* Checks whether all given topics exist in the cluster.
*
* @param {string[]} topics
* @return {Promise<{ exists: boolean, missing: string[] }>}
*/
async topicsExist(topics) {
await ensureConnected();
const existing = await admin.listTopics();
const existingSet = new Set(existing);
const missing = topics.filter((t) => !existingSet.has(t));
return { exists: missing.length === 0, missing };
},
/**
* Disconnect the admin client.
* @return {Promise<void>}
*/
async disconnect() {
if (connected) {
await admin.disconnect();
connected = false;
}
},
};
};
module.exports = KafkaAdmin;