@drarzter/kafka-client 0.2.1 → 0.2.2

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
@@ -86,6 +86,10 @@ var KafkaRetryExhaustedError = class extends KafkaProcessingError {
86
86
  };
87
87
 
88
88
  // src/client/kafka.client.ts
89
+ var ACKS_ALL = -1;
90
+ function toError(error) {
91
+ return error instanceof Error ? error : new Error(String(error));
92
+ }
89
93
  var KafkaClient = class {
90
94
  kafka;
91
95
  producer;
@@ -118,71 +122,17 @@ var KafkaClient = class {
118
122
  });
119
123
  this.admin = this.kafka.admin();
120
124
  }
121
- getOrCreateConsumer(groupId) {
122
- const gid = groupId || this.defaultGroupId;
123
- if (!this.consumers.has(gid)) {
124
- this.consumers.set(gid, this.kafka.consumer({ groupId: gid }));
125
- }
126
- return this.consumers.get(gid);
127
- }
128
- resolveTopicName(topicOrDescriptor) {
129
- if (typeof topicOrDescriptor === "string") return topicOrDescriptor;
130
- if (topicOrDescriptor && typeof topicOrDescriptor === "object" && "__topic" in topicOrDescriptor) {
131
- return topicOrDescriptor.__topic;
132
- }
133
- return String(topicOrDescriptor);
134
- }
135
- async ensureTopic(topic2) {
136
- if (!this.autoCreateTopicsEnabled || this.ensuredTopics.has(topic2)) return;
137
- if (!this.isAdminConnected) {
138
- await this.admin.connect();
139
- this.isAdminConnected = true;
140
- }
141
- await this.admin.createTopics({
142
- topics: [{ topic: topic2, numPartitions: 1 }]
143
- });
144
- this.ensuredTopics.add(topic2);
145
- }
146
- validateMessage(topicOrDesc, message) {
147
- if (topicOrDesc?.__schema) {
148
- const topic2 = this.resolveTopicName(topicOrDesc);
149
- this.schemaRegistry.set(topic2, topicOrDesc.__schema);
150
- return topicOrDesc.__schema.parse(message);
151
- }
152
- if (this.strictSchemasEnabled && typeof topicOrDesc === "string") {
153
- const schema = this.schemaRegistry.get(topicOrDesc);
154
- if (schema) return schema.parse(message);
155
- }
156
- return message;
157
- }
158
125
  async sendMessage(topicOrDesc, message, options = {}) {
159
- const validated = this.validateMessage(topicOrDesc, message);
160
- const topic2 = this.resolveTopicName(topicOrDesc);
161
- await this.ensureTopic(topic2);
162
- await this.producer.send({
163
- topic: topic2,
164
- messages: [
165
- {
166
- value: JSON.stringify(validated),
167
- key: options.key ?? null,
168
- headers: options.headers
169
- }
170
- ],
171
- acks: -1
172
- });
126
+ const payload = this.buildSendPayload(topicOrDesc, [
127
+ { value: message, key: options.key, headers: options.headers }
128
+ ]);
129
+ await this.ensureTopic(payload.topic);
130
+ await this.producer.send(payload);
173
131
  }
174
132
  async sendBatch(topicOrDesc, messages) {
175
- const topic2 = this.resolveTopicName(topicOrDesc);
176
- await this.ensureTopic(topic2);
177
- await this.producer.send({
178
- topic: topic2,
179
- messages: messages.map((m) => ({
180
- value: JSON.stringify(this.validateMessage(topicOrDesc, m.value)),
181
- key: m.key ?? null,
182
- headers: m.headers
183
- })),
184
- acks: -1
185
- });
133
+ const payload = this.buildSendPayload(topicOrDesc, messages);
134
+ await this.ensureTopic(payload.topic);
135
+ await this.producer.send(payload);
186
136
  }
187
137
  /** Execute multiple sends atomically. Commits on success, aborts on error. */
