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