@platformatic/kafka 1.6.0 → 1.7.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/README.md CHANGED
@@ -224,6 +224,10 @@ await consumer.close()
224
224
  await producer.close()
225
225
  ```
226
226
 
227
+ ### Avro
228
+
229
+ An example on how to use [Avro]() for (de)serialisation can be found in the [examples](./examples) folder.
230
+
227
231
  ### Error Handling
228
232
 
229
233
  `@platformatic/kafka` defines its hierarchy of errors.
@@ -1,10 +1,12 @@
1
1
  import { type CallbackWithPromise } from '../../apis/callbacks.ts';
2
2
  import { type Callback } from '../../apis/definitions.ts';
3
3
  import { Base } from '../base/base.ts';
4
- import { type AdminOptions, type CreatedTopic, type CreateTopicsOptions, type DeleteGroupsOptions, type DeleteTopicsOptions, type DescribeGroupsOptions, type Group, type GroupBase, type ListGroupsOptions } from './types.ts';
4
+ import { type AdminOptions, type CreatedTopic, type CreateTopicsOptions, type DeleteGroupsOptions, type DeleteTopicsOptions, type DescribeGroupsOptions, type Group, type GroupBase, type ListGroupsOptions, type ListTopicsOptions } from './types.ts';
5
5
  export declare class Admin extends Base<AdminOptions> {
6
6
  #private;
7
7
  constructor(options: AdminOptions);
8
+ listTopics(options: ListTopicsOptions, callback: Callback<string[]>): void;
9
+ listTopics(options?: ListTopicsOptions): Promise<string[]>;
8
10
  createTopics(options: CreateTopicsOptions, callback: Callback<CreatedTopic[]>): void;
9
11
  createTopics(options: CreateTopicsOptions): Promise<CreatedTopic[]>;
10
12
  deleteTopics(options: DeleteTopicsOptions, callback: CallbackWithPromise<void>): void;
@@ -3,12 +3,30 @@ import { FindCoordinatorKeyTypes } from "../../apis/enumerations.js";
3
3
  import { adminGroupsChannel, adminTopicsChannel, createDiagnosticContext } from "../../diagnostic.js";
4
4
  import { Reader } from "../../protocol/reader.js";
5
5
  import { Base, kAfterCreate, kCheckNotClosed, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js";
6
- import { createTopicsOptionsValidator, deleteGroupsOptionsValidator, deleteTopicsOptionsValidator, describeGroupsOptionsValidator, listGroupsOptionsValidator } from "./options.js";
6
+ import { createTopicsOptionsValidator, deleteGroupsOptionsValidator, deleteTopicsOptionsValidator, describeGroupsOptionsValidator, listGroupsOptionsValidator, listTopicsOptionsValidator } from "./options.js";
7
7
  export class Admin extends Base {
8
8
  constructor(options) {
9
9
  super(options);
10
10
  this[kAfterCreate]('admin');
11
11
  }
12
+ listTopics(options, callback) {
13
+ if (!callback) {
14
+ callback = createPromisifiedCallback();
15
+ }
16
+ if (this[kCheckNotClosed](callback)) {
17
+ return callback[kCallbackPromise];
18
+ }
19
+ if (!options) {
20
+ options = {};
21
+ }
22
+ const validationError = this[kValidateOptions](options, listTopicsOptionsValidator, '/options', false);
23
+ if (validationError) {
24
+ callback(validationError, undefined);
25
+ return callback[kCallbackPromise];
26
+ }
27
+ adminTopicsChannel.traceCallback(this.#listTopics, 1, createDiagnosticContext({ client: this, operation: 'listTopics', options }), this, options, callback);
28
+ return callback[kCallbackPromise];
29
+ }
12
30
  createTopics(options, callback) {
13
31
  if (!callback) {
14
32
  callback = createPromisifiedCallback();
@@ -88,6 +106,40 @@ export class Admin extends Base {
88
106
  adminGroupsChannel.traceCallback(this.#deleteGroups, 1, createDiagnosticContext({ client: this, operation: 'deleteGroups', options }), this, options, callback);
89
107
  return callback[kCallbackPromise];
90
108
  }
109
+ #listTopics(options, callback) {
110
+ const includeInternals = options.includeInternals ?? false;
111
+ this[kPerformDeduplicated]('metadata', deduplicateCallback => {
112
+ this[kPerformWithRetry]('metadata', retryCallback => {
113
+ this[kGetBootstrapConnection]((error, connection) => {
114
+ if (error) {
115
+ retryCallback(error, undefined);
116
+ return;
117
+ }
118
+ this[kGetApi]('Metadata', (error, api) => {
119
+ if (error) {
120
+ retryCallback(error, undefined);
121
+ return;
122
+ }
123
+ api(connection, null, false, false, retryCallback);
124
+ });
125
+ });
126
+ }, (error, metadata) => {
127
+ if (error) {
128
+ deduplicateCallback(error, undefined);
129
+ return;
130
+ }
131
+ const topics = new Set();
132
+ for (const { name, isInternal } of metadata.topics) {
133
+ /* c8 ignore next 3 - Sometimes internal topics might be returned by Kafka */
134
+ if (isInternal && !includeInternals) {
135
+ continue;
136
+ }
137
+ topics.add(name);
138
+ }
139
+ deduplicateCallback(null, Array.from(topics).sort());
140
+ }, 0);
141
+ }, callback);
142
+ }
91
143
  #createTopics(options, callback) {
92
144
  const numPartitions = options.partitions ?? 1;
93
145
  const replicationFactor = options.replicas ?? 1;
@@ -50,6 +50,16 @@ export declare const createTopicOptionsSchema: {
50
50
  required: string[];
51
51
  additionalProperties: boolean;
52
52
  };
53
+ export declare const listTopicOptionsSchema: {
54
+ type: string;
55
+ properties: {
56
+ includeInternals: {
57
+ type: string;
58
+ default: boolean;
59
+ };
60
+ };
61
+ additionalProperties: boolean;
62
+ };
53
63
  export declare const deleteTopicOptionsSchema: {
54
64
  type: string;
55
65
  properties: {
@@ -71,8 +81,10 @@ export declare const listGroupsOptionsSchema: {
71
81
  type: string;
72
82
  items: {
73
83
  type: string;
74
- enum: readonly ["PREPARING_REBALANCE", "COMPLETING_REBALANCE", "STABLE", "DEAD", "EMPTY"];
75
- errorMessage: string;
84
+ enumeration: {
85
+ allowed: readonly ["PREPARING_REBALANCE", "COMPLETING_REBALANCE", "STABLE", "DEAD", "EMPTY"];
86
+ errorMessage: string;
87
+ };
76
88
  };
77
89
  minItems: number;
78
90
  };
@@ -123,6 +135,7 @@ export declare const deleteGroupsOptionsSchema: {
123
135
  export declare const createTopicsOptionsValidator: import("ajv").ValidateFunction<{
124
136
  [x: string]: {};
125
137
  }>;
138
+ export declare const listTopicsOptionsValidator: import("ajv").ValidateFunction<unknown>;
126
139
  export declare const deleteTopicsOptionsValidator: import("ajv").ValidateFunction<{
127
140
  [x: string]: {};
128
141
  }>;
@@ -31,6 +31,13 @@ export const createTopicOptionsSchema = {
31
31
  required: ['topics'],
32
32
  additionalProperties: false
33
33
  };
34
+ export const listTopicOptionsSchema = {
35
+ type: 'object',
36
+ properties: {
37
+ includeInternals: { type: 'boolean', default: false }
38
+ },
39
+ additionalProperties: false
40
+ };
34
41
  export const deleteTopicOptionsSchema = {
35
42
  type: 'object',
36
43
  properties: {
@@ -46,8 +53,10 @@ export const listGroupsOptionsSchema = {
46
53
  type: 'array',
47
54
  items: {
48
55
  type: 'string',
49
- enum: ConsumerGroupStates,
50
- errorMessage: listErrorMessage(ConsumerGroupStates)
56
+ enumeration: {
57
+ allowed: ConsumerGroupStates,
58
+ errorMessage: listErrorMessage(ConsumerGroupStates)
59
+ }
51
60
  },
52
61
  minItems: 0
53
62
  },
@@ -75,6 +84,7 @@ export const deleteGroupsOptionsSchema = {
75
84
  additionalProperties: false
76
85
  };
77
86
  export const createTopicsOptionsValidator = ajv.compile(createTopicOptionsSchema);
87
+ export const listTopicsOptionsValidator = ajv.compile(listTopicOptionsSchema);
78
88
  export const deleteTopicsOptionsValidator = ajv.compile(deleteTopicOptionsSchema);
79
89
  export const listGroupsOptionsValidator = ajv.compile(listGroupsOptionsSchema);
80
90
  export const describeGroupsOptionsValidator = ajv.compile(describeGroupsOptionsSchema);
@@ -40,6 +40,9 @@ export interface CreateTopicsOptions {
40
40
  replicas?: number;
41
41
  assignments?: BrokerAssignment[];
42
42
  }
43
+ export interface ListTopicsOptions {
44
+ includeInternals?: boolean;
45
+ }
43
46
  export interface DeleteTopicsOptions {
44
47
  topics: string[];
45
48
  }
@@ -1,3 +1,3 @@
1
- export { Base } from './base.ts';
1
+ export { Base, kCheckNotClosed, kClearMetadata, kGetApi, kGetBootstrapConnection, kGetConnection, kListApis, kMetadata, kOptions, kParseBroker, 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 } from "./base.js";
1
+ export { Base, kCheckNotClosed, kClearMetadata, kGetApi, kGetBootstrapConnection, kGetConnection, kListApis, kMetadata, kOptions, kParseBroker, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "./base.js";
2
2
  export * from "./options.js";
3
3
  export * from "./types.js";
@@ -78,6 +78,11 @@ export declare const baseOptionsSchema: {
78
78
  type: string;
79
79
  additionalProperties: boolean;
80
80
  };
81
+ tlsServerName: {
82
+ oneOf: {
83
+ type: string;
84
+ }[];
85
+ };
81
86
  sasl: {
82
87
  type: string;
83
88
  properties: {
@@ -33,6 +33,7 @@ export const baseOptionsSchema = {
33
33
  retryDelay: { type: 'number', minimum: 0 },
34
34
  maxInflights: { type: 'number', minimum: 0 },
35
35
  tls: { type: 'object', additionalProperties: true }, // No validation as they come from Node.js
36
+ tlsServerName: { oneOf: [{ type: 'boolean' }, { type: 'string' }] },
36
37
  sasl: {
37
38
  type: 'object',
38
39
  properties: {
@@ -4,7 +4,7 @@ import { type ConnectionPool } from '../../network/connection-pool.ts';
4
4
  import { Base, kFetchConnections } from '../base/base.ts';
5
5
  import { MessagesStream } from './messages-stream.ts';
6
6
  import { TopicsMap } from './topics-map.ts';
7
- import { type CommitOptions, type ConsumeOptions, type ConsumerOptions, type FetchOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets } from './types.ts';
7
+ import { type CommitOptions, type ConsumeOptions, type ConsumerOptions, type FetchOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets, type OffsetsWithTimestamps } from './types.ts';
8
8
  export declare class Consumer<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends Base<ConsumerOptions<Key, Value, HeaderKey, HeaderValue>> {
9
9
  #private;
10
10
  groupId: string;
@@ -25,6 +25,8 @@ export declare class Consumer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
25
25
  commit(options: CommitOptions): Promise<void>;
26
26
  listOffsets(options: ListOffsetsOptions, callback: CallbackWithPromise<Offsets>): void;
27
27
  listOffsets(options: ListOffsetsOptions): Promise<Offsets>;
28
+ listOffsetsWithTimestamps(options: ListOffsetsOptions, callback: CallbackWithPromise<OffsetsWithTimestamps>): void;
29
+ listOffsetsWithTimestamps(options: ListOffsetsOptions): Promise<OffsetsWithTimestamps>;
28
30
  listCommittedOffsets(options: ListCommitsOptions, callback: CallbackWithPromise<Offsets>): void;
29
31
  listCommittedOffsets(options: ListCommitsOptions): Promise<Offsets>;
30
32
  findGroupCoordinator(callback: CallbackWithPromise<number>): void;
@@ -9,6 +9,7 @@ import { defaultBaseOptions } from "../base/options.js";
9
9
  import { ensureMetric } from "../metrics.js";
10
10
  import { MessagesStream } from "./messages-stream.js";
11
11
  import { commitOptionsValidator, consumeOptionsValidator, consumerOptionsValidator, defaultConsumerOptions, fetchOptionsValidator, groupIdAndOptionsValidator, groupOptionsValidator, listCommitsOptionsValidator, listOffsetsOptionsValidator } from "./options.js";
12
+ import { roundRobinAssigner } from "./partitions-assigners.js";
12
13
  import { TopicsMap } from "./topics-map.js";
13
14
  export class Consumer extends Base {
14
15
  groupId;
@@ -23,6 +24,7 @@ export class Consumer extends Base {
23
24
  #coordinatorId;
24
25
  #heartbeatInterval;
25
26
  #streams;
27
+ #partitionsAssigner;
26
28
  /*
27
29
  The following requests are blocking in Kafka:
28
30
 
@@ -56,6 +58,7 @@ export class Consumer extends Base {
56
58
  this.#coordinatorId = null;
57
59
  this.#heartbeatInterval = null;
58
60
  this.#streams = new Set();
61
+ this.#partitionsAssigner = this[kOptions].partitionAssigner ?? roundRobinAssigner;
59
62
  this.#validateGroupOptions(this[kOptions], groupIdAndOptionsValidator);
60
63
  // Initialize connection pool
61
64
  this[kFetchConnections] = this[kCreateConnectionPool]();
@@ -176,7 +179,22 @@ export class Consumer extends Base {
176
179
  callback(validationError, undefined);
177
180
  return callback[kCallbackPromise];
178
181
  }
179
- consumerOffsetsChannel.traceCallback(this.#listOffsets, 1, createDiagnosticContext({ client: this, operation: 'listOffsets', options }), this, options, callback);
182
+ consumerOffsetsChannel.traceCallback(this.#listOffsets, 2, createDiagnosticContext({ client: this, operation: 'listOffsets', options }), this, false, options, callback);
183
+ return callback[kCallbackPromise];
184
+ }
185
+ listOffsetsWithTimestamps(options, callback) {
186
+ if (!callback) {
187
+ callback = createPromisifiedCallback();
188
+ }
189
+ if (this[kCheckNotClosed](callback)) {
190
+ return callback[kCallbackPromise];
191
+ }
192
+ const validationError = this[kValidateOptions](options, listOffsetsOptionsValidator, '/options', false);
193
+ if (validationError) {
194
+ callback(validationError, undefined);
195
+ return callback[kCallbackPromise];
196
+ }
197
+ consumerOffsetsChannel.traceCallback(this.#listOffsets, 2, createDiagnosticContext({ client: this, operation: 'listOffsets', options }), this, true, options, callback);
180
198
  return callback[kCallbackPromise];
181
199
  }
182
200
  listCommittedOffsets(options, callback) {
@@ -309,7 +327,7 @@ export class Consumer extends Base {
309
327
  callback(error);
310
328
  });
311
329
  }
312
- #listOffsets(options, callback) {
330
+ #listOffsets(withTimestamps, options, callback) {
313
331
  this[kMetadata]({ topics: options.topics }, (error, metadata) => {
314
332
  if (error) {
315
333
  callback(error, undefined);
@@ -318,7 +336,12 @@ export class Consumer extends Base {
318
336
  const requests = new Map();
319
337
  for (const name of options.topics) {
320
338
  const topic = metadata.topics.get(name);
339
+ const toInclude = options.partitions?.[name] ?? [];
340
+ const hasPartitionsFilter = toInclude.length > 0;
321
341
  for (let i = 0; i < topic.partitionsCount; i++) {
342
+ if (hasPartitionsFilter && !toInclude.includes(i)) {
343
+ continue;
344
+ }
322
345
  const partition = topic.partitions[i];
323
346
  const { leader, leaderEpoch } = partition;
324
347
  let leaderRequests = requests.get(leader);
@@ -359,16 +382,34 @@ export class Consumer extends Base {
359
382
  callback(error, undefined);
360
383
  return;
361
384
  }
362
- const offsets = new Map();
363
- for (const response of responses) {
364
- for (const { name: topic, partitions } of response.topics) {
365
- let topicOffsets = offsets.get(topic);
366
- if (!topicOffsets) {
367
- topicOffsets = Array(metadata.topics.get(topic).partitionsCount);
368
- offsets.set(topic, topicOffsets);
385
+ let offsets = new Map();
386
+ if (withTimestamps) {
387
+ offsets = new Map();
388
+ for (const response of responses) {
389
+ for (const { name: topic, partitions } of response.topics) {
390
+ let topicOffsets = offsets.get(topic);
391
+ if (!topicOffsets) {
392
+ topicOffsets = new Map();
393
+ offsets.set(topic, topicOffsets);
394
+ }
395
+ for (const { partitionIndex: index, offset, timestamp } of partitions) {
396
+ topicOffsets.set(index, { offset, timestamp });
397
+ }
369
398
  }
370
- for (const { partitionIndex: index, offset } of partitions) {
371
- topicOffsets[index] = offset;
399
+ }
400
+ }
401
+ else {
402
+ offsets = new Map();
403
+ for (const response of responses) {
404
+ for (const { name: topic, partitions } of response.topics) {
405
+ let topicOffsets = offsets.get(topic);
406
+ if (!topicOffsets) {
407
+ topicOffsets = Array(metadata.topics.get(topic).partitionsCount);
408
+ offsets.set(topic, topicOffsets);
409
+ }
410
+ for (const { partitionIndex: index, offset } of partitions) {
411
+ topicOffsets[index] = offset;
412
+ }
372
413
  }
373
414
  }
374
415
  }
@@ -684,7 +725,7 @@ export class Consumer extends Base {
684
725
  callback(error, undefined);
685
726
  return;
686
727
  }
687
- this.#performSyncGroup(this.#roundRobinAssignments(metadata), callback);
728
+ this.#performSyncGroup(this.#createAssignments(metadata), callback);
688
729
  });
689
730
  return;
690
731
  }
@@ -785,7 +826,7 @@ export class Consumer extends Base {
785
826
  w.appendString(topic).appendArray(partitions, (w, a) => w.appendInt32(a), true, false);
786
827
  }, true, false).buffer;
787
828
  }
788
- #roundRobinAssignments(metadata) {
829
+ #createAssignments(metadata) {
789
830
  const partitionTracker = new Map();
790
831
  // First of all, layout topics-partitions in a list
791
832
  for (const [topic, partitions] of metadata.topics) {
@@ -805,37 +846,14 @@ export class Consumer extends Base {
805
846
  }
806
847
  return [{ memberId: this.memberId, assignment: this.#encodeProtocolAssignment(assignments) }];
807
848
  }
808
- // Flat the list of members and subscribed topics
809
- const members = [];
810
- const subscribedTopics = new Set();
811
- for (const [memberId, subscription] of this.#members) {
812
- members.push({ memberId, assignments: new Map() });
813
- for (const topic of subscription.topics) {
814
- subscribedTopics.add(topic);
815
- }
816
- }
817
- // Assign topic-partitions in round robin
818
- let currentMember = 0;
819
- for (const topic of subscribedTopics) {
820
- const partitionsCount = metadata.topics.get(topic).partitionsCount;
821
- for (let i = 0; i < partitionsCount; i++) {
822
- const member = members[currentMember++ % membersSize];
823
- let topicAssignments = member.assignments.get(topic);
824
- if (!topicAssignments) {
825
- topicAssignments = { topic, partitions: [] };
826
- member.assignments.set(topic, topicAssignments);
827
- }
828
- topicAssignments?.partitions.push(i);
829
- }
830
- }
831
- const assignments = [];
832
- for (const member of members) {
833
- assignments.push({
849
+ const encodedAssignments = [];
850
+ for (const member of this.#partitionsAssigner(this.memberId, this.#members, new Set(this.topics.current), metadata)) {
851
+ encodedAssignments.push({
834
852
  memberId: member.memberId,
835
853
  assignment: this.#encodeProtocolAssignment(Array.from(member.assignments.values()))
836
854
  });
837
855
  }
838
- return assignments;
856
+ return encodedAssignments;
839
857
  }
840
858
  #getRejoinError(error) {
841
859
  const protocolError = error.findBy?.('needsRejoin', true);
@@ -43,6 +43,9 @@ export declare const groupOptionsProperties: {
43
43
  };
44
44
  };
45
45
  };
46
+ partitionAssigner: {
47
+ function: boolean;
48
+ };
46
49
  };
47
50
  export declare const groupOptionsAdditionalValidations: {
48
51
  rebalanceTimeout: {
@@ -161,6 +164,9 @@ export declare const groupOptionsSchema: {
161
164
  };
162
165
  };
163
166
  };
167
+ partitionAssigner: {
168
+ function: boolean;
169
+ };
164
170
  };
165
171
  additionalProperties: boolean;
166
172
  };
@@ -257,6 +263,9 @@ export declare const consumeOptionsSchema: {
257
263
  };
258
264
  };
259
265
  };
266
+ partitionAssigner: {
267
+ function: boolean;
268
+ };
260
269
  topics: {
261
270
  type: string;
262
271
  items: {
@@ -393,6 +402,9 @@ export declare const consumerOptionsSchema: {
393
402
  };
394
403
  };
395
404
  };
405
+ partitionAssigner: {
406
+ function: boolean;
407
+ };
396
408
  groupId: {
397
409
  type: string;
398
410
  pattern: string;
@@ -494,6 +506,9 @@ export declare const fetchOptionsSchema: {
494
506
  };
495
507
  };
496
508
  };
509
+ partitionAssigner: {
510
+ function: boolean;
511
+ };
497
512
  node: {
498
513
  type: string;
499
514
  minimum: number;
@@ -607,6 +622,16 @@ export declare const listOffsetsOptionsSchema: {
607
622
  pattern: string;
608
623
  };
609
624
  };
625
+ partitions: {
626
+ type: string;
627
+ additionalProperties: {
628
+ type: string;
629
+ items: {
630
+ type: string;
631
+ minimum: number;
632
+ };
633
+ };
634
+ };
610
635
  isolationLevel: {
611
636
  type: string;
612
637
  enum: string[];
@@ -21,7 +21,8 @@ export const groupOptionsProperties = {
21
21
  metadata: { oneOf: [{ type: 'string' }, { buffer: true }] }
22
22
  }
23
23
  }
24
- }
24
+ },
25
+ partitionAssigner: { function: true }
25
26
  };
26
27
  export const groupOptionsAdditionalValidations = {
27
28
  rebalanceTimeout: {
@@ -178,6 +179,13 @@ export const listOffsetsOptionsSchema = {
178
179
  type: 'object',
179
180
  properties: {
180
181
  topics: { type: 'array', items: idProperty },
182
+ partitions: {
183
+ type: 'object',
184
+ additionalProperties: {
185
+ type: 'array',
186
+ items: { type: 'number', minimum: 0 }
187
+ }
188
+ },
181
189
  isolationLevel: { type: 'string', enum: Object.keys(FetchIsolationLevels) },
182
190
  timestamp: { bigint: true }
183
191
  },
@@ -0,0 +1,3 @@
1
+ import { type ClusterMetadata } from '../base/types.ts';
2
+ import { type ExtendedGroupProtocolSubscription, type GroupPartitionsAssignments } from './types.ts';
3
+ export declare function roundRobinAssigner(_current: string, members: Map<string, ExtendedGroupProtocolSubscription>, topics: Set<string>, metadata: ClusterMetadata): GroupPartitionsAssignments[];
@@ -0,0 +1,23 @@
1
+ export function roundRobinAssigner(_current, members, topics, metadata) {
2
+ const membersSize = members.size;
3
+ const assignments = [];
4
+ // Flat the list of members and subscribed topics
5
+ for (const memberId of members.keys()) {
6
+ assignments.push({ memberId, assignments: new Map() });
7
+ }
8
+ // Assign topic-partitions in round robin
9
+ let currentMember = 0;
10
+ for (const topic of topics) {
11
+ const partitionsCount = metadata.topics.get(topic).partitionsCount;
12
+ for (let i = 0; i < partitionsCount; i++) {
13
+ const member = assignments[currentMember++ % membersSize];
14
+ let topicAssignments = member.assignments.get(topic);
15
+ if (!topicAssignments) {
16
+ topicAssignments = { topic, partitions: [] };
17
+ member.assignments.set(topic, topicAssignments);
18
+ }
19
+ topicAssignments?.partitions.push(i);
20
+ }
21
+ }
22
+ return assignments;
23
+ }
@@ -1,7 +1,7 @@
1
1
  import { type FetchRequestTopic } from '../../apis/consumer/fetch-v17.ts';
2
2
  import { type FetchIsolationLevel } from '../../apis/enumerations.ts';
3
3
  import { type KafkaRecord, type Message } from '../../protocol/records.ts';
4
- import { type BaseOptions, type TopicWithPartitionAndOffset } from '../base/types.ts';
4
+ import { type BaseOptions, type ClusterMetadata, type TopicWithPartitionAndOffset } from '../base/types.ts';
5
5
  import { type Deserializers } from '../serde.ts';
6
6
  export interface GroupProtocolSubscription {
7
7
  name: string;
@@ -12,12 +12,21 @@ export interface GroupAssignment {
12
12
  topic: string;
13
13
  partitions: number[];
14
14
  }
15
+ export interface GroupPartitionsAssignments {
16
+ memberId: string;
17
+ assignments: Map<string, GroupAssignment>;
18
+ }
15
19
  export interface ExtendedGroupProtocolSubscription extends Omit<GroupProtocolSubscription, 'name'> {
16
20
  topics?: string[];
17
21
  memberId: string;
18
22
  }
19
23
  export type Offsets = Map<string, bigint[]>;
24
+ export type OffsetsWithTimestamps = Map<string, Map<number, {
25
+ offset: bigint;
26
+ timestamp: bigint;
27
+ }>>;
20
28
  export type CorruptedMessageHandler = (record: KafkaRecord, topic: string, partition: number, firstTimestamp: bigint, firstOffset: bigint, commit: Message['commit']) => boolean;
29
+ export type GroupPartitionsAssigner = (current: string, members: Map<string, ExtendedGroupProtocolSubscription>, topics: Set<string>, metadata: ClusterMetadata) => GroupPartitionsAssignments[];
21
30
  export declare const MessagesStreamModes: {
22
31
  readonly LATEST: "latest";
23
32
  readonly EARLIEST: "earliest";
@@ -38,6 +47,7 @@ export interface GroupOptions {
38
47
  rebalanceTimeout?: number;
39
48
  heartbeatInterval?: number;
40
49
  protocols?: GroupProtocolSubscription[];
50
+ partitionAssigner?: GroupPartitionsAssigner;
41
51
  }
42
52
  export interface ConsumeBaseOptions<Key, Value, HeaderKey, HeaderValue> {
43
53
  autocommit?: boolean | number;
@@ -74,6 +84,7 @@ export interface ListCommitsOptions {
74
84
  }
75
85
  export interface ListOffsetsOptions {
76
86
  topics: string[];
87
+ partitions?: Record<string, number[]>;
77
88
  timestamp?: bigint;
78
89
  isolationLevel?: FetchIsolationLevel;
79
90
  }
@@ -10,13 +10,17 @@ export declare const produceOptionsProperties: {
10
10
  };
11
11
  acks: {
12
12
  type: string;
13
- enum: (0 | 1 | -1)[];
14
- errorMessage: string;
13
+ enumeration: {
14
+ allowed: (0 | 1 | -1)[];
15
+ errorMessage: string;
16
+ };
15
17
  };
16
18
  compression: {
17
19
  type: string;
18
- enum: string[];
19
- errorMessage: string;
20
+ enumeration: {
21
+ allowed: string[];
22
+ errorMessage: string;
23
+ };
20
24
  };
21
25
  partitioner: {
22
26
  function: boolean;
@@ -42,13 +46,17 @@ export declare const produceOptionsSchema: {
42
46
  };
43
47
  acks: {
44
48
  type: string;
45
- enum: (0 | 1 | -1)[];
46
- errorMessage: string;
49
+ enumeration: {
50
+ allowed: (0 | 1 | -1)[];
51
+ errorMessage: string;
52
+ };
47
53
  };
48
54
  compression: {
49
55
  type: string;
50
- enum: string[];
51
- errorMessage: string;
56
+ enumeration: {
57
+ allowed: string[];
58
+ errorMessage: string;
59
+ };
52
60
  };
53
61
  partitioner: {
54
62
  function: boolean;
@@ -78,13 +86,17 @@ export declare const sendOptionsSchema: {
78
86
  };
79
87
  acks: {
80
88
  type: string;
81
- enum: (0 | 1 | -1)[];
82
- errorMessage: string;
89
+ enumeration: {
90
+ allowed: (0 | 1 | -1)[];
91
+ errorMessage: string;
92
+ };
83
93
  };
84
94
  compression: {
85
95
  type: string;
86
- enum: string[];
87
- errorMessage: string;
96
+ enumeration: {
97
+ allowed: string[];
98
+ errorMessage: string;
99
+ };
88
100
  };
89
101
  partitioner: {
90
102
  function: boolean;
@@ -9,13 +9,17 @@ export const produceOptionsProperties = {
9
9
  idempotent: { type: 'boolean' },
10
10
  acks: {
11
11
  type: 'number',
12
- enum: Object.values(ProduceAcks),
13
- errorMessage: enumErrorMessage(ProduceAcks)
12
+ enumeration: {
13
+ allowed: Object.values(ProduceAcks),
14
+ errorMessage: enumErrorMessage(ProduceAcks)
15
+ }
14
16
  },
15
17
  compression: {
16
18
  type: 'string',
17
- enum: Object.keys(compressionsAlgorithms),
18
- errorMessage: enumErrorMessage(compressionsAlgorithms, true)
19
+ enumeration: {
20
+ allowed: Object.keys(compressionsAlgorithms),
21
+ errorMessage: enumErrorMessage(compressionsAlgorithms, true)
22
+ }
19
23
  },
20
24
  partitioner: { function: true },
21
25
  autocreateTopics: { type: 'boolean' },
@@ -1,6 +1,7 @@
1
1
  import { type CallbackWithPromise } from '../../apis/callbacks.ts';
2
2
  import { Base } from '../base/base.ts';
3
3
  import { type ProduceOptions, type ProduceResult, type ProducerInfo, type ProducerOptions, type SendOptions } from './types.ts';
4
+ export declare function noopSerializer(data?: Buffer): Buffer | undefined;
4
5
  export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends Base<ProducerOptions<Key, Value, HeaderKey, HeaderValue>> {
5
6
  #private;
6
7
  constructor(options: ProducerOptions<Key, Value, HeaderKey, HeaderValue>);
@@ -9,7 +9,7 @@ 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.
11
11
  // For futher info, ask Matteo.
12
- function noopSerializer(data) {
12
+ export function noopSerializer(data) {
13
13
  return data;
14
14
  }
15
15
  export class Producer extends Base {
@@ -18,6 +18,7 @@ export interface ConnectionOptions {
18
18
  connectTimeout?: number;
19
19
  maxInflights?: number;
20
20
  tls?: TLSConnectionOptions;
21
+ tlsServerName?: string | boolean;
21
22
  sasl?: SASLOptions;
22
23
  ownerId?: number;
23
24
  }
@@ -99,6 +99,10 @@ export class Connection extends EventEmitter {
99
99
  const connectionOptions = {
100
100
  timeout: this.#options.connectTimeout
101
101
  };
102
+ if (this.#options.tlsServerName) {
103
+ connectionOptions.servername =
104
+ typeof this.#options.tlsServerName === 'string' ? this.#options.tlsServerName : host;
105
+ }
102
106
  const connectionTimeoutHandler = () => {
103
107
  const error = new TimeoutError(`Connection to ${host}:${port} timed out.`);
104
108
  diagnosticContext.error = error;
@@ -114,14 +118,14 @@ export class Connection extends EventEmitter {
114
118
  this.#onConnectionError(host, port, diagnosticContext, error);
115
119
  };
116
120
  this.emit('connecting');
117
- /* c8 ignore next 3 - TLS connection is not tested but we rely on Node.js tests */
118
121
  this.#host = host;
119
122
  this.#port = port;
123
+ /* c8 ignore next 3 - TLS connection is not tested but we rely on Node.js tests */
120
124
  this.#socket = this.#options.tls
121
125
  ? createTLSConnection(port, host, { ...this.#options.tls, ...connectionOptions })
122
126
  : createConnection({ ...connectionOptions, port, host });
123
127
  this.#socket.setNoDelay(true);
124
- this.#socket.once('connect', () => {
128
+ this.#socket.once(this.#options.tls ? 'secureConnect' : 'connect', () => {
125
129
  this.#socket.removeListener('timeout', connectionTimeoutHandler);
126
130
  this.#socket.removeListener('error', connectionErrorHandler);
127
131
  this.#socket.on('error', this.#onError.bind(this));
@@ -3,7 +3,7 @@ import zlib from 'node:zlib';
3
3
  import { UnsupportedCompressionError } from "../errors.js";
4
4
  import { DynamicBuffer } from "./dynamic-buffer.js";
5
5
  const require = createRequire(import.meta.url);
6
- // @ts-ignore
6
+ // @ts-ignore - Added in Node.js 22.15.0
7
7
  const { zstdCompressSync, zstdDecompressSync, gzipSync, gunzipSync } = zlib;
8
8
  function ensureBuffer(data) {
9
9
  return DynamicBuffer.isDynamicBuffer(data) ? data.slice() : data;
@@ -208,7 +208,10 @@ export class Reader {
208
208
  return this.readNullableBytes(compact) || EMPTY_BUFFER;
209
209
  }
210
210
  readVarIntBytes() {
211
- const length = this.readVarInt();
211
+ let length = this.readVarInt();
212
+ if (length === -1) {
213
+ length = 0;
214
+ }
212
215
  const value = this.buffer.slice(this.position, this.position + length);
213
216
  this.position += length;
214
217
  return value;
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { Ajv2020 } from 'ajv/dist/2020.js';
2
2
  import debug from 'debug';
3
3
  import { type DynamicBuffer } from './protocol/dynamic-buffer.ts';
4
+ export interface EnumerationDefinition<T> {
5
+ allowed: T[];
6
+ errorMessage?: string;
7
+ }
8
+ export type KeywordSchema<T> = {
9
+ schema: T;
10
+ };
4
11
  export interface DataValidationContext {
5
12
  parentData: {
6
13
  [k: string | number]: any;
package/dist/utils.js CHANGED
@@ -1,4 +1,3 @@
1
- import ajvErrors from 'ajv-errors';
2
1
  import { Ajv2020 } from 'ajv/dist/2020.js';
3
2
  import debug from 'debug';
4
3
  import { inspect } from 'node:util';
@@ -12,8 +11,6 @@ export const loggers = {
12
11
  'consumer:heartbeat': debug('plt:kafka:consumer:heartbeat'),
13
12
  admin: debug('plt:kafka:admin')
14
13
  };
15
- // @ts-ignore
16
- ajvErrors(ajv);
17
14
  let debugDumpLogger = console.error;
18
15
  ajv.addKeyword({
19
16
  keyword: 'bigint',
@@ -75,6 +72,17 @@ ajv.addKeyword({
75
72
  }
76
73
  }
77
74
  });
75
+ ajv.addKeyword({
76
+ keyword: 'enumeration', // This mimics the enum keyword but defines a custom error message
77
+ validate(property, current) {
78
+ return property.allowed.includes(current);
79
+ },
80
+ error: {
81
+ message({ schema }) {
82
+ return schema.errorMessage;
83
+ }
84
+ }
85
+ });
78
86
  export class NumericMap extends Map {
79
87
  getWithDefault(key, fallback) {
80
88
  return this.get(key) ?? fallback;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/kafka",
3
- "version": "1.6.0",
3
+ "version": "1.7.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)",
@@ -26,7 +26,6 @@
26
26
  "types": "./dist/index.d.ts",
27
27
  "dependencies": {
28
28
  "ajv": "^8.17.1",
29
- "ajv-errors": "^3.0.0",
30
29
  "debug": "^4.4.0",
31
30
  "fastq": "^1.19.1",
32
31
  "mnemonist": "^0.40.3",
@@ -42,6 +41,7 @@
42
41
  "@types/node": "^22.13.5",
43
42
  "@types/semver": "^7.7.0",
44
43
  "@watchable/unpromise": "^1.0.2",
44
+ "avsc": "^5.7.7",
45
45
  "c8": "^10.1.3",
46
46
  "cleaner-spec-reporter": "^0.5.0",
47
47
  "cronometro": "^5.3.0",
@@ -53,6 +53,7 @@
53
53
  "node-rdkafka": "^3.3.1",
54
54
  "parse5": "^7.2.1",
55
55
  "prettier": "^3.5.3",
56
+ "prettier-plugin-space-before-function-paren": "^0.0.8",
56
57
  "prom-client": "^15.1.3",
57
58
  "semver": "^7.7.1",
58
59
  "table": "^6.9.0",