@kaapi/kafka-messaging 0.0.39 → 0.0.41
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/CHANGELOG.md +48 -0
- package/README.md +195 -38
- package/lib/index.d.ts +279 -10
- package/lib/index.js +426 -68
- package/lib/index.js.map +1 -1
- package/package.json +7 -3
package/lib/index.js
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var _KafkaMessaging_config, _KafkaMessaging_producerConfig, _KafkaMessaging_address, _KafkaMessaging_name, _KafkaMessaging_consumers, _KafkaMessaging_producers, _KafkaMessaging_admins;
|
|
2
|
+
var _KafkaMessaging_config, _KafkaMessaging_producerConfig, _KafkaMessaging_address, _KafkaMessaging_name, _KafkaMessaging_consumers, _KafkaMessaging_producers, _KafkaMessaging_admins, _KafkaMessaging_producerPromise, _KafkaMessaging_sharedAdmin, _KafkaMessaging_sharedAdminPromise;
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.KafkaMessaging = void 0;
|
|
5
5
|
exports.safeDisconnect = safeDisconnect;
|
|
6
6
|
const tslib_1 = require("tslib");
|
|
7
7
|
const kafkajs_1 = require("kafkajs");
|
|
8
8
|
const crypto_1 = require("crypto");
|
|
9
|
+
/**
|
|
10
|
+
* A lightweight wrapper around KafkaJS that integrates with the Kaapi framework
|
|
11
|
+
* to provide a clean and consistent message publishing and consuming interface.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const messaging = new KafkaMessaging({
|
|
16
|
+
* clientId: 'my-app',
|
|
17
|
+
* brokers: ['localhost:9092'],
|
|
18
|
+
* name: 'my-service',
|
|
19
|
+
* address: 'service-1'
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* await messaging.publish('my-topic', { event: 'user.created' });
|
|
23
|
+
* await messaging.subscribe('my-topic', (msg, ctx) => console.log(msg));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
9
26
|
class KafkaMessaging {
|
|
10
27
|
get activeConsumers() {
|
|
11
28
|
return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_consumers, "f");
|
|
@@ -13,6 +30,11 @@ class KafkaMessaging {
|
|
|
13
30
|
get activeProducers() {
|
|
14
31
|
return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producers, "f");
|
|
15
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new KafkaMessaging instance.
|
|
35
|
+
*
|
|
36
|
+
* @param arg - Configuration options for the Kafka client
|
|
37
|
+
*/
|
|
16
38
|
constructor(arg) {
|
|
17
39
|
_KafkaMessaging_config.set(this, void 0);
|
|
18
40
|
_KafkaMessaging_producerConfig.set(this, void 0);
|
|
@@ -21,6 +43,10 @@ class KafkaMessaging {
|
|
|
21
43
|
_KafkaMessaging_consumers.set(this, new Set());
|
|
22
44
|
_KafkaMessaging_producers.set(this, new Set());
|
|
23
45
|
_KafkaMessaging_admins.set(this, new Set());
|
|
46
|
+
_KafkaMessaging_producerPromise.set(this, void 0);
|
|
47
|
+
/** Shared admin instance for internal operations (lazy initialized) */
|
|
48
|
+
_KafkaMessaging_sharedAdmin.set(this, void 0);
|
|
49
|
+
_KafkaMessaging_sharedAdminPromise.set(this, void 0);
|
|
24
50
|
const { logger, address, name, producer } = arg, kafkaConfig = tslib_1.__rest(arg, ["logger", "address", "name", "producer"]);
|
|
25
51
|
tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_config, kafkaConfig, "f");
|
|
26
52
|
tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_producerConfig, producer, "f");
|
|
@@ -34,7 +60,7 @@ class KafkaMessaging {
|
|
|
34
60
|
if (!tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_config, "f").brokers)
|
|
35
61
|
return;
|
|
36
62
|
return new kafkajs_1.Kafka(Object.assign({ logCreator: () => ({ namespace, level, label, log }) => {
|
|
37
|
-
var _a, _b, _c, _d,
|
|
63
|
+
var _a, _b, _c, _d, _f;
|
|
38
64
|
let lvl = level;
|
|
39
65
|
if (!log[level])
|
|
40
66
|
lvl = null;
|
|
@@ -53,16 +79,109 @@ class KafkaMessaging {
|
|
|
53
79
|
(_d = this.logger) === null || _d === void 0 ? void 0 : _d.debug('KAFKA', label, namespace, log.message);
|
|
54
80
|
break;
|
|
55
81
|
default:
|
|
56
|
-
(
|
|
82
|
+
(_f = this.logger) === null || _f === void 0 ? void 0 : _f.silly('KAFKA', label, namespace, log.message);
|
|
57
83
|
}
|
|
58
84
|
} }, tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_config, "f")));
|
|
59
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Internal method to initialize the shared admin.
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
_initializeSharedAdmin() {
|
|
91
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
92
|
+
var _a;
|
|
93
|
+
if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdmin, "f")) {
|
|
94
|
+
return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdmin, "f");
|
|
95
|
+
}
|
|
96
|
+
const admin = yield this.createAdmin();
|
|
97
|
+
if (!admin)
|
|
98
|
+
return;
|
|
99
|
+
tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_sharedAdmin, admin, "f");
|
|
100
|
+
admin.on(admin.events.DISCONNECT, () => {
|
|
101
|
+
if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdmin, "f") === admin) {
|
|
102
|
+
tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_sharedAdmin, undefined, "f");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug('✔️ Shared admin connected');
|
|
106
|
+
return admin;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Internal method to initialize the producer.
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
_initializeProducer() {
|
|
114
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
115
|
+
var _a;
|
|
116
|
+
// Double-check in case producer was created while waiting
|
|
117
|
+
if (this.producer) {
|
|
118
|
+
return this.producer;
|
|
119
|
+
}
|
|
120
|
+
const producer = yield this.createProducer(tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producerConfig, "f"));
|
|
121
|
+
if (!producer)
|
|
122
|
+
return;
|
|
123
|
+
const producerId = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
124
|
+
this.producer = producer;
|
|
125
|
+
this.currentProducerId = producerId;
|
|
126
|
+
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug('✔️ Producer connected');
|
|
127
|
+
producer.on(producer.events.DISCONNECT, () => {
|
|
128
|
+
var _a;
|
|
129
|
+
if (this.currentProducerId === producerId) {
|
|
130
|
+
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn('⚠️ Producer disconnected');
|
|
131
|
+
this.producer = undefined;
|
|
132
|
+
this.currentProducerId = undefined;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return producer;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
60
138
|
getKafka() {
|
|
61
139
|
if (!this.kafka) {
|
|
62
140
|
this.kafka = this._createInstance();
|
|
63
141
|
}
|
|
64
142
|
return this.kafka;
|
|
65
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Gets or creates a shared admin instance for internal operations.
|
|
146
|
+
* Uses lazy initialization to avoid unnecessary connections.
|
|
147
|
+
*
|
|
148
|
+
* @returns A promise that resolves to the shared admin instance
|
|
149
|
+
*/
|
|
150
|
+
getSharedAdmin() {
|
|
151
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
152
|
+
// Return existing admin if available and connected
|
|
153
|
+
if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdmin, "f")) {
|
|
154
|
+
return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdmin, "f");
|
|
155
|
+
}
|
|
156
|
+
// If an admin is already being created, wait for it
|
|
157
|
+
if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdminPromise, "f")) {
|
|
158
|
+
return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdminPromise, "f");
|
|
159
|
+
}
|
|
160
|
+
// Create the admin with a lock
|
|
161
|
+
tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_sharedAdminPromise, this._initializeSharedAdmin(), "f");
|
|
162
|
+
try {
|
|
163
|
+
const admin = yield tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_sharedAdminPromise, "f");
|
|
164
|
+
return admin;
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_sharedAdminPromise, undefined, "f");
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Creates and connects a Kafka admin client.
|
|
173
|
+
* The admin client is automatically tracked and will be disconnected during shutdown.
|
|
174
|
+
*
|
|
175
|
+
* @param config - Optional admin client configuration
|
|
176
|
+
* @returns A promise that resolves to the connected admin client, or undefined if Kafka is unavailable
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* const admin = await messaging.createAdmin();
|
|
181
|
+
* const topics = await admin?.listTopics();
|
|
182
|
+
* await admin?.disconnect();
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
66
185
|
createAdmin(config) {
|
|
67
186
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
68
187
|
// Get kafka instance
|
|
@@ -79,6 +198,26 @@ class KafkaMessaging {
|
|
|
79
198
|
return admin;
|
|
80
199
|
});
|
|
81
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Creates a new Kafka topic with the specified configuration.
|
|
203
|
+
*
|
|
204
|
+
* @param topic - The topic configuration including name, partitions, and replication factor
|
|
205
|
+
* @param config - Optional creation options
|
|
206
|
+
* @param config.validateOnly - If true, only validates the request without creating the topic
|
|
207
|
+
* @param config.waitForLeaders - If true, waits for partition leaders to be elected
|
|
208
|
+
* @param config.timeout - Timeout in milliseconds for the operation
|
|
209
|
+
*
|
|
210
|
+
* @throws {Error} If the admin client cannot be created
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* await messaging.createTopic({
|
|
215
|
+
* topic: 'my-topic',
|
|
216
|
+
* numPartitions: 3,
|
|
217
|
+
* replicationFactor: 1
|
|
218
|
+
* }, { waitForLeaders: true });
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
82
221
|
createTopic(topic, config) {
|
|
83
222
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
84
223
|
const admin = yield this.createAdmin();
|
|
@@ -128,10 +267,42 @@ class KafkaMessaging {
|
|
|
128
267
|
});
|
|
129
268
|
}
|
|
130
269
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* @
|
|
270
|
+
* Fetches and logs partition offsets for a topic.
|
|
271
|
+
* Uses the shared admin instance to minimize connections.
|
|
272
|
+
*
|
|
273
|
+
* @param topic - The topic to fetch offsets for
|
|
274
|
+
* @returns The partition offset information, or undefined if unavailable
|
|
275
|
+
*/
|
|
276
|
+
fetchTopicOffsets(topic) {
|
|
277
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
278
|
+
var _a;
|
|
279
|
+
const admin = yield this.getSharedAdmin();
|
|
280
|
+
if (!admin)
|
|
281
|
+
return;
|
|
282
|
+
try {
|
|
283
|
+
const partitions = yield admin.fetchTopicOffsets(topic);
|
|
284
|
+
return partitions;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(`Failed to fetch offsets for topic "${topic}":`, error);
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Creates and connects a new Kafka consumer.
|
|
294
|
+
* The consumer is automatically tracked and will be disconnected during shutdown.
|
|
295
|
+
*
|
|
296
|
+
* @param groupId - The consumer group ID
|
|
297
|
+
* @param config - Optional consumer configuration overrides
|
|
298
|
+
* @returns A promise that resolves to the connected consumer, or undefined if Kafka is unavailable
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```ts
|
|
302
|
+
* const consumer = await messaging.createConsumer('my-group', {
|
|
303
|
+
* sessionTimeout: 30000
|
|
304
|
+
* });
|
|
305
|
+
* ```
|
|
135
306
|
*/
|
|
136
307
|
createConsumer(groupId, config) {
|
|
137
308
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
@@ -158,9 +329,18 @@ class KafkaMessaging {
|
|
|
158
329
|
});
|
|
159
330
|
}
|
|
160
331
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
332
|
+
* Creates and connects a new Kafka producer.
|
|
333
|
+
* The producer is automatically tracked and will be disconnected during shutdown.
|
|
334
|
+
*
|
|
335
|
+
* @param config - Optional producer configuration overrides
|
|
336
|
+
* @returns A promise that resolves to the connected producer, or undefined if Kafka is unavailable
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```ts
|
|
340
|
+
* const producer = await messaging.createProducer({
|
|
341
|
+
* idempotent: true
|
|
342
|
+
* });
|
|
343
|
+
* ```
|
|
164
344
|
*/
|
|
165
345
|
createProducer(config) {
|
|
166
346
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
@@ -192,33 +372,37 @@ class KafkaMessaging {
|
|
|
192
372
|
});
|
|
193
373
|
}
|
|
194
374
|
/**
|
|
195
|
-
*
|
|
375
|
+
* Gets or creates the singleton producer instance.
|
|
376
|
+
* Uses a promise-based lock to prevent race conditions when called concurrently.
|
|
377
|
+
*
|
|
378
|
+
* @returns A promise that resolves to the producer instance, or undefined if unavailable.
|
|
196
379
|
*/
|
|
197
380
|
getProducer() {
|
|
198
381
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
382
|
+
// Return existing producer if available
|
|
383
|
+
if (this.producer) {
|
|
384
|
+
return this.producer;
|
|
385
|
+
}
|
|
386
|
+
// If a producer is already being created, wait for it
|
|
387
|
+
if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producerPromise, "f")) {
|
|
388
|
+
return tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producerPromise, "f");
|
|
389
|
+
}
|
|
390
|
+
// Create the producer with a lock
|
|
391
|
+
tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_producerPromise, this._initializeProducer(), "f");
|
|
392
|
+
try {
|
|
393
|
+
const producer = yield tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_producerPromise, "f");
|
|
394
|
+
return producer;
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
// Clear the promise after resolution (success or failure)
|
|
398
|
+
tslib_1.__classPrivateFieldSet(this, _KafkaMessaging_producerPromise, undefined, "f");
|
|
216
399
|
}
|
|
217
|
-
return this.producer;
|
|
218
400
|
});
|
|
219
401
|
}
|
|
220
402
|
/**
|
|
221
|
-
*
|
|
403
|
+
* Disconnects the singleton producer instance.
|
|
404
|
+
*
|
|
405
|
+
* @returns A promise that resolves when the producer is disconnected
|
|
222
406
|
*/
|
|
223
407
|
disconnectProducer() {
|
|
224
408
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
@@ -229,9 +413,86 @@ class KafkaMessaging {
|
|
|
229
413
|
}
|
|
230
414
|
});
|
|
231
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Publishes multiple messages to a Kafka topic in a single batch.
|
|
418
|
+
* More efficient than multiple `publish()` calls for high-throughput scenarios.
|
|
419
|
+
*
|
|
420
|
+
* @typeParam T - The type of the message payload
|
|
421
|
+
* @param topic - The Kafka topic to publish to
|
|
422
|
+
* @param messages - Array of messages to publish
|
|
423
|
+
*
|
|
424
|
+
* @throws {Error} If the batch fails to send
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* ```ts
|
|
428
|
+
* await messaging.publishBatch('user-events', [
|
|
429
|
+
* { value: { event: 'user.created', userId: '1' } },
|
|
430
|
+
* { value: { event: 'user.created', userId: '2' } },
|
|
431
|
+
* { value: { event: 'user.updated', userId: '3' }, key: 'user-3' },
|
|
432
|
+
* ]);
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
publishBatch(topic, messages) {
|
|
436
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
437
|
+
var _a, _b, _c;
|
|
438
|
+
if (!messages.length)
|
|
439
|
+
return;
|
|
440
|
+
const producer = yield this.getProducer();
|
|
441
|
+
// If we don't have a producer, abort
|
|
442
|
+
if (!producer)
|
|
443
|
+
return (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error('❌ Could not get producer');
|
|
444
|
+
const baseHeaders = {};
|
|
445
|
+
if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_name, "f"))
|
|
446
|
+
baseHeaders.name = tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_name, "f");
|
|
447
|
+
if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_address, "f"))
|
|
448
|
+
baseHeaders.address = tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_address, "f");
|
|
449
|
+
const kafkaMessages = messages.map((msg) => {
|
|
450
|
+
var _a;
|
|
451
|
+
return ({
|
|
452
|
+
value: (msg.value instanceof Buffer ?
|
|
453
|
+
msg.value : typeof msg.value === 'string' ?
|
|
454
|
+
msg.value : (msg.value === null ? null : JSON.stringify(msg.value))),
|
|
455
|
+
key: msg.key,
|
|
456
|
+
partition: msg.partition,
|
|
457
|
+
timestamp: `${Date.now()}`,
|
|
458
|
+
headers: Object.assign(Object.assign({}, baseHeaders), ((_a = msg.headers) !== null && _a !== void 0 ? _a : {})),
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
try {
|
|
462
|
+
const res = yield producer.send({
|
|
463
|
+
topic,
|
|
464
|
+
messages: kafkaMessages,
|
|
465
|
+
});
|
|
466
|
+
(_b = this.logger) === null || _b === void 0 ? void 0 : _b.verbose(`📤 Sent batch to KAFKA topic "${topic}" (${messages.length} messages, offset ${res[0].baseOffset})`);
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
(_c = this.logger) === null || _c === void 0 ? void 0 : _c.error(`❌ Failed to publish batch to "${topic}":`, error);
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Publishes a message to the specified Kafka topic.
|
|
476
|
+
* Automatically manages the producer lifecycle and includes service metadata in headers.
|
|
477
|
+
*
|
|
478
|
+
* @typeParam T - The type of the message payload
|
|
479
|
+
* @param topic - The Kafka topic to publish to
|
|
480
|
+
* @param message - The message payload (will be JSON serialized)
|
|
481
|
+
*
|
|
482
|
+
* @throws {Error} If the message fails to send
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* ```ts
|
|
486
|
+
* await messaging.publish('user-events', {
|
|
487
|
+
* event: 'user.created',
|
|
488
|
+
* userId: '123',
|
|
489
|
+
* timestamp: Date.now()
|
|
490
|
+
* });
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
232
493
|
publish(topic, message) {
|
|
233
494
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
234
|
-
var _a, _b;
|
|
495
|
+
var _a, _b, _c;
|
|
235
496
|
// Get kafka producer
|
|
236
497
|
const producer = yield this.getProducer();
|
|
237
498
|
// If we don't have a producer, abort
|
|
@@ -244,20 +505,59 @@ class KafkaMessaging {
|
|
|
244
505
|
if (tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_address, "f")) {
|
|
245
506
|
headers.address = tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_address, "f");
|
|
246
507
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
508
|
+
try {
|
|
509
|
+
// Send message to the topic
|
|
510
|
+
const res = yield producer.send({
|
|
511
|
+
topic,
|
|
512
|
+
messages: [{
|
|
513
|
+
value: (message instanceof Buffer ?
|
|
514
|
+
message : typeof message === 'string' ?
|
|
515
|
+
message : (message === null ? null : JSON.stringify(message))),
|
|
516
|
+
timestamp: `${Date.now()}`,
|
|
517
|
+
headers
|
|
518
|
+
}],
|
|
519
|
+
});
|
|
520
|
+
(_b = this.logger) === null || _b === void 0 ? void 0 : _b.verbose(`📤 Sent to KAFKA topic "${topic}" (offset ${res[0].baseOffset})`);
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
(_c = this.logger) === null || _c === void 0 ? void 0 : _c.error(`❌ Failed to publish to "${topic}":`, error);
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
257
526
|
});
|
|
258
527
|
}
|
|
259
528
|
/**
|
|
260
|
-
*
|
|
529
|
+
* Subscribes to a Kafka topic and processes messages with the provided handler.
|
|
530
|
+
* Creates a new consumer for each subscription with an auto-generated group ID.
|
|
531
|
+
*
|
|
532
|
+
* @typeParam T - The expected type of incoming messages
|
|
533
|
+
* @param topic - The Kafka topic to subscribe to
|
|
534
|
+
* @param handler - Callback function invoked for each message. Can be async.
|
|
535
|
+
* @param config - Optional subscription configuration
|
|
536
|
+
* @param config.fromBeginning - Start consuming from the beginning of the topic
|
|
537
|
+
* @param config.onReady - Callback invoked when the consumer is ready
|
|
538
|
+
* @param config.groupId - Override the auto-generated consumer group ID
|
|
539
|
+
* @param config.groupIdPrefix - Prefix for auto-generated group ID (default: service name)
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* ```ts
|
|
543
|
+
* // Using auto-generated group ID (e.g., "my-service.user-events")
|
|
544
|
+
* await messaging.subscribe('user-events', handler);
|
|
545
|
+
*
|
|
546
|
+
* // Using custom group ID
|
|
547
|
+
* await messaging.subscribe('user-events', handler, {
|
|
548
|
+
* groupId: 'my-custom-consumer-group'
|
|
549
|
+
* });
|
|
550
|
+
*
|
|
551
|
+
* // Using custom prefix (e.g., "analytics.user-events")
|
|
552
|
+
* await messaging.subscribe('user-events', handler, {
|
|
553
|
+
* groupIdPrefix: 'analytics'
|
|
554
|
+
* });
|
|
555
|
+
*
|
|
556
|
+
* // With offset logging enabled
|
|
557
|
+
* await messaging.subscribe('user-events', handler, {
|
|
558
|
+
* logOffsets: true
|
|
559
|
+
* });
|
|
560
|
+
* ```
|
|
261
561
|
*/
|
|
262
562
|
subscribe(topic, handler, config) {
|
|
263
563
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
@@ -266,46 +566,51 @@ class KafkaMessaging {
|
|
|
266
566
|
let consumerConfig;
|
|
267
567
|
let fromBeginning;
|
|
268
568
|
let onReady;
|
|
569
|
+
let groupId;
|
|
570
|
+
let groupIdPrefix;
|
|
571
|
+
let logOffsets = false;
|
|
572
|
+
let onError;
|
|
269
573
|
if (config) {
|
|
270
|
-
const { fromBeginning: tmpFromBeginning, onReady: tmpOnReady } = config, tmpConsumerConfig = tslib_1.__rest(config, ["fromBeginning", "onReady"]);
|
|
574
|
+
const { fromBeginning: tmpFromBeginning, onReady: tmpOnReady, groupId: tmpGroupId, groupIdPrefix: tmpGroupIdPrefix, logOffsets: tmpLogOffsets, onError: tmpOnError } = config, tmpConsumerConfig = tslib_1.__rest(config, ["fromBeginning", "onReady", "groupId", "groupIdPrefix", "logOffsets", "onError"]);
|
|
271
575
|
fromBeginning = tmpFromBeginning;
|
|
272
576
|
onReady = tmpOnReady;
|
|
577
|
+
groupId = tmpGroupId;
|
|
578
|
+
groupIdPrefix = tmpGroupIdPrefix;
|
|
579
|
+
logOffsets = tmpLogOffsets !== null && tmpLogOffsets !== void 0 ? tmpLogOffsets : false;
|
|
580
|
+
onError = tmpOnError;
|
|
273
581
|
consumerConfig = tmpConsumerConfig;
|
|
274
582
|
}
|
|
583
|
+
// Determine the consumer group ID
|
|
584
|
+
const resolvedGroupId = groupId !== null && groupId !== void 0 ? groupId : `${(_b = groupIdPrefix !== null && groupIdPrefix !== void 0 ? groupIdPrefix : tslib_1.__classPrivateFieldGet(this, _KafkaMessaging_name, "f")) !== null && _b !== void 0 ? _b : 'group'}.${topic}`;
|
|
275
585
|
// Get kafka consumer
|
|
276
|
-
const consumer = yield this.createConsumer(
|
|
586
|
+
const consumer = yield this.createConsumer(resolvedGroupId, consumerConfig);
|
|
277
587
|
// If we don't have a consumer, abort
|
|
278
588
|
if (!consumer)
|
|
279
|
-
return (
|
|
589
|
+
return (_c = this.logger) === null || _c === void 0 ? void 0 : _c.error('❌ Could not get consumer');
|
|
280
590
|
// Listen to the topic
|
|
281
591
|
yield consumer.subscribe({
|
|
282
592
|
topic,
|
|
283
593
|
fromBeginning
|
|
284
594
|
});
|
|
285
|
-
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
});
|
|
294
|
-
}
|
|
595
|
+
// Only fetch offsets if explicitly requested (avoids admin overhead)
|
|
596
|
+
if (logOffsets) {
|
|
597
|
+
const partitions = yield this.fetchTopicOffsets(topic);
|
|
598
|
+
if (partitions) {
|
|
599
|
+
partitions.forEach((partition) => {
|
|
600
|
+
var _a;
|
|
601
|
+
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.info(`👂 Start "${topic}" partition: ${partition.partition} | offset: ${partition.offset} | high: ${partition.high} | low: ${partition.low}`);
|
|
602
|
+
});
|
|
295
603
|
}
|
|
296
|
-
catch (error) {
|
|
297
|
-
(_c = this.logger) === null || _c === void 0 ? void 0 : _c.error(error);
|
|
298
|
-
}
|
|
299
|
-
yield admin.disconnect();
|
|
300
604
|
}
|
|
301
605
|
yield consumer.run({
|
|
302
606
|
eachBatchAutoResolve: false,
|
|
303
607
|
eachBatch: (_a) => tslib_1.__awaiter(this, [_a], void 0, function* ({ batch, resolveOffset, heartbeat, }) {
|
|
304
|
-
var _b, _c, _d,
|
|
608
|
+
var _b, _c, _d, _f, _g, _h, _j;
|
|
305
609
|
(_b = this.logger) === null || _b === void 0 ? void 0 : _b.verbose(`📥 Received from KAFKA topic "${topic}" (${batch.messages.length} messages)`);
|
|
306
610
|
for (const message of batch.messages) {
|
|
611
|
+
const context = {};
|
|
612
|
+
let value = message.value;
|
|
307
613
|
try {
|
|
308
|
-
const context = {};
|
|
309
614
|
try {
|
|
310
615
|
// unbufferize header values
|
|
311
616
|
Object.keys((message.headers || {})).forEach(key => {
|
|
@@ -320,14 +625,34 @@ class KafkaMessaging {
|
|
|
320
625
|
catch (e) {
|
|
321
626
|
(_c = this.logger) === null || _c === void 0 ? void 0 : _c.error(`KafkaMessaging.subscribe('${topic}', …) error:`, e);
|
|
322
627
|
}
|
|
323
|
-
const
|
|
628
|
+
const tmpValue = ((_f = (_d = message.value) === null || _d === void 0 ? void 0 : _d.toString) === null || _f === void 0 ? void 0 : _f.call(_d)) || null;
|
|
629
|
+
if (tmpValue) {
|
|
630
|
+
try {
|
|
631
|
+
value = JSON.parse(tmpValue);
|
|
632
|
+
}
|
|
633
|
+
catch (_e) {
|
|
634
|
+
// not important - keep original value
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const res = handler(value, context);
|
|
324
638
|
if (res)
|
|
325
639
|
yield res;
|
|
326
640
|
resolveOffset(message.offset);
|
|
327
|
-
(
|
|
641
|
+
(_g = this.logger) === null || _g === void 0 ? void 0 : _g.debug(`✔️ Resolved offset ${message.offset} from KAFKA topic "${topic}"`);
|
|
328
642
|
}
|
|
329
643
|
catch (e) {
|
|
330
|
-
(
|
|
644
|
+
(_h = this.logger) === null || _h === void 0 ? void 0 : _h.error(`KafkaMessaging.subscribe('${topic}', …) handler throwed an error:`, e);
|
|
645
|
+
// Call custom error handler if provided
|
|
646
|
+
if (onError) {
|
|
647
|
+
try {
|
|
648
|
+
const errorResult = onError(e, value, context);
|
|
649
|
+
if (errorResult)
|
|
650
|
+
yield errorResult;
|
|
651
|
+
}
|
|
652
|
+
catch (onErrorError) {
|
|
653
|
+
(_j = this.logger) === null || _j === void 0 ? void 0 : _j.error(`KafkaMessaging.subscribe('${topic}', …) onError callback failed:`, onErrorError);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
331
656
|
}
|
|
332
657
|
yield heartbeat();
|
|
333
658
|
}
|
|
@@ -336,14 +661,39 @@ class KafkaMessaging {
|
|
|
336
661
|
onReady === null || onReady === void 0 ? void 0 : onReady(consumer);
|
|
337
662
|
});
|
|
338
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Safely disconnects a Kafka client with timeout protection.
|
|
666
|
+
* Prevents hanging if the client fails to disconnect gracefully.
|
|
667
|
+
*
|
|
668
|
+
* @param client - The Kafka client (producer, consumer, or admin) to disconnect
|
|
669
|
+
* @param timeoutMs - Maximum time to wait for disconnection (default: 5000ms)
|
|
670
|
+
* @returns A promise that resolves when disconnected or rejects on timeout
|
|
671
|
+
*
|
|
672
|
+
* @throws {Error} If the disconnect times out
|
|
673
|
+
*/
|
|
339
674
|
safeDisconnect(client_1) {
|
|
340
675
|
return tslib_1.__awaiter(this, arguments, void 0, function* (client, timeoutMs = 5000) {
|
|
341
676
|
return safeDisconnect(client, timeoutMs);
|
|
342
677
|
});
|
|
343
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* Gracefully shuts down all tracked Kafka clients (producers, consumers, and admins).
|
|
681
|
+
* Should be called during application teardown to release resources.
|
|
682
|
+
*
|
|
683
|
+
* @returns A summary of the shutdown operation including success and error counts
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* ```ts
|
|
687
|
+
* process.on('SIGTERM', async () => {
|
|
688
|
+
* const result = await messaging.shutdown();
|
|
689
|
+
* console.log(`Shutdown complete: ${result.successProducers} producers, ${result.errorCount} errors`);
|
|
690
|
+
* process.exit(0);
|
|
691
|
+
* });
|
|
692
|
+
* ```
|
|
693
|
+
*/
|
|
344
694
|
shutdown() {
|
|
345
695
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
346
|
-
var _a, _b, _c, _d,
|
|
696
|
+
var _a, _b, _c, _d, _f, _g;
|
|
347
697
|
let errorCount = 0;
|
|
348
698
|
let successProducers = 0;
|
|
349
699
|
let successConsumers = 0;
|
|
@@ -381,8 +731,8 @@ class KafkaMessaging {
|
|
|
381
731
|
errorCount++;
|
|
382
732
|
}
|
|
383
733
|
}
|
|
384
|
-
(
|
|
385
|
-
(
|
|
734
|
+
(_f = this.logger) === null || _f === void 0 ? void 0 : _f.info('KafkaMessaging shutdown complete');
|
|
735
|
+
(_g = this.logger) === null || _g === void 0 ? void 0 : _g.info(`Disconnected ${successProducers} producers, ${successConsumers} consumers and ${successAdmins} admins with ${errorCount} errors.`);
|
|
386
736
|
return {
|
|
387
737
|
successProducers,
|
|
388
738
|
successConsumers,
|
|
@@ -393,7 +743,15 @@ class KafkaMessaging {
|
|
|
393
743
|
}
|
|
394
744
|
}
|
|
395
745
|
exports.KafkaMessaging = KafkaMessaging;
|
|
396
|
-
_KafkaMessaging_config = new WeakMap(), _KafkaMessaging_producerConfig = new WeakMap(), _KafkaMessaging_address = new WeakMap(), _KafkaMessaging_name = new WeakMap(), _KafkaMessaging_consumers = new WeakMap(), _KafkaMessaging_producers = new WeakMap(), _KafkaMessaging_admins = new WeakMap();
|
|
746
|
+
_KafkaMessaging_config = new WeakMap(), _KafkaMessaging_producerConfig = new WeakMap(), _KafkaMessaging_address = new WeakMap(), _KafkaMessaging_name = new WeakMap(), _KafkaMessaging_consumers = new WeakMap(), _KafkaMessaging_producers = new WeakMap(), _KafkaMessaging_admins = new WeakMap(), _KafkaMessaging_producerPromise = new WeakMap(), _KafkaMessaging_sharedAdmin = new WeakMap(), _KafkaMessaging_sharedAdminPromise = new WeakMap();
|
|
747
|
+
/**
|
|
748
|
+
* Safely disconnects a Kafka client with timeout protection.
|
|
749
|
+
* Standalone utility function.
|
|
750
|
+
*
|
|
751
|
+
* @param client - The Kafka client to disconnect
|
|
752
|
+
* @param timeoutMs - Maximum time to wait (default: 5000ms)
|
|
753
|
+
* @returns A promise that resolves when disconnected or rejects on timeout
|
|
754
|
+
*/
|
|
397
755
|
function safeDisconnect(client_1) {
|
|
398
756
|
return tslib_1.__awaiter(this, arguments, void 0, function* (client, timeoutMs = 5000) {
|
|
399
757
|
return Promise.race([
|