@kaapi/kafka-messaging 0.0.40 → 0.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,11 +1,28 @@
1
1
  "use strict";
2
- var _KafkaMessaging_config, _KafkaMessaging_producerConfig, _KafkaMessaging_address, _KafkaMessaging_name, _KafkaMessaging_consumers, _KafkaMessaging_producers, _KafkaMessaging_admins;
2
+ var _KafkaMessaging_config, _KafkaMessaging_producerConfig, _KafkaMessaging_address, _KafkaMessaging_name, _KafkaMessaging_consumers, _KafkaMessaging_producers, _KafkaMessaging_admins, _KafkaMessaging_producerPromise, _KafkaMessaging_sharedAdmin, _KafkaMessaging_sharedAdminPromise;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.KafkaMessaging = void 0;
5
5
  exports.safeDisconnect = safeDisconnect;
6
6
  const tslib_1 = require("tslib");
7
7
  const kafkajs_1 = require("kafkajs");
8
8
  const crypto_1 = require("crypto");
9
+ /**
10
+ * A lightweight wrapper around KafkaJS that integrates with the Kaapi framework
11
+ * to provide a clean and consistent message publishing and consuming interface.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const messaging = new KafkaMessaging({
16
+ * clientId: 'my-app',
17
+ * brokers: ['localhost:9092'],
18
+ * name: 'my-service',
19
+ * address: 'service-1'
20
+ * });
21
+ *
22
+ * await messaging.publish('my-topic', { event: 'user.created' });
23
+ * await messaging.subscribe('my-topic', (msg, ctx) => console.log(msg));
24
+ * ```
25
+ */
9
26
  class KafkaMessaging {
10
27
  get activeConsumers() {
11
28
  return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_consumers, "f");
@@ -13,6 +30,11 @@ class KafkaMessaging {
13
30
  get activeProducers() {
14
31
  return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producers, "f");
15
32
  }
33
+ /**
34
+ * Creates a new KafkaMessaging instance.
35
+ *
36
+ * @param arg - Configuration options for the Kafka client
37
+ */
16
38
  constructor(arg) {
17
39
  _KafkaMessaging_config.set(this, void 0);
18
40
  _KafkaMessaging_producerConfig.set(this, void 0);
@@ -21,6 +43,10 @@ class KafkaMessaging {
21
43
  _KafkaMessaging_consumers.set(this, new Set());
22
44
  _KafkaMessaging_producers.set(this, new Set());
23
45
  _KafkaMessaging_admins.set(this, new Set());
46
+ _KafkaMessaging_producerPromise.set(this, void 0);
47
+ /** Shared admin instance for internal operations (lazy initialized) */
48
+ _KafkaMessaging_sharedAdmin.set(this, void 0);
49
+ _KafkaMessaging_sharedAdminPromise.set(this, void 0);
24
50
  const { logger, address, name, producer } = arg, kafkaConfig = tslib_1.__rest(arg, ["logger", "address", "name", "producer"]);
25
51
  tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_config, kafkaConfig, "f");
26
52
  tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_producerConfig, producer, "f");
@@ -34,7 +60,7 @@ class KafkaMessaging {
34
60
  if (!tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_config, "f").brokers)
35
61
  return;
36
62
  return new kafkajs_1.Kafka(Object.assign({ logCreator: () => ({ namespace, level, label, log }) => {
37
- var _a, _b, _c, _d, _e;
63
+ var _a, _b, _c, _d, _f;
38
64
  let lvl = level;
39
65
  if (!log[level])
40
66
  lvl = null;
@@ -53,16 +79,109 @@ class KafkaMessaging {
53
79
  (_d = this.logger) === null || _d === void 0 ? void 0 : _d.debug('KAFKA', label, namespace, log.message);
54
80
  break;
55
81
  default:
56
- (_e = this.logger) === null || _e === void 0 ? void 0 : _e.silly('KAFKA', label, namespace, log.message);
82
+ (_f = this.logger) === null || _f === void 0 ? void 0 : _f.silly('KAFKA', label, namespace, log.message);
57
83
  }
58
84
  } }, tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_config, "f")));
59
85
  }
