@drarzter/kafka-client 0.2.2 → 0.3.1
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 +104 -12
- package/dist/chunk-A56D7HXR.mjs +545 -0
- package/dist/chunk-A56D7HXR.mjs.map +1 -0
- package/dist/chunk-EQQGB2QZ.mjs +17 -0
- package/dist/chunk-EQQGB2QZ.mjs.map +1 -0
- package/dist/core.d.mts +118 -0
- package/dist/core.d.ts +118 -0
- package/dist/core.js +575 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +16 -0
- package/dist/core.mjs.map +1 -0
- package/dist/index.d.mts +5 -324
- package/dist/index.d.ts +5 -324
- package/dist/index.js +73 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29 -534
- package/dist/index.mjs.map +1 -1
- package/dist/testing.d.mts +104 -0
- package/dist/testing.d.ts +104 -0
- package/dist/testing.js +151 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +127 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-CtwJihJ3.d.mts +224 -0
- package/dist/types-CtwJihJ3.d.ts +224 -0
- package/package.json +23 -1
package/dist/index.mjs
CHANGED
|
@@ -1,525 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import {
|
|
2
|
+
KafkaClient,
|
|
3
|
+
KafkaProcessingError,
|
|
4
|
+
KafkaRetryExhaustedError,
|
|
5
|
+
KafkaValidationError,
|
|
6
|
+
topic
|
|
7
|
+
} from "./chunk-A56D7HXR.mjs";
|
|
8
|
+
import {
|
|
9
|
+
__decorateClass,
|
|
10
|
+
__decorateParam
|
|
11
|
+
} from "./chunk-EQQGB2QZ.mjs";
|
|
12
12
|
|
|
13
|
-
// src/
|
|
14
|
-
import { Module } from "@nestjs/common";
|
|
13
|
+
// src/nest/kafka.module.ts
|
|
14
|
+
import { Module, Logger as Logger2 } from "@nestjs/common";
|
|
15
15
|
import { DiscoveryModule } from "@nestjs/core";
|
|
16
16
|
|
|
17
|
-
// src/
|
|
18
|
-
import { Kafka, Partitioners } from "kafkajs";
|
|
19
|
-
import { Logger } from "@nestjs/common";
|
|
20
|
-
|
|
21
|
-
// src/client/errors.ts
|
|
22
|
-
var KafkaProcessingError = class extends Error {
|
|
23
|
-
constructor(message, topic2, originalMessage, options) {
|
|
24
|
-
super(message, options);
|
|
25
|
-
this.topic = topic2;
|
|
26
|
-
this.originalMessage = originalMessage;
|
|
27
|
-
this.name = "KafkaProcessingError";
|
|
28
|
-
if (options?.cause) this.cause = options.cause;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
var KafkaValidationError = class extends Error {
|
|
32
|
-
constructor(topic2, originalMessage, options) {
|
|
33
|
-
super(`Schema validation failed for topic "${topic2}"`, options);
|
|
34
|
-
this.topic = topic2;
|
|
35
|
-
this.originalMessage = originalMessage;
|
|
36
|
-
this.name = "KafkaValidationError";
|
|
37
|
-
if (options?.cause) this.cause = options.cause;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
var KafkaRetryExhaustedError = class extends KafkaProcessingError {
|
|
41
|
-
constructor(topic2, originalMessage, attempts, options) {
|
|
42
|
-
super(
|
|
43
|
-
`Message processing failed after ${attempts} attempts on topic "${topic2}"`,
|
|
44
|
-
topic2,
|
|
45
|
-
originalMessage,
|
|
46
|
-
options
|
|
47
|
-
);
|
|
48
|
-
this.attempts = attempts;
|
|
49
|
-
this.name = "KafkaRetryExhaustedError";
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
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
|
-
}
|
|
58
|
-
var KafkaClient = class {
|
|
59
|
-
kafka;
|
|
60
|
-
producer;
|
|
61
|
-
consumers = /* @__PURE__ */ new Map();
|
|
62
|
-
admin;
|
|
63
|
-
logger;
|
|
64
|
-
autoCreateTopicsEnabled;
|
|
65
|
-
strictSchemasEnabled;
|
|
66
|
-
ensuredTopics = /* @__PURE__ */ new Set();
|
|
67
|
-
defaultGroupId;
|
|
68
|
-
schemaRegistry = /* @__PURE__ */ new Map();
|
|
69
|
-
runningConsumers = /* @__PURE__ */ new Map();
|
|
70
|
-
isAdminConnected = false;
|
|
71
|
-
clientId;
|
|
72
|
-
constructor(clientId, groupId, brokers, options) {
|
|
73
|
-
this.clientId = clientId;
|
|
74
|
-
this.defaultGroupId = groupId;
|
|
75
|
-
this.logger = new Logger(`KafkaClient:${clientId}`);
|
|
76
|
-
this.autoCreateTopicsEnabled = options?.autoCreateTopics ?? false;
|
|
77
|
-
this.strictSchemasEnabled = options?.strictSchemas ?? true;
|
|
78
|
-
this.kafka = new Kafka({
|
|
79
|
-
clientId: this.clientId,
|
|
80
|
-
brokers
|
|
81
|
-
});
|
|
82
|
-
this.producer = this.kafka.producer({
|
|
83
|
-
createPartitioner: Partitioners.DefaultPartitioner,
|
|
84
|
-
idempotent: true,
|
|
85
|
-
transactionalId: `${clientId}-tx`,
|
|
86
|
-
maxInFlightRequests: 1
|
|
87
|
-
});
|
|
88
|
-
this.admin = this.kafka.admin();
|
|
89
|
-
}
|
|
90
|
-
async sendMessage(topicOrDesc, message, options = {}) {
|
|
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);
|
|
96
|
-
}
|
|
97
|
-
async sendBatch(topicOrDesc, messages) {
|
|
98
|
-
const payload = this.buildSendPayload(topicOrDesc, messages);
|
|
99
|
-
await this.ensureTopic(payload.topic);
|
|
100
|
-
await this.producer.send(payload);
|
|
101
|
-
}
|
|
102
|
-
/** Execute multiple sends atomically. Commits on success, aborts on error. */
|
|
103
|
-
async transaction(fn) {
|
|
104
|
-
const tx = await this.producer.transaction();
|
|
105
|
-
try {
|
|
106
|
-
const ctx = {
|
|
107
|
-
send: async (topicOrDesc, message, options = {}) => {
|
|
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);
|
|
113
|
-
},
|
|
114
|
-
sendBatch: async (topicOrDesc, messages) => {
|
|
115
|
-
const payload = this.buildSendPayload(topicOrDesc, messages);
|
|
116
|
-
await this.ensureTopic(payload.topic);
|
|
117
|
-
await tx.send(payload);
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
await fn(ctx);
|
|
121
|
-
await tx.commit();
|
|
122
|
-
} catch (error) {
|
|
123
|
-
await tx.abort();
|
|
124
|
-
throw error;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// ── Producer lifecycle ───────────────────────────────────────────
|
|
128
|
-
/** Connect the idempotent producer. Called automatically by `KafkaModule.register()`. */
|
|
129
|
-
async connectProducer() {
|
|
130
|
-
await this.producer.connect();
|
|
131
|
-
this.logger.log("Producer connected");
|
|
132
|
-
}
|
|
133
|
-
async disconnectProducer() {
|
|
134
|
-
await this.producer.disconnect();
|
|
135
|
-
this.logger.log("Producer disconnected");
|
|
136
|
-
}
|
|
137
|
-
async startConsumer(topics, handleMessage, options = {}) {
|
|
138
|
-
const { consumer, schemaMap, gid, dlq, interceptors, retry } = await this.setupConsumer(topics, "eachMessage", options);
|
|
139
|
-
await consumer.run({
|
|
140
|
-
autoCommit: options.autoCommit ?? true,
|
|
141
|
-
eachMessage: async ({ topic: topic2, message }) => {
|
|
142
|
-
if (!message.value) {
|
|
143
|
-
this.logger.warn(`Received empty message from topic ${topic2}`);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const raw = message.value.toString();
|
|
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
|
-
);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
this.runningConsumers.set(gid, "eachMessage");
|
|
165
|
-
}
|
|
166
|
-
async startBatchConsumer(topics, handleBatch, options = {}) {
|
|
167
|
-
const { consumer, schemaMap, gid, dlq, interceptors, retry } = await this.setupConsumer(topics, "eachBatch", options);
|
|
168
|
-
await consumer.run({
|
|
169
|
-
autoCommit: options.autoCommit ?? true,
|
|
170
|
-
eachBatch: async ({
|
|
171
|
-
batch,
|
|
172
|
-
heartbeat,
|
|
173
|
-
resolveOffset,
|
|
174
|
-
commitOffsetsIfNecessary
|
|
175
|
-
}) => {
|
|
176
|
-
const validMessages = [];
|
|
177
|
-
const rawMessages = [];
|
|
178
|
-
for (const message of batch.messages) {
|
|
179
|
-
if (!message.value) {
|
|
180
|
-
this.logger.warn(
|
|
181
|
-
`Received empty message from topic ${batch.topic}`
|
|
182
|
-
);
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
const raw = message.value.toString();
|
|
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);
|
|
199
|
-
}
|
|
200
|
-
if (validMessages.length === 0) return;
|
|
201
|
-
const meta = {
|
|
202
|
-
partition: batch.partition,
|
|
203
|
-
highWatermark: batch.highWatermark,
|
|
204
|
-
heartbeat,
|
|
205
|
-
resolveOffset,
|
|
206
|
-
commitOffsetsIfNecessary
|
|
207
|
-
};
|
|
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
|
|
218
|
-
}
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
this.runningConsumers.set(gid, "eachBatch");
|
|
223
|
-
}
|
|
224
|
-
// ── Consumer lifecycle ───────────────────────────────────────────
|
|
225
|
-
async stopConsumer() {
|
|
226
|
-
const tasks = [];
|
|
227
|
-
for (const consumer of this.consumers.values()) {
|
|
228
|
-
tasks.push(consumer.disconnect());
|
|
229
|
-
}
|
|
230
|
-
await Promise.allSettled(tasks);
|
|
231
|
-
this.consumers.clear();
|
|
232
|
-
this.runningConsumers.clear();
|
|
233
|
-
this.logger.log("All consumers disconnected");
|
|
234
|
-
}
|
|
235
|
-
/** Check broker connectivity and return available topics. */
|
|
236
|
-
async checkStatus() {
|
|
237
|
-
if (!this.isAdminConnected) {
|
|
238
|
-
await this.admin.connect();
|
|
239
|
-
this.isAdminConnected = true;
|
|
240
|
-
}
|
|
241
|
-
const topics = await this.admin.listTopics();
|
|
242
|
-
return { topics };
|
|
243
|
-
}
|
|
244
|
-
getClientId() {
|
|
245
|
-
return this.clientId;
|
|
246
|
-
}
|
|
247
|
-
/** Gracefully disconnect producer, all consumers, and admin. */
|
|
248
|
-
async disconnect() {
|
|
249
|
-
const tasks = [this.producer.disconnect()];
|
|
250
|
-
for (const consumer of this.consumers.values()) {
|
|
251
|
-
tasks.push(consumer.disconnect());
|
|
252
|
-
}
|
|
253
|
-
if (this.isAdminConnected) {
|
|
254
|
-
tasks.push(this.admin.disconnect());
|
|
255
|
-
this.isAdminConnected = false;
|
|
256
|
-
}
|
|
257
|
-
await Promise.allSettled(tasks);
|
|
258
|
-
this.consumers.clear();
|
|
259
|
-
this.runningConsumers.clear();
|
|
260
|
-
this.logger.log("All connections closed");
|
|
261
|
-
}
|
|
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
|
-
}
|
|
353
|
-
buildSchemaMap(topics, optionSchemas) {
|
|
354
|
-
const schemaMap = /* @__PURE__ */ new Map();
|
|
355
|
-
for (const t of topics) {
|
|
356
|
-
if (t?.__schema) {
|
|
357
|
-
const name = this.resolveTopicName(t);
|
|
358
|
-
schemaMap.set(name, t.__schema);
|
|
359
|
-
this.schemaRegistry.set(name, t.__schema);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (optionSchemas) {
|
|
363
|
-
for (const [k, v] of optionSchemas) {
|
|
364
|
-
schemaMap.set(k, v);
|
|
365
|
-
this.schemaRegistry.set(k, v);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return schemaMap;
|
|
369
|
-
}
|
|
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;
|
|
414
|
-
const maxAttempts = retry ? retry.maxRetries + 1 : 1;
|
|
415
|
-
const backoffMs = retry?.backoffMs ?? 1e3;
|
|
416
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
417
|
-
try {
|
|
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
|
-
}
|
|
428
|
-
}
|
|
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
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return;
|
|
442
|
-
} catch (error) {
|
|
443
|
-
const err = toError(error);
|
|
444
|
-
const isLastAttempt = attempt === maxAttempts;
|
|
445
|
-
if (isLastAttempt && maxAttempts > 1) {
|
|
446
|
-
const exhaustedError = new KafkaRetryExhaustedError(
|
|
447
|
-
topic2,
|
|
448
|
-
messages,
|
|
449
|
-
maxAttempts,
|
|
450
|
-
{ cause: err }
|
|
451
|
-
);
|
|
452
|
-
for (const interceptor of interceptors) {
|
|
453
|
-
await interceptor.onError?.(messages, topic2, exhaustedError);
|
|
454
|
-
}
|
|
455
|
-
} else {
|
|
456
|
-
for (const interceptor of interceptors) {
|
|
457
|
-
await interceptor.onError?.(messages, topic2, err);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
this.logger.error(
|
|
461
|
-
`Error processing ${isBatch ? "batch" : "message"} from topic ${topic2} (attempt ${attempt}/${maxAttempts}):`,
|
|
462
|
-
err.stack
|
|
463
|
-
);
|
|
464
|
-
if (isLastAttempt) {
|
|
465
|
-
if (dlq) {
|
|
466
|
-
for (const raw of rawMessages) {
|
|
467
|
-
await this.sendToDlq(topic2, raw);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
} else {
|
|
471
|
-
await this.sleep(backoffMs * attempt);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
async sendToDlq(topic2, rawMessage) {
|
|
477
|
-
const dlqTopic = `${topic2}.dlq`;
|
|
478
|
-
try {
|
|
479
|
-
await this.producer.send({
|
|
480
|
-
topic: dlqTopic,
|
|
481
|
-
messages: [{ value: rawMessage }],
|
|
482
|
-
acks: ACKS_ALL
|
|
483
|
-
});
|
|
484
|
-
this.logger.warn(`Message sent to DLQ: ${dlqTopic}`);
|
|
485
|
-
} catch (error) {
|
|
486
|
-
this.logger.error(
|
|
487
|
-
`Failed to send message to DLQ ${dlqTopic}:`,
|
|
488
|
-
toError(error).stack
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
async subscribeWithRetry(consumer, topics, fromBeginning, retryOpts) {
|
|
493
|
-
const maxAttempts = retryOpts?.retries ?? 5;
|
|
494
|
-
const backoffMs = retryOpts?.backoffMs ?? 5e3;
|
|
495
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
496
|
-
try {
|
|
497
|
-
await consumer.subscribe({ topics, fromBeginning });
|
|
498
|
-
return;
|
|
499
|
-
} catch (error) {
|
|
500
|
-
if (attempt === maxAttempts) throw error;
|
|
501
|
-
const msg = toError(error).message;
|
|
502
|
-
this.logger.warn(
|
|
503
|
-
`Failed to subscribe to [${topics.join(", ")}] (attempt ${attempt}/${maxAttempts}): ${msg}. Retrying in ${backoffMs}ms...`
|
|
504
|
-
);
|
|
505
|
-
await this.sleep(backoffMs);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
sleep(ms) {
|
|
510
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
// src/module/kafka.constants.ts
|
|
17
|
+
// src/nest/kafka.constants.ts
|
|
515
18
|
var KAFKA_CLIENT = "KAFKA_CLIENT";
|
|
516
19
|
var getKafkaClientToken = (name) => name ? `KAFKA_CLIENT_${name}` : KAFKA_CLIENT;
|
|
517
20
|
|
|
518
|
-
// src/
|
|
519
|
-
import { Inject as Inject2, Injectable, Logger
|
|
21
|
+
// src/nest/kafka.explorer.ts
|
|
22
|
+
import { Inject as Inject2, Injectable, Logger } from "@nestjs/common";
|
|
520
23
|
import { DiscoveryService, ModuleRef } from "@nestjs/core";
|
|
521
24
|
|
|
522
|
-
// src/
|
|
25
|
+
// src/nest/kafka.decorator.ts
|
|
523
26
|
import { Inject } from "@nestjs/common";
|
|
524
27
|
var KAFKA_SUBSCRIBER_METADATA = "KAFKA_SUBSCRIBER_METADATA";
|
|
525
28
|
var InjectKafkaClient = (name) => Inject(getKafkaClientToken(name));
|
|
@@ -553,13 +56,13 @@ var SubscribeTo = (topics, options) => {
|
|
|
553
56
|
};
|
|
554
57
|
};
|
|
555
58
|
|
|
556
|
-
// src/
|
|
59
|
+
// src/nest/kafka.explorer.ts
|
|
557
60
|
var KafkaExplorer = class {
|
|
558
61
|
constructor(discoveryService, moduleRef) {
|
|
559
62
|
this.discoveryService = discoveryService;
|
|
560
63
|
this.moduleRef = moduleRef;
|
|
561
64
|
}
|
|
562
|
-
logger = new
|
|
65
|
+
logger = new Logger(KafkaExplorer.name);
|
|
563
66
|
async onModuleInit() {
|
|
564
67
|
const providers = this.discoveryService.getProviders();
|
|
565
68
|
for (const wrapper of providers) {
|
|
@@ -616,7 +119,7 @@ KafkaExplorer = __decorateClass([
|
|
|
616
119
|
__decorateParam(1, Inject2(ModuleRef))
|
|
617
120
|
], KafkaExplorer);
|
|
618
121
|
|
|
619
|
-
// src/
|
|
122
|
+
// src/nest/kafka.module.ts
|
|
620
123
|
var KafkaModule = class {
|
|
621
124
|
/** Register a Kafka client with static options. */
|
|
622
125
|
static register(options) {
|
|
@@ -628,7 +131,10 @@ var KafkaModule = class {
|
|
|
628
131
|
options.clientId,
|
|
629
132
|
options.groupId,
|
|
630
133
|
options.brokers,
|
|
631
|
-
{
|
|
134
|
+
{
|
|
135
|
+
autoCreateTopics: options.autoCreateTopics,
|
|
136
|
+
logger: new Logger2(`KafkaClient:${options.clientId}`)
|
|
137
|
+
}
|
|
632
138
|
);
|
|
633
139
|
await client.connectProducer();
|
|
634
140
|
return client;
|
|
@@ -660,7 +166,10 @@ var KafkaModule = class {
|
|
|
660
166
|
options.clientId,
|
|
661
167
|
options.groupId,
|
|
662
168
|
options.brokers,
|
|
663
|
-
{
|
|
169
|
+
{
|
|
170
|
+
autoCreateTopics: options.autoCreateTopics,
|
|
171
|
+
logger: new Logger2(`KafkaClient:${options.clientId}`)
|
|
172
|
+
}
|
|
664
173
|
);
|
|
665
174
|
await client.connectProducer();
|
|
666
175
|
return client;
|
|
@@ -687,21 +196,7 @@ KafkaModule = __decorateClass([
|
|
|
687
196
|
Module({})
|
|
688
197
|
], KafkaModule);
|
|
689
198
|
|
|
690
|
-
// src/
|
|
691
|
-
function topic(name) {
|
|
692
|
-
const fn = () => ({
|
|
693
|
-
__topic: name,
|
|
694
|
-
__type: void 0
|
|
695
|
-
});
|
|
696
|
-
fn.schema = (schema) => ({
|
|
697
|
-
__topic: name,
|
|
698
|
-
__type: void 0,
|
|
699
|
-
__schema: schema
|
|
700
|
-
});
|
|
701
|
-
return fn;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// src/health/kafka.health.ts
|
|
199
|
+
// src/nest/kafka.health.ts
|
|
705
200
|
import { Injectable as Injectable2 } from "@nestjs/common";
|
|
706
201
|
var KafkaHealthIndicator = class {
|
|
707
202
|
async check(client) {
|