@drarzter/kafka-client 0.5.5 → 0.5.6

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/dist/index.js CHANGED
@@ -55,10 +55,10 @@ __export(index_exports, {
55
55
  });
56
56
  module.exports = __toCommonJS(index_exports);
57
57
 
58
- // src/client/kafka.client.ts
58
+ // src/client/kafka.client/index.ts
59
59
  var import_kafka_javascript = require("@confluentinc/kafka-javascript");
60
60
 
61
- // src/client/envelope.ts
61
+ // src/client/message/envelope.ts
62
62
  var import_node_async_hooks = require("async_hooks");
63
63
  var import_node_crypto = require("crypto");
64
64
  var HEADER_EVENT_ID = "x-event-id";
@@ -118,6 +118,108 @@ function extractEnvelope(payload, headers, topic2, partition, offset) {
118
118
  };
119
119
  }
120
120
 
121
+ // src/client/kafka.client/producer-ops.ts
122
+ function resolveTopicName(topicOrDescriptor) {
123
+ if (typeof topicOrDescriptor === "string") return topicOrDescriptor;
124
+ if (topicOrDescriptor && typeof topicOrDescriptor === "object" && "__topic" in topicOrDescriptor) {
125
+ return topicOrDescriptor.__topic;
126
+ }
127
+ return String(topicOrDescriptor);
128
+ }
129
+ function registerSchema(topicOrDesc, schemaRegistry) {
130
+ if (topicOrDesc?.__schema) {
131
+ const topic2 = resolveTopicName(topicOrDesc);
132
+ schemaRegistry.set(topic2, topicOrDesc.__schema);
133
+ }
134
+ }
135
+ async function validateMessage(topicOrDesc, message, deps) {
136
+ if (topicOrDesc?.__schema) {
137
+ return await topicOrDesc.__schema.parse(message);
138
+ }
139
+ if (deps.strictSchemasEnabled && typeof topicOrDesc === "string") {
140
+ const schema = deps.schemaRegistry.get(topicOrDesc);
141
+ if (schema) return await schema.parse(message);
142
+ }
143
+ return message;
144
+ }
145
+ async function buildSendPayload(topicOrDesc, messages, deps) {
146
+ registerSchema(topicOrDesc, deps.schemaRegistry);
147
+ const topic2 = resolveTopicName(topicOrDesc);
148
+ const builtMessages = await Promise.all(
149
+ messages.map(async (m) => {
150
+ const envelopeHeaders = buildEnvelopeHeaders({
151
+ correlationId: m.correlationId,
152
+ schemaVersion: m.schemaVersion,
153
+ eventId: m.eventId,
154
+ headers: m.headers
155
+ });
156
+ for (const inst of deps.instrumentation) {
157
+ inst.beforeSend?.(topic2, envelopeHeaders);
158
+ }
159
+ return {
160
+ value: JSON.stringify(
161
+ await validateMessage(topicOrDesc, m.value, deps)
162
+ ),
163
+ key: m.key ?? null,
164
+ headers: envelopeHeaders
165
+ };
166
+ })
167
+ );
168
+ return { topic: topic2, messages: builtMessages };
169
+ }
170
+
171
+ // src/client/kafka.client/consumer-ops.ts
172
+ function getOrCreateConsumer(groupId, fromBeginning, autoCommit, deps) {
173
+ const { consumers, consumerCreationOptions, kafka, onRebalance, logger } = deps;
174
+ if (consumers.has(groupId)) {
175
+ const prev = consumerCreationOptions.get(groupId);
176
+ if (prev.fromBeginning !== fromBeginning || prev.autoCommit !== autoCommit) {
177
+ logger.warn(
178
+ `Consumer group "${groupId}" already exists with options (fromBeginning: ${prev.fromBeginning}, autoCommit: ${prev.autoCommit}) \u2014 new options (fromBeginning: ${fromBeginning}, autoCommit: ${autoCommit}) ignored. Use a different groupId to apply different options.`
179
+ );
180
+ }
181
+ return consumers.get(groupId);
182
+ }
183
+ consumerCreationOptions.set(groupId, { fromBeginning, autoCommit });
184
+ const config = {
185
+ kafkaJS: { groupId, fromBeginning, autoCommit }
186
+ };
187
+ if (onRebalance) {
188
+ const cb = onRebalance;
189
+ config["rebalance_cb"] = (err, assignment) => {
190
+ const type = err.code === -175 ? "assign" : "revoke";
191
+ try {
192
+ cb(
193
+ type,
194
+ assignment.map((p) => ({ topic: p.topic, partition: p.partition }))
195
+ );
196
+ } catch (e) {
197
+ logger.warn(`onRebalance callback threw: ${e.message}`);
198
+ }
199
+ };
200
+ }
201
+ const consumer = kafka.consumer(config);
202
+ consumers.set(groupId, consumer);
203
+ return consumer;
204
+ }
205
+ function buildSchemaMap(topics, schemaRegistry, optionSchemas) {
206
+ const schemaMap = /* @__PURE__ */ new Map();
207
+ for (const t of topics) {
208
+ if (t?.__schema) {
209
+ const name = resolveTopicName(t);
210
+ schemaMap.set(name, t.__schema);
211
+ schemaRegistry.set(name, t.__schema);
212
+ }
213
+ }
214
+ if (optionSchemas) {
215
+ for (const [k, v] of optionSchemas) {
216
+ schemaMap.set(k, v);
217
+ schemaRegistry.set(k, v);
218
+ }
219
+ }
220
+ return schemaMap;
221
+ }
222
+
121
223
  // src/client/errors.ts
122
224
  var KafkaProcessingError = class extends Error {
123
225
  constructor(message, topic2, originalMessage, options) {
@@ -150,7 +252,7 @@ var KafkaRetryExhaustedError = class extends KafkaProcessingError {
150
252
  }
151
253
  };
152
254
 
153
- // src/client/consumer-pipeline.ts
255
+ // src/client/consumer/pipeline.ts
154
256
  function toError(error) {
155
257
  return error instanceof Error ? error : new Error(String(error));
156
258
  }
@@ -269,6 +371,53 @@ async function sendToRetryTopic(originalTopic, rawMessages, attempt, maxRetries,
269
371
  );
270
372
  }
271
373
  }
