@platformatic/kafka 2.3.0 → 2.4.0-alpha.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.
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
3
3
  import { FetchIsolationLevels, FindCoordinatorKeyTypes } from "../../apis/enumerations.js";
4
4
  import { consumerCommitsChannel, consumerConsumesChannel, consumerFetchesChannel, consumerGroupChannel, consumerHeartbeatChannel, consumerLagChannel, consumerOffsetsChannel, createDiagnosticContext } from "../../diagnostic.js";
5
- import { protocolErrors, UserError } from "../../errors.js";
5
+ import { NetworkError, protocolErrors, UserError } from "../../errors.js";
6
6
  import { INT32_SIZE } from "../../protocol/definitions.js";
7
7
  import { Reader } from "../../protocol/reader.js";
8
8
  import { IS_CONTROL } from "../../protocol/records.js";
@@ -395,6 +395,10 @@ export class Consumer extends Base {
395
395
  options.rebalanceTimeout ??= this[kOptions].rebalanceTimeout;
396
396
  options.heartbeatInterval ??= this[kOptions].heartbeatInterval;
397
397
  options.protocols ??= this[kOptions].protocols;
398
+ const protocolsMetadata = this[kOptions].protocolsMetadata;
399
+ if (!options.protocolsMetadata && protocolsMetadata) {
400
+ options.protocolsMetadata = protocolsMetadata;
401
+ }
398
402
  this.#validateGroupOptions(options);
399
403
  this.#membershipActive = true;
400
404
  this.#joinGroup(options, callback);
@@ -512,10 +516,64 @@ export class Consumer extends Base {
512
516
  retryCallback(error);
513
517
  return;
514
518
  }
515
- const session = this.#fetchSessions.get(node);
519
+ let session = this.#fetchSessions.get(node);
520
+ if (session && session.pool !== pool) {
521
+ session = undefined;
522
+ }
516
523
  const sessionId = session?.id ?? 0;
517
524
  const sessionEpoch = session?.epoch ?? 0;
518
- api(connection, options.maxWaitTime ?? this[kOptions].maxWaitTime, options.minBytes ?? this[kOptions].minBytes, options.maxBytes ?? this[kOptions].maxBytes, isolationLevel, sessionId, sessionEpoch, options.topics, [], this.#clientRack, (error, result) => {
525
+ // The desired state of the session: options.topics always lists all the wanted partitions
526
+ const desired = new Map();
527
+ for (const topic of options.topics) {
528
+ let partitions = desired.get(topic.topicId);
529
+ if (!partitions) {
530
+ partitions = new Map();
531
+ desired.set(topic.topicId, partitions);
532
+ }
533
+ for (const partition of topic.partitions) {
534
+ // Copy the partition since the data is mutated in the error paths
535
+ partitions.set(partition.partition, { ...partition });
536
+ }
537
+ }
538
+ let requestTopics = options.topics;
539
+ const forgottenTopicsData = [];
540
+ if (session) {
541
+ // Only send the partitions which are new or whose state changed: the broker
542
+ // keeps serving the unchanged ones from the session (KIP-227)
543
+ requestTopics = [];
544
+ for (const [topicId, partitions] of desired) {
545
+ const cached = session.partitions.get(topicId);
546
+ const changed = [];
547
+ for (const partition of partitions.values()) {
548
+ const previous = cached?.get(partition.partition);
549
+ if (!previous ||
550
+ previous.fetchOffset !== partition.fetchOffset ||
551
+ previous.currentLeaderEpoch !== partition.currentLeaderEpoch ||
552
+ previous.lastFetchedEpoch !== partition.lastFetchedEpoch ||
553
+ previous.partitionMaxBytes !== partition.partitionMaxBytes) {
554
+ changed.push(partition);
555
+ }
556
+ }
557
+ if (changed.length > 0) {
558
+ requestTopics.push({ topicId, partitions: changed });
559
+ }
560
+ }
561
+ // Ask the broker to remove from the session the partitions which are no
562
+ // longer wanted, otherwise it keeps serving them forever
563
+ for (const [topicId, partitions] of session.partitions) {
564
+ const stillDesired = desired.get(topicId);
565
+ const forgotten = [];
566
+ for (const partition of partitions.keys()) {
567
+ if (!stillDesired?.has(partition)) {
568
+ forgotten.push(partition);
569
+ }
570
+ }
571
+ if (forgotten.length > 0) {
572
+ forgottenTopicsData.push({ topic: topicId, partitions: forgotten });
573
+ }
574
+ }
575
+ }
576
+ api(connection, options.maxWaitTime ?? this[kOptions].maxWaitTime, options.minBytes ?? this[kOptions].minBytes, options.maxBytes ?? this[kOptions].maxBytes, isolationLevel, sessionId, sessionEpoch, requestTopics, forgottenTopicsData, this.#clientRack, (error, result) => {
519
577
  if (error) {
520
578
  this.#clearPreferredReadReplicas(options.topics, topicIds);
521
579
  const genericError = error;
@@ -537,7 +595,7 @@ export class Consumer extends Base {
537
595
  }
538
596
  else {
539
597
  this.#updatePreferredReadReplicas(metadata, topicIds, result);
540
- this.#updateFetchSession(node, sessionEpoch, result.sessionId);
598
+ this.#updateFetchSession(node, pool, sessionEpoch, result.sessionId, desired);
541
599
  }
542
600
  retryCallback(error, result);
543
601
  });
@@ -556,12 +614,19 @@ export class Consumer extends Base {
556
614
  callback(error, response);
557
615
  }, 0);
558
616
  }