188
138
  async transaction(fn) {
@@ -190,33 +140,16 @@ var KafkaClient = class {
190
140
  try {
191
141
  const ctx = {
192
142
  send: async (topicOrDesc, message, options = {}) => {
193
- const validated = this.validateMessage(topicOrDesc, message);
194
- const topic2 = this.resolveTopicName(topicOrDesc);
195
- await this.ensureTopic(topic2);
196
- await tx.send({
197
- topic: topic2,
198
- messages: [
199
- {
200
- value: JSON.stringify(validated),
201
- key: options.key ?? null,
202
- headers: options.headers
203
- }
204
- ],
205
- acks: -1
206
- });
143
+ const payload = this.buildSendPayload(topicOrDesc, [
144
+ { value: message, key: options.key, headers: options.headers }
145
+ ]);
146
+ await this.ensureTopic(payload.topic);
147
+ await tx.send(payload);
207
148
  },
208
149
  sendBatch: async (topicOrDesc, messages) => {
209
- const topic2 = this.resolveTopicName(topicOrDesc);
210
- await this.ensureTopic(topic2);
211
- await tx.send({
212
- topic: topic2,
213
- messages: messages.map((m) => ({
214
- value: JSON.stringify(this.validateMessage(topicOrDesc, m.value)),
215
- key: m.key ?? null,
216
- headers: m.headers
217
- })),
218
- acks: -1
219
- });
150
+ const payload = this.buildSendPayload(topicOrDesc, messages);
151
+ await this.ensureTopic(payload.topic);
152
+ await tx.send(payload);
220
153
  }
221
154
  };
222
155
  await fn(ctx);
@@ -226,6 +159,7 @@ var KafkaClient = class {
226
159
  throw error;
227
160
  }
228
161
  }
162
+ // ── Producer lifecycle ───────────────────────────────────────────
229
163
  /** Connect the idempotent producer. Called automatically by `KafkaModule.register()`. */
230
164
  async connectProducer() {
231
165
  await this.producer.connect();
@@ -236,112 +170,38 @@ var KafkaClient = class {
236
170
  this.logger.log("Producer disconnected");
237
171
  }
238
172
  async startConsumer(topics, handleMessage, options = {}) {
239
- const {
240
- groupId: optGroupId,
241
- fromBeginning = false,
242
- autoCommit = true,
243
- retry,
244
- dlq = false,
245
- interceptors = [],
246
- schemas: optionSchemas
247
- } = options;
248
- const gid = optGroupId || this.defaultGroupId;
249
- const existingMode = this.runningConsumers.get(gid);
250
- if (existingMode === "eachBatch") {
251
- throw new Error(
252
- `Cannot use eachMessage on consumer group "${gid}" \u2014 it is already running with eachBatch. Use a different groupId for this consumer.`
253
- );
254
- }
255
- const consumer = this.getOrCreateConsumer(optGroupId);
256
- const schemaMap = this.buildSchemaMap(topics, optionSchemas);
257
- const topicNames = topics.map(
258
- (t) => this.resolveTopicName(t)
259
- );
260
- await consumer.connect();
261
- await this.subscribeWithRetry(consumer, topicNames, fromBeginning, options.subscribeRetry);
262
- this.logger.log(`Consumer subscribed to topics: ${topicNames.join(", ")}`);
173
+ const { consumer, schemaMap, gid, dlq, interceptors, retry } = await this.setupConsumer(topics, "eachMessage", options);
263
174
  await consumer.run({
264
- autoCommit,
175
+ autoCommit: options.autoCommit ?? true,
265
176
  eachMessage: async ({ topic: topic2, message }) => {
266
177
  if (!message.value) {
267
178
  this.logger.warn(`Received empty message from topic ${topic2}`);
268
179
  return;
269
180
  }
270
181
  const raw = message.value.toString();
271
- let parsedMessage;
272
- try {
273
- parsedMessage = JSON.parse(raw);
274
- } catch (error) {
275
- this.logger.error(
276
- `Failed to parse message from topic ${topic2}:`,
277
- error instanceof Error ? error.stack : String(error)
278
- );
279
- return;
280
- }
281
- const schema = schemaMap.get(topic2);
282
- if (schema) {
283
- try {
284
- parsedMessage = schema.parse(parsedMessage);
285
- } catch (error) {
286
- const err = error instanceof Error ? error : new Error(String(error));
287
- const validationError = new KafkaValidationError(
288
- topic2,
289
- parsedMessage,
290
- { cause: err }
291
- );
292
- this.logger.error(
293
- `Schema validation failed for topic ${topic2}:`,
294
- err.message
295
- );
296
- if (dlq) await this.sendToDlq(topic2, raw);
297
- for (const interceptor of interceptors) {
298
- await interceptor.onError?.(
299
- parsedMessage,
300
- topic2,
301
- validationError
302
- );
303
- }
304
- return;
305
- }
306
- }
307
- await this.processMessage(parsedMessage, raw, topic2, handleMessage, {
308
- retry,
309
- dlq,
310
- interceptors
311
- });
182
+ const parsed = this.parseJsonMessage(raw, topic2);
183
+ if (parsed === null) return;
184
+ const validated = await this.validateWithSchema(
185
+ parsed,
186
+ raw,
187
+ topic2,
188
+ schemaMap,
189
+ interceptors,
190
+ dlq
191
+ );
192
+ if (validated === null) return;
193
+ await this.executeWithRetry(
194
+ () => handleMessage(validated, topic2),
195
+ { topic: topic2, messages: validated, rawMessages: [raw], interceptors, dlq, retry }
196
+ );
312
197
  }
313
198
  });
314
199
  this.runningConsumers.set(gid, "eachMessage");
315
200
  }
316
201
  async startBatchConsumer(topics, handleBatch, options = {}) {
317
- const {
318
- groupId: optGroupId,
319
- fromBeginning = false,
320
- autoCommit = true,
321
- retry,
322
- dlq = false,
323
- interceptors = [],
324
- schemas: optionSchemas
325
- } = options;
326
- const gid = optGroupId || this.defaultGroupId;
327
- const existingMode = this.runningConsumers.get(gid);
328
- if (existingMode === "eachMessage") {
329
- throw new Error(
330
- `Cannot use eachBatch on consumer group "${gid}" \u2014 it is already running with eachMessage. Use a different groupId for this consumer.`
331
- );
332
- }
333
- const consumer = this.getOrCreateConsumer(optGroupId);
334
- const schemaMap = this.buildSchemaMap(topics, optionSchemas);
335
- const topicNames = topics.map(
336
- (t) => this.resolveTopicName(t)
337
- );
338
- await consumer.connect();
339
- await this.subscribeWithRetry(consumer, topicNames, fromBeginning, options.subscribeRetry);
340
- this.logger.log(
341
- `Batch consumer subscribed to topics: ${topicNames.join(", ")}`
342
- );
202
+ const { consumer, schemaMap, gid, dlq, interceptors, retry } = await this.setupConsumer(topics, "eachBatch", options);
343
203
  await consumer.run({
344
- autoCommit,
204
+ autoCommit: options.autoCommit ?? true,
345
205
  eachBatch: async ({
346
206
  batch,
347
207
  heartbeat,
@@ -349,6 +209,7 @@ var KafkaClient = class {
349
209
  commitOffsetsIfNecessary
350
210
  }) => {
351
211
  const validMessages = [];
212
+ const rawMessages = [];
352
213
  for (const message of batch.messages) {
353
214
  if (!message.value) {
354
215
  this.logger.warn(
@@ -357,43 +218,19 @@ var KafkaClient = class {
357
218
  continue;
358
219
  }
359
220
  const raw = message.value.toString();
360
- let parsedMessage;
361
- try {
362
- parsedMessage = JSON.parse(raw);
363
- } catch (error) {
364
- this.logger.error(
365
- `Failed to parse message from topic ${batch.topic}:`,
366
- error instanceof Error ? error.stack : String(error)
367
- );
368
- continue;
369
- }
370
- const schema = schemaMap.get(batch.topic);
371
- if (schema) {
372
- try {
373
- parsedMessage = schema.parse(parsedMessage);
374
- } catch (error) {
375
- const err = error instanceof Error ? error : new Error(String(error));
376
- const validationError = new KafkaValidationError(
377
- batch.topic,
378
- parsedMessage,
379
- { cause: err }
380
- );
381
- this.logger.error(
382
- `Schema validation failed for topic ${batch.topic}:`,
383
- err.message
384
- );
385
- if (dlq) await this.sendToDlq(batch.topic, raw);
386
- for (const interceptor of interceptors) {
387
- await interceptor.onError?.(
388
- parsedMessage,
389
- batch.topic,
390
- validationError
391
- );
392
- }
393
- continue;
394
- }
395
- }
396
- validMessages.push(parsedMessage);
221
+ const parsed = this.parseJsonMessage(raw, batch.topic);
222
+ if (parsed === null) continue;
223
+ const validated = await this.validateWithSchema(
224
+ parsed,
225
+ raw,
226
+ batch.topic,
227
+ schemaMap,
228
+ interceptors,
229
+ dlq
230
+ );
231
+ if (validated === null) continue;
232
+ validMessages.push(validated);
233
+ rawMessages.push(raw);
397
234
  }
398
235
  if (validMessages.length === 0) return;
399
236
  const meta = {
@@ -403,69 +240,23 @@ var KafkaClient = class {
403
240
  resolveOffset,
404
241
  commitOffsetsIfNecessary
405
242
  };
406
- const maxAttempts = retry ? retry.maxRetries + 1 : 1;
407
- const backoffMs = retry?.backoffMs ?? 1e3;
408
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
409
- try {
410
- for (const interceptor of interceptors) {
411
- for (const msg of validMessages) {
412
- await interceptor.before?.(msg, batch.topic);
413
- }
414
- }
415
- await handleBatch(validMessages, batch.topic, meta);
416
- for (const interceptor of interceptors) {
417
- for (const msg of validMessages) {
418
- await interceptor.after?.(msg, batch.topic);
419
- }
420
- }
421
- return;
422
- } catch (error) {
423
- const err = error instanceof Error ? error : new Error(String(error));
424
- const isLastAttempt = attempt === maxAttempts;
425
- if (isLastAttempt && maxAttempts > 1) {
426
- const exhaustedError = new KafkaRetryExhaustedError(
427
- batch.topic,
428
- validMessages,
429
- maxAttempts,
430
- { cause: err }
431
- );
432
- for (const interceptor of interceptors) {
433
- await interceptor.onError?.(
434
- validMessages,
435
- batch.topic,
436
- exhaustedError
437
- );
438
- }
439
- } else {
440
- for (const interceptor of interceptors) {
441
- await interceptor.onError?.(
442
- validMessages,
443
- batch.topic,
444
- err
445
- );
446
- }
447
- }
448
- this.logger.error(
449
- `Error processing batch from topic ${batch.topic} (attempt ${attempt}/${maxAttempts}):`,
450
- err.stack
451
- );
452
- if (isLastAttempt) {
453
- if (dlq) {
454
- for (const msg of batch.messages) {
455
- if (msg.value) {
456
- await this.sendToDlq(batch.topic, msg.value.toString());
457
- }
458
- }
459
- }
460
- } else {
461
- await this.sleep(backoffMs * attempt);
462
- }
243
+ await this.executeWithRetry(
244
+ () => handleBatch(validMessages, batch.topic, meta),
245
+ {
246
+ topic: batch.topic,
247
+ messages: validMessages,
248
+ rawMessages: batch.messages.filter((m) => m.value).map((m) => m.value.toString()),
249
+ interceptors,
250
+ dlq,
251
+ retry,
252
+ isBatch: true
463
253
  }
464
- }
254
+ );
465
255
  }
466
256
  });
467
257
  this.runningConsumers.set(gid, "eachBatch");
468
258
  }
259
+ // ── Consumer lifecycle ───────────────────────────────────────────
469
260
  async stopConsumer() {
470
261
  const tasks = [];
471
262
  for (const consumer of this.consumers.values()) {
@@ -503,7 +294,97 @@ var KafkaClient = class {
503
294
  this.runningConsumers.clear();
504
295
  this.logger.log("All connections closed");
505
296
  }
506
- // --- Private helpers ---
297
+ // ── Private helpers ──────────────────────────────────────────────
298
+ getOrCreateConsumer(groupId) {
299
+ const gid = groupId || this.defaultGroupId;
300
+ if (!this.consumers.has(gid)) {
301
+ this.consumers.set(gid, this.kafka.consumer({ groupId: gid }));
302
+ }
303
+ return this.consumers.get(gid);
304
+ }
305
+ resolveTopicName(topicOrDescriptor) {
306
+ if (typeof topicOrDescriptor === "string") return topicOrDescriptor;
307
+ if (topicOrDescriptor && typeof topicOrDescriptor === "object" && "__topic" in topicOrDescriptor) {
308
+ return topicOrDescriptor.__topic;
309
+ }
310
+ return String(topicOrDescriptor);
311
+ }
312
+ async ensureTopic(topic2) {
313
+ if (!this.autoCreateTopicsEnabled || this.ensuredTopics.has(topic2)) return;
314
+ if (!this.isAdminConnected) {
315
+ await this.admin.connect();
316
+ this.isAdminConnected = true;
317
+ }
318
+ await this.admin.createTopics({
319
+ topics: [{ topic: topic2, numPartitions: 1 }]
320
+ });
321
+ this.ensuredTopics.add(topic2);
322
+ }
323
+ /** Register schema from descriptor into global registry (side-effect). */
324
+ registerSchema(topicOrDesc) {
325
+ if (topicOrDesc?.__schema) {
326
+ const topic2 = this.resolveTopicName(topicOrDesc);
327
+ this.schemaRegistry.set(topic2, topicOrDesc.__schema);
328
+ }
329
+ }
330
+ /** Validate message against schema. Pure — no side-effects on registry. */
331
+ validateMessage(topicOrDesc, message) {
332
+ if (topicOrDesc?.__schema) {
333
+ return topicOrDesc.__schema.parse(message);
334
+ }
335
+ if (this.strictSchemasEnabled && typeof topicOrDesc === "string") {
336
+ const schema = this.schemaRegistry.get(topicOrDesc);
337
+ if (schema) return schema.parse(message);
338
+ }
339
+ return message;
340
+ }
341
+ /**
342
+ * Build a kafkajs-ready send payload.
343
+ * Handles: topic resolution, schema registration, validation, JSON serialization.
344
+ */
345
+ buildSendPayload(topicOrDesc, messages) {
346
+ this.registerSchema(topicOrDesc);
347
+ const topic2 = this.resolveTopicName(topicOrDesc);
348
+ return {
349
+ topic: topic2,
350
+ messages: messages.map((m) => ({
351
+ value: JSON.stringify(this.validateMessage(topicOrDesc, m.value)),
352
+ key: m.key ?? null,
353
+ headers: m.headers
354
+ })),
355
+ acks: ACKS_ALL
356
+ };
357
+ }
358
+ /** Shared consumer setup: groupId check, schema map, connect, subscribe. */
359
+ async setupConsumer(topics, mode, options) {
360
+ const {
361
+ groupId: optGroupId,
362
+ fromBeginning = false,
363
+ retry,
364
+ dlq = false,
365
+ interceptors = [],
366
+ schemas: optionSchemas
367
+ } = options;
368
+ const gid = optGroupId || this.defaultGroupId;
369
+ const existingMode = this.runningConsumers.get(gid);
370
+ const oppositeMode = mode === "eachMessage" ? "eachBatch" : "eachMessage";
371
+ if (existingMode === oppositeMode) {
372
+ throw new Error(
373
+ `Cannot use ${mode} on consumer group "${gid}" \u2014 it is already running with ${oppositeMode}. Use a different groupId for this consumer.`
374
+ );
375
+ }
376
+ const consumer = this.getOrCreateConsumer(optGroupId);
377
+ const schemaMap = this.buildSchemaMap(topics, optionSchemas);
378
+ const topicNames = topics.map(
379
+ (t) => this.resolveTopicName(t)
380
+ );
381
+ await consumer.connect();
382
+ await this.subscribeWithRetry(consumer, topicNames, fromBeginning, options.subscribeRetry);
383
+ this.logger.log(
384
+ `${mode === "eachBatch" ? "Batch consumer" : "Consumer"} subscribed to topics: ${topicNames.join(", ")}`
385
+ );
386
+ return { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry };
387
+ }
507
388
  buildSchemaMap(topics, optionSchemas) {
508
389
  const schemaMap = /* @__PURE__ */ new Map();
509
390
  for (const t of topics) {
@@ -521,44 +402,106 @@ var KafkaClient = class {
521
402
  }
522
403
  return schemaMap;
523
404
  }
524
- async processMessage(parsedMessage, raw, topic2, handleMessage, opts) {
525
- const { retry, dlq = false, interceptors = [] } = opts;
405
+ /** Parse raw message as JSON. Returns null on failure (logs error). */
406
+ parseJsonMessage(raw, topic2) {
407
+ try {
408
+ return JSON.parse(raw);
409
+ } catch (error) {
410
+ this.logger.error(
411
+ `Failed to parse message from topic ${topic2}:`,
412
+ toError(error).stack
413
+ );
414
+ return null;
415
+ }
416
+ }
417
+ /**
418
+ * Validate a parsed message against the schema map.
419
+ * On failure: logs error, sends to DLQ if enabled, calls interceptor.onError.
420
+ * Returns validated message or null.
421
+ */
422
+ async validateWithSchema(message, raw, topic2, schemaMap, interceptors, dlq) {
423
+ const schema = schemaMap.get(topic2);
424
+ if (!schema) return message;
425
+ try {
426
+ return schema.parse(message);
427
+ } catch (error) {
428
+ const err = toError(error);
429
+ const validationError = new KafkaValidationError(topic2, message, {
430
+ cause: err
431
+ });
432
+ this.logger.error(
433
+ `Schema validation failed for topic ${topic2}:`,
434
+ err.message
435
+ );
436
+ if (dlq) await this.sendToDlq(topic2, raw);
437
+ for (const interceptor of interceptors) {
438
+ await interceptor.onError?.(message, topic2, validationError);
439
+ }
440
+ return null;
441
+ }
442
+ }
443
+ /**
444
+ * Execute a handler with retry, interceptors, and DLQ support.
445
+ * Used by both single-message and batch consumers.
446
+ */
447
+ async executeWithRetry(fn, ctx) {
448
+ const { topic: topic2, messages, rawMessages, interceptors, dlq, retry, isBatch } = ctx;
526
449
  const maxAttempts = retry ? retry.maxRetries + 1 : 1;
527
450
  const backoffMs = retry?.backoffMs ?? 1e3;
528
451
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
529
452
  try {
530
- for (const interceptor of interceptors) {
531
- await interceptor.before?.(parsedMessage, topic2);
453
+ if (isBatch) {
454
+ for (const interceptor of interceptors) {
455
+ for (const msg of messages) {
456
+ await interceptor.before?.(msg, topic2);
457
+ }
458
+ }
459
+ } else {
460
+ for (const interceptor of interceptors) {
461
+ await interceptor.before?.(messages, topic2);
462
+ }
532
463
  }
533
- await handleMessage(parsedMessage, topic2);
534
- for (const interceptor of interceptors) {
535
- await interceptor.after?.(parsedMessage, topic2);
464
+ await fn();
465
+ if (isBatch) {
466
+ for (const interceptor of interceptors) {
467
+ for (const msg of messages) {
468
+ await interceptor.after?.(msg, topic2);
469
+ }
470
+ }
471
+ } else {
472
+ for (const interceptor of interceptors) {
473
+ await interceptor.after?.(messages, topic2);
474
+ }
536
475
  }
537
476
  return;
538
477
  } catch (error) {
539
- const err = error instanceof Error ? error : new Error(String(error));
478
+ const err = toError(error);
540
479
  const isLastAttempt = attempt === maxAttempts;
541
480
  if (isLastAttempt && maxAttempts > 1) {
542
481
  const exhaustedError = new KafkaRetryExhaustedError(
543
482
  topic2,
544
- parsedMessage,
483
+ messages,
545
484
  maxAttempts,
546
485
  { cause: err }
547
486
  );
548
487
  for (const interceptor of interceptors) {
549
- await interceptor.onError?.(parsedMessage, topic2, exhaustedError);
488
+ await interceptor.onError?.(messages, topic2, exhaustedError);
550
489
  }
551
490
  } else {
552
491
  for (const interceptor of interceptors) {
553
- await interceptor.onError?.(parsedMessage, topic2, err);
492
+ await interceptor.onError?.(messages, topic2, err);
554
493
  }
555
494
  }
556
495
  this.logger.error(
557
- `Error processing message from topic ${topic2} (attempt ${attempt}/${maxAttempts}):`,
496
+ `Error processing ${isBatch ? "batch" : "message"} from topic ${topic2} (attempt ${attempt}/${maxAttempts}):`,
558
497
  err.stack
559
498
  );
560
499
  if (isLastAttempt) {
561
- if (dlq) await this.sendToDlq(topic2, raw);
500
+ if (dlq) {
501
+ for (const raw of rawMessages) {
502
+ await this.sendToDlq(topic2, raw);
503
+ }
504
+ }
562
505
  } else {
563
506
  await this.sleep(backoffMs * attempt);
564
507
  }
@@ -571,13 +514,13 @@ var KafkaClient = class {
571
514
  await this.producer.send({
572
515
  topic: dlqTopic,
573
516
  messages: [{ value: rawMessage }],
574
- acks: -1
517
+ acks: ACKS_ALL
575
518
  });
576
519
  this.logger.warn(`Message sent to DLQ: ${dlqTopic}`);
577
520
  } catch (error) {
578
521
  this.logger.error(
579
522
  `Failed to send message to DLQ ${dlqTopic}:`,
580
- error instanceof Error ? error.stack : String(error)
523
+ toError(error).stack
581
524
  );
582
525
  }
583
526
  }
@@ -590,7 +533,7 @@ var KafkaClient = class {
590
533
  return;
591
534
  } catch (error) {
592
535
  if (attempt === maxAttempts) throw error;
593
- const msg = error instanceof Error ? error.message : String(error);
536
+ const msg = toError(error).message;
594
537
  this.logger.warn(
595
538
  `Failed to subscribe to [${topics.join(", ")}] (attempt ${attempt}/${maxAttempts}): ${msg}. Retrying in ${backoffMs}ms...`
596
539
  );