86
+ /**
87
+ * Internal method to initialize the shared admin.
88
+ * @private
89
+ */
90
+ _initializeSharedAdmin() {
91
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
92
+ var _a;
93
+ if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdmin, "f")) {
94
+ return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdmin, "f");
95
+ }
96
+ const admin = yield this.createAdmin();
97
+ if (!admin)
98
+ return;
99
+ tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_sharedAdmin, admin, "f");
100
+ admin.on(admin.events.DISCONNECT, () => {
101
+ if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdmin, "f") === admin) {
102
+ tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_sharedAdmin, undefined, "f");
103
+ }
104
+ });
105
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug('✔️ Shared admin connected');
106
+ return admin;
107
+ });
108
+ }
109
+ /**
110
+ * Internal method to initialize the producer.
111
+ * @private
112
+ */
113
+ _initializeProducer() {
114
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
115
+ var _a;
116
+ // Double-check in case producer was created while waiting
117
+ if (this.producer) {
118
+ return this.producer;
119
+ }
120
+ const producer = yield this.createProducer(tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producerConfig, "f"));
121
+ if (!producer)
122
+ return;
123
+ const producerId = (0, crypto_1.randomBytes)(16).toString('hex');
124
+ this.producer = producer;
125
+ this.currentProducerId = producerId;
126
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug('✔️ Producer connected');
127
+ producer.on(producer.events.DISCONNECT, () => {
128
+ var _a;
129
+ if (this.currentProducerId === producerId) {
130
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn('⚠️ Producer disconnected');
131
+ this.producer = undefined;
132
+ this.currentProducerId = undefined;
133
+ }
134
+ });
135
+ return producer;
136
+ });
137
+ }
60
138
  getKafka() {
61
139
  if (!this.kafka) {
62
140
  this.kafka = this._createInstance();
63
141
  }
64
142
  return this.kafka;
65
143
  }
144
+ /**
145
+ * Gets or creates a shared admin instance for internal operations.
146
+ * Uses lazy initialization to avoid unnecessary connections.
147
+ *
148
+ * @returns A promise that resolves to the shared admin instance
149
+ */
150
+ getSharedAdmin() {
151
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
152
+ // Return existing admin if available and connected
153
+ if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdmin, "f")) {
154
+ return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdmin, "f");
155
+ }
156
+ // If an admin is already being created, wait for it
157
+ if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdminPromise, "f")) {
158
+ return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdminPromise, "f");
159
+ }
160
+ // Create the admin with a lock
161
+ tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_sharedAdminPromise, this._initializeSharedAdmin(), "f");
162
+ try {
163
+ const admin = yield tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdminPromise, "f");
164
+ return admin;
165
+ }
166
+ finally {
167
+ tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_sharedAdminPromise, undefined, "f");
168
+ }
169
+ });
170
+ }
171
+ /**
172
+ * Creates and connects a Kafka admin client.
173
+ * The admin client is automatically tracked and will be disconnected during shutdown.
174
+ *
175
+ * @param config - Optional admin client configuration
176
+ * @returns A promise that resolves to the connected admin client, or undefined if Kafka is unavailable
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * const admin = await messaging.createAdmin();
181
+ * const topics = await admin?.listTopics();
182
+ * await admin?.disconnect();
183
+ * ```
184
+ */
66
185
  createAdmin(config) {
67
186
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
68
187
  // Get kafka instance
@@ -79,6 +198,26 @@ class KafkaMessaging {
79
198
  return admin;
80
199
  });
81
200
  }
201
+ /**
202
+ * Creates a new Kafka topic with the specified configuration.
203
+ *
204
+ * @param topic - The topic configuration including name, partitions, and replication factor
205
+ * @param config - Optional creation options
206
+ * @param config.validateOnly - If true, only validates the request without creating the topic
207
+ * @param config.waitForLeaders - If true, waits for partition leaders to be elected
208
+ * @param config.timeout - Timeout in milliseconds for the operation
209
+ *
210
+ * @throws {Error} If the admin client cannot be created
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * await messaging.createTopic({
215
+ * topic: 'my-topic',
216
+ * numPartitions: 3,
217
+ * replicationFactor: 1
218
+ * }, { waitForLeaders: true });
219
+ * ```
220
+ */
82
221
  createTopic(topic, config) {
83
222
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
84
223
  const admin = yield this.createAdmin();
@@ -128,10 +267,42 @@ class KafkaMessaging {
128
267
  });
129
268
  }