559
- #updateFetchSession(node, sentEpoch, responseSessionId) {
617
+ #updateFetchSession(node, pool, sentEpoch, responseSessionId, desired) {
560
618
  if (!responseSessionId) {
561
619
  this.#fetchSessions.delete(node);
562
620
  return;
563
621
  }
564
- this.#fetchSessions.set(node, { id: responseSessionId, epoch: nextFetchEpoch(sentEpoch) });
622
+ // After a successful fetch the broker session contains exactly the desired partitions:
623
+ // unchanged ones already matched it, changed ones were sent and removed ones were forgotten
624
+ this.#fetchSessions.set(node, {
625
+ id: responseSessionId,
626
+ epoch: nextFetchEpoch(sentEpoch),
627
+ pool,
628
+ partitions: desired
629
+ });
565
630
  }
566
631
  #commit(options, callback, rejoinAttempts = 0) {
567
632
  this.#performGroupOperation('commit', (connection, groupCallback) => {
@@ -1085,45 +1150,20 @@ export class Consumer extends Base {
1085
1150
  return;
1086
1151
  }
1087
1152
  this.#cancelHeartbeat();
1088
- const protocols = [];
1089
- for (const protocol of options.protocols) {
1090
- protocols.push({
1091
- name: protocol.name,
1092
- metadata: this.#encodeProtocolSubscriptionMetadata(protocol, this.topics.current)
1093
- });
1094
- }
1095
- this.#performDeduplicateGroupOperaton('joinGroup', (connection, groupCallback) => {
1096
- this[kGetApi]('JoinGroup', (error, api) => {
1097
- if (error) {
1098
- groupCallback(error);
1099
- return;
1100
- }
1101
- api(connection, this.groupId, options.sessionTimeout, options.rebalanceTimeout, this.memberId ?? '', this.groupInstanceId, 'consumer', protocols, '', groupCallback);
1102
- });
1103
- }, (error, response) => {
1104
- if (!this.#membershipActive) {
1105
- callback(null);
1106
- return;
1107
- }
1153
+ this.#createJoinGroupProtocols(options, (error, protocols) => {
1108
1154
  if (error) {
1109
- if (this.#getRejoinError(error)) {
1110
- this.#performJoinGroup(options, callback);
1111
- return;
1112
- }
1113
1155
  callback(error);
1114
1156
  return;
1115
1157
  }
1116
- // This is for Azure Event Hubs compatibility, which does not respond with an error on the first join
1117
- this.memberId = response.memberId;
1118
- this.generationId = response.generationId;
1119
- this.#isLeader = response.leader === this.memberId;
1120
- this.#protocol = response.protocolName;
1121
- this.#members = new Map();
1122
- for (const member of response.members) {
1123
- this.#members.set(member.memberId, this.#decodeProtocolSubscriptionMetadata(member.memberId, member.metadata));
1124
- }
1125
- // Send a syncGroup request
1126
- this.#syncGroup(options.partitionAssigner, (error, response) => {
1158
+ this.#performDeduplicateGroupOperaton('joinGroup', (connection, groupCallback) => {
1159
+ this[kGetApi]('JoinGroup', (error, api) => {
1160
+ if (error) {
1161
+ groupCallback(error);
1162
+ return;
1163
+ }
1164
+ api(connection, this.groupId, options.sessionTimeout, options.rebalanceTimeout, this.memberId ?? '', this.groupInstanceId, 'consumer', protocols, '', groupCallback);
1165
+ });
1166
+ }, (error, response) => {
1127
1167
  if (!this.#membershipActive) {
1128
1168
  callback(null);
1129
1169
  return;
@@ -1136,20 +1176,44 @@ export class Consumer extends Base {
1136
1176
  callback(error);
1137
1177
  return;
1138
1178
  }
1139
- this.assignments = response;
1140
- this.#syncPreferredReadReplicas();
1141
- this.#cancelHeartbeat();
1142
- this.#heartbeatInterval = setTimeout(() => {
1143
- this.#heartbeat(options);
1144
- }, options.heartbeatInterval);
1145
- this.emitWithDebug('consumer', 'group:join', {
1146
- groupId: this.groupId,
1147
- memberId: this.memberId,
1148
- generationId: this.generationId,
1149
- isLeader: this.#isLeader,
1150
- assignments: this.assignments
1179
+ // This is for Azure Event Hubs compatibility, which does not respond with an error on the first join
1180
+ this.memberId = response.memberId;
1181
+ this.generationId = response.generationId;
1182
+ this.#isLeader = response.leader === this.memberId;
1183
+ this.#protocol = response.protocolName;
1184
+ this.#members = new Map();
1185
+ for (const member of response.members) {
1186
+ this.#members.set(member.memberId, this.#decodeProtocolSubscriptionMetadata(member.memberId, member.metadata));
1187
+ }
1188
+ // Send a syncGroup request
1189
+ this.#syncGroup(options.partitionAssigner, (error, response) => {
1190
+ if (!this.#membershipActive) {
1191
+ callback(null);
1192
+ return;
1193
+ }
1194
+ if (error) {
1195
+ if (this.#getRejoinError(error)) {
1196
+ this.#performJoinGroup(options, callback);
1197
+ return;
1198
+ }
1199
+ callback(error);
1200
+ return;
1201
+ }
1202
+ this.assignments = response;
1203
+ this.#syncPreferredReadReplicas();
1204
+ this.#cancelHeartbeat();
1205
+ this.#heartbeatInterval = setTimeout(() => {
1206
+ this.#heartbeat(options);
1207
+ }, options.heartbeatInterval);
1208
+ this.emitWithDebug('consumer', 'group:join', {
1209
+ groupId: this.groupId,
1210
+ memberId: this.memberId,
1211
+ generationId: this.generationId,
1212
+ isLeader: this.#isLeader,
1213
+ assignments: this.assignments
1214
+ });
1215
+ callback(null, this.memberId);
1151
1216
  });
