@drarzter/kafka-client 0.5.4 → 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/README.md +74 -0
- package/dist/chunk-6B72MJPU.mjs +1160 -0
- package/dist/chunk-6B72MJPU.mjs.map +1 -0
- package/dist/core.d.mts +29 -38
- package/dist/core.d.ts +29 -38
- package/dist/core.js +649 -436
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +1 -1
- package/dist/{envelope-BR8d1m8c.d.mts → envelope-LeO5e3ob.d.mts} +45 -6
- package/dist/{envelope-BR8d1m8c.d.ts → envelope-LeO5e3ob.d.ts} +45 -6
- package/dist/index.d.mts +17 -13
- package/dist/index.d.ts +17 -13
- package/dist/index.js +688 -485
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +40 -50
- package/dist/index.mjs.map +1 -1
- package/dist/otel.d.mts +1 -1
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/otel.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +29 -16
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +29 -16
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3QXTW66R.mjs +0 -947
- package/dist/chunk-3QXTW66R.mjs.map +0 -1
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
|
|
255
|
+
// src/client/consumer/pipeline.ts
|
|
154
256
|
function toError(error) {
|
|
155
257
|
return error instanceof Error ? error : new Error(String(error));
|
|
156
258
|
}
|
|
@@ -189,9 +291,20 @@ async function validateWithSchema(message, raw, topic2, schemaMap, interceptors,
|
|
|
189
291
|
originalHeaders: deps.originalHeaders
|
|
190
292
|
});
|
|
191
293
|
} else {
|
|
192
|
-
await deps.onMessageLost?.({
|
|
294
|
+
await deps.onMessageLost?.({
|
|
295
|
+
topic: topic2,
|
|
296
|
+
error: validationError,
|
|
297
|
+
attempt: 0,
|
|
298
|
+
headers: deps.originalHeaders ?? {}
|
|
299
|
+
});
|
|
193
300
|
}
|
|
194
|
-
const errorEnvelope = extractEnvelope(
|
|
301
|
+
const errorEnvelope = extractEnvelope(
|
|
302
|
+
message,
|
|
303
|
+
deps.originalHeaders ?? {},
|
|
304
|
+
topic2,
|
|
305
|
+
-1,
|
|
306
|
+
""
|
|
307
|
+
);
|
|
195
308
|
for (const interceptor of interceptors) {
|
|
196
309
|
await interceptor.onError?.(errorEnvelope, validationError);
|
|
197
310
|
}
|
|
@@ -243,7 +356,10 @@ async function sendToRetryTopic(originalTopic, rawMessages, attempt, maxRetries,
|
|
|
243
356
|
};
|
|
244
357
|
try {
|
|
245
358
|
for (const raw of rawMessages) {
|
|
246
|
-
await deps.producer.send({
|
|
359
|
+
await deps.producer.send({
|
|
360
|
+
topic: retryTopic,
|
|
361
|
+
messages: [{ value: raw, headers }]
|
|
362
|
+
});
|
|
247
363
|
}
|
|
248
364
|
deps.logger.warn(
|
|
249
365
|
`Message queued in retry topic ${retryTopic} (attempt ${attempt}/${maxRetries})`
|
|
@@ -255,106 +371,244 @@ async function sendToRetryTopic(originalTopic, rawMessages, attempt, maxRetries,
|
|
|
255
371
|
);
|
|
256
372
|
}
|
|
257
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
|
+
}
|
|
258
421
|
async function executeWithRetry(fn, ctx, deps) {
|
|
259
|
-
const {
|
|
422
|
+
const {
|
|
423
|
+
envelope,
|
|
424
|
+
rawMessages,
|
|
425
|
+
interceptors,
|
|
426
|
+
dlq,
|
|
427
|
+
retry,
|
|
428
|
+
isBatch,
|
|
429
|
+
retryTopics
|
|
430
|
+
} = ctx;
|
|
260
431
|
const maxAttempts = retryTopics ? 1 : retry ? retry.maxRetries + 1 : 1;
|
|
261
432
|
const backoffMs = retry?.backoffMs ?? 1e3;
|
|
262
433
|
const maxBackoffMs = retry?.maxBackoffMs ?? 3e4;
|
|
263
434
|
const envelopes = Array.isArray(envelope) ? envelope : [envelope];
|
|
264
435
|
const topic2 = envelopes[0]?.topic ?? "unknown";
|
|
265
436
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (isLastAttempt && maxAttempts > 1) {
|
|
297
|
-
const exhaustedError = new KafkaRetryExhaustedError(
|
|
298
|
-
topic2,
|
|
299
|
-
envelopes.map((e) => e.payload),
|
|
300
|
-
maxAttempts,
|
|
301
|
-
{ cause: err }
|
|
302
|
-
);
|
|
303
|
-
for (const env of envelopes) {
|
|
304
|
-
for (const interceptor of interceptors) {
|
|
305
|
-
await interceptor.onError?.(env, exhaustedError);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
for (const env of envelopes) {
|
|
310
|
-
for (const interceptor of interceptors) {
|
|
311
|
-
await interceptor.onError?.(env, err);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
deps.logger.error(
|
|
316
|
-
`Error processing ${isBatch ? "batch" : "message"} from topic ${topic2} (attempt ${attempt}/${maxAttempts}):`,
|
|
317
|
-
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
|
|
318
467
|
);
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
envelopes[0]?.headers ?? {},
|
|
329
|
-
deps
|
|
330
|
-
);
|
|
331
|
-
} else if (isLastAttempt) {
|
|
332
|
-
if (dlq) {
|
|
333
|
-
const dlqMeta = {
|
|
334
|
-
error: err,
|
|
335
|
-
attempt,
|
|
336
|
-
originalHeaders: envelopes[0]?.headers
|
|
337
|
-
};
|
|
338
|
-
for (const raw of rawMessages) {
|
|
339
|
-
await sendToDlq(topic2, raw, deps, dlqMeta);
|
|
340
|
-
}
|
|
341
|
-
} else {
|
|
342
|
-
await deps.onMessageLost?.({
|
|
343
|
-
topic: topic2,
|
|
344
|
-
error: err,
|
|
345
|
-
attempt,
|
|
346
|
-
headers: envelopes[0]?.headers ?? {}
|
|
347
|
-
});
|
|
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);
|
|
348
477
|
}
|
|
349
478
|
} else {
|
|
350
|
-
|
|
351
|
-
|
|
479
|
+
await deps.onMessageLost?.({
|
|
480
|
+
topic: topic2,
|
|
481
|
+
error,
|
|
482
|
+
attempt,
|
|
483
|
+
headers: envelopes[0]?.headers ?? {}
|
|
484
|
+
});
|
|
352
485
|
}
|
|
486
|
+
} else {
|
|
487
|
+
const cap = Math.min(backoffMs * 2 ** (attempt - 1), maxBackoffMs);
|
|
488
|
+
await sleep(Math.random() * cap);
|
|
353
489
|
}
|
|
354
490
|
}
|
|
355
491
|
}
|
|
356
492
|
|
|
357
|
-
// src/client/
|
|
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
|
|
358
612
|
async function subscribeWithRetry(consumer, topics, logger, retryOpts) {
|
|
359
613
|
const maxAttempts = retryOpts?.retries ?? 5;
|
|
360
614
|
const backoffMs = retryOpts?.backoffMs ?? 5e3;
|
|
@@ -373,7 +627,156 @@ async function subscribeWithRetry(consumer, topics, logger, retryOpts) {
|
|
|
373
627
|
}
|
|
374
628
|
}
|
|
375
629
|
|
|
376
|
-
// 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
|
|
377
780
|
var { Kafka: KafkaClass, logLevel: KafkaLogLevel } = import_kafka_javascript.KafkaJS;
|
|
378
781
|
var KafkaClient = class {
|
|
379
782
|
kafka;
|
|
@@ -389,8 +792,10 @@ var KafkaClient = class {
|
|
|
389
792
|
defaultGroupId;
|
|
390
793
|
schemaRegistry = /* @__PURE__ */ new Map();
|
|
391
794
|
runningConsumers = /* @__PURE__ */ new Map();
|
|
795
|
+
consumerCreationOptions = /* @__PURE__ */ new Map();
|
|
392
796
|
instrumentation;
|
|
393
797
|
onMessageLost;
|
|
798
|
+
onRebalance;
|
|
394
799
|
isAdminConnected = false;
|
|
395
800
|
clientId;
|
|
396
801
|
constructor(clientId, groupId, brokers, options) {
|
|
@@ -406,6 +811,7 @@ var KafkaClient = class {
|
|
|
406
811
|
this.numPartitions = options?.numPartitions ?? 1;
|
|
407
812
|
this.instrumentation = options?.instrumentation ?? [];
|
|
408
813
|
this.onMessageLost = options?.onMessageLost;
|
|
814
|
+
this.onRebalance = options?.onRebalance;
|
|
409
815
|
this.kafka = new KafkaClass({
|
|
410
816
|
kafkaJS: {
|
|
411
817
|
clientId: this.clientId,
|
|
@@ -421,7 +827,7 @@ var KafkaClient = class {
|
|
|
421
827
|
this.admin = this.kafka.admin();
|
|
422
828
|
}
|
|
423
829
|
async sendMessage(topicOrDesc, message, options = {}) {
|
|
424
|
-
const payload = await this.
|
|
830
|
+
const payload = await this.preparePayload(topicOrDesc, [
|
|
425
831
|
{
|
|
426
832
|
value: message,
|
|
427
833
|
key: options.key,
|
|
@@ -431,19 +837,13 @@ var KafkaClient = class {
|
|
|
431
837
|
eventId: options.eventId
|
|
432
838
|
}
|
|
433
839
|
]);
|
|
434
|
-
await this.ensureTopic(payload.topic);
|
|
435
840
|
await this.producer.send(payload);
|
|
436
|
-
|
|
437
|
-
inst.afterSend?.(payload.topic);
|
|
438
|
-
}
|
|
841
|
+
this.notifyAfterSend(payload.topic, payload.messages.length);
|
|
439
842
|
}
|
|
440
843
|
async sendBatch(topicOrDesc, messages) {
|
|
441
|
-
const payload = await this.
|
|
442
|
-
await this.ensureTopic(payload.topic);
|
|
844
|
+
const payload = await this.preparePayload(topicOrDesc, messages);
|
|
443
845
|
await this.producer.send(payload);
|
|
444
|
-
|
|
445
|
-
inst.afterSend?.(payload.topic);
|
|
446
|
-
}
|
|
846
|
+
this.notifyAfterSend(payload.topic, payload.messages.length);
|
|
447
847
|
}
|
|
448
848
|
/** Execute multiple sends atomically. Commits on success, aborts on error. */
|
|
449
849
|
async transaction(fn) {
|
|
@@ -462,7 +862,7 @@ var KafkaClient = class {
|
|
|
462
862
|
try {
|
|
463
863
|
const ctx = {
|
|
464
864
|
send: async (topicOrDesc, message, options = {}) => {
|
|
465
|
-
const payload = await this.
|
|
865
|
+
const payload = await this.preparePayload(topicOrDesc, [
|
|
466
866
|
{
|
|
467
867
|
value: message,
|
|
468
868
|
key: options.key,
|
|
@@ -472,13 +872,10 @@ var KafkaClient = class {
|
|
|
472
872
|
eventId: options.eventId
|
|
473
873
|
}
|
|
474
874
|
]);
|
|
475
|
-
await this.ensureTopic(payload.topic);
|
|
476
875
|
await tx.send(payload);
|
|
477
876
|
},
|
|
478
877
|
sendBatch: async (topicOrDesc, messages) => {
|
|
479
|
-
|
|
480
|
-
await this.ensureTopic(payload.topic);
|
|
481
|
-
await tx.send(payload);
|
|
878
|
+
await tx.send(await this.preparePayload(topicOrDesc, messages));
|
|
482
879
|
}
|
|
483
880
|
};
|
|
484
881
|
await fn(ctx);
|
|
@@ -512,131 +909,76 @@ var KafkaClient = class {
|
|
|
512
909
|
);
|
|
513
910
|
}
|
|
514
911
|
const { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry } = await this.setupConsumer(topics, "eachMessage", options);
|
|
515
|
-
const deps =
|
|
912
|
+
const deps = this.messageDeps;
|
|
913
|
+
const timeoutMs = options.handlerTimeoutMs;
|
|
516
914
|
await consumer.run({
|
|
517
|
-
eachMessage:
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
const raw = message.value.toString();
|
|
523
|
-
const parsed = parseJsonMessage(raw, topic2, this.logger);
|
|
524
|
-
if (parsed === null) return;
|
|
525
|
-
const headers = decodeHeaders(message.headers);
|
|
526
|
-
const validated = await validateWithSchema(
|
|
527
|
-
parsed,
|
|
528
|
-
raw,
|
|
529
|
-
topic2,
|
|
915
|
+
eachMessage: (payload) => handleEachMessage(
|
|
916
|
+
payload,
|
|
917
|
+
{
|
|
530
918
|
schemaMap,
|
|
919
|
+
handleMessage,
|
|
531
920
|
interceptors,
|
|
532
921
|
dlq,
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
partition,
|
|
541
|
-
message.offset
|
|
542
|
-
);
|
|
543
|
-
await executeWithRetry(
|
|
544
|
-
() => runWithEnvelopeContext(
|
|
545
|
-
{ correlationId: envelope.correlationId, traceparent: envelope.traceparent },
|
|
546
|
-
() => handleMessage(envelope)
|
|
547
|
-
),
|
|
548
|
-
{ envelope, rawMessages: [raw], interceptors, dlq, retry, retryTopics: options.retryTopics },
|
|
549
|
-
deps
|
|
550
|
-
);
|
|
551
|
-
}
|
|
922
|
+
retry,
|
|
923
|
+
retryTopics: options.retryTopics,
|
|
924
|
+
timeoutMs,
|
|
925
|
+
wrapWithTimeout: this.wrapWithTimeoutWarning.bind(this)
|
|
926
|
+
},
|
|
927
|
+
deps
|
|
928
|
+
)
|
|
552
929
|
});
|
|
553
930
|
this.runningConsumers.set(gid, "eachMessage");
|
|
554
931
|
if (options.retryTopics && retry) {
|
|
555
|
-
await
|
|
932
|
+
await startRetryTopicConsumers(
|
|
556
933
|
topicNames,
|
|
557
934
|
gid,
|
|
558
935
|
handleMessage,
|
|
559
936
|
retry,
|
|
560
937
|
dlq,
|
|
561
938
|
interceptors,
|
|
562
|
-
schemaMap
|
|
939
|
+
schemaMap,
|
|
940
|
+
this.retryTopicDeps
|
|
563
941
|
);
|
|
564
942
|
}
|
|
943
|
+
return { groupId: gid, stop: () => this.stopConsumer(gid) };
|
|
565
944
|
}
|
|
566
945
|
async startBatchConsumer(topics, handleBatch, options = {}) {
|
|
567
946
|
const { consumer, schemaMap, gid, dlq, interceptors, retry } = await this.setupConsumer(topics, "eachBatch", options);
|
|
568
|
-
const deps =
|
|
947
|
+
const deps = this.messageDeps;
|
|
948
|
+
const timeoutMs = options.handlerTimeoutMs;
|
|
569
949
|
await consumer.run({
|
|
570
|
-
eachBatch:
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
585
|
-
const raw = message.value.toString();
|
|
586
|
-
const parsed = parseJsonMessage(raw, batch.topic, this.logger);
|
|
587
|
-
if (parsed === null) continue;
|
|
588
|
-
const headers = decodeHeaders(message.headers);
|
|
589
|
-
const validated = await validateWithSchema(
|
|
590
|
-
parsed,
|
|
591
|
-
raw,
|
|
592
|
-
batch.topic,
|
|
593
|
-
schemaMap,
|
|
594
|
-
interceptors,
|
|
595
|
-
dlq,
|
|
596
|
-
{ ...deps, originalHeaders: headers }
|
|
597
|
-
);
|
|
598
|
-
if (validated === null) continue;
|
|
599
|
-
envelopes.push(
|
|
600
|
-
extractEnvelope(validated, headers, batch.topic, batch.partition, message.offset)
|
|
601
|
-
);
|
|
602
|
-
rawMessages.push(raw);
|
|
603
|
-
}
|
|
604
|
-
if (envelopes.length === 0) return;
|
|
605
|
-
const meta = {
|
|
606
|
-
partition: batch.partition,
|
|
607
|
-
highWatermark: batch.highWatermark,
|
|
608
|
-
heartbeat,
|
|
609
|
-
resolveOffset,
|
|
610
|
-
commitOffsetsIfNecessary
|
|
611
|
-
};
|
|
612
|
-
await executeWithRetry(
|
|
613
|
-
() => handleBatch(envelopes, meta),
|
|
614
|
-
{
|
|
615
|
-
envelope: envelopes,
|
|
616
|
-
rawMessages: batch.messages.filter((m) => m.value).map((m) => m.value.toString()),
|
|
617
|
-
interceptors,
|
|
618
|
-
dlq,
|
|
619
|
-
retry,
|
|
620
|
-
isBatch: true
|
|
621
|
-
},
|
|
622
|
-
deps
|
|
623
|
-
);
|
|
624
|
-
}
|
|
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
|
+
)
|
|
625
963
|
});
|
|
626
964
|
this.runningConsumers.set(gid, "eachBatch");
|
|
965
|
+
return { groupId: gid, stop: () => this.stopConsumer(gid) };
|
|
627
966
|
}
|
|
628
967
|
// ── Consumer lifecycle ───────────────────────────────────────────
|
|
629
968
|
async stopConsumer(groupId) {
|
|
630
969
|
if (groupId !== void 0) {
|
|
631
970
|
const consumer = this.consumers.get(groupId);
|
|
632
971
|
if (!consumer) {
|
|
633
|
-
this.logger.warn(
|
|
972
|
+
this.logger.warn(
|
|
973
|
+
`stopConsumer: no active consumer for group "${groupId}"`
|
|
974
|
+
);
|
|
634
975
|
return;
|
|
635
976
|
}
|
|
636
977
|
await consumer.disconnect().catch(() => {
|
|
637
978
|
});
|
|
638
979
|
this.consumers.delete(groupId);
|
|
639
980
|
this.runningConsumers.delete(groupId);
|
|
981
|
+
this.consumerCreationOptions.delete(groupId);
|
|
640
982
|
this.logger.log(`Consumer disconnected: group "${groupId}"`);
|
|
641
983
|
} else {
|
|
642
984
|
const tasks = Array.from(this.consumers.values()).map(
|
|
@@ -646,9 +988,36 @@ var KafkaClient = class {
|
|
|
646
988
|
await Promise.allSettled(tasks);
|
|
647
989
|
this.consumers.clear();
|
|
648
990
|
this.runningConsumers.clear();
|
|
991
|
+
this.consumerCreationOptions.clear();
|
|
649
992
|
this.logger.log("All consumers disconnected");
|
|
650
993
|
}
|
|
651
994
|
}
|
|
995
|
+
/**
|
|
996
|
+
* Query consumer group lag per partition.
|
|
997
|
+
* Lag = broker high-watermark − last committed offset.
|
|
998
|
+
* A committed offset of -1 (nothing committed yet) counts as full lag.
|
|
999
|
+
*/
|
|
1000
|
+
async getConsumerLag(groupId) {
|
|
1001
|
+
const gid = groupId ?? this.defaultGroupId;
|
|
1002
|
+
if (!this.isAdminConnected) {
|
|
1003
|
+
await this.admin.connect();
|
|
1004
|
+
this.isAdminConnected = true;
|
|
1005
|
+
}
|
|
1006
|
+
const committedByTopic = await this.admin.fetchOffsets({ groupId: gid });
|
|
1007
|
+
const result = [];
|
|
1008
|
+
for (const { topic: topic2, partitions } of committedByTopic) {
|
|
1009
|
+
const brokerOffsets = await this.admin.fetchTopicOffsets(topic2);
|
|
1010
|
+
for (const { partition, offset } of partitions) {
|
|
1011
|
+
const broker = brokerOffsets.find((o) => o.partition === partition);
|
|
1012
|
+
if (!broker) continue;
|
|
1013
|
+
const committed = parseInt(offset, 10);
|
|
1014
|
+
const high = parseInt(broker.high, 10);
|
|
1015
|
+
const lag = committed === -1 ? high : Math.max(0, high - committed);
|
|
1016
|
+
result.push({ topic: topic2, partition, lag });
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return result;
|
|
1020
|
+
}
|
|
652
1021
|
/** Check broker connectivity and return status, clientId, and available topics. */
|
|
653
1022
|
async checkStatus() {
|
|
654
1023
|
if (!this.isAdminConnected) {
|
|
@@ -678,183 +1047,42 @@ var KafkaClient = class {
|
|
|
678
1047
|
await Promise.allSettled(tasks);
|
|
679
1048
|
this.consumers.clear();
|
|
680
1049
|
this.runningConsumers.clear();
|
|
1050
|
+
this.consumerCreationOptions.clear();
|
|
681
1051
|
this.logger.log("All connections closed");
|
|
682
1052
|
}
|
|
683
|
-
// ──
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
* 1. Sleep until `x-retry-after` (scheduled by the main consumer or previous retry hop)
|
|
690
|
-
* 2. Call the original handler
|
|
691
|
-
* 3. On failure: if retries remain → re-send to `<originalTopic>.retry` with incremented attempt
|
|
692
|
-
* if exhausted → DLQ or onMessageLost
|
|
693
|
-
*/
|
|
694
|
-
async startRetryTopicConsumers(originalTopics, originalGroupId, handleMessage, retry, dlq, interceptors, schemaMap) {
|
|
695
|
-
const retryTopicNames = originalTopics.map((t) => `${t}.retry`);
|
|
696
|
-
const retryGroupId = `${originalGroupId}-retry`;
|
|
697
|
-
const backoffMs = retry.backoffMs ?? 1e3;
|
|
698
|
-
const maxBackoffMs = retry.maxBackoffMs ?? 3e4;
|
|
699
|
-
const deps = {
|
|
700
|
-
logger: this.logger,
|
|
701
|
-
producer: this.producer,
|
|
702
|
-
instrumentation: this.instrumentation,
|
|
703
|
-
onMessageLost: this.onMessageLost
|
|
704
|
-
};
|
|
705
|
-
for (const rt of retryTopicNames) {
|
|
706
|
-
await this.ensureTopic(rt);
|
|
707
|
-
}
|
|
708
|
-
const consumer = this.getOrCreateConsumer(retryGroupId, false, true);
|
|
709
|
-
await consumer.connect();
|
|
710
|
-
await subscribeWithRetry(consumer, retryTopicNames, this.logger);
|
|
711
|
-
await consumer.run({
|
|
712
|
-
eachMessage: async ({ topic: retryTopic, partition, message }) => {
|
|
713
|
-
if (!message.value) return;
|
|
714
|
-
const raw = message.value.toString();
|
|
715
|
-
const parsed = parseJsonMessage(raw, retryTopic, this.logger);
|
|
716
|
-
if (parsed === null) return;
|
|
717
|
-
const headers = decodeHeaders(message.headers);
|
|
718
|
-
const originalTopic = headers[RETRY_HEADER_ORIGINAL_TOPIC] ?? retryTopic.replace(/\.retry$/, "");
|
|
719
|
-
const currentAttempt = parseInt(
|
|
720
|
-
headers[RETRY_HEADER_ATTEMPT] ?? "1",
|
|
721
|
-
10
|
|
722
|
-
);
|
|
723
|
-
const maxRetries = parseInt(
|
|
724
|
-
headers[RETRY_HEADER_MAX_RETRIES] ?? String(retry.maxRetries),
|
|
725
|
-
10
|
|
726
|
-
);
|
|
727
|
-
const retryAfter = parseInt(
|
|
728
|
-
headers[RETRY_HEADER_AFTER] ?? "0",
|
|
729
|
-
10
|
|
730
|
-
);
|
|
731
|
-
const remaining = retryAfter - Date.now();
|
|
732
|
-
if (remaining > 0) {
|
|
733
|
-
consumer.pause([{ topic: retryTopic, partitions: [partition] }]);
|
|
734
|
-
await sleep(remaining);
|
|
735
|
-
consumer.resume([{ topic: retryTopic, partitions: [partition] }]);
|
|
736
|
-
}
|
|
737
|
-
const validated = await validateWithSchema(
|
|
738
|
-
parsed,
|
|
739
|
-
raw,
|
|
740
|
-
originalTopic,
|
|
741
|
-
schemaMap,
|
|
742
|
-
interceptors,
|
|
743
|
-
dlq,
|
|
744
|
-
{ ...deps, originalHeaders: headers }
|
|
745
|
-
);
|
|
746
|
-
if (validated === null) return;
|
|
747
|
-
const envelope = extractEnvelope(
|
|
748
|
-
validated,
|
|
749
|
-
headers,
|
|
750
|
-
originalTopic,
|
|
751
|
-
partition,
|
|
752
|
-
message.offset
|
|
753
|
-
);
|
|
754
|
-
try {
|
|
755
|
-
const cleanups = [];
|
|
756
|
-
for (const inst of this.instrumentation) {
|
|
757
|
-
const c = inst.beforeConsume?.(envelope);
|
|
758
|
-
if (typeof c === "function") cleanups.push(c);
|
|
759
|
-
}
|
|
760
|
-
for (const interceptor of interceptors) await interceptor.before?.(envelope);
|
|
761
|
-
await runWithEnvelopeContext(
|
|
762
|
-
{ correlationId: envelope.correlationId, traceparent: envelope.traceparent },
|
|
763
|
-
() => handleMessage(envelope)
|
|
764
|
-
);
|
|
765
|
-
for (const interceptor of interceptors) await interceptor.after?.(envelope);
|
|
766
|
-
for (const cleanup of cleanups) cleanup();
|
|
767
|
-
} catch (error) {
|
|
768
|
-
const err = toError(error);
|
|
769
|
-
const nextAttempt = currentAttempt + 1;
|
|
770
|
-
const exhausted = currentAttempt >= maxRetries;
|
|
771
|
-
for (const inst of this.instrumentation) inst.onConsumeError?.(envelope, err);
|
|
772
|
-
const reportedError = exhausted && maxRetries > 1 ? new KafkaRetryExhaustedError(originalTopic, [envelope.payload], maxRetries, { cause: err }) : err;
|
|
773
|
-
for (const interceptor of interceptors) {
|
|
774
|
-
await interceptor.onError?.(envelope, reportedError);
|
|
775
|
-
}
|
|
776
|
-
this.logger.error(
|
|
777
|
-
`Retry consumer error for ${originalTopic} (attempt ${currentAttempt}/${maxRetries}):`,
|
|
778
|
-
err.stack
|
|
779
|
-
);
|
|
780
|
-
if (!exhausted) {
|
|
781
|
-
const cap = Math.min(backoffMs * 2 ** currentAttempt, maxBackoffMs);
|
|
782
|
-
const delay = Math.floor(Math.random() * cap);
|
|
783
|
-
await sendToRetryTopic(
|
|
784
|
-
originalTopic,
|
|
785
|
-
[raw],
|
|
786
|
-
nextAttempt,
|
|
787
|
-
maxRetries,
|
|
788
|
-
delay,
|
|
789
|
-
headers,
|
|
790
|
-
deps
|
|
791
|
-
);
|
|
792
|
-
} else if (dlq) {
|
|
793
|
-
await sendToDlq(originalTopic, raw, deps, {
|
|
794
|
-
error: err,
|
|
795
|
-
// +1 to account for the main consumer's initial attempt before
|
|
796
|
-
// routing to the retry topic, making this consistent with the
|
|
797
|
-
// in-process retry path where attempt counts all tries.
|
|
798
|
-
attempt: currentAttempt + 1,
|
|
799
|
-
originalHeaders: headers
|
|
800
|
-
});
|
|
801
|
-
} else {
|
|
802
|
-
await deps.onMessageLost?.({
|
|
803
|
-
topic: originalTopic,
|
|
804
|
-
error: err,
|
|
805
|
-
attempt: currentAttempt,
|
|
806
|
-
headers
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
});
|
|
812
|
-
this.runningConsumers.set(retryGroupId, "eachMessage");
|
|
813
|
-
await this.waitForPartitionAssignment(consumer, retryTopicNames);
|
|
814
|
-
this.logger.log(
|
|
815
|
-
`Retry topic consumers started for: ${originalTopics.join(", ")} (group: ${retryGroupId})`
|
|
1053
|
+
// ── Private helpers ──────────────────────────────────────────────
|
|
1054
|
+
async preparePayload(topicOrDesc, messages) {
|
|
1055
|
+
const payload = await buildSendPayload(
|
|
1056
|
+
topicOrDesc,
|
|
1057
|
+
messages,
|
|
1058
|
+
this.producerOpsDeps
|
|
816
1059
|
);
|
|
1060
|
+
await this.ensureTopic(payload.topic);
|
|
1061
|
+
return payload;
|
|
817
1062
|
}
|
|
818
|
-
//
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
* the caller — in the worst case a message sent immediately after would be
|
|
824
|
-
* missed, which is the same behaviour as before this guard was added.
|
|
825
|
-
*/
|
|
826
|
-
async waitForPartitionAssignment(consumer, topics, timeoutMs = 1e4) {
|
|
827
|
-
const topicSet = new Set(topics);
|
|
828
|
-
const deadline = Date.now() + timeoutMs;
|
|
829
|
-
while (Date.now() < deadline) {
|
|
830
|
-
try {
|
|
831
|
-
const assigned = consumer.assignment();
|
|
832
|
-
if (assigned.some((a) => topicSet.has(a.topic))) return;
|
|
833
|
-
} catch {
|
|
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);
|
|
834
1068
|
}
|
|
835
|
-
await sleep(200);
|
|
836
1069
|
}
|
|
837
|
-
this.logger.warn(
|
|
838
|
-
`Retry consumer did not receive partition assignments for [${topics.join(", ")}] within ${timeoutMs}ms`
|
|
839
|
-
);
|
|
840
1070
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1071
|
+
/**
|
|
1072
|
+
* Start a timer that logs a warning if `fn` hasn't resolved within `timeoutMs`.
|
|
1073
|
+
* The handler itself is not cancelled — the warning is diagnostic only.
|
|
1074
|
+
*/
|
|
1075
|
+
wrapWithTimeoutWarning(fn, timeoutMs, topic2) {
|
|
1076
|
+
let timer;
|
|
1077
|
+
const promise = fn().finally(() => {
|
|
1078
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1079
|
+
});
|
|
1080
|
+
timer = setTimeout(() => {
|
|
1081
|
+
this.logger.warn(
|
|
1082
|
+
`Handler for topic "${topic2}" has not resolved after ${timeoutMs}ms \u2014 possible stuck handler`
|
|
848
1083
|
);
|
|
849
|
-
}
|
|
850
|
-
return
|
|
851
|
-
}
|
|
852
|
-
resolveTopicName(topicOrDescriptor) {
|
|
853
|
-
if (typeof topicOrDescriptor === "string") return topicOrDescriptor;
|
|
854
|
-
if (topicOrDescriptor && typeof topicOrDescriptor === "object" && "__topic" in topicOrDescriptor) {
|
|
855
|
-
return topicOrDescriptor.__topic;
|
|
856
|
-
}
|
|
857
|
-
return String(topicOrDescriptor);
|
|
1084
|
+
}, timeoutMs);
|
|
1085
|
+
return promise;
|
|
858
1086
|
}
|
|
859
1087
|
async ensureTopic(topic2) {
|
|
860
1088
|
if (!this.autoCreateTopicsEnabled || this.ensuredTopics.has(topic2)) return;
|
|
@@ -867,52 +1095,6 @@ var KafkaClient = class {
|
|
|
867
1095
|
});
|
|
868
1096
|
this.ensuredTopics.add(topic2);
|
|
869
1097
|
}
|
|
870
|
-
/** Register schema from descriptor into global registry (side-effect). */
|
|
871
|
-
registerSchema(topicOrDesc) {
|
|
872
|
-
if (topicOrDesc?.__schema) {
|
|
873
|
-
const topic2 = this.resolveTopicName(topicOrDesc);
|
|
874
|
-
this.schemaRegistry.set(topic2, topicOrDesc.__schema);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
/** Validate message against schema. Pure — no side-effects on registry. */
|
|
878
|
-
async validateMessage(topicOrDesc, message) {
|
|
879
|
-
if (topicOrDesc?.__schema) {
|
|
880
|
-
return await topicOrDesc.__schema.parse(message);
|
|
881
|
-
}
|
|
882
|
-
if (this.strictSchemasEnabled && typeof topicOrDesc === "string") {
|
|
883
|
-
const schema = this.schemaRegistry.get(topicOrDesc);
|
|
884
|
-
if (schema) return await schema.parse(message);
|
|
885
|
-
}
|
|
886
|
-
return message;
|
|
887
|
-
}
|
|
888
|
-
/**
|
|
889
|
-
* Build a kafkajs-ready send payload.
|
|
890
|
-
* Handles: topic resolution, schema registration, validation, JSON serialization,
|
|
891
|
-
* envelope header generation, and instrumentation hooks.
|
|
892
|
-
*/
|
|
893
|
-
async buildSendPayload(topicOrDesc, messages) {
|
|
894
|
-
this.registerSchema(topicOrDesc);
|
|
895
|
-
const topic2 = this.resolveTopicName(topicOrDesc);
|
|
896
|
-
const builtMessages = await Promise.all(
|
|
897
|
-
messages.map(async (m) => {
|
|
898
|
-
const envelopeHeaders = buildEnvelopeHeaders({
|
|
899
|
-
correlationId: m.correlationId,
|
|
900
|
-
schemaVersion: m.schemaVersion,
|
|
901
|
-
eventId: m.eventId,
|
|
902
|
-
headers: m.headers
|
|
903
|
-
});
|
|
904
|
-
for (const inst of this.instrumentation) {
|
|
905
|
-
inst.beforeSend?.(topic2, envelopeHeaders);
|
|
906
|
-
}
|
|
907
|
-
return {
|
|
908
|
-
value: JSON.stringify(await this.validateMessage(topicOrDesc, m.value)),
|
|
909
|
-
key: m.key ?? null,
|
|
910
|
-
headers: envelopeHeaders
|
|
911
|
-
};
|
|
912
|
-
})
|
|
913
|
-
);
|
|
914
|
-
return { topic: topic2, messages: builtMessages };
|
|
915
|
-
}
|
|
916
1098
|
/** Shared consumer setup: groupId check, schema map, connect, subscribe. */
|
|
917
1099
|
async setupConsumer(topics, mode, options) {
|
|
918
1100
|
const {
|
|
@@ -931,11 +1113,18 @@ var KafkaClient = class {
|
|
|
931
1113
|
`Cannot use ${mode} on consumer group "${gid}" \u2014 it is already running with ${oppositeMode}. Use a different groupId for this consumer.`
|
|
932
1114
|
);
|
|
933
1115
|
}
|
|
934
|
-
const consumer =
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1116
|
+
const consumer = getOrCreateConsumer(
|
|
1117
|
+
gid,
|
|
1118
|
+
fromBeginning,
|
|
1119
|
+
options.autoCommit ?? true,
|
|
1120
|
+
this.consumerOpsDeps
|
|
938
1121
|
);
|
|
1122
|
+
const schemaMap = buildSchemaMap(
|
|
1123
|
+
topics,
|
|
1124
|
+
this.schemaRegistry,
|
|
1125
|
+
optionSchemas
|
|
1126
|
+
);
|
|
1127
|
+
const topicNames = topics.map((t) => resolveTopicName(t));
|
|
939
1128
|
for (const t of topicNames) {
|
|
940
1129
|
await this.ensureTopic(t);
|
|
941
1130
|
}
|
|
@@ -945,32 +1134,56 @@ var KafkaClient = class {
|
|
|
945
1134
|
}
|
|
946
1135
|
}
|
|
947
1136
|
await consumer.connect();
|
|
948
|
-
await subscribeWithRetry(
|
|
1137
|
+
await subscribeWithRetry(
|
|
1138
|
+
consumer,
|
|
1139
|
+
topicNames,
|
|
1140
|
+
this.logger,
|
|
1141
|
+
options.subscribeRetry
|
|
1142
|
+
);
|
|
949
1143
|
this.logger.log(
|
|
950
1144
|
`${mode === "eachBatch" ? "Batch consumer" : "Consumer"} subscribed to topics: ${topicNames.join(", ")}`
|
|
951
1145
|
);
|
|
952
1146
|
return { consumer, schemaMap, topicNames, gid, dlq, interceptors, retry };
|
|
953
1147
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
+
};
|
|
970
1183
|
}
|
|
971
1184
|
};
|
|
972
1185
|
|
|
973
|
-
// src/client/topic.ts
|
|
1186
|
+
// src/client/message/topic.ts
|
|
974
1187
|
function topic(name) {
|
|
975
1188
|
const fn = () => ({
|
|
976
1189
|
__topic: name,
|
|
@@ -1100,35 +1313,17 @@ var KafkaModule = class {
|
|
|
1100
1313
|
const token = getKafkaClientToken(options.name);
|
|
1101
1314
|
const kafkaClientProvider = {
|
|
1102
1315
|
provide: token,
|
|
1103
|
-
useFactory:
|
|
1104
|
-
const client = new KafkaClient(
|
|
1105
|
-
options.clientId,
|
|
1106
|
-
options.groupId,
|
|
1107
|
-
options.brokers,
|
|
1108
|
-
{
|
|
1109
|
-
autoCreateTopics: options.autoCreateTopics,
|
|
1110
|
-
strictSchemas: options.strictSchemas,
|
|
1111
|
-
numPartitions: options.numPartitions,
|
|
1112
|
-
instrumentation: options.instrumentation,
|
|
1113
|
-
logger: new import_common3.Logger(`KafkaClient:${options.clientId}`)
|
|
1114
|
-
}
|
|
1115
|
-
);
|
|
1116
|
-
await client.connectProducer();
|
|
1117
|
-
return client;
|
|
1118
|
-
}
|
|
1119
|
-
};
|
|
1120
|
-
const destroyProvider = {
|
|
1121
|
-
provide: `${token}_DESTROY`,
|
|
1122
|
-
useFactory: (client) => ({
|
|
1123
|
-
onModuleDestroy: () => client.disconnect()
|
|
1124
|
-
}),
|
|
1125
|
-
inject: [token]
|
|
1316
|
+
useFactory: () => KafkaModule.buildClient(options)
|
|
1126
1317
|
};
|
|
1127
1318
|
return {
|
|
1128
1319
|
global: options.isGlobal ?? false,
|
|
1129
1320
|
module: KafkaModule,
|
|
1130
1321
|
imports: [import_core2.DiscoveryModule],
|
|
1131
|
-
providers: [
|
|
1322
|
+
providers: [
|
|
1323
|
+
kafkaClientProvider,
|
|
1324
|
+
KafkaModule.buildDestroyProvider(token),
|
|
1325
|
+
KafkaExplorer
|
|
1326
|
+
],
|
|
1132
1327
|
exports: [kafkaClientProvider]
|
|
1133
1328
|
};
|
|
1134
1329
|
}
|
|
@@ -1137,40 +1332,48 @@ var KafkaModule = class {
|
|
|
1137
1332
|
const token = getKafkaClientToken(asyncOptions.name);
|
|
1138
1333
|
const kafkaClientProvider = {
|
|
1139
1334
|
provide: token,
|
|
1140
|
-
useFactory: async (...args) =>
|
|
1141
|
-
const options = await asyncOptions.useFactory(...args);
|
|
1142
|
-
const client = new KafkaClient(
|
|
1143
|
-
options.clientId,
|
|
1144
|
-
options.groupId,
|
|
1145
|
-
options.brokers,
|
|
1146
|
-
{
|
|
1147
|
-
autoCreateTopics: options.autoCreateTopics,
|
|
1148
|
-
strictSchemas: options.strictSchemas,
|
|
1149
|
-
numPartitions: options.numPartitions,
|
|
1150
|
-
instrumentation: options.instrumentation,
|
|
1151
|
-
logger: new import_common3.Logger(`KafkaClient:${options.clientId}`)
|
|
1152
|
-
}
|
|
1153
|
-
);
|
|
1154
|
-
await client.connectProducer();
|
|
1155
|
-
return client;
|
|
1156
|
-
},
|
|
1335
|
+
useFactory: async (...args) => KafkaModule.buildClient(await asyncOptions.useFactory(...args)),
|
|
1157
1336
|
inject: asyncOptions.inject || []
|
|
1158
1337
|
};
|
|
1159
|
-
const destroyProvider = {
|
|
1160
|
-
provide: `${token}_DESTROY`,
|
|
1161
|
-
useFactory: (client) => ({
|
|
1162
|
-
onModuleDestroy: () => client.disconnect()
|
|
1163
|
-
}),
|
|
1164
|
-
inject: [token]
|
|
1165
|
-
};
|
|
1166
1338
|
return {
|
|
1167
1339
|
global: asyncOptions.isGlobal ?? false,
|
|
1168
1340
|
module: KafkaModule,
|
|
1169
1341
|
imports: [...asyncOptions.imports || [], import_core2.DiscoveryModule],
|
|
1170
|
-
providers: [
|
|
1342
|
+
providers: [
|
|
1343
|
+
kafkaClientProvider,
|
|
1344
|
+
KafkaModule.buildDestroyProvider(token),
|
|
1345
|
+
KafkaExplorer
|
|
1346
|
+
],
|
|
1171
1347
|
exports: [kafkaClientProvider]
|
|
1172
1348
|
};
|
|
1173
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
|
+
}
|
|
1174
1377
|
};
|
|
1175
1378
|
KafkaModule = __decorateClass([
|
|
1176
1379
|
(0, import_common3.Module)({})
|