130
269
  /**
131
- * Create a new consumer with optional configuration overrides
132
- * @param groupId Consumer group id
133
- * @param config Consumer configuration overrides
134
- * @returns
270
+ * Fetches and logs partition offsets for a topic.
271
+ * Uses the shared admin instance to minimize connections.
272
+ *
273
+ * @param topic - The topic to fetch offsets for
274
+ * @returns The partition offset information, or undefined if unavailable
275
+ */
276
+ fetchTopicOffsets(topic) {
277
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
278
+ var _a;
279
+ const admin = yield this.getSharedAdmin();
280
+ if (!admin)
281
+ return;
282
+ try {
283
+ const partitions = yield admin.fetchTopicOffsets(topic);
284
+ return partitions;
285
+ }
286
+ catch (error) {
287
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`Failed to fetch offsets for topic "${topic}":`, error);
288
+ return undefined;
289
+ }
290
+ });
291
+ }
292
+ /**
293
+ * Creates and connects a new Kafka consumer.
294
+ * The consumer is automatically tracked and will be disconnected during shutdown.
295
+ *
296
+ * @param groupId - The consumer group ID
297
+ * @param config - Optional consumer configuration overrides
298
+ * @returns A promise that resolves to the connected consumer, or undefined if Kafka is unavailable
299
+ *
300
+ * @example
301
+ * ```ts
302
+ * const consumer = await messaging.createConsumer('my-group', {
303
+ * sessionTimeout: 30000
304
+ * });
305
+ * ```
135
306
  */
136
307
  createConsumer(groupId, config) {
137
308
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
@@ -158,9 +329,18 @@ class KafkaMessaging {
158
329
  });
159
330
  }
160
331
  /**
161
- * Create a new producer with optional configuration overrides
162
- * @param config Producer configuration overrides
163
- * @returns
332
+ * Creates and connects a new Kafka producer.
333
+ * The producer is automatically tracked and will be disconnected during shutdown.
334
+ *
335
+ * @param config - Optional producer configuration overrides
336
+ * @returns A promise that resolves to the connected producer, or undefined if Kafka is unavailable
337
+ *
338
+ * @example
339
+ * ```ts
340
+ * const producer = await messaging.createProducer({
341
+ * idempotent: true
342
+ * });
343
+ * ```
164
344
  */
165
345
  createProducer(config) {
166
346
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
@@ -192,33 +372,37 @@ class KafkaMessaging {
192
372
  });
193
373
  }
194
374
  /**
195
- * Get the producer
375
+ * Gets or creates the singleton producer instance.
376
+ * Uses a promise-based lock to prevent race conditions when called concurrently.
377
+ *
378
+ * @returns A promise that resolves to the producer instance, or undefined if unavailable.
196
379
  */
197
380
  getProducer() {
198
381
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
199
- var _a;
200
- if (!this.producer) {
201
- const producer = yield this.createProducer(tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producerConfig, "f"));
202
- if (!producer)
203
- return;
204
- const producerId = (0, crypto_1.randomBytes)(16).toString('hex');
205
- this.producer = producer;
206
- this.currentProducerId = producerId;
207
- (_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug('✔️ Producer connected');
208
- producer.on(producer.events.DISCONNECT, () => {
209
- var _a;
210
- if (this.currentProducerId === producerId) {
211
- (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn('⚠️ Producer disconnected');
212
- this.producer = undefined;
213
- this.currentProducerId = undefined;
214
- }
215
- });
382
+ // Return existing producer if available
383
+ if (this.producer) {
384
+ return this.producer;
385
+ }
386
+ // If a producer is already being created, wait for it
387
+ if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producerPromise, "f")) {
388
+ return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producerPromise, "f");
389
+ }
390
+ // Create the producer with a lock
391
+ tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_producerPromise, this._initializeProducer(), "f");
392
+ try {
393
+ const producer = yield tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producerPromise, "f");
394
+ return producer;
395
+ }
396
+ finally {
397
+ // Clear the promise after resolution (success or failure)
398
+ tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_producerPromise, undefined, "f");
216
399
  }
217
- return this.producer;
218
400
  });
219
401
  }
220
402
  /**
221
- * Disconnect the producer
403
+ * Disconnects the singleton producer instance.
404
+ *
405
+ * @returns A promise that resolves when the producer is disconnected
222
406
  */
223
407
  disconnectProducer() {
224
408
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
@@ -229,9 +413,86 @@ class KafkaMessaging {
229
413
  }
230
414
  });
231
415
  }