1152
- callback(null, this.memberId);
1153
1217
  });
1154
1218
  });
1155
1219
  }
@@ -1230,11 +1294,17 @@ export class Consumer extends Base {
1230
1294
  topicSubscriptions.push(subscription);
1231
1295
  }
1232
1296
  }
1233
- this[kMetadata]({ topics: Array.from(topicsSubscriptions.keys()) }, (error, metadata) => {
1297
+ const subscribedTopics = Array.from(topicsSubscriptions.keys());
1298
+ this[kMetadata]({ topics: subscribedTopics }, (error, metadata) => {
1234
1299
  if (error) {
1235
1300
  callback(this.#handleError(error));
1236
1301
  return;
1237
1302
  }
1303
+ const missingTopic = subscribedTopics.find(topic => !metadata.topics.has(topic));
1304
+ if (missingTopic) {
1305
+ callback(this.#handleError(new NetworkError(`Cannot compute group assignments: metadata for subscribed topic "${missingTopic}" is not available in the current cluster metadata.`, { canRetry: true, hasStaleMetadata: true })));
1306
+ return;
1307
+ }
1238
1308
  this.#performSyncGroup(partitionsAssigner, this.#createAssignments(partitionsAssigner, metadata), callback);
1239
1309
  });
1240
1310
  return;
@@ -1303,6 +1373,61 @@ export class Consumer extends Base {
1303
1373
  throw new UserError(this[kFormatValidationErrors](validator, '/options'));
1304
1374
  }
1305
1375
  }
1376
+ #createJoinGroupProtocols(options, callback) {
1377
+ if (typeof options.protocolsMetadata !== 'function') {
1378
+ callback(null, options.protocols.map(protocol => ({
1379
+ name: protocol.name,
1380
+ metadata: this.#encodeProtocolSubscriptionMetadata(protocol, this.topics.current)
1381
+ })));
1382
+ return;
1383
+ }
1384
+ this[kMetadata]({ topics: this.topics.current, forceUpdate: true }, (error, metadata) => {
1385
+ if (error) {
1386
+ callback(this.#handleError(error));
1387
+ return;
1388
+ }
1389
+ this.#resolveProtocolsMetadata(options.protocolsMetadata, options.protocols, this.topics.current, metadata, (error, userData) => {
1390
+ if (error) {
1391
+ callback(error);
1392
+ return;
1393
+ }
1394
+ else if (!Buffer.isBuffer(userData)) {
1395
+ callback(new UserError('protocolsMetadata must resolve to a Buffer.'));
1396
+ return;
1397
+ }
1398
+ callback(null, options.protocols.map(protocol => ({
1399
+ name: protocol.name,
1400
+ metadata: this.#encodeProtocolSubscriptionMetadata({ ...protocol, metadata: userData }, this.topics.current)
1401
+ })));
1402
+ });
1403
+ });
1404
+ }
1405
+ #resolveProtocolsMetadata(protocolsMetadata, protocols, topics, metadata, callback) {
1406
+ let settled = false;
1407
+ function resolveCallback(error, userData) {
1408
+ if (settled) {
1409
+ return;
1410
+ }
1411
+ settled = true;
1412
+ callback(error, userData);
1413
+ }
1414
+ let result;
1415
+ try {
1416
+ result = protocolsMetadata(protocols, topics, metadata, resolveCallback);
1417
+ if (typeof result !== 'undefined') {
1418
+ if (typeof result.then === 'function') {
1419
+ ;
1420
+ result.then(userData => process.nextTick(resolveCallback, null, userData), error => process.nextTick(resolveCallback, error));
1421
+ }
1422
+ else {
1423
+ resolveCallback(null, result);
1424
+ }
1425
+ }
1426
+ }
1427
+ catch (error) {
1428
+ resolveCallback(error);
1429
+ }
1430
+ }
1306
1431
  /*
1307
1432
  The following two methods follow:
1308
1433
  https://github.com/apache/kafka/blob/trunk/clients/src/main/resources/common/message/ConsumerProtocolSubscription.json
@@ -553,6 +553,11 @@ export class MessagesStream extends Readable {
553
553
  const topic = topicIds.get(topicResponse.topicId);
554
554
  for (const { records: recordsBatches, partitionIndex: partition } of topicResponse.partitions) {
555
555
  const key = partitionKey(topic, partition);
556
+ // The broker can return partitions which were not requested when they linger
557
+ // in an incremental fetch session - their data is stale and must not rewind offsets
558
+ if (!requestedOffsets.has(key)) {
559
+ continue;
560
+ }
556
561
  if (!recordsBatches) {
557
562
  continue;
558
563
  }
@@ -860,7 +865,13 @@ export class MessagesStream extends Readable {
860
865
  const requests = [];
861
866
  // Create the pre-deserialization requests
862
867
  for (const topicResponse of response.responses) {
868
+ const topic = topicIds.get(topicResponse.topicId);
863
869
  for (const { records: recordsBatches, partitionIndex: partition } of topicResponse.partitions) {
870
+ // The broker can return partitions which were not requested when they linger
871
+ // in an incremental fetch session - do not run hooks on their stale records
872
+ if (!requestedOffsets.has(partitionKey(topic, partition))) {
873
+ continue;
874
+ }
864
875
  /* c8 ignore next 3 - Hard to test */