374
+ async function broadcastToInterceptors(envelopes, interceptors, cb) {
375
+ for (const env of envelopes) {
376
+ for (const interceptor of interceptors) {
377
+ await cb(interceptor, env);
378
+ }
379
+ }
380
+ }
381
+ async function runHandlerWithPipeline(fn, envelopes, interceptors, instrumentation) {
382
+ const cleanups = [];
383
+ try {
384
+ for (const env of envelopes) {
385
+ for (const inst of instrumentation) {
386
+ const cleanup = inst.beforeConsume?.(env);
387
+ if (typeof cleanup === "function") cleanups.push(cleanup);
388
+ }
389
+ }
390
+ for (const env of envelopes) {
391
+ for (const interceptor of interceptors) {
392
+ await interceptor.before?.(env);
393
+ }
394
+ }
395
+ await fn();
396
+ for (const env of envelopes) {
397
+ for (const interceptor of interceptors) {
398
+ await interceptor.after?.(env);
399
+ }
400
+ }
401
+ for (const cleanup of cleanups) cleanup();
402
+ return null;
403
+ } catch (error) {
404
+ const err = toError(error);
405
+ for (const env of envelopes) {
406
+ for (const inst of instrumentation) {
407
+ inst.onConsumeError?.(env, err);
408
+ }
409
+ }
410
+ for (const cleanup of cleanups) cleanup();
411
+ return err;
412
+ }
413
+ }
414
+ async function notifyInterceptorsOnError(envelopes, interceptors, error) {
415
+ await broadcastToInterceptors(
416
+ envelopes,
417
+ interceptors,
418
+ (i, env) => i.onError?.(env, error)
419
+ );
420
+ }
272
421
  async function executeWithRetry(fn, ctx, deps) {
273
422
  const {
274
423
  envelope,
@@ -285,98 +434,181 @@ async function executeWithRetry(fn, ctx, deps) {
285
434
  const envelopes = Array.isArray(envelope) ? envelope : [envelope];
286
435
  const topic2 = envelopes[0]?.topic ?? "unknown";
287
436
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
288
- const cleanups = [];
289
- try {
290
- for (const env of envelopes) {
291
- for (const inst of deps.instrumentation) {
292
- const cleanup = inst.beforeConsume?.(env);
293
- if (typeof cleanup === "function") cleanups.push(cleanup);
294
- }
295
- }
296
- for (const env of envelopes) {
297
- for (const interceptor of interceptors) {
298
- await interceptor.before?.(env);
299
- }
300
- }
301
- await fn();
302
- for (const env of envelopes) {
303
- for (const interceptor of interceptors) {
304
- await interceptor.after?.(env);
305
- }
306
- }
307
- for (const cleanup of cleanups) cleanup();
308
- return;
309
- } catch (error) {
310
- const err = toError(error);
311
- const isLastAttempt = attempt === maxAttempts;
312
- for (const env of envelopes) {
313
- for (const inst of deps.instrumentation) {
314
- inst.onConsumeError?.(env, err);
315
- }
316
- }
317
- for (const cleanup of cleanups) cleanup();
318
- if (isLastAttempt && maxAttempts > 1) {
319
- const exhaustedError = new KafkaRetryExhaustedError(
320
- topic2,
321
- envelopes.map((e) => e.payload),
322
- maxAttempts,
323
- { cause: err }
324
- );
325
- for (const env of envelopes) {
326
- for (const interceptor of interceptors) {
327
- await interceptor.onError?.(env, exhaustedError);
328
- }
329
- }
330
- } else {
331
- for (const env of envelopes) {
332
- for (const interceptor of interceptors) {
333
- await interceptor.onError?.(env, err);
334
- }
335
- }
336
- }
337
- deps.logger.error(
338
- `Error processing ${isBatch ? "batch" : "message"} from topic ${topic2} (attempt ${attempt}/${maxAttempts}):`,
339
- err.stack
437
+ const error = await runHandlerWithPipeline(
438
+ fn,
439
+ envelopes,
440
+ interceptors,
441
+ deps.instrumentation
442
+ );
443
+ if (!error) return;
444
+ const isLastAttempt = attempt === maxAttempts;
445
+ const reportedError = isLastAttempt && maxAttempts > 1 ? new KafkaRetryExhaustedError(
446
+ topic2,
447
+ envelopes.map((e) => e.payload),
448
+ maxAttempts,
449
+ { cause: error }
450
+ ) : error;
451
+ await notifyInterceptorsOnError(envelopes, interceptors, reportedError);
452
+ deps.logger.error(
453
+ `Error processing ${isBatch ? "batch" : "message"} from topic ${topic2} (attempt ${attempt}/${maxAttempts}):`,
454
+ error.stack
455
+ );
456
+ if (retryTopics && retry) {
457
+ const cap = Math.min(backoffMs, maxBackoffMs);
458
+ const delay = Math.floor(Math.random() * cap);
459
+ await sendToRetryTopic(
460
+ topic2,
461
+ rawMessages,
462
+ 1,
463
+ retry.maxRetries,
464
+ delay,
465
+ envelopes[0]?.headers ?? {},
466
+ deps
340
467
  );
341
- if (retryTopics && retry) {
342
- const cap = Math.min(backoffMs, maxBackoffMs);
343
- const delay = Math.floor(Math.random() * cap);
344
- await sendToRetryTopic(
345
- topic2,
346
- rawMessages,
347
- 1,
348
- retry.maxRetries,
349
- delay,
350
- envelopes[0]?.headers ?? {},
351
- deps
352
- );
353
- } else if (isLastAttempt) {
354
- if (dlq) {
355
- const dlqMeta = {
356
- error: err,
357
- attempt,
358
- originalHeaders: envelopes[0]?.headers
359
- };
360
- for (const raw of rawMessages) {
361
- await sendToDlq(topic2, raw, deps, dlqMeta);
362
- }
363
- } else {
364
- await deps.onMessageLost?.({
365
- topic: topic2,
366
- error: err,
367
- attempt,
368
- headers: envelopes[0]?.headers ?? {}
369
- });
468
+ } else if (isLastAttempt) {
469
+ if (dlq) {
470
+ const dlqMeta = {
471
+ error,
472
+ attempt,
473
+ originalHeaders: envelopes[0]?.headers
474
+ };
475
+ for (const raw of rawMessages) {
476
+ await sendToDlq(topic2, raw, deps, dlqMeta);
370
477
  }
371
478
  } else {
372
- const cap = Math.min(backoffMs * 2 ** (attempt - 1), maxBackoffMs);
373
- await sleep(Math.random() * cap);
479
+ await deps.onMessageLost?.({
480
+ topic: topic2,
481
+ error,
482
+ attempt,
483
+ headers: envelopes[0]?.headers ?? {}
484
+ });
374
485
  }
486
+ } else {
487
+ const cap = Math.min(backoffMs * 2 ** (attempt - 1), maxBackoffMs);
488
+ await sleep(Math.random() * cap);
375
489
  }
376
490
  }
377
491
  }
378
492
 
379
- // src/client/subscribe-retry.ts
493
+ // src/client/kafka.client/message-handler.ts
494
+ async function parseSingleMessage(message, topic2, partition, schemaMap, interceptors, dlq, deps) {
495
+ if (!message.value) {
496
+ deps.logger.warn(`Received empty message from topic ${topic2}`);
497
+ return null;
498
+ }
499
+ const raw = message.value.toString();
500
+ const parsed = parseJsonMessage(raw, topic2, deps.logger);
501
+ if (parsed === null) return null;
502
+ const headers = decodeHeaders(message.headers);
503
+ const validated = await validateWithSchema(
504
+ parsed,
505
+ raw,
506
+ topic2,
507
+ schemaMap,
508
+ interceptors,
509
+ dlq,
510
+ { ...deps, originalHeaders: headers }
511
+ );
512
+ if (validated === null) return null;
513
+ return extractEnvelope(validated, headers, topic2, partition, message.offset);
514
+ }
515
+ async function handleEachMessage(payload, opts, deps) {
516
+ const { topic: topic2, partition, message } = payload;
517
+ const {
518
+ schemaMap,
519
+ handleMessage,
520
+ interceptors,
521
+ dlq,
522
+ retry,
523
+ retryTopics,
524
+ timeoutMs,
525
+ wrapWithTimeout
526
+ } = opts;
527
+ const envelope = await parseSingleMessage(
528
+ message,
529
+ topic2,
530
+ partition,
531
+ schemaMap,
532
+ interceptors,
533
+ dlq,
534
+ deps
535
+ );
536
+ if (envelope === null) return;
537
+ await executeWithRetry(
538
+ () => {
539
+ const fn = () => runWithEnvelopeContext(
540
+ {
541
+ correlationId: envelope.correlationId,
542
+ traceparent: envelope.traceparent
543
+ },
544
+ () => handleMessage(envelope)
545
+ );
546
+ return timeoutMs ? wrapWithTimeout(fn, timeoutMs, topic2) : fn();
547
+ },
548
+ {
549
+ envelope,
550
+ rawMessages: [message.value.toString()],
551
+ interceptors,
552
+ dlq,
553
+ retry,
554
+ retryTopics
555
+ },
556
+ deps
557
+ );
558
+ }
559
+ async function handleEachBatch(payload, opts, deps) {
560
+ const { batch, heartbeat, resolveOffset, commitOffsetsIfNecessary } = payload;
561
+ const {
562
+ schemaMap,
563
+ handleBatch,
564
+ interceptors,
565
+ dlq,
566
+ retry,
567
+ timeoutMs,
568
+ wrapWithTimeout
569
+ } = opts;
570
+ const envelopes = [];
571
+ const rawMessages = [];
572
+ for (const message of batch.messages) {
573
+ const envelope = await parseSingleMessage(
574
+ message,
575
+ batch.topic,
576
+ batch.partition,
577
+ schemaMap,
578
+ interceptors,
579
+ dlq,
580
+ deps
581
+ );
582
+ if (envelope === null) continue;
583
+ envelopes.push(envelope);
584
+ rawMessages.push(message.value.toString());
585
+ }
586
+ if (envelopes.length === 0) return;
587
+ const meta = {
588
+ partition: batch.partition,
589
+ highWatermark: batch.highWatermark,
590
+ heartbeat,
591
+ resolveOffset,
592
+ commitOffsetsIfNecessary
593
+ };
594
+ await executeWithRetry(
595
+ () => {
596
+ const fn = () => handleBatch(envelopes, meta);
597
+ return timeoutMs ? wrapWithTimeout(fn, timeoutMs, batch.topic) : fn();
598
+ },
599
+ {
600
+ envelope: envelopes,
601
+ rawMessages: batch.messages.filter((m) => m.value).map((m) => m.value.toString()),
602
+ interceptors,
603
+ dlq,
604
+ retry,
605
+ isBatch: true
606
+ },
607
+ deps
608
+ );
609
+ }
610
+
611
+ // src/client/consumer/subscribe-retry.ts
380
612
  async function subscribeWithRetry(consumer, topics, logger, retryOpts) {
381
613
  const maxAttempts = retryOpts?.retries ?? 5;
382
614
  const backoffMs = retryOpts?.backoffMs ?? 5e3;
@@ -395,7 +627,156 @@ async function subscribeWithRetry(consumer, topics, logger, retryOpts) {
395
627
  }
396
628
  }
397
629
 
398
- // src/client/kafka.client.ts
630
+ // src/client/kafka.client/retry-topic.ts
631
+ async function waitForPartitionAssignment(consumer, topics, logger, timeoutMs = 1e4) {
632
+ const topicSet = new Set(topics);
633
+ const deadline = Date.now() + timeoutMs;
634
+ while (Date.now() < deadline) {
635
+ try {
636
+ const assigned = consumer.assignment();
637
+ if (assigned.some((a) => topicSet.has(a.topic))) return;
638
+ } catch {
639
+ }
640
+ await sleep(200);
641
+ }
642
+ logger.warn(
643
+ `Retry consumer did not receive partition assignments for [${topics.join(", ")}] within ${timeoutMs}ms`
644
+ );
645
+ }
646
+ async function startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap, deps) {
647
+ const {
648
+ logger,
649
+ producer,
650
+ instrumentation,
651
+ onMessageLost,
652
+ ensureTopic,
653
+ getOrCreateConsumer: getOrCreateConsumer2,
654
+ runningConsumers
655
+ } = deps;
656
+ const retryTopicNames = originalTopics.map((t) => `${t}.retry`);
657
+ const retryGroupId = `${originalGroupId}-retry`;
658
+ const backoffMs = retry.backoffMs ?? 1e3;
659
+ const maxBackoffMs = retry.maxBackoffMs ?? 3e4;
660
+ const pipelineDeps = { logger, producer, instrumentation, onMessageLost };
661
+ for (const rt of retryTopicNames) {
662
+ await ensureTopic(rt);
663
+ }
664
+ const consumer = getOrCreateConsumer2(retryGroupId, false, true);
665
+ await consumer.connect();
666
+ await subscribeWithRetry(consumer, retryTopicNames, logger);
667
+ await consumer.run({
668
+ eachMessage: async ({ topic: retryTopic, partition, message }) => {
669
+ if (!message.value) return;
670
+ const raw = message.value.toString();
671
+ const parsed = parseJsonMessage(raw, retryTopic, logger);
672
+ if (parsed === null) return;
673
+ const headers = decodeHeaders(message.headers);
674
+ const originalTopic = headers[RETRY_HEADER_ORIGINAL_TOPIC] ?? retryTopic.replace(/\.retry$/, "");
675
+ const currentAttempt = parseInt(
676
+ headers[RETRY_HEADER_ATTEMPT] ?? "1",
677
+ 10
678
+ );
679
+ const maxRetries = parseInt(
680
+ headers[RETRY_HEADER_MAX_RETRIES] ?? String(retry.maxRetries),
681
+ 10
682
+ );
683
+ const retryAfter = parseInt(
684
+ headers[RETRY_HEADER_AFTER] ?? "0",
685
+ 10
686
+ );
687
+ const remaining = retryAfter - Date.now();
688
+ if (remaining > 0) {
689
+ consumer.pause([{ topic: retryTopic, partitions: [partition] }]);
690
+ await sleep(remaining);
691
+ consumer.resume([{ topic: retryTopic, partitions: [partition] }]);
692
+ }
693
+ const validated = await validateWithSchema(
694
+ parsed,
695
+ raw,
696
+ originalTopic,
697
+ schemaMap,
698
+ interceptors,
699
+ dlq,
700
+ { ...pipelineDeps, originalHeaders: headers }
701
+ );
702
+ if (validated === null) return;
703
+ const envelope = extractEnvelope(
704
+ validated,
705
+ headers,
706
+ originalTopic,
707
+ partition,
708
+ message.offset
709
+ );
710
+ const error = await runHandlerWithPipeline(
711
+ () => runWithEnvelopeContext(
712
+ {
713
+ correlationId: envelope.correlationId,
714
+ traceparent: envelope.traceparent
715
+ },
716
+ () => handleMessage(envelope)
717
+ ),
718
+ [envelope],
719
+ interceptors,
720
+ instrumentation
721
+ );
722
+ if (error) {
723
+ const nextAttempt = currentAttempt + 1;
724
+ const exhausted = currentAttempt >= maxRetries;
725
+ const reportedError = exhausted && maxRetries > 1 ? new KafkaRetryExhaustedError(
726
+ originalTopic,
727
+ [envelope.payload],
728
+ maxRetries,
729
+ { cause: error }
730
+ ) : error;
731
+ await notifyInterceptorsOnError(
732
+ [envelope],
733
+ interceptors,
734
+ reportedError
735
+ );
736
+ logger.error(
737
+ `Retry consumer error for ${originalTopic} (attempt ${currentAttempt}/${maxRetries}):`,
738
+ error.stack
739
+ );
740
+ if (!exhausted) {
741
+ const cap = Math.min(backoffMs * 2 ** currentAttempt, maxBackoffMs);
742
+ const delay = Math.floor(Math.random() * cap);
743
+ await sendToRetryTopic(
744
+ originalTopic,
745
+ [raw],
746
+ nextAttempt,
747
+ maxRetries,
748
+ delay,
749
+ headers,
750
+ pipelineDeps
751
+ );
752
+ } else if (dlq) {
753
+ await sendToDlq(originalTopic, raw, pipelineDeps, {
754
+ error,
755
+ // +1 to account for the main consumer's initial attempt before
756
+ // routing to the retry topic, making this consistent with the
757
+ // in-process retry path where attempt counts all tries.
758
+ attempt: currentAttempt + 1,
759
+ originalHeaders: headers
760
+ });
761
+ } else {
762
+ await onMessageLost?.({
763
+ topic: originalTopic,
764
+ error,
765
+ attempt: currentAttempt,
766
+ headers
767
+ });
768
+ }
769
+ }
770
+ }
771
+ });
772
+ runningConsumers.set(retryGroupId, "eachMessage");
773
+ await waitForPartitionAssignment(consumer, retryTopicNames, logger);
774
+ logger.log(
775
+ `Retry topic consumers started for: ${originalTopics.join(", ")} (group: ${retryGroupId})`
776
+ );
777
+ }
778
+
779
+ // src/client/kafka.client/index.ts
399
780
  var { Kafka: KafkaClass, logLevel: KafkaLogLevel } = import_kafka_javascript.KafkaJS;
400
781
  var KafkaClient = class {
401
782
  kafka;
@@ -411,6 +792,7 @@ var KafkaClient = class {
411
792
  defaultGroupId;
412
793
  schemaRegistry = /* @__PURE__ */ new Map();
413
794
  runningConsumers = /* @__PURE__ */ new Map();
795
+ consumerCreationOptions = /* @__PURE__ */ new Map();
414
796
  instrumentation;
415
797
  onMessageLost;
416
798
  onRebalance;
@@ -445,7 +827,7 @@ var KafkaClient = class {
445
827
  this.admin = this.kafka.admin();
446
828
  }
447
829
  async sendMessage(topicOrDesc, message, options = {}) {
448
- const payload = await this.buildSendPayload(topicOrDesc, [
830
+ const payload = await this.preparePayload(topicOrDesc, [
449
831
  {
450
832
  value: message,
451
833
  key: options.key,
@@ -455,19 +837,13 @@ var KafkaClient = class {
455
837
  eventId: options.eventId
456
838
  }
457
839
  ]);
458
- await this.ensureTopic(payload.topic);
459
840
  await this.producer.send(payload);
460
- for (const inst of this.instrumentation) {
461
- inst.afterSend?.(payload.topic);
462
- }
841
+ this.notifyAfterSend(payload.topic, payload.messages.length);
463
842
  }
464
843
  async sendBatch(topicOrDesc, messages) {
465
- const payload = await this.buildSendPayload(topicOrDesc, messages);
466
- await this.ensureTopic(payload.topic);
844
+ const payload = await this.preparePayload(topicOrDesc, messages);
467
845
  await this.producer.send(payload);
468
- for (const inst of this.instrumentation) {
469
- inst.afterSend?.(payload.topic);
470
- }
846
+ this.notifyAfterSend(payload.topic, payload.messages.length);
471
847
  }
472
848
  /** Execute multiple sends atomically. Commits on success, aborts on error. */
473
849
  async transaction(fn) {
@@ -486,7 +862,7 @@ var KafkaClient = class {
486
862
  try {
487
863
  const ctx = {
488
864
  send: async (topicOrDesc, message, options = {}) => {
489
- const payload = await this.buildSendPayload(topicOrDesc, [
865
+ const payload = await this.preparePayload(topicOrDesc, [
490
866
  {
491
867
  value: message,
492
868
  key: options.key,
@@ -496,13 +872,10 @@ var KafkaClient = class {
496
872
  eventId: options.eventId
497
873
  }
498
874
  ]);
499
- await this.ensureTopic(payload.topic);
500
875
  await tx.send(payload);
501
876
  },
502
877
  sendBatch: async (topicOrDesc, messages) => {
503
- const payload = await this.buildSendPayload(topicOrDesc, messages);
504
- await this.ensureTopic(payload.topic);
505
- await tx.send(payload);
878
+ await tx.send(await this.preparePayload(topicOrDesc, messages));
506
879
  }
507
880
  };
508
881
  await fn(ctx);
@@ -536,151 +909,57 @@ var KafkaClient = class {
536
909
  );
537
910
  }
538
911
  const { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry } = await this.setupConsumer(topics, "eachMessage", options);
539
- const deps = {
540
- logger: this.logger,
541
- producer: this.producer,
542
- instrumentation: this.instrumentation,
543
- onMessageLost: this.onMessageLost
544
- };
912
+ const deps = this.messageDeps;
545
913
  const timeoutMs = options.handlerTimeoutMs;
546
914
  await consumer.run({
547
- eachMessage: async ({ topic: topic2, partition, message }) => {
548
- if (!message.value) {
549
- this.logger.warn(`Received empty message from topic ${topic2}`);
550
- return;
551
- }
552
- const raw = message.value.toString();
553
- const parsed = parseJsonMessage(raw, topic2, this.logger);
554
- if (parsed === null) return;
555
- const headers = decodeHeaders(message.headers);
556
- const validated = await validateWithSchema(
557
- parsed,
558
- raw,
559
- topic2,
915
+ eachMessage: (payload) => handleEachMessage(
916
+ payload,
917
+ {
560
918
  schemaMap,
919
+ handleMessage,
561
920
  interceptors,
562
921
  dlq,
563
- { ...deps, originalHeaders: headers }
564
- );
565
- if (validated === null) return;
566
- const envelope = extractEnvelope(
567
- validated,
568
- headers,
569
- topic2,
570
- partition,
571
- message.offset
572
- );
573
- await executeWithRetry(
574
- () => {
575
- const fn = () => runWithEnvelopeContext(
576
- {
577
- correlationId: envelope.correlationId,
578
- traceparent: envelope.traceparent
579
- },
580
- () => handleMessage(envelope)
581
- );
582
- return timeoutMs ? this.wrapWithTimeoutWarning(fn, timeoutMs, topic2) : fn();
583
- },
584
- {
585
- envelope,
586
- rawMessages: [raw],
587
- interceptors,
588
- dlq,
589
- retry,
590
- retryTopics: options.retryTopics
591
- },
592
- deps
593
- );
594
- }
922
+ retry,
923
+ retryTopics: options.retryTopics,
924
+ timeoutMs,
925
+ wrapWithTimeout: this.wrapWithTimeoutWarning.bind(this)
926
+ },
927
+ deps
928
+ )
595
929
  });
596
930
  this.runningConsumers.set(gid, "eachMessage");
597
931
  if (options.retryTopics && retry) {
598
- await this.startRetryTopicConsumers(
932
+ await startRetryTopicConsumers(
599
933
  topicNames,
600
934
  gid,
601
935
  handleMessage,
602
936
  retry,
603
937
  dlq,
604
938
  interceptors,
605
- schemaMap
939
+ schemaMap,
940
+ this.retryTopicDeps
606
941
  );
607
942
  }
608
943
  return { groupId: gid, stop: () => this.stopConsumer(gid) };
609
944
  }
610
945
  async startBatchConsumer(topics, handleBatch, options = {}) {
611
946
  const { consumer, schemaMap, gid, dlq, interceptors, retry } = await this.setupConsumer(topics, "eachBatch", options);
612
- const deps = {
613
- logger: this.logger,
614
- producer: this.producer,
615
- instrumentation: this.instrumentation,
616
- onMessageLost: this.onMessageLost
617
- };
947
+ const deps = this.messageDeps;
618
948
  const timeoutMs = options.handlerTimeoutMs;
619
949
  await consumer.run({
620
- eachBatch: async ({
621
- batch,
622
- heartbeat,
623
- resolveOffset,
624
- commitOffsetsIfNecessary
625
- }) => {
626
- const envelopes = [];
627
- const rawMessages = [];
628
- for (const message of batch.messages) {
629
- if (!message.value) {
630
- this.logger.warn(
631
- `Received empty message from topic ${batch.topic}`
632
- );
633
- continue;
634
- }
635
- const raw = message.value.toString();
636
- const parsed = parseJsonMessage(raw, batch.topic, this.logger);
637
- if (parsed === null) continue;
638
- const headers = decodeHeaders(message.headers);
639
- const validated = await validateWithSchema(
640
- parsed,
641
- raw,
642
- batch.topic,
643
- schemaMap,
644
- interceptors,
645
- dlq,
646
- { ...deps, originalHeaders: headers }
647
- );
648
- if (validated === null) continue;
649
- envelopes.push(
650
- extractEnvelope(
651
- validated,
652
- headers,
653
- batch.topic,
654
- batch.partition,
655
- message.offset
656
- )
657
- );
658
- rawMessages.push(raw);
659
- }
660
- if (envelopes.length === 0) return;
661
- const meta = {
662
- partition: batch.partition,
663
- highWatermark: batch.highWatermark,
664
- heartbeat,
665
- resolveOffset,
666
- commitOffsetsIfNecessary
667
- };
668
- await executeWithRetry(
669
- () => {
670
- const fn = () => handleBatch(envelopes, meta);
671
- return timeoutMs ? this.wrapWithTimeoutWarning(fn, timeoutMs, batch.topic) : fn();
672
- },
673
- {
674
- envelope: envelopes,
675
- rawMessages: batch.messages.filter((m) => m.value).map((m) => m.value.toString()),
676
- interceptors,
677
- dlq,
678
- retry,
679
- isBatch: true
680
- },
681
- deps
682
- );
683
- }
950
+ eachBatch: (payload) => handleEachBatch(
951
+ payload,
952
+ {
953
+ schemaMap,
954
+ handleBatch,
955
+ interceptors,
956
+ dlq,
957
+ retry,
958
+ timeoutMs,
959
+ wrapWithTimeout: this.wrapWithTimeoutWarning.bind(this)
960
+ },
961
+ deps
962
+ )
684
963
  });
685
964
  this.runningConsumers.set(gid, "eachBatch");
686
965
  return { groupId: gid, stop: () => this.stopConsumer(gid) };
@@ -699,6 +978,7 @@ var KafkaClient = class {
699
978
  });
700
979
  this.consumers.delete(groupId);
701
980
  this.runningConsumers.delete(groupId);
981
+ this.consumerCreationOptions.delete(groupId);
702
982
  this.logger.log(`Consumer disconnected: group "${groupId}"`);
703
983
  } else {
704
984
  const tasks = Array.from(this.consumers.values()).map(
@@ -708,6 +988,7 @@ var KafkaClient = class {
708
988
  await Promise.allSettled(tasks);
709
989
  this.consumers.clear();
710
990
  this.runningConsumers.clear();
991
+ this.consumerCreationOptions.clear();
711
992
  this.logger.log("All consumers disconnected");
712
993
  }
713
994
  }
@@ -766,204 +1047,26 @@ var KafkaClient = class {
766
1047
  await Promise.allSettled(tasks);
767
1048
  this.consumers.clear();
768
1049
  this.runningConsumers.clear();
1050
+ this.consumerCreationOptions.clear();
769
1051
  this.logger.log("All connections closed");
770
1052
  }
771
- // ── Retry topic chain ────────────────────────────────────────────
772
- /**
773
- * Auto-start companion consumers on `<topic>.retry` for each original topic.
774
- * Called by `startConsumer` when `retryTopics: true`.
775
- *
776
- * Flow per message:
777
- * 1. Sleep until `x-retry-after` (scheduled by the main consumer or previous retry hop)
778
- * 2. Call the original handler
779
- * 3. On failure: if retries remain → re-send to `<originalTopic>.retry` with incremented attempt
780
- * if exhausted → DLQ or onMessageLost
781
- */
782
- async startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap) {
783
- const retryTopicNames = originalTopics.map((t) => `${t}.retry`);
784
- const retryGroupId = `${originalGroupId}-retry`;
785
- const backoffMs = retry.backoffMs ?? 1e3;
786
- const maxBackoffMs = retry.maxBackoffMs ?? 3e4;
787
- const deps = {
788
- logger: this.logger,
789
- producer: this.producer,
790
- instrumentation: this.instrumentation,
791
- onMessageLost: this.onMessageLost
792
- };
793
- for (const rt of retryTopicNames) {
794
- await this.ensureTopic(rt);
795
- }
796
- const consumer = this.getOrCreateConsumer(retryGroupId, false, true);
797
- await consumer.connect();
798
- await subscribeWithRetry(consumer, retryTopicNames, this.logger);
799
- await consumer.run({
800
- eachMessage: async ({ topic: retryTopic, partition, message }) => {
801
- if (!message.value) return;
802
- const raw = message.value.toString();
803
- const parsed = parseJsonMessage(raw, retryTopic, this.logger);
804
- if (parsed === null) return;
805
- const headers = decodeHeaders(message.headers);
806
- const originalTopic = headers[RETRY_HEADER_ORIGINAL_TOPIC] ?? retryTopic.replace(/\.retry$/, "");
807
- const currentAttempt = parseInt(
808
- headers[RETRY_HEADER_ATTEMPT] ?? "1",
809
- 10
810
- );
811
- const maxRetries = parseInt(
812
- headers[RETRY_HEADER_MAX_RETRIES] ?? String(retry.maxRetries),
813
- 10
814
- );
815
- const retryAfter = parseInt(
816
- headers[RETRY_HEADER_AFTER] ?? "0",
817
- 10
818
- );
819
- const remaining = retryAfter - Date.now();
820
- if (remaining > 0) {
821
- consumer.pause([{ topic: retryTopic, partitions: [partition] }]);
822
- await sleep(remaining);
823
- consumer.resume([{ topic: retryTopic, partitions: [partition] }]);
824
- }
825
- const validated = await validateWithSchema(
826
- parsed,
827
- raw,
828
- originalTopic,
829
- schemaMap,
830
- interceptors,
831
- dlq,
832
- { ...deps, originalHeaders: headers }
833
- );
834
- if (validated === null) return;
835
- const envelope = extractEnvelope(
836
- validated,
837
- headers,
838
- originalTopic,
839
- partition,
840
- message.offset
841
- );
842
- try {
843
- const cleanups = [];
844
- for (const inst of this.instrumentation) {
845
- const c = inst.beforeConsume?.(envelope);
846
- if (typeof c === "function") cleanups.push(c);
847
- }
848
- for (const interceptor of interceptors)
849
- await interceptor.before?.(envelope);
850
- await runWithEnvelopeContext(
851
- {
852
- correlationId: envelope.correlationId,
853
- traceparent: envelope.traceparent
854
- },
855
- () => handleMessage(envelope)
856
- );
857
- for (const interceptor of interceptors)
858
- await interceptor.after?.(envelope);
859
- for (const cleanup of cleanups) cleanup();
860
- } catch (error) {
861
- const err = toError(error);
862
- const nextAttempt = currentAttempt + 1;
863
- const exhausted = currentAttempt >= maxRetries;
864
- for (const inst of this.instrumentation)
865
- inst.onConsumeError?.(envelope, err);
866
- const reportedError = exhausted && maxRetries > 1 ? new KafkaRetryExhaustedError(
867
- originalTopic,
868
- [envelope.payload],
869
- maxRetries,
870
- { cause: err }
871
- ) : err;
872
- for (const interceptor of interceptors) {
873
- await interceptor.onError?.(envelope, reportedError);
874
- }
875
- this.logger.error(
876
- `Retry consumer error for ${originalTopic} (attempt ${currentAttempt}/${maxRetries}):`,
877
- err.stack
878
- );
879
- if (!exhausted) {
880
- const cap = Math.min(backoffMs * 2 ** currentAttempt, maxBackoffMs);
881
- const delay = Math.floor(Math.random() * cap);
882
- await sendToRetryTopic(
883
- originalTopic,
884
- [raw],
885
- nextAttempt,
886
- maxRetries,
887
- delay,
888
- headers,
889
- deps
890
- );
891
- } else if (dlq) {
892
- await sendToDlq(originalTopic, raw, deps, {
893
- error: err,
894
- // +1 to account for the main consumer's initial attempt before
895
- // routing to the retry topic, making this consistent with the
896
- // in-process retry path where attempt counts all tries.
897
- attempt: currentAttempt + 1,
898
- originalHeaders: headers
899
- });
900
- } else {
901
- await deps.onMessageLost?.({
902
- topic: originalTopic,
903
- error: err,
904
- attempt: currentAttempt,
905
- headers
906
- });
907
- }
908
- }
909
- }
910
- });
911
- this.runningConsumers.set(retryGroupId, "eachMessage");
912
- await this.waitForPartitionAssignment(consumer, retryTopicNames);
913
- this.logger.log(
914
- `Retry topic consumers started for: ${originalTopics.join(", ")} (group: ${retryGroupId})`
915
- );
916
- }
917
1053
  // ── Private helpers ──────────────────────────────────────────────
918
- /**
919
- * Poll `consumer.assignment()` until the consumer has received at least one
920
- * partition for the given topics, then return. Logs a warning and returns
921
- * (rather than throwing) on timeout so that a slow broker does not break
922
- * the caller — in the worst case a message sent immediately after would be
923
- * missed, which is the same behaviour as before this guard was added.
924
- */
925
- async waitForPartitionAssignment(consumer, topics, timeoutMs = 1e4) {
926
- const topicSet = new Set(topics);
927
- const deadline = Date.now() + timeoutMs;
928
- while (Date.now() < deadline) {
929
- try {
930
- const assigned = consumer.assignment();
931
- if (assigned.some((a) => topicSet.has(a.topic))) return;
932
- } catch {
933
- }
934
- await sleep(200);
935
- }
936
- this.logger.warn(
937
- `Retry consumer did not receive partition assignments for [${topics.join(", ")}] within ${timeoutMs}ms`
1054
+ async preparePayload(topicOrDesc, messages) {
1055
+ const payload = await buildSendPayload(
1056
+ topicOrDesc,
1057
+ messages,
1058
+ this.producerOpsDeps
938
1059
  );
1060
+ await this.ensureTopic(payload.topic);
1061
+ return payload;
939
1062
  }
940
- getOrCreateConsumer(groupId, fromBeginning, autoCommit) {
941
- if (!this.consumers.has(groupId)) {
942
- const config = {
943
- kafkaJS: { groupId, fromBeginning, autoCommit }
944
- };
945
- if (this.onRebalance) {
946
- const onRebalance = this.onRebalance;
947
- config["rebalance_cb"] = (err, assignment) => {
948
- const type = err.code === -175 ? "assign" : "revoke";
949
- try {
950
- onRebalance(
951
- type,
952
- assignment.map((p) => ({
953
- topic: p.topic,
954
- partition: p.partition
955
- }))
956
- );
957
- } catch (e) {
958
- this.logger.warn(
959
- `onRebalance callback threw: ${e.message}`
960
- );
961
- }
962
- };
1063
+ // afterSend is called once per message — symmetric with beforeSend in buildSendPayload.
1064
+ notifyAfterSend(topic2, count) {
1065
+ for (let i = 0; i < count; i++) {
1066
+ for (const inst of this.instrumentation) {
1067
+ inst.afterSend?.(topic2);
963
1068
  }
964
- this.consumers.set(groupId, this.kafka.consumer(config));
965
1069
  }
966
- return this.consumers.get(groupId);
967
1070
  }
968
1071
  /**
969
1072
  * Start a timer that logs a warning if `fn` hasn't resolved within `timeoutMs`.
@@ -981,13 +1084,6 @@ var KafkaClient = class {
981
1084
  }, timeoutMs);
982
1085
  return promise;
983
1086
  }
984
- resolveTopicName(topicOrDescriptor) {
985
- if (typeof topicOrDescriptor === "string") return topicOrDescriptor;
986
- if (topicOrDescriptor && typeof topicOrDescriptor === "object" && "__topic" in topicOrDescriptor) {
987
- return topicOrDescriptor.__topic;
988
- }
989
- return String(topicOrDescriptor);
990
- }
991
1087
  async ensureTopic(topic2) {
992
1088
  if (!this.autoCreateTopicsEnabled || this.ensuredTopics.has(topic2)) return;
993
1089
  if (!this.isAdminConnected) {
@@ -999,54 +1095,6 @@ var KafkaClient = class {
999
1095
  });
1000
1096
  this.ensuredTopics.add(topic2);
1001
1097
  }
1002
- /** Register schema from descriptor into global registry (side-effect). */
1003
- registerSchema(topicOrDesc) {
1004
- if (topicOrDesc?.__schema) {
1005
- const topic2 = this.resolveTopicName(topicOrDesc);
1006
- this.schemaRegistry.set(topic2, topicOrDesc.__schema);
1007
- }
1008
- }
1009
- /** Validate message against schema. Pure — no side-effects on registry. */
1010
- async validateMessage(topicOrDesc, message) {
1011
- if (topicOrDesc?.__schema) {
1012
- return await topicOrDesc.__schema.parse(message);
1013
- }
1014
- if (this.strictSchemasEnabled && typeof topicOrDesc === "string") {
1015
- const schema = this.schemaRegistry.get(topicOrDesc);
1016
- if (schema) return await schema.parse(message);
1017
- }
1018
- return message;
1019
- }
1020
- /**
1021
- * Build a kafkajs-ready send payload.
1022
- * Handles: topic resolution, schema registration, validation, JSON serialization,
1023
- * envelope header generation, and instrumentation hooks.
1024
- */
1025
- async buildSendPayload(topicOrDesc, messages) {
1026
- this.registerSchema(topicOrDesc);
1027
- const topic2 = this.resolveTopicName(topicOrDesc);
1028
- const builtMessages = await Promise.all(
1029
- messages.map(async (m) => {
1030
- const envelopeHeaders = buildEnvelopeHeaders({
1031
- correlationId: m.correlationId,
1032
- schemaVersion: m.schemaVersion,
1033
- eventId: m.eventId,
1034
- headers: m.headers
1035
- });
1036
- for (const inst of this.instrumentation) {
1037
- inst.beforeSend?.(topic2, envelopeHeaders);
1038
- }
1039
- return {
1040
- value: JSON.stringify(
1041
- await this.validateMessage(topicOrDesc, m.value)
1042
- ),
1043
- key: m.key ?? null,
1044
- headers: envelopeHeaders
1045
- };
1046
- })
1047
- );
1048
- return { topic: topic2, messages: builtMessages };
1049
- }
1050
1098
  /** Shared consumer setup: groupId check, schema map, connect, subscribe. */
1051
1099
  async setupConsumer(topics, mode, options) {
1052
1100
  const {
@@ -1065,15 +1113,18 @@ var KafkaClient = class {
1065
1113
  `Cannot use ${mode} on consumer group "${gid}" \u2014 it is already running with ${oppositeMode}. Use a different groupId for this consumer.`
1066
1114
  );
1067
1115
  }
1068
- const consumer = this.getOrCreateConsumer(
1116
+ const consumer = getOrCreateConsumer(
1069
1117
  gid,
1070
1118
  fromBeginning,
1071
- options.autoCommit ?? true
1119
+ options.autoCommit ?? true,
1120
+ this.consumerOpsDeps
1072
1121
  );
1073
- const schemaMap = this.buildSchemaMap(topics, optionSchemas);
1074
- const topicNames = topics.map(
1075
- (t) => this.resolveTopicName(t)
1122
+ const schemaMap = buildSchemaMap(
1123
+ topics,
1124
+ this.schemaRegistry,
1125
+ optionSchemas
1076
1126
  );
1127
+ const topicNames = topics.map((t) => resolveTopicName(t));
1077
1128
  for (const t of topicNames) {
1078
1129
  await this.ensureTopic(t);
1079
1130
  }
@@ -1094,26 +1145,45 @@ var KafkaClient = class {
1094
1145
  );
1095
1146
  return { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry };
1096
1147
  }
1097
- buildSchemaMap(topics, optionSchemas) {
1098
- const schemaMap = /* @__PURE__ */ new Map();
1099
- for (const t of topics) {
1100
- if (t?.__schema) {
1101
- const name = this.resolveTopicName(t);
1102
- schemaMap.set(name, t.__schema);
1103
- this.schemaRegistry.set(name, t.__schema);
1104
- }
1105
- }
1106
- if (optionSchemas) {
1107
- for (const [k, v] of optionSchemas) {
1108
- schemaMap.set(k, v);
1109
- this.schemaRegistry.set(k, v);
1110
- }
1111
- }
1112
- return schemaMap;
1148
+ // ── Deps object getters ──────────────────────────────────────────
1149
+ get producerOpsDeps() {
1150
+ return {
1151
+ schemaRegistry: this.schemaRegistry,
1152
+ strictSchemasEnabled: this.strictSchemasEnabled,
1153
+ instrumentation: this.instrumentation
1154
+ };
1155
+ }
1156
+ get consumerOpsDeps() {
1157
+ return {
1158
+ consumers: this.consumers,
1159
+ consumerCreationOptions: this.consumerCreationOptions,
1160
+ kafka: this.kafka,
1161
+ onRebalance: this.onRebalance,
1162
+ logger: this.logger
1163
+ };
1164
+ }
1165
+ get messageDeps() {
1166
+ return {
1167
+ logger: this.logger,
1168
+ producer: this.producer,
1169
+ instrumentation: this.instrumentation,
1170
+ onMessageLost: this.onMessageLost
1171
+ };
1172
+ }
1173
+ get retryTopicDeps() {
1174
+ return {
1175
+ logger: this.logger,
1176
+ producer: this.producer,
1177
+ instrumentation: this.instrumentation,
1178
+ onMessageLost: this.onMessageLost,
1179
+ ensureTopic: (t) => this.ensureTopic(t),
1180
+ getOrCreateConsumer: (gid, fb, ac) => getOrCreateConsumer(gid, fb, ac, this.consumerOpsDeps),
1181
+ runningConsumers: this.runningConsumers
1182
+ };
1113
1183
  }
1114
1184
  };
1115
1185
 
1116
- // src/client/topic.ts
1186
+ // src/client/message/topic.ts
1117
1187
  function topic(name) {
1118
1188
  const fn = () => ({
1119
1189
  __topic: name,
@@ -1243,35 +1313,17 @@ var KafkaModule = class {
1243
1313
  const token = getKafkaClientToken(options.name);
1244
1314
  const kafkaClientProvider = {
1245
1315
  provide: token,
1246
- useFactory: async () => {
1247
- const client = new KafkaClient(
1248
- options.clientId,
1249
- options.groupId,
1250
- options.brokers,
1251
- {
1252
- autoCreateTopics: options.autoCreateTopics,
1253
- strictSchemas: options.strictSchemas,
1254
- numPartitions: options.numPartitions,
1255
- instrumentation: options.instrumentation,
1256
- logger: new import_common3.Logger(`KafkaClient:${options.clientId}`)
1257
- }
1258
- );
1259
- await client.connectProducer();
1260
- return client;
1261
- }
1262
- };
1263
- const destroyProvider = {
1264
- provide: `${token}_DESTROY`,
1265
- useFactory: (client) => ({
1266
- onModuleDestroy: () => client.disconnect()
1267
- }),
1268
- inject: [token]
1316
+ useFactory: () => KafkaModule.buildClient(options)
1269
1317
  };
1270
1318
  return {
1271
1319
  global: options.isGlobal ?? false,
1272
1320
  module: KafkaModule,
1273
1321
  imports: [import_core2.DiscoveryModule],
1274
- providers: [kafkaClientProvider, destroyProvider, KafkaExplorer],
1322
+ providers: [
1323
+ kafkaClientProvider,
1324
+ KafkaModule.buildDestroyProvider(token),
1325
+ KafkaExplorer
1326
+ ],
1275
1327
  exports: [kafkaClientProvider]
1276
1328
  };
1277
1329
  }
@@ -1280,40 +1332,48 @@ var KafkaModule = class {
1280
1332
  const token = getKafkaClientToken(asyncOptions.name);
1281
1333
  const kafkaClientProvider = {
1282
1334
  provide: token,
1283
- useFactory: async (...args) => {
1284
- const options = await asyncOptions.useFactory(...args);
1285
- const client = new KafkaClient(
1286
- options.clientId,
1287
- options.groupId,
1288
- options.brokers,
1289
- {
1290
- autoCreateTopics: options.autoCreateTopics,
1291
- strictSchemas: options.strictSchemas,
1292
- numPartitions: options.numPartitions,
1293
- instrumentation: options.instrumentation,
1294
- logger: new import_common3.Logger(`KafkaClient:${options.clientId}`)
1295
- }
1296
- );
1297
- await client.connectProducer();
1298
- return client;
1299
- },
1335
+ useFactory: async (...args) => KafkaModule.buildClient(await asyncOptions.useFactory(...args)),
1300
1336
  inject: asyncOptions.inject || []
1301
1337
  };
1302
- const destroyProvider = {
1303
- provide: `${token}_DESTROY`,
1304
- useFactory: (client) => ({
1305
- onModuleDestroy: () => client.disconnect()
1306
- }),
1307
- inject: [token]
1308
- };
1309
1338
  return {
1310
1339
  global: asyncOptions.isGlobal ?? false,
1311
1340
  module: KafkaModule,
1312
1341
  imports: [...asyncOptions.imports || [], import_core2.DiscoveryModule],
1313
- providers: [kafkaClientProvider, destroyProvider, KafkaExplorer],
1342
+ providers: [
1343
+ kafkaClientProvider,
1344
+ KafkaModule.buildDestroyProvider(token),
1345
+ KafkaExplorer
1346
+ ],
1314
1347
  exports: [kafkaClientProvider]
1315
1348
  };
1316
1349
  }
1350
+ static async buildClient(options) {
1351
+ const client = new KafkaClient(
1352
+ options.clientId,
1353
+ options.groupId,
1354
+ options.brokers,
1355
+ {
1356
+ autoCreateTopics: options.autoCreateTopics,
1357
+ strictSchemas: options.strictSchemas,
1358
+ numPartitions: options.numPartitions,
1359
+ instrumentation: options.instrumentation,
1360
+ onMessageLost: options.onMessageLost,
1361
+ onRebalance: options.onRebalance,
1362
+ logger: new import_common3.Logger(`KafkaClient:${options.clientId}`)
1363
+ }
1364
+ );
1365
+ await client.connectProducer();
1366
+ return client;
1367
+ }
1368
+ static buildDestroyProvider(token) {
1369
+ return {
1370
+ provide: `${token}_DESTROY`,
1371
+ useFactory: (client) => ({
1372
+ onModuleDestroy: () => client.disconnect()
1373
+ }),
1374
+ inject: [token]
1375
+ };
1376
+ }
1317
1377
  };
1318
1378
  KafkaModule = __decorateClass([
1319
1379
  (0, import_common3.Module)({})