416
+ /**
417
+ * Publishes multiple messages to a Kafka topic in a single batch.
418
+ * More efficient than multiple `publish()` calls for high-throughput scenarios.
419
+ *
420
+ * @typeParam T - The type of the message payload
421
+ * @param topic - The Kafka topic to publish to
422
+ * @param messages - Array of messages to publish
423
+ *
424
+ * @throws {Error} If the batch fails to send
425
+ *
426
+ * @example
427
+ * ```ts
428
+ * await messaging.publishBatch('user-events', [
429
+ * { value: { event: 'user.created', userId: '1' } },
430
+ * { value: { event: 'user.created', userId: '2' } },
431
+ * { value: { event: 'user.updated', userId: '3' }, key: 'user-3' },
432
+ * ]);
433
+ * ```
434
+ */
435
+ publishBatch(topic, messages) {
436
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
437
+ var _a, _b, _c;
438
+ if (!messages.length)
439
+ return;
440
+ const producer = yield this.getProducer();
441
+ // If we don't have a producer, abort
442
+ if (!producer)
443
+ return (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error('❌ Could not get producer');
444
+ const baseHeaders = {};
445
+ if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_name, "f"))
446
+ baseHeaders.name = tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_name, "f");
447
+ if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_address, "f"))
448
+ baseHeaders.address = tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_address, "f");
449
+ const kafkaMessages = messages.map((msg) => {
450
+ var _a;
451
+ return ({
452
+ value: (msg.value instanceof Buffer ?
453
+ msg.value : typeof msg.value === 'string' ?
454
+ msg.value : (msg.value === null ? null : JSON.stringify(msg.value))),
455
+ key: msg.key,
456
+ partition: msg.partition,
457
+ timestamp: `${Date.now()}`,
458
+ headers: Object.assign(Object.assign({}, baseHeaders), ((_a = msg.headers) !== null && _a !== void 0 ? _a : {})),
459
+ });
460
+ });
461
+ try {
462
+ const res = yield producer.send({
463
+ topic,
464
+ messages: kafkaMessages,
465
+ });
466
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.verbose(`📤 Sent batch to KAFKA topic "${topic}" (${messages.length} messages, offset ${res[0].baseOffset})`);
467
+ }
468
+ catch (error) {
469
+ (_c = this.logger) === null || _c === void 0 ? void 0 : _c.error(`❌ Failed to publish batch to "${topic}":`, error);
470
+ throw error;
471
+ }
472
+ });
473
+ }
474
+ /**
475
+ * Publishes a message to the specified Kafka topic.
476
+ * Automatically manages the producer lifecycle and includes service metadata in headers.
477
+ *
478
+ * @typeParam T - The type of the message payload
479
+ * @param topic - The Kafka topic to publish to
480
+ * @param message - The message payload (will be JSON serialized)
481
+ *
482
+ * @throws {Error} If the message fails to send
483
+ *
484
+ * @example
485
+ * ```ts
486
+ * await messaging.publish('user-events', {
487
+ * event: 'user.created',
488
+ * userId: '123',
489
+ * timestamp: Date.now()
490
+ * });
491
+ * ```
492
+ */
232
493
  publish(topic, message) {
233
494
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
234
- var _a, _b;
495
+ var _a, _b, _c;
235
496
  // Get kafka producer
236
497
  const producer = yield this.getProducer();
237
498
  // If we don't have a producer, abort
@@ -244,20 +505,59 @@ class KafkaMessaging {
244
505
  if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_address, "f")) {
245
506
  headers.address = tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_address, "f");
246
507
  }
247
- // Listen to the topic
248
- const res = yield producer.send({
249
- topic,
250
- messages: [{
251
- value: JSON.stringify(message),
252
- timestamp: `${Date.now()}`,
253
- headers
254
- }],
255
- });
256
- (_b = this.logger) === null || _b === void 0 ? void 0 : _b.verbose(`📤 Sent to KAFKA topic "${topic}" (offset ${res[0].baseOffset})`);
508
+ try {
509
+ // Send message to the topic
510
+ const res = yield producer.send({
511
+ topic,
512
+ messages: [{
513
+ value: (message instanceof Buffer ?
514
+ message : typeof message === 'string' ?
515
+ message : (message === null ? null : JSON.stringify(message))),
516
+ timestamp: `${Date.now()}`,
517
+ headers
518
+ }],
519
+ });
520
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.verbose(`📤 Sent to KAFKA topic "${topic}" (offset ${res[0].baseOffset})`);
521
+ }
522
+ catch (error) {
523
+ (_c = this.logger) === null || _c === void 0 ? void 0 : _c.error(`❌ Failed to publish to "${topic}":`, error);
524
+ throw error;
525
+ }
257
526
  });