865
876
  if (!recordsBatches) {
866
877
  continue;
@@ -872,7 +883,7 @@ export class MessagesStream extends Readable {
872
883
  continue;
873
884
  }
874
885
  for (const message of batch.records) {
875
- message.topic = topicIds.get(topicResponse.topicId);
886
+ message.topic = topic;
876
887
  message.partition = partition;
877
888
  requests.push([message.key, 'key', message]);
878
889
  requests.push([message.value, 'value', message]);
@@ -53,6 +53,9 @@ export declare const groupOptionsProperties: {
53
53
  };
54
54
  };
55
55
  };
56
+ protocolsMetadata: {
57
+ function: boolean;
58
+ };
56
59
  partitionAssigner: {
57
60
  function: boolean;
58
61
  };
@@ -195,6 +198,9 @@ export declare const groupOptionsSchema: {
195
198
  };
196
199
  };
197
200
  };
201
+ protocolsMetadata: {
202
+ function: boolean;
203
+ };
198
204
  partitionAssigner: {
199
205
  function: boolean;
200
206
  };
@@ -315,6 +321,9 @@ export declare const consumeOptionsSchema: {
315
321
  };
316
322
  };
317
323
  };
324
+ protocolsMetadata: {
325
+ function: boolean;
326
+ };
318
327
  partitionAssigner: {
319
328
  function: boolean;
320
329
  };
