@platformatic/kafka 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apis/admin/create-partitions-v1.d.ts +24 -0
- package/dist/apis/admin/create-partitions-v1.js +53 -0
- package/dist/apis/admin/create-partitions-v2.d.ts +24 -0
- package/dist/apis/admin/create-partitions-v2.js +54 -0
- package/dist/apis/admin/describe-configs-v2.d.ts +38 -0
- package/dist/apis/admin/describe-configs-v2.js +84 -0
- package/dist/apis/admin/describe-configs-v3.d.ts +38 -0
- package/dist/apis/admin/describe-configs-v3.js +84 -0
- package/dist/apis/admin/index.d.ts +4 -0
- package/dist/apis/admin/index.js +4 -0
- package/dist/apis/callbacks.js +1 -0
- package/dist/apis/consumer/fetch-v12.d.ts +46 -0
- package/dist/apis/consumer/fetch-v12.js +123 -0
- package/dist/apis/consumer/fetch-v13.d.ts +46 -0
- package/dist/apis/consumer/fetch-v13.js +123 -0
- package/dist/apis/consumer/fetch-v14.d.ts +46 -0
- package/dist/apis/consumer/fetch-v14.js +123 -0
- package/dist/apis/consumer/index.d.ts +4 -0
- package/dist/apis/consumer/index.js +4 -0
- package/dist/apis/consumer/offset-for-leader-epoch-v4.d.ts +29 -0
- package/dist/apis/consumer/offset-for-leader-epoch-v4.js +65 -0
- package/dist/apis/metadata/index.d.ts +3 -0
- package/dist/apis/metadata/index.js +3 -0
- package/dist/apis/metadata/metadata-v10.d.ts +37 -0
- package/dist/apis/metadata/metadata-v10.js +96 -0
- package/dist/apis/metadata/metadata-v11.d.ts +37 -0
- package/dist/apis/metadata/metadata-v11.js +96 -0
- package/dist/apis/metadata/metadata-v9.d.ts +37 -0
- package/dist/apis/metadata/metadata-v9.js +96 -0
- package/dist/apis/producer/index.d.ts +2 -0
- package/dist/apis/producer/index.js +2 -0
- package/dist/apis/producer/produce-v7.d.ts +29 -0
- package/dist/apis/producer/produce-v7.js +88 -0
- package/dist/apis/producer/produce-v8.d.ts +29 -0
- package/dist/apis/producer/produce-v8.js +101 -0
- package/dist/clients/base/base.d.ts +1 -2
- package/dist/clients/base/base.js +100 -98
- package/dist/clients/base/index.d.ts +1 -1
- package/dist/clients/base/index.js +1 -1
- package/dist/clients/consumer/consumer.js +4 -2
- package/dist/clients/consumer/messages-stream.js +5 -1
- package/dist/clients/producer/producer.js +2 -2
- package/dist/network/connection.js +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -23,7 +23,6 @@ export const kClosed = Symbol('plt.kafka.base.closed');
|
|
|
23
23
|
export const kListApis = Symbol('plt.kafka.base.listApis');
|
|
24
24
|
export const kMetadata = Symbol('plt.kafka.base.metadata');
|
|
25
25
|
export const kCheckNotClosed = Symbol('plt.kafka.base.checkNotClosed');
|
|
26
|
-
export const kClearMetadata = Symbol('plt.kafka.base.clearMetadata');
|
|
27
26
|
export const kPerformWithRetry = Symbol('plt.kafka.base.performWithRetry');
|
|
28
27
|
export const kPerformDeduplicated = Symbol('plt.kafka.base.performDeduplicated');
|
|
29
28
|
export const kValidateOptions = Symbol('plt.kafka.base.validateOptions');
|
|
@@ -118,7 +117,7 @@ export class Base extends EventEmitter {
|
|
|
118
117
|
callback(validationError, undefined);
|
|
119
118
|
return callback[kCallbackPromise];
|
|
120
119
|
}
|
|
121
|
-
|
|
120
|
+
this[kMetadata](options, callback);
|
|
122
121
|
return callback[kCallbackPromise];
|
|
123
122
|
}
|
|
124
123
|
connectToBrokers(nodeIds, callback) {
|
|
@@ -209,101 +208,7 @@ export class Base extends EventEmitter {
|
|
|
209
208
|
}, callback);
|
|
210
209
|
}
|
|
211
210
|
[kMetadata](options, callback) {
|
|
212
|
-
|
|
213
|
-
let topicsToFetch = [];
|
|
214
|
-
// Determine which topics we need to fetch
|
|
215
|
-
if (!this.#metadata || options.forceUpdate) {
|
|
216
|
-
topicsToFetch = options.topics;
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
for (const topic of options.topics) {
|
|
220
|
-
const existingTopic = this.#metadata.topics.get(topic);
|
|
221
|
-
if (!existingTopic || existingTopic.lastUpdate < expiralDate) {
|
|
222
|
-
topicsToFetch.push(topic);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
// All topics are already up-to-date, simply return them
|
|
227
|
-
if (this.#metadata && !topicsToFetch.length && !options.forceUpdate) {
|
|
228
|
-
callback(null, {
|
|
229
|
-
...this.#metadata,
|
|
230
|
-
topics: new Map(options.topics.map(topic => [topic, this.#metadata.topics.get(topic)]))
|
|
231
|
-
});
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const autocreateTopics = options.autocreateTopics ?? this[kOptions].autocreateTopics;
|
|
235
|
-
this[kPerformDeduplicated](
|
|
236
|
-
// Unique key to avoid mixing callbacks
|
|
237
|
-
`metadata-${options.topics.sort().join(',')}-${autocreateTopics}-${options.forceUpdate}`, deduplicateCallback => {
|
|
238
|
-
this[kPerformWithRetry]('metadata', retryCallback => {
|
|
239
|
-
this[kGetBootstrapConnection]((error, connection) => {
|
|
240
|
-
if (error) {
|
|
241
|
-
retryCallback(error, undefined);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
this[kGetApi]('Metadata', (error, api) => {
|
|
245
|
-
if (error) {
|
|
246
|
-
retryCallback(error, undefined);
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
api(connection, topicsToFetch, autocreateTopics, true, retryCallback);
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
}, (error, metadata) => {
|
|
253
|
-
if (error) {
|
|
254
|
-
const hasStaleMetadata = error.findBy('hasStaleMetadata', true);
|
|
255
|
-
// Stale metadata, we need to fetch everything again
|
|
256
|
-
if (hasStaleMetadata) {
|
|
257
|
-
this[kClearMetadata]();
|
|
258
|
-
topicsToFetch = options.topics;
|
|
259
|
-
}
|
|
260
|
-
deduplicateCallback(error, undefined);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const lastUpdate = Date.now();
|
|
264
|
-
if (!this.#metadata) {
|
|
265
|
-
this.#metadata = {
|
|
266
|
-
id: metadata.clusterId,
|
|
267
|
-
brokers: new Map(),
|
|
268
|
-
topics: new Map(),
|
|
269
|
-
lastUpdate
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
this.#metadata.lastUpdate = lastUpdate;
|
|
274
|
-
}
|
|
275
|
-
const brokers = new Map();
|
|
276
|
-
// This should never change, but we act defensively here
|
|
277
|
-
for (const broker of metadata.brokers) {
|
|
278
|
-
const { host, port } = broker;
|
|
279
|
-
brokers.set(broker.nodeId, { host, port });
|
|
280
|
-
}
|
|
281
|
-
this.#metadata.brokers = brokers;
|
|
282
|
-
// Update all the topics in the cache
|
|
283
|
-
for (const { name, topicId: id, partitions: rawPartitions, isInternal } of metadata.topics) {
|
|
284
|
-
/* c8 ignore next 3 - Sometimes internal topics might be returned by Kafka */
|
|
285
|
-
if (isInternal) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
const partitions = [];
|
|
289
|
-
for (const rawPartition of rawPartitions.sort((a, b) => a.partitionIndex - b.partitionIndex)) {
|
|
290
|
-
partitions[rawPartition.partitionIndex] = {
|
|
291
|
-
leader: rawPartition.leaderId,
|
|
292
|
-
leaderEpoch: rawPartition.leaderEpoch,
|
|
293
|
-
replicas: rawPartition.replicaNodes
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
this.#metadata.topics.set(name, { id, partitions, partitionsCount: rawPartitions.length, lastUpdate });
|
|
297
|
-
}
|
|
298
|
-
// Now build the object to return
|
|
299
|
-
const updatedMetadata = {
|
|
300
|
-
...this.#metadata,
|
|
301
|
-
topics: new Map(options.topics.map(topic => [topic, this.#metadata.topics.get(topic)]))
|
|
302
|
-
};
|
|
303
|
-
this.emitWithDebug('client', 'metadata', updatedMetadata);
|
|
304
|
-
deduplicateCallback(null, updatedMetadata);
|
|
305
|
-
}, 0);
|
|
306
|
-
}, callback);
|
|
211
|
+
baseMetadataChannel.traceCallback(this.#performMetadata, 1, createDiagnosticContext({ client: this, operation: 'metadata' }), this, options, callback);
|
|
307
212
|
}
|
|
308
213
|
[kCheckNotClosed](callback) {
|
|
309
214
|
if (this[kClosed]) {
|
|
@@ -313,7 +218,7 @@ export class Base extends EventEmitter {
|
|
|
313
218
|
}
|
|
314
219
|
return false;
|
|
315
220
|
}
|
|
316
|
-
|
|
221
|
+
clearMetadata() {
|
|
317
222
|
this.#metadata = undefined;
|
|
318
223
|
}
|
|
319
224
|
[kPerformWithRetry](operationId, operation, callback, attempt = 0, errors = [], shouldSkipRetry) {
|
|
@@ -430,6 +335,103 @@ export class Base extends EventEmitter {
|
|
|
430
335
|
this[kClientType] = type;
|
|
431
336
|
notifyCreation(type, this);
|
|
432
337
|
}
|
|
338
|
+
#performMetadata(options, callback) {
|
|
339
|
+
const expiralDate = Date.now() - (options.metadataMaxAge ?? this[kOptions].metadataMaxAge);
|
|
340
|
+
let topicsToFetch = [];
|
|
341
|
+
// Determine which topics we need to fetch
|
|
342
|
+
if (!this.#metadata || options.forceUpdate) {
|
|
343
|
+
topicsToFetch = options.topics;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
for (const topic of options.topics) {
|
|
347
|
+
const existingTopic = this.#metadata.topics.get(topic);
|
|
348
|
+
if (!existingTopic || existingTopic.lastUpdate < expiralDate) {
|
|
349
|
+
topicsToFetch.push(topic);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// All topics are already up-to-date, simply return them
|
|
354
|
+
if (this.#metadata && !topicsToFetch.length && !options.forceUpdate) {
|
|
355
|
+
callback(null, {
|
|
356
|
+
...this.#metadata,
|
|
357
|
+
topics: new Map(options.topics.map(topic => [topic, this.#metadata.topics.get(topic)]))
|
|
358
|
+
});
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const autocreateTopics = options.autocreateTopics ?? this[kOptions].autocreateTopics;
|
|
362
|
+
this[kPerformDeduplicated](
|
|
363
|
+
// Unique key to avoid mixing callbacks
|
|
364
|
+
`metadata-${options.topics.sort().join(',')}-${autocreateTopics}-${options.forceUpdate}`, deduplicateCallback => {
|
|
365
|
+
this[kPerformWithRetry]('metadata', retryCallback => {
|
|
366
|
+
this[kGetBootstrapConnection]((error, connection) => {
|
|
367
|
+
if (error) {
|
|
368
|
+
retryCallback(error, undefined);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this[kGetApi]('Metadata', (error, api) => {
|
|
372
|
+
if (error) {
|
|
373
|
+
retryCallback(error, undefined);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
api(connection, topicsToFetch, autocreateTopics, true, retryCallback);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}, (error, metadata) => {
|
|
380
|
+
if (error) {
|
|
381
|
+
const hasStaleMetadata = error.findBy('hasStaleMetadata', true);
|
|
382
|
+
// Stale metadata, we need to fetch everything again
|
|
383
|
+
if (hasStaleMetadata) {
|
|
384
|
+
this.clearMetadata();
|
|
385
|
+
topicsToFetch = options.topics;
|
|
386
|
+
}
|
|
387
|
+
deduplicateCallback(error, undefined);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const lastUpdate = Date.now();
|
|
391
|
+
if (!this.#metadata) {
|
|
392
|
+
this.#metadata = {
|
|
393
|
+
id: metadata.clusterId,
|
|
394
|
+
brokers: new Map(),
|
|
395
|
+
topics: new Map(),
|
|
396
|
+
lastUpdate
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
this.#metadata.lastUpdate = lastUpdate;
|
|
401
|
+
}
|
|
402
|
+
const brokers = new Map();
|
|
403
|
+
// This should never change, but we act defensively here
|
|
404
|
+
for (const broker of metadata.brokers) {
|
|
405
|
+
const { host, port } = broker;
|
|
406
|
+
brokers.set(broker.nodeId, { host, port });
|
|
407
|
+
}
|
|
408
|
+
this.#metadata.brokers = brokers;
|
|
409
|
+
// Update all the topics in the cache
|
|
410
|
+
for (const { name, topicId: id, partitions: rawPartitions, isInternal } of metadata.topics) {
|
|
411
|
+
/* c8 ignore next 3 - Sometimes internal topics might be returned by Kafka */
|
|
412
|
+
if (isInternal) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const partitions = [];
|
|
416
|
+
for (const rawPartition of rawPartitions.sort((a, b) => a.partitionIndex - b.partitionIndex)) {
|
|
417
|
+
partitions[rawPartition.partitionIndex] = {
|
|
418
|
+
leader: rawPartition.leaderId,
|
|
419
|
+
leaderEpoch: rawPartition.leaderEpoch,
|
|
420
|
+
replicas: rawPartition.replicaNodes
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
this.#metadata.topics.set(name, { id, partitions, partitionsCount: rawPartitions.length, lastUpdate });
|
|
424
|
+
}
|
|
425
|
+
// Now build the object to return
|
|
426
|
+
const updatedMetadata = {
|
|
427
|
+
...this.#metadata,
|
|
428
|
+
topics: new Map(options.topics.map(topic => [topic, this.#metadata.topics.get(topic)]))
|
|
429
|
+
};
|
|
430
|
+
this.emitWithDebug('client', 'metadata', updatedMetadata);
|
|
431
|
+
deduplicateCallback(null, updatedMetadata);
|
|
432
|
+
}, 0);
|
|
433
|
+
}, callback);
|
|
434
|
+
}
|
|
433
435
|
#forwardEvents(source, events) {
|
|
434
436
|
for (const event of events) {
|
|
435
437
|
source.on(event, (...args) => {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { Base, kCheckNotClosed,
|
|
1
|
+
export { Base, kCheckNotClosed, kGetApi, kGetBootstrapConnection, kGetConnection, kListApis, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from './base.ts';
|
|
2
2
|
export * from './options.ts';
|
|
3
3
|
export * from './types.ts';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { Base, kCheckNotClosed,
|
|
1
|
+
export { Base, kCheckNotClosed, kGetApi, kGetBootstrapConnection, kGetConnection, kListApis, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "./base.js";
|
|
2
2
|
export * from "./options.js";
|
|
3
3
|
export * from "./types.js";
|
|
@@ -6,7 +6,7 @@ import { INT32_SIZE } from "../../protocol/definitions.js";
|
|
|
6
6
|
import { Reader } from "../../protocol/reader.js";
|
|
7
7
|
import { Writer } from "../../protocol/writer.js";
|
|
8
8
|
import { kAutocommit, kRefreshOffsetsAndFetch } from "../../symbols.js";
|
|
9
|
-
import { Base, kAfterCreate, kCheckNotClosed,
|
|
9
|
+
import { Base, kAfterCreate, kCheckNotClosed, kClosed, kCreateConnectionPool, kFetchConnections, kFormatValidationErrors, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
|
|
10
10
|
import { defaultBaseOptions } from "../base/options.js";
|
|
11
11
|
import { ensureMetric } from "../metrics.js";
|
|
12
12
|
import { MessagesStream } from "./messages-stream.js";
|
|
@@ -933,6 +933,8 @@ export class Consumer extends Base {
|
|
|
933
933
|
callback(error, undefined);
|
|
934
934
|
return;
|
|
935
935
|
}
|
|
936
|
+
// This is for Azure Event Hubs compatibility, which does not respond with an error on the first join
|
|
937
|
+
this.memberId = response.memberId;
|
|
936
938
|
this.generationId = response.generationId;
|
|
937
939
|
this.#isLeader = response.leader === this.memberId;
|
|
938
940
|
this.#protocol = response.protocolName;
|
|
@@ -1212,7 +1214,7 @@ export class Consumer extends Base {
|
|
|
1212
1214
|
}
|
|
1213
1215
|
#handleMetadataError(error) {
|
|
1214
1216
|
if (error && error?.findBy('hasStaleMetadata', true)) {
|
|
1215
|
-
this
|
|
1217
|
+
this.clearMetadata();
|
|
1216
1218
|
}
|
|
1217
1219
|
return error;
|
|
1218
1220
|
}
|
|
@@ -53,7 +53,7 @@ export class MessagesStream extends Readable {
|
|
|
53
53
|
if (!offsets && mode === MessagesStreamModes.MANUAL) {
|
|
54
54
|
throw new UserError('Must specify offsets when the stream mode is MANUAL.');
|
|
55
55
|
}
|
|
56
|
-
/* c8 ignore next - Unless is initialized directly, highWaterMark is always defined */
|
|
56
|
+
/* c8 ignore next 4 - Unless is initialized directly, highWaterMark is always defined */
|
|
57
57
|
super({
|
|
58
58
|
objectMode: true,
|
|
59
59
|
highWaterMark: maxFetches ?? options.highWaterMark ?? defaultConsumerOptions.highWaterMark
|
|
@@ -113,16 +113,20 @@ export class MessagesStream extends Readable {
|
|
|
113
113
|
}
|
|
114
114
|
notifyCreation('messages-stream', this);
|
|
115
115
|
}
|
|
116
|
+
/* c8 ignore next 3 - Simple getter */
|
|
116
117
|
get offsetsToFetch() {
|
|
117
118
|
return this.#offsetsToFetch;
|
|
118
119
|
}
|
|
120
|
+
/* c8 ignore next 3 - Simple getter */
|
|
119
121
|
get offsetsToCommit() {
|
|
120
122
|
return this.#offsetsToCommit;
|
|
121
123
|
}
|
|
124
|
+
/* c8 ignore next 3 - Simple getter */
|
|
122
125
|
get offsetsCommitted() {
|
|
123
126
|
return this.#offsetsCommitted;
|
|
124
127
|
}
|
|
125
128
|
// TODO: This is deprecated alias, remove in future major version
|
|
129
|
+
/* c8 ignore next 3 - Simple getter */
|
|
126
130
|
get committedOffsets() {
|
|
127
131
|
return this.#offsetsCommitted;
|
|
128
132
|
}
|
|
@@ -4,7 +4,7 @@ import { createDiagnosticContext, producerInitIdempotentChannel, producerSendsCh
|
|
|
4
4
|
import { UserError } from "../../errors.js";
|
|
5
5
|
import { murmur2 } from "../../protocol/murmur2.js";
|
|
6
6
|
import { NumericMap } from "../../utils.js";
|
|
7
|
-
import { Base, kAfterCreate, kCheckNotClosed,
|
|
7
|
+
import { Base, kAfterCreate, kCheckNotClosed, kClosed, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
|
|
8
8
|
import { ensureMetric } from "../metrics.js";
|
|
9
9
|
import { produceOptionsValidator, producerOptionsValidator, sendOptionsValidator } from "./options.js";
|
|
10
10
|
// Don't move this function as being in the same file will enable V8 to remove.
|
|
@@ -311,7 +311,7 @@ export class Producer extends Base {
|
|
|
311
311
|
// since the partition is already set, it should attempt on the new destination
|
|
312
312
|
const hasStaleMetadata = error.findBy('hasStaleMetadata', true);
|
|
313
313
|
if (hasStaleMetadata && repeatOnStaleMetadata) {
|
|
314
|
-
this
|
|
314
|
+
this.clearMetadata();
|
|
315
315
|
this.#performSingleDestinationSend(topics, messages, timeout, acks, autocreateTopics, false, produceOptions, callback);
|
|
316
316
|
return;
|
|
317
317
|
}
|
|
@@ -61,7 +61,7 @@ export class Connection extends EventEmitter {
|
|
|
61
61
|
this.#correlationId = 0;
|
|
62
62
|
this.#nextMessage = 0;
|
|
63
63
|
this.#afterDrainRequests = [];
|
|
64
|
-
this.#requestsQueue = fastq((op, cb) => op(cb), this.#options.maxInflights);
|
|
64
|
+
this.#requestsQueue = fastq((op, cb) => op(cb), this.#options.maxInflights ?? defaultOptions.maxInflights);
|
|
65
65
|
this.#inflightRequests = new Map();
|
|
66
66
|
this.#responseBuffer = new DynamicBuffer();
|
|
67
67
|
this.#responseReader = new Reader(this.#responseBuffer);
|
|
@@ -230,7 +230,7 @@ export class Connection extends EventEmitter {
|
|
|
230
230
|
catch (err) {
|
|
231
231
|
diagnostic.error = err;
|
|
232
232
|
connectionsApiChannel.error.publish(diagnostic);
|
|
233
|
-
|
|
233
|
+
return callback(err, undefined);
|
|
234
234
|
}
|
|
235
235
|
writer.appendFrom(payload).prependLength();
|
|
236
236
|
const request = {
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const name = "@platformatic/kafka";
|
|
2
|
-
export const version = "1.
|
|
2
|
+
export const version = "1.21.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/kafka",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "Modern and performant client for Apache Kafka",
|
|
5
5
|
"homepage": "https://github.com/platformatic/kafka",
|
|
6
6
|
"author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
|