258
527
  }
259
528
  /**
260
- * Listen to a topic
529
+ * Subscribes to a Kafka topic and processes messages with the provided handler.
530
+ * Creates a new consumer for each subscription with an auto-generated group ID.
531
+ *
532
+ * @typeParam T - The expected type of incoming messages
533
+ * @param topic - The Kafka topic to subscribe to
534
+ * @param handler - Callback function invoked for each message. Can be async.
535
+ * @param config - Optional subscription configuration
536
+ * @param config.fromBeginning - Start consuming from the beginning of the topic
537
+ * @param config.onReady - Callback invoked when the consumer is ready
538
+ * @param config.groupId - Override the auto-generated consumer group ID
539
+ * @param config.groupIdPrefix - Prefix for auto-generated group ID (default: service name)
540
+ *
541
+ * @example
542
+ * ```ts
543
+ * // Using auto-generated group ID (e.g., "my-service.user-events")
544
+ * await messaging.subscribe('user-events', handler);
545
+ *
546
+ * // Using custom group ID
547
+ * await messaging.subscribe('user-events', handler, {
548
+ * groupId: 'my-custom-consumer-group'
549
+ * });
550
+ *
551
+ * // Using custom prefix (e.g., "analytics.user-events")
552
+ * await messaging.subscribe('user-events', handler, {
553
+ * groupIdPrefix: 'analytics'
554
+ * });
555
+ *
556
+ * // With offset logging enabled
557
+ * await messaging.subscribe('user-events', handler, {
558
+ * logOffsets: true
559
+ * });
560
+ * ```
261
561
  */