@@ -481,6 +490,9 @@ export declare const consumerOptionsSchema: {
481
490
  };
482
491
  };
483
492
  };
493
+ protocolsMetadata: {
494
+ function: boolean;
495
+ };
484
496
  partitionAssigner: {
485
497
  function: boolean;
486
498
  };
@@ -608,6 +620,9 @@ export declare const fetchOptionsSchema: {
608
620
  };
609
621
  };
610
622
  };
623
+ protocolsMetadata: {
624
+ function: boolean;
625
+ };
611
626
  partitionAssigner: {
612
627
  function: boolean;
613
628
  };
@@ -25,6 +25,7 @@ export const groupOptionsProperties = {
25
25
  }
26
26
  }
27
27
  },
28
+ protocolsMetadata: { function: true },
28
29
  partitionAssigner: { function: true }
29
30
  };
30
31
  export const groupOptionsAdditionalValidations = {
@@ -1,3 +1,4 @@
1
+ import { type Callback } from '../../apis/definitions.ts';
1
2
  import { type FetchRequestTopic } from '../../apis/consumer/fetch-v17.ts';
2
3
  import { type GroupProtocols } from '../../apis/enumerations.ts';
3
4
  import { type ConnectionPool } from '../../network/connection-pool.ts';
@@ -33,6 +34,7 @@ export interface PreferredReadReplica {
33
34
  }
34
35
  export type CorruptedMessageHandler = (record: KafkaRecord, topic: string, partition: number, firstTimestamp: bigint, firstOffset: bigint, commit: Message['commit']) => boolean;
35
36
  export type GroupPartitionsAssigner = (current: string, members: Map<string, ExtendedGroupProtocolSubscription>, topics: Set<string>, metadata: ClusterMetadata) => GroupPartitionsAssignments[];
37
+ export type GroupProtocolsMetadataCallback = (protocols: GroupProtocolSubscription[], topics: string[], metadata: ClusterMetadata, callback: Callback<Buffer>) => Buffer | Promise<Buffer> | undefined;
36
38
  export declare const MessagesStreamModes: {
37
39
  readonly LATEST: "latest";
38
40
  readonly EARLIEST: "earliest";
@@ -57,6 +59,7 @@ export interface GroupOptions {
57
59
  rebalanceTimeout?: number;
58
60
  heartbeatInterval?: number;
59
61
  protocols?: GroupProtocolSubscription[];
62
+ protocolsMetadata?: GroupProtocolsMetadataCallback;
60
63
  partitionAssigner?: GroupPartitionsAssigner;
61
64
  assignmentUserData?: Buffer;
62
65
  }
@@ -342,7 +342,7 @@ export class Producer extends Base {
342
342
  }
343
343
  const transactionalId = this.#transaction.id;
344
344
  this[kPerformDeduplicated]('findCoordinator', deduplicateCallback => {
345
- this[kPerformWithRetry]('findCoordinator', retryCallback => {
345
+ this[kPerformWithRetry]('findCoordinator', (retryCallback, attempt) => {
346
346
  this[kGetBootstrapConnection]((error, connection) => {
347
347
  if (error) {
348
348
  retryCallback(error);
@@ -355,7 +355,7 @@ export class Producer extends Base {
355
355
  }
356
356
  api(connection, FindCoordinatorKeyTypes.TRANSACTION, [transactionalId], retryCallback);
357
357
  });
358
- });
358
+ }, attempt);
359
359
  }, (error, response) => {
360
360
  if (error) {
361
361
  deduplicateCallback(error);
@@ -531,11 +531,8 @@ export class Producer extends Base {
531
531
  #initIdempotentProducer(options, callback) {
532
532
  const transactionalId = this.#transaction?.id;
533
533
  this[kPerformDeduplicated]('initProducerId', deduplicateCallback => {
534
- this[kPerformWithRetry]('initProducerId', retryCallback => {
535
- const connector = transactionalId
536
- ? this.#getCoordinatorConnection.bind(this)
537
- : this[kGetBootstrapConnection].bind(this);
538
- connector((error, connection) => {
534
+ this[kPerformWithRetry]('initProducerId', (retryCallback, attempt) => {
535
+ const onConnection = (error, connection) => {
539
536
  if (error) {
540
537
  retryCallback(error);
541
538
  return;
@@ -549,7 +546,16 @@ export class Producer extends Base {
549
546
  retryCallback(this.#handleError(error), response);
550
547
  });
551
548
  });
552
- });
549
+ };
550
+ // The transactional path fails over inside #getCoordinatorConnection (via
551
+ // #findCoordinator -> kGetBootstrapConnection). The idempotent path must thread
552
+ // `attempt` so kGetBootstrapConnection rotates past a reachable-but-hung broker.
553
+ if (transactionalId) {
554
+ this.#getCoordinatorConnection(onConnection);
555
+ }
556
+ else {
557
+ this[kGetBootstrapConnection](onConnection, attempt);
558
+ }
553
559
  }, (error, response) => {
554
560
  if (error) {
555
561
  this.#handleFencingError(error);
@@ -1,6 +1,6 @@
1
1
  import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../apis/callbacks.js";
2
2
  import { connectionsPoolGetsChannel, createDiagnosticContext, notifyCreation } from "../diagnostic.js";
3
- import { MultipleErrors } from "../errors.js";
3
+ import { MultipleErrors, NetworkError } from "../errors.js";
4
4
  import { TypedEventEmitter } from "../events.js";
5
5
  import { Connection, ConnectionStatuses } from "./connection.js";
6
6
  let currentInstance = 0;
@@ -95,6 +95,14 @@ export class ConnectionPool extends TypedEventEmitter {
95
95
  callback(new Error('Connection pool is closed.'));
96
96
  return;
97
97
  }
98
+ if (broker == null || broker.host == null || broker.port == null) {
99
+ callback(new NetworkError('Cannot get a connection: the broker is not available in the current cluster metadata.', {
100
+ canRetry: true,
101
+ hasStaleMetadata: true,
102
+ broker
103
+ }));
104
+ return;
105
+ }
98
106
  const key = `${broker.host}:${broker.port}`;
99
107
  const existing = this.#connections.get(key);
100
108
  if (existing) {
@@ -53,6 +53,9 @@ export declare const groupOptionsProperties: {
53
53
  };
54
54
  };
55
55
  };
56
+ protocolsMetadata: {
57
+ function: boolean;
58
+ };
56
59
  partitionAssigner: {
57
60
  function: boolean;
58
61
  };
@@ -195,6 +198,9 @@ export declare const groupOptionsSchema: {
195
198
  };
196
199
  };
197
200
  };
201
+ protocolsMetadata: {
202
+ function: boolean;
203
+ };
198
204
  partitionAssigner: {
199
205
  function: boolean;
200
206
  };
@@ -315,6 +321,9 @@ export declare const consumeOptionsSchema: {
315
321
  };
316
322
  };
317
323
  };
324
+ protocolsMetadata: {
325
+ function: boolean;
326
+ };
318
327
  partitionAssigner: {
319
328
  function: boolean;
320
329
  };
@@ -481,6 +490,9 @@ export declare const consumerOptionsSchema: {
481
490
  };
482
491
  };
483
492
  };
493
+ protocolsMetadata: {
494
+ function: boolean;
495
+ };
484
496
  partitionAssigner: {
485
497
  function: boolean;
486
498
  };
@@ -608,6 +620,9 @@ export declare const fetchOptionsSchema: {
608
620
  };
609
621
  };
610
622
  };
623
+ protocolsMetadata: {
624
+ function: boolean;
625
+ };
611
626
  partitionAssigner: {
612
627
  function: boolean;
613
628
  };
@@ -1,3 +1,4 @@
1
+ import { type Callback } from "../../apis/definitions";
1
2
  import { type FetchRequestTopic } from "../../apis/consumer/fetch-v17";
2
3
  import { type GroupProtocols } from "../../apis/enumerations";
3
4
  import { type ConnectionPool } from "../../network/connection-pool";
@@ -33,6 +34,7 @@ export interface PreferredReadReplica {
33
34
  }
34
35
  export type CorruptedMessageHandler = (record: KafkaRecord, topic: string, partition: number, firstTimestamp: bigint, firstOffset: bigint, commit: Message['commit']) => boolean;
35
36
  export type GroupPartitionsAssigner = (current: string, members: Map<string, ExtendedGroupProtocolSubscription>, topics: Set<string>, metadata: ClusterMetadata) => GroupPartitionsAssignments[];
37
+ export type GroupProtocolsMetadataCallback = (protocols: GroupProtocolSubscription[], topics: string[], metadata: ClusterMetadata, callback: Callback<Buffer>) => Buffer | Promise<Buffer> | undefined;
36
38
  export declare const MessagesStreamModes: {
37
39
  readonly LATEST: "latest";
38
40
  readonly EARLIEST: "earliest";
@@ -57,6 +59,7 @@ export interface GroupOptions {
57
59
  rebalanceTimeout?: number;
58
60
  heartbeatInterval?: number;
59
61
  protocols?: GroupProtocolSubscription[];
62
+ protocolsMetadata?: GroupProtocolsMetadataCallback;
60
63
  partitionAssigner?: GroupPartitionsAssigner;
61
64
  assignmentUserData?: Buffer;
62
65
  }
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = "@platformatic/kafka";
2
- export const version = "2.3.0";
2
+ export const version = "2.4.0-alpha.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/kafka",
3
- "version": "2.3.0",
3
+ "version": "2.4.0-alpha.1",
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)",