262
562
  subscribe(topic, handler, config) {
263
563
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
@@ -266,46 +566,51 @@ class KafkaMessaging {
266
566
  let consumerConfig;
267
567
  let fromBeginning;
268
568
  let onReady;
569
+ let groupId;
570
+ let groupIdPrefix;
571
+ let logOffsets = false;
572
+ let onError;
269
573
  if (config) {
270
- const { fromBeginning: tmpFromBeginning, onReady: tmpOnReady } = config, tmpConsumerConfig = tslib_1.__rest(config, ["fromBeginning", "onReady"]);
574
+ const { fromBeginning: tmpFromBeginning, onReady: tmpOnReady, groupId: tmpGroupId, groupIdPrefix: tmpGroupIdPrefix, logOffsets: tmpLogOffsets, onError: tmpOnError } = config, tmpConsumerConfig = tslib_1.__rest(config, ["fromBeginning", "onReady", "groupId", "groupIdPrefix", "logOffsets", "onError"]);
271
575
  fromBeginning = tmpFromBeginning;
272
576
  onReady = tmpOnReady;
577
+ groupId = tmpGroupId;
578
+ groupIdPrefix = tmpGroupIdPrefix;
579
+ logOffsets = tmpLogOffsets !== null && tmpLogOffsets !== void 0 ? tmpLogOffsets : false;
580
+ onError = tmpOnError;
273
581
  consumerConfig = tmpConsumerConfig;
274
582
  }
583
+ // Determine the consumer group ID
584
+ const resolvedGroupId = groupId !== null && groupId !== void 0 ? groupId : `${(_b = groupIdPrefix !== null && groupIdPrefix !== void 0 ? groupIdPrefix : tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_name, "f")) !== null && _b !== void 0 ? _b : 'group'}.${topic}`;
275
585
  // Get kafka consumer
276
- const consumer = yield this.createConsumer(`${tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_name, "f") || 'group'}---${topic}`, consumerConfig);
586
+ const consumer = yield this.createConsumer(resolvedGroupId, consumerConfig);
277
587
  // If we don't have a consumer, abort
278
588
  if (!consumer)
279
- return (_b = this.logger) === null || _b === void 0 ? void 0 : _b.error('❌ Could not get consumer');
589
+ return (_c = this.logger) === null || _c === void 0 ? void 0 : _c.error('❌ Could not get consumer');
280
590
  // Listen to the topic
281
591
  yield consumer.subscribe({
282
592
  topic,
283
593
  fromBeginning
284
594
  });
285
- const admin = yield this.createAdmin();
286
- if (admin) {
287
- try {
288
- const partitions = yield admin.fetchTopicOffsets(topic);
289
- if (partitions) {
290
- partitions.forEach((partition) => {
291
- var _a;
292
- (_a = this.logger) === null || _a === void 0 ? void 0 : _a.info(`👂 Start "${topic}" partition: ${partition.partition} | offset: ${partition.offset} | high: ${partition.high} | low: ${partition.low}`);
293
- });
294
- }
595
+ // Only fetch offsets if explicitly requested (avoids admin overhead)
596
+ if (logOffsets) {
597
+ const partitions = yield this.fetchTopicOffsets(topic);
598
+ if (partitions) {
599
+ partitions.forEach((partition) => {
600
+ var _a;
601
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.info(`👂 Start "${topic}" partition: ${partition.partition} | offset: ${partition.offset} | high: ${partition.high} | low: ${partition.low}`);
602
+ });
295
603
  }
296
- catch (error) {
297
- (_c = this.logger) === null || _c === void 0 ? void 0 : _c.error(error);
298
- }
299
- yield admin.disconnect();
300
604
  }
301
605
  yield consumer.run({
302
606
  eachBatchAutoResolve: false,
303
607
  eachBatch: (_a) => tslib_1.__awaiter(this, [_a], void 0, function* ({ batch, resolveOffset, heartbeat, }) {
304
- var _b, _c, _d, _e, _f, _g;
608
+ var _b, _c, _d, _f, _g, _h, _j;
305
609
  (_b = this.logger) === null || _b === void 0 ? void 0 : _b.verbose(`📥 Received from KAFKA topic "${topic}" (${batch.messages.length} messages)`);
306
610
  for (const message of batch.messages) {
611
+ const context = {};
612
+ let value = message.value;
307
613
  try {
308
- const context = {};
309
614
  try {
310
615
  // unbufferize header values
311
616
  Object.keys((message.headers || {})).forEach(key => {
@@ -320,14 +625,34 @@ class KafkaMessaging {
320
625
  catch (e) {
321
626
  (_c = this.logger) === null || _c === void 0 ? void 0 : _c.error(`KafkaMessaging.subscribe('${topic}', …) error:`, e);
322
627
  }
323
- const res = handler(JSON.parse(((_e = (_d = message.value) === null || _d === void 0 ? void 0 : _d.toString) === null || _e === void 0 ? void 0 : _e.call(_d)) || ''), context);
628
+ const tmpValue = ((_f = (_d = message.value) === null || _d === void 0 ? void 0 : _d.toString) === null || _f === void 0 ? void 0 : _f.call(_d)) || null;
629
+ if (tmpValue) {
630
+ try {
631
+ value = JSON.parse(tmpValue);
632
+ }
633
+ catch (_e) {
634
+ // not important - keep original value
635
+ }
636
+ }
637
+ const res = handler(value, context);
324
638
  if (res)
325
639
  yield res;
326
640
  resolveOffset(message.offset);
327
- (_f = this.logger) === null || _f === void 0 ? void 0 : _f.debug(`✔️ Resolved offset ${message.offset} from KAFKA topic "${topic}"`);
641
+ (_g = this.logger) === null || _g === void 0 ? void 0 : _g.debug(`✔️ Resolved offset ${message.offset} from KAFKA topic "${topic}"`);
328
642
  }
329
643
  catch (e) {
330
- (_g = this.logger) === null || _g === void 0 ? void 0 : _g.error(`KafkaMessaging.subscribe('${topic}', …) handler throwed an error:`, e);
644
+ (_h = this.logger) === null || _h === void 0 ? void 0 : _h.error(`KafkaMessaging.subscribe('${topic}', …) handler throwed an error:`, e);
645
+ // Call custom error handler if provided
646
+ if (onError) {
647
+ try {
648
+ const errorResult = onError(e, value, context);
649
+ if (errorResult)
650
+ yield errorResult;
651
+ }
652
+ catch (onErrorError) {
653
+ (_j = this.logger) === null || _j === void 0 ? void 0 : _j.error(`KafkaMessaging.subscribe('${topic}', …) onError callback failed:`, onErrorError);
654
+ }
655
+ }
331
656
  }
332
657
  yield heartbeat();
333
658
  }
@@ -336,14 +661,39 @@ class KafkaMessaging {
336
661
  onReady === null || onReady === void 0 ? void 0 : onReady(consumer);
337
662
  });
338
663
  }
664
+ /**
665
+ * Safely disconnects a Kafka client with timeout protection.
666
+ * Prevents hanging if the client fails to disconnect gracefully.
667
+ *
668
+ * @param client - The Kafka client (producer, consumer, or admin) to disconnect
669
+ * @param timeoutMs - Maximum time to wait for disconnection (default: 5000ms)
670
+ * @returns A promise that resolves when disconnected or rejects on timeout
671
+ *
672
+ * @throws {Error} If the disconnect times out
673
+ */
339
674
  safeDisconnect(client_1) {
340
675
  return tslib_1.__awaiter(this, arguments, void 0, function* (client, timeoutMs = 5000) {
341
676
  return safeDisconnect(client, timeoutMs);
342
677
  });
343
678
  }
679
+ /**
680
+ * Gracefully shuts down all tracked Kafka clients (producers, consumers, and admins).
681
+ * Should be called during application teardown to release resources.
682
+ *
683
+ * @returns A summary of the shutdown operation including success and error counts
684
+ *
685
+ * @example
686
+ * ```ts
687
+ * process.on('SIGTERM', async () => {
688
+ * const result = await messaging.shutdown();
689
+ * console.log(`Shutdown complete: ${result.successProducers} producers, ${result.errorCount} errors`);
690
+ * process.exit(0);
691
+ * });
692
+ * ```
693
+ */
344
694
  shutdown() {
345
695
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
346
- var _a, _b, _c, _d, _e, _f;
696
+ var _a, _b, _c, _d, _f, _g;
347
697
  let errorCount = 0;
348
698
  let successProducers = 0;
349
699
  let successConsumers = 0;
@@ -381,8 +731,8 @@ class KafkaMessaging {
381
731
  errorCount++;
382
732
  }
383
733
  }
384
- (_e = this.logger) === null || _e === void 0 ? void 0 : _e.info('KafkaMessaging shutdown complete');
385
- (_f = this.logger) === null || _f === void 0 ? void 0 : _f.info(`Disconnected ${successProducers} producers, ${successConsumers} consumers and ${successAdmins} admins with ${errorCount} errors.`);
734
+ (_f = this.logger) === null || _f === void 0 ? void 0 : _f.info('KafkaMessaging shutdown complete');
735
+ (_g = this.logger) === null || _g === void 0 ? void 0 : _g.info(`Disconnected ${successProducers} producers, ${successConsumers} consumers and ${successAdmins} admins with ${errorCount} errors.`);
386
736
  return {
387
737
  successProducers,
388
738
  successConsumers,
@@ -393,7 +743,15 @@ class KafkaMessaging {
393
743
  }
394
744
  }
395
745
  exports.KafkaMessaging = KafkaMessaging;
396
- _KafkaMessaging_config = new WeakMap(), _KafkaMessaging_producerConfig = new WeakMap(), _KafkaMessaging_address = new WeakMap(), _KafkaMessaging_name = new WeakMap(), _KafkaMessaging_consumers = new WeakMap(), _KafkaMessaging_producers = new WeakMap(), _KafkaMessaging_admins = new WeakMap();
746
+ _KafkaMessaging_config = new WeakMap(), _KafkaMessaging_producerConfig = new WeakMap(), _KafkaMessaging_address = new WeakMap(), _KafkaMessaging_name = new WeakMap(), _KafkaMessaging_consumers = new WeakMap(), _KafkaMessaging_producers = new WeakMap(), _KafkaMessaging_admins = new WeakMap(), _KafkaMessaging_producerPromise = new WeakMap(), _KafkaMessaging_sharedAdmin = new WeakMap(), _KafkaMessaging_sharedAdminPromise = new WeakMap();
747
+ /**
748
+ * Safely disconnects a Kafka client with timeout protection.
749
+ * Standalone utility function.
750
+ *
751
+ * @param client - The Kafka client to disconnect
752
+ * @param timeoutMs - Maximum time to wait (default: 5000ms)
753
+ * @returns A promise that resolves when disconnected or rejects on timeout
754
+ */
397
755
  function safeDisconnect(client_1) {
398
756
  return tslib_1.__awaiter(this, arguments, void 0, function* (client, timeoutMs = 5000) {
399
757
  return Promise.race([