@platformatic/kafka 2.11.0 → 2.12.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.
Files changed (68) hide show
  1. package/README.md +6 -0
  2. package/dist/apis/definitions.d.ts +1 -1
  3. package/dist/apis/producer/produce-v10.d.ts +1 -0
  4. package/dist/apis/producer/produce-v10.js +26 -1
  5. package/dist/apis/producer/produce-v11.d.ts +1 -0
  6. package/dist/apis/producer/produce-v11.js +26 -1
  7. package/dist/apis/producer/produce-v7.d.ts +1 -0
  8. package/dist/apis/producer/produce-v7.js +23 -1
  9. package/dist/apis/producer/produce-v8.d.ts +1 -0
  10. package/dist/apis/producer/produce-v8.js +23 -1
  11. package/dist/apis/producer/produce-v9.d.ts +1 -0
  12. package/dist/apis/producer/produce-v9.js +26 -1
  13. package/dist/apis/producer/utils.d.ts +11 -0
  14. package/dist/apis/producer/utils.js +31 -0
  15. package/dist/clients/admin/admin.js +10 -13
  16. package/dist/clients/base/base.d.ts +1 -1
  17. package/dist/clients/base/base.js +6 -6
  18. package/dist/clients/base/options.d.ts +4 -0
  19. package/dist/clients/base/options.js +2 -1
  20. package/dist/clients/base/types.d.ts +1 -0
  21. package/dist/clients/consumer/consumer-protocol.d.ts +32 -0
  22. package/dist/clients/consumer/consumer-protocol.js +116 -0
  23. package/dist/clients/consumer/consumer.d.ts +3 -2
  24. package/dist/clients/consumer/consumer.js +305 -93
  25. package/dist/clients/consumer/index.d.ts +2 -0
  26. package/dist/clients/consumer/index.js +2 -0
  27. package/dist/clients/consumer/messages-stream.d.ts +4 -2
  28. package/dist/clients/consumer/messages-stream.js +126 -16
  29. package/dist/clients/consumer/partitions-assigners.d.ts +3 -0
  30. package/dist/clients/consumer/partitions-assigners.js +259 -0
  31. package/dist/clients/consumer/types.d.ts +11 -0
  32. package/dist/clients/producer/producer.d.ts +1 -1
  33. package/dist/clients/producer/producer.js +1 -1
  34. package/dist/errors.js +12 -1
  35. package/dist/network/connection.d.ts +1 -1
  36. package/dist/network/connection.js +83 -40
  37. package/dist/protocol/compression.d.ts +6 -0
  38. package/dist/protocol/compression.js +14 -1
  39. package/dist/protocol/records.d.ts +1 -0
  40. package/dist/protocol/records.js +40 -17
  41. package/dist/registries/confluent-schema-registry.d.ts +2 -1
  42. package/dist/registries/confluent-schema-registry.js +2 -1
  43. package/dist/symbols.d.ts +1 -0
  44. package/dist/symbols.js +1 -0
  45. package/dist/typescript-4/dist/apis/definitions.d.ts +1 -1
  46. package/dist/typescript-4/dist/apis/producer/produce-v10.d.ts +1 -0
  47. package/dist/typescript-4/dist/apis/producer/produce-v11.d.ts +1 -0
  48. package/dist/typescript-4/dist/apis/producer/produce-v7.d.ts +1 -0
  49. package/dist/typescript-4/dist/apis/producer/produce-v8.d.ts +1 -0
  50. package/dist/typescript-4/dist/apis/producer/produce-v9.d.ts +1 -0
  51. package/dist/typescript-4/dist/apis/producer/utils.d.ts +11 -0
  52. package/dist/typescript-4/dist/clients/base/base.d.ts +1 -1
  53. package/dist/typescript-4/dist/clients/base/options.d.ts +4 -0
  54. package/dist/typescript-4/dist/clients/base/types.d.ts +1 -0
  55. package/dist/typescript-4/dist/clients/consumer/consumer-protocol.d.ts +32 -0
  56. package/dist/typescript-4/dist/clients/consumer/consumer.d.ts +3 -2
  57. package/dist/typescript-4/dist/clients/consumer/index.d.ts +2 -0
  58. package/dist/typescript-4/dist/clients/consumer/messages-stream.d.ts +4 -2
  59. package/dist/typescript-4/dist/clients/consumer/partitions-assigners.d.ts +4 -1
  60. package/dist/typescript-4/dist/clients/consumer/types.d.ts +11 -0
  61. package/dist/typescript-4/dist/clients/producer/producer.d.ts +1 -1
  62. package/dist/typescript-4/dist/network/connection.d.ts +1 -1
  63. package/dist/typescript-4/dist/protocol/compression.d.ts +7 -1
  64. package/dist/typescript-4/dist/protocol/records.d.ts +1 -0
  65. package/dist/typescript-4/dist/registries/confluent-schema-registry.d.ts +2 -1
  66. package/dist/typescript-4/dist/symbols.d.ts +1 -0
  67. package/dist/version.js +1 -1
  68. package/package.json +6 -3
package/README.md CHANGED
@@ -108,6 +108,12 @@ await forEach(
108
108
  await consumer.close()
109
109
  ```
110
110
 
111
+ ### Consuming messages in batches
112
+
113
+ The [`examples/batching`](./examples/batching) example uses the [`hwp`](https://www.npmjs.com/package/hwp)
114
+ `batchIterator` operator to process messages in batches and commit offsets after each batch. The batch is emitted
115
+ when it reaches the configured size or when its timeout expires, so the final batch can contain fewer messages.
116
+
111
117
  ### Admin
112
118
 
113
119
  ```typescript
@@ -4,7 +4,7 @@ import { type Reader } from '../protocol/reader.ts';
4
4
  import { type Writer } from '../protocol/writer.ts';
5
5
  export type Callback<ReturnType> = (error: Error | null, payload?: ReturnType) => void;
6
6
  export type CallbackArguments<ReturnType> = [cb: Callback<ReturnType>];
7
- export type RequestCreator = (...args: any[]) => Writer;
7
+ export type RequestCreator = (...args: any[]) => Writer | Promise<Writer>;
8
8
  export type ResponseParser<ReturnType> = (correlationId: number, apiKey: number, apiVersion: number, reader: Reader) => ReturnType | Promise<ReturnType>;
9
9
  export type ResponseErrorWithLocation = [string, [number, NullableString]];
10
10
  export type APIWithCallback<RequestArguments extends Array<unknown>, ResponseType> = (connection: Connection, ...args: [...RequestArguments, ...Partial<CallbackArguments<ResponseType>>]) => void;
@@ -37,5 +37,6 @@ export interface ProduceResponse {
37
37
  nodeEndpoints?: ProduceResponseNodeEndpoint[];
38
38
  }
39
39
  export declare function createRequest(acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions>): Writer;
40
+ export declare function createRequestAsync(acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions>): Promise<Writer>;
40
41
  export declare function parseResponse(_correlationId: number, apiKey: number, apiVersion: number, reader: Reader): ProduceResponse;
41
42
  export declare const api: import("../definitions.ts").API<[acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions> | undefined], boolean | ProduceResponse>;
@@ -5,6 +5,7 @@ import { groupByProperty } from "../../utils.js";
5
5
  import { createAPI } from "../definitions.js";
6
6
  import { ProduceAcks } from "../enumerations.js";
7
7
  import { readKnownTaggedFields } from "../tagged-fields.js";
8
+ import { encodeProduceTopicData } from "./utils.js";
8
9
  /*
9
10
  Produce Request (Version: 10) => transactional_id acks timeout_ms [topic_data] TAG_BUFFER
10
11
  transactional_id => COMPACT_NULLABLE_STRING
@@ -45,6 +46,30 @@ export function createRequest(acks = 1, timeout = 0, topicData, options = {}) {
45
46
  }
46
47
  return writer;
47
48
  }
49
+ export async function createRequestAsync(acks = 1, timeout = 0, topicData, options = {}) {
50
+ const topics = await encodeProduceTopicData(topicData, options);
51
+ const writer = Writer.create()
52
+ .appendString(options.transactionalId)
53
+ .appendInt16(acks)
54
+ .appendInt32(timeout)
55
+ .appendArray(topics, (w, { topic, partitions }) => {
56
+ w.appendString(topic).appendArray(partitions, (w, { partition, records }) => {
57
+ w.appendInt32(partition)
58
+ .appendUnsignedVarInt(records.length + 1)
59
+ .appendFrom(records);
60
+ });
61
+ })
62
+ .appendTaggedFields();
63
+ if (acks === ProduceAcks.NO_RESPONSE) {
64
+ writer.context.noResponse = true;
65
+ }
66
+ return writer;
67
+ }
68
+ function createApiRequest(acks = 1, timeout = 0, topicData, options = {}) {
69
+ return options.compression === 'gzip'
70
+ ? createRequestAsync(acks, timeout, topicData, options)
71
+ : createRequest(acks, timeout, topicData, options);
72
+ }
48
73
  /*
49
74
  Produce Response (Version: 10) => [responses] throttle_time_ms TAG_BUFFER
50
75
  responses => name [partition_responses] TAG_BUFFER
@@ -143,4 +168,4 @@ export function parseResponse(_correlationId, apiKey, apiVersion, reader) {
143
168
  }
144
169
  return response;
145
170
  }
146
- export const api = createAPI(0, 10, createRequest, parseResponse);
171
+ export const api = createAPI(0, 10, createApiRequest, parseResponse);
@@ -37,5 +37,6 @@ export interface ProduceResponse {
37
37
  nodeEndpoints?: ProduceResponseNodeEndpoint[];
38
38
  }
39
39
  export declare function createRequest(acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions>): Writer;
40
+ export declare function createRequestAsync(acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions>): Promise<Writer>;
40
41
  export declare function parseResponse(_correlationId: number, apiKey: number, apiVersion: number, reader: Reader): ProduceResponse;
41
42
  export declare const api: import("../definitions.ts").API<[acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions> | undefined], boolean | ProduceResponse>;
@@ -5,6 +5,7 @@ import { groupByProperty } from "../../utils.js";
5
5
  import { createAPI } from "../definitions.js";
6
6
  import { ProduceAcks } from "../enumerations.js";
7
7
  import { readKnownTaggedFields } from "../tagged-fields.js";
8
+ import { encodeProduceTopicData } from "./utils.js";
8
9
  /*
9
10
  Produce Request (Version: 11) => transactional_id acks timeout_ms [topic_data] TAG_BUFFER
10
11
  transactional_id => COMPACT_NULLABLE_STRING
@@ -45,6 +46,30 @@ export function createRequest(acks = 1, timeout = 0, topicData, options = {}) {
45
46
  }
46
47
  return writer;
47
48
  }
49
+ export async function createRequestAsync(acks = 1, timeout = 0, topicData, options = {}) {
50
+ const topics = await encodeProduceTopicData(topicData, options);
51
+ const writer = Writer.create()
52
+ .appendString(options.transactionalId)
53
+ .appendInt16(acks)
54
+ .appendInt32(timeout)
55
+ .appendArray(topics, (w, { topic, partitions }) => {
56
+ w.appendString(topic).appendArray(partitions, (w, { partition, records }) => {
57
+ w.appendInt32(partition)
58
+ .appendUnsignedVarInt(records.length + 1)
59
+ .appendFrom(records);
60
+ });
61
+ })
62
+ .appendTaggedFields();
63
+ if (acks === ProduceAcks.NO_RESPONSE) {
64
+ writer.context.noResponse = true;
65
+ }
66
+ return writer;
67
+ }
68
+ function createApiRequest(acks = 1, timeout = 0, topicData, options = {}) {
69
+ return options.compression === 'gzip'
70
+ ? createRequestAsync(acks, timeout, topicData, options)
71
+ : createRequest(acks, timeout, topicData, options);
72
+ }
48
73
  /*
49
74
  Produce Response (Version: 11) => [responses] throttle_time_ms TAG_BUFFER
50
75
  responses => name [partition_responses] TAG_BUFFER
@@ -143,4 +168,4 @@ export function parseResponse(_correlationId, apiKey, apiVersion, reader) {
143
168
  }
144
169
  return response;
145
170
  }
146
- export const api = createAPI(0, 11, createRequest, parseResponse);
171
+ export const api = createAPI(0, 11, createApiRequest, parseResponse);
@@ -25,5 +25,6 @@ export interface ProduceResponse {
25
25
  throttleTimeMs: number;
26
26
  }
27
27
  export declare function createRequest(acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions>): Writer;
28
+ export declare function createRequestAsync(acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions>): Promise<Writer>;
28
29
  export declare function parseResponse(_correlationId: number, apiKey: number, apiVersion: number, reader: Reader): ProduceResponse;
29
30
  export declare const api: import("../definitions.ts").API<[acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions> | undefined], boolean | ProduceResponse>;
@@ -4,6 +4,7 @@ import { Writer } from "../../protocol/writer.js";
4
4
  import { groupByProperty } from "../../utils.js";
5
5
  import { createAPI } from "../definitions.js";
6
6
  import { ProduceAcks } from "../enumerations.js";
7
+ import { encodeProduceTopicData } from "./utils.js";
7
8
  /*
8
9
  Produce Request (Version: 7) => transactional_id acks timeout_ms [topic_data]
9
10
  transactional_id => NULLABLE_STRING
@@ -41,6 +42,27 @@ export function createRequest(acks = 1, timeout = 0, topicData, options = {}) {
41
42
  }
42
43
  return writer;
43
44
  }
45
+ export async function createRequestAsync(acks = 1, timeout = 0, topicData, options = {}) {
46
+ const topics = await encodeProduceTopicData(topicData, options);
47
+ const writer = Writer.create()
48
+ .appendString(options.transactionalId, false)
49
+ .appendInt16(acks)
50
+ .appendInt32(timeout)
51
+ .appendArray(topics, (w, { topic, partitions }) => {
52
+ w.appendString(topic, false).appendArray(partitions, (w, { partition, records }) => {
53
+ w.appendInt32(partition).appendInt32(records.length).appendFrom(records);
54
+ }, false, false);
55
+ }, false, false);
56
+ if (acks === ProduceAcks.NO_RESPONSE) {
57
+ writer.context.noResponse = true;
58
+ }
59
+ return writer;
60
+ }
61
+ function createApiRequest(acks = 1, timeout = 0, topicData, options = {}) {
62
+ return options.compression === 'gzip'
63
+ ? createRequestAsync(acks, timeout, topicData, options)
64
+ : createRequest(acks, timeout, topicData, options);
65
+ }
44
66
  /*
45
67
  Produce Response (Version: 7) => [responses] throttle_time_ms
46
68
  responses => name [partition_responses]
@@ -85,4 +107,4 @@ export function parseResponse(_correlationId, apiKey, apiVersion, reader) {
85
107
  }
86
108
  return response;
87
109
  }
88
- export const api = createAPI(0, 7, createRequest, parseResponse, false, false);
110
+ export const api = createAPI(0, 7, createApiRequest, parseResponse, false, false);
@@ -25,5 +25,6 @@ export interface ProduceResponse {
25
25
  throttleTimeMs: number;
26
26
  }
27
27
  export declare function createRequest(acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions>): Writer;
28
+ export declare function createRequestAsync(acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions>): Promise<Writer>;
28
29
  export declare function parseResponse(_correlationId: number, apiKey: number, apiVersion: number, reader: Reader): ProduceResponse;
29
30
  export declare const api: import("../definitions.ts").API<[acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions> | undefined], boolean | ProduceResponse>;
@@ -4,6 +4,7 @@ import { Writer } from "../../protocol/writer.js";
4
4
  import { groupByProperty } from "../../utils.js";
5
5
  import { createAPI } from "../definitions.js";
6
6
  import { ProduceAcks } from "../enumerations.js";
7
+ import { encodeProduceTopicData } from "./utils.js";
7
8
  /*
8
9
  Produce Request (Version: 8) => transactional_id acks timeout_ms [topic_data]
9
10
  transactional_id => NULLABLE_STRING
@@ -41,6 +42,27 @@ export function createRequest(acks = 1, timeout = 0, topicData, options = {}) {
41
42
  }
42
43
  return writer;
43
44
  }
45
+ export async function createRequestAsync(acks = 1, timeout = 0, topicData, options = {}) {
46
+ const topics = await encodeProduceTopicData(topicData, options);
47
+ const writer = Writer.create()
48
+ .appendString(options.transactionalId, false)
49
+ .appendInt16(acks)
50
+ .appendInt32(timeout)
51
+ .appendArray(topics, (w, { topic, partitions }) => {
52
+ w.appendString(topic, false).appendArray(partitions, (w, { partition, records }) => {
53
+ w.appendInt32(partition).appendInt32(records.length).appendFrom(records);
54
+ }, false, false);
55
+ }, false, false);
56
+ if (acks === ProduceAcks.NO_RESPONSE) {
57
+ writer.context.noResponse = true;
58
+ }
59
+ return writer;
60
+ }
61
+ function createApiRequest(acks = 1, timeout = 0, topicData, options = {}) {
62
+ return options.compression === 'gzip'
63
+ ? createRequestAsync(acks, timeout, topicData, options)
64
+ : createRequest(acks, timeout, topicData, options);
65
+ }
44
66
  /*
45
67
  Produce Response (Version: 8) => [responses] throttle_time_ms
46
68
  responses => name [partition_responses]
@@ -103,4 +125,4 @@ export function parseResponse(_correlationId, apiKey, apiVersion, reader) {
103
125
  }
104
126
  return response;
105
127
  }
106
- export const api = createAPI(0, 8, createRequest, parseResponse, false, false);
128
+ export const api = createAPI(0, 8, createApiRequest, parseResponse, false, false);
@@ -25,5 +25,6 @@ export interface ProduceResponse {
25
25
  throttleTimeMs: number;
26
26
  }
27
27
  export declare function createRequest(acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions>): Writer;
28
+ export declare function createRequestAsync(acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions>): Promise<Writer>;
28
29
  export declare function parseResponse(_correlationId: number, apiKey: number, apiVersion: number, reader: Reader): ProduceResponse;
29
30
  export declare const api: import("../definitions.ts").API<[acks: number | undefined, timeout: number | undefined, topicData: MessageRecord[], options?: Partial<CreateRecordsBatchOptions> | undefined], boolean | ProduceResponse>;
@@ -4,6 +4,7 @@ import { Writer } from "../../protocol/writer.js";
4
4
  import { groupByProperty } from "../../utils.js";
5
5
  import { createAPI } from "../definitions.js";
6
6
  import { ProduceAcks } from "../enumerations.js";
7
+ import { encodeProduceTopicData } from "./utils.js";
7
8
  /*
8
9
  Produce Request (Version: 9) => transactional_id acks timeout_ms [topic_data] TAG_BUFFER
9
10
  transactional_id => COMPACT_NULLABLE_STRING
@@ -44,6 +45,30 @@ export function createRequest(acks = 1, timeout = 0, topicData, options = {}) {
44
45
  }
45
46
  return writer;
46
47
  }
48
+ export async function createRequestAsync(acks = 1, timeout = 0, topicData, options = {}) {
49
+ const topics = await encodeProduceTopicData(topicData, options);
50
+ const writer = Writer.create()
51
+ .appendString(options.transactionalId)
52
+ .appendInt16(acks)
53
+ .appendInt32(timeout)
54
+ .appendArray(topics, (w, { topic, partitions }) => {
55
+ w.appendString(topic).appendArray(partitions, (w, { partition, records }) => {
56
+ w.appendInt32(partition)
57
+ .appendUnsignedVarInt(records.length + 1)
58
+ .appendFrom(records);
59
+ });
60
+ })
61
+ .appendTaggedFields();
62
+ if (acks === ProduceAcks.NO_RESPONSE) {
63
+ writer.context.noResponse = true;
64
+ }
65
+ return writer;
66
+ }
67
+ function createApiRequest(acks = 1, timeout = 0, topicData, options = {}) {
68
+ return options.compression === 'gzip'
69
+ ? createRequestAsync(acks, timeout, topicData, options)
70
+ : createRequest(acks, timeout, topicData, options);
71
+ }
47
72
  /*
48
73
  Produce Response (Version: 9) => [responses] throttle_time_ms TAG_BUFFER
49
74
  responses => name [partition_responses] TAG_BUFFER
@@ -110,4 +135,4 @@ export function parseResponse(_correlationId, apiKey, apiVersion, reader) {
110
135
  }
111
136
  return response;
112
137
  }
113
- export const api = createAPI(0, 9, createRequest, parseResponse);
138
+ export const api = createAPI(0, 9, createApiRequest, parseResponse);
@@ -0,0 +1,11 @@
1
+ import { type CreateRecordsBatchOptions, type MessageRecord } from '../../protocol/records.ts';
2
+ import { type Writer } from '../../protocol/writer.ts';
3
+ export interface EncodedProducePartition {
4
+ partition: number;
5
+ records: Writer;
6
+ }
7
+ export interface EncodedProduceTopic {
8
+ topic: string;
9
+ partitions: EncodedProducePartition[];
10
+ }
11
+ export declare function encodeProduceTopicData(topicData: MessageRecord[], options: Partial<CreateRecordsBatchOptions>): Promise<EncodedProduceTopic[]>;
@@ -0,0 +1,31 @@
1
+ import { createRecordsBatchAsync } from "../../protocol/records.js";
2
+ import { groupByProperty } from "../../utils.js";
3
+ const gzipConcurrency = 4;
4
+ export async function encodeProduceTopicData(topicData, options) {
5
+ const now = BigInt(Date.now());
6
+ for (const message of topicData) {
7
+ message.partition ??= 0;
8
+ message.timestamp ??= now;
9
+ }
10
+ const topics = groupByProperty(topicData, 'topic').map(([topic, messages]) => ({
11
+ topic,
12
+ partitions: groupByProperty(messages, 'partition').map(([partition, messages]) => ({
13
+ partition: Number(partition),
14
+ messages
15
+ }))
16
+ }));
17
+ const tasks = topics.flatMap(topic => topic.partitions);
18
+ let nextTask = 0;
19
+ async function worker() {
20
+ while (nextTask < tasks.length) {
21
+ const task = tasks[nextTask++];
22
+ task.records = await createRecordsBatchAsync(task.messages, options);
23
+ }
24
+ }
25
+ const workers = Array.from({ length: Math.min(gzipConcurrency, tasks.length) }, worker);
26
+ await Promise.all(workers);
27
+ return topics.map(({ topic, partitions }) => ({
28
+ topic,
29
+ partitions: partitions.map(({ partition, records }) => ({ partition, records: records }))
30
+ }));
31
+ }
@@ -2,8 +2,8 @@ import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } f
2
2
  import { ConfigResourceTypes, FetchIsolationLevels, FindCoordinatorKeyTypes, legacyConsumerGroupStates } from "../../apis/enumerations.js";
3
3
  import { adminAclsChannel, adminClientQuotasChannel, adminConfigsChannel, adminConsumerGroupOffsetsChannel, adminGroupsChannel, adminLogDirsChannel, adminOffsetsChannel, adminTopicsChannel, createDiagnosticContext } from "../../diagnostic.js";
4
4
  import { findErrorBy, MultipleErrors, NetworkError, UserError } from "../../errors.js";
5
- import { Reader } from "../../protocol/reader.js";
6
5
  import { Base, kAfterCreate, kCheckNotClosed, kConnections, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js";
6
+ import { decodeConsumerProtocolAssignment, decodeConsumerProtocolSubscription } from "../consumer/consumer-protocol.js";
7
7
  import { adminListOffsetsOptionsValidator, alterClientQuotasOptionsValidator, alterConfigsOptionsValidator, alterConsumerGroupOffsetsOptionsValidator, createAclsOptionsValidator, createPartitionsOptionsValidator, createTopicsOptionsValidator, deleteAclsOptionsValidator, deleteConsumerGroupOffsetsOptionsValidator, deleteGroupsOptionsValidator, deleteRecordsOptionsValidator, deleteTopicsOptionsValidator, describeAclsOptionsValidator, describeClientQuotasOptionsValidator, describeConfigsOptionsValidator, describeGroupsOptionsValidator, describeLogDirsOptionsValidator, findCoordinatorOptionsValidator, incrementalAlterConfigsOptionsValidator, listConsumerGroupOffsetsOptionsValidator, listGroupsOptionsValidator, listTopicsOptionsValidator, removeMembersFromConsumerGroupOptionsValidator } from "./options.js";
8
8
  export class Admin extends Base {
9
9
  #controller = null;
@@ -649,22 +649,19 @@ export class Admin extends Base {
649
649
  authorizedOperations: raw.authorizedOperations
650
650
  };
651
651
  for (const member of raw.members) {
652
- const reader = Reader.from(member.memberMetadata);
653
652
  let memberMetadata;
654
653
  let memberAssignments;
655
- if (reader.remaining > 0) {
654
+ if (member.memberMetadata.length > 0) {
655
+ const subscription = decodeConsumerProtocolSubscription(member.memberMetadata);
656
656
  memberMetadata = {
657
- version: reader.readInt16(),
658
- topics: reader.readArray(r => r.readString(false), false, false),
659
- metadata: reader.readBytes(false)
657
+ version: subscription.version,
658
+ topics: subscription.topics,
659
+ metadata: subscription.userData,
660
+ ownedPartitions: subscription.ownedPartitions,
661
+ generationId: subscription.generationId,
662
+ rackId: subscription.rackId
660
663
  };
661
- reader.reset(member.memberAssignment);
662
- reader.skip(2); // Ignore Version information
663
- memberAssignments = reader.readMap(r => {
664
- const topic = r.readString(false);
665
- return [topic, { topic, partitions: reader.readArray(r => r.readInt32(), false, false) }];
666
- }, false, false);
667
- // Ignore the user data
664
+ memberAssignments = new Map(decodeConsumerProtocolAssignment(member.memberAssignment).assignedPartitions.map(assignment => [assignment.topic, assignment]));
668
665
  }
669
666
  group.members.set(member.memberId, {
670
667
  id: member.memberId,
@@ -84,7 +84,7 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions, EventsT
84
84
  [kMetadata](options: MetadataOptions, callback: CallbackWithPromise<ClusterMetadata>): void;
85
85
  [kCheckNotClosed](callback: CallbackWithPromise<any>): boolean;
86
86
  clearMetadata(): void;
87
- [kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType>, attempt: number) => void, callback: CallbackWithPromise<ReturnType>, attempt?: number, errors?: Error[], shouldSkipRetry?: (e: Error) => boolean): void | Promise<ReturnType>;
87
+ [kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType>, attempt: number) => void, callback: CallbackWithPromise<ReturnType>, attempt?: number, errors?: Error[], shouldSkipRetry?: (e: Error) => boolean, retryLimit?: number): void | Promise<ReturnType>;
88
88
  [kPerformDeduplicated]<ReturnType>(operationId: string, operation: (callback: CallbackWithPromise<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>): void | Promise<ReturnType>;
89
89
  [kGetApi]<RequestArguments extends Array<unknown>, ResponseType>(name: string, callback: Callback<API<RequestArguments, ResponseType>>): void;
90
90
  [kGetConnection](broker: Broker, callback: Callback<Connection>): void;
@@ -267,12 +267,12 @@ export class Base extends TypedEventEmitter {
267
267
  clearMetadata() {
268
268
  this.#metadata = undefined;
269
269
  }
270
- [kPerformWithRetry](operationId, operation, callback, attempt = 0, errors = [], shouldSkipRetry) {
271
- const retries = this[kOptions].retries;
270
+ [kPerformWithRetry](operationId, operation, callback, attempt = 0, errors = [], shouldSkipRetry, retryLimit) {
271
+ const retries = retryLimit ?? this[kOptions].retries;
272
272
  this.emitWithDebug('client', 'performWithRetry', operationId, attempt, retries);
273
273
  operation((error, result) => {
274
274
  if (error) {
275
- // Only retry if all the errors in the chain are retriable
275
+ // Only retry if all the errors in the chain are retriable.
276
276
  const retriable = !findErrorBy(error, 'canRetry', false);
277
277
  errors.push(error);
278
278
  if (attempt < retries && retriable && !shouldSkipRetry?.(error)) {
@@ -289,7 +289,7 @@ export class Base extends TypedEventEmitter {
289
289
  const timeout = setTimeout(() => {
290
290
  this.removeListener('client:close', onClose);
291
291
  try {
292
- this[kPerformWithRetry](operationId, operation, callback, attempt + 1, errors, shouldSkipRetry);
292
+ this[kPerformWithRetry](operationId, operation, callback, attempt + 1, errors, shouldSkipRetry, retries);
293
293
  }
294
294
  catch (error) {
295
295
  errors.push(error);
@@ -435,7 +435,7 @@ export class Base extends TypedEventEmitter {
435
435
  const autocreateTopics = options.autocreateTopics ?? this[kOptions].autocreateTopics;
436
436
  this[kPerformDeduplicated](
437
437
  // Unique key to avoid mixing callbacks
438
- `metadata-${topics.sort().join(',')}-${autocreateTopics}-${options.forceUpdate}`, deduplicateCallback => {
438
+ `metadata-${topics.sort().join(',')}-${autocreateTopics}-${options.forceUpdate}-${options.retries ?? 'default'}`, deduplicateCallback => {
439
439
  this[kPerformWithRetry]('metadata', (retryCallback, attempt) => {
440
440
  this[kGetBootstrapConnection]((error, connection) => {
441
441
  if (error) {
@@ -532,7 +532,7 @@ export class Base extends TypedEventEmitter {
532
532
  };
533
533
  this.emitWithDebug('client', 'metadata', updatedMetadata);
534
534
  deduplicateCallback(null, updatedMetadata);
535
- }, 0);
535
+ }, 0, [], undefined, options.retries);
536
536
  }, callback);
537
537
  }
538
538
  #forwardEvents(source, events) {
@@ -199,6 +199,10 @@ export declare const metadataOptionsSchema: {
199
199
  type: string;
200
200
  minimum: number;
201
201
  };
202
+ retries: {
203
+ type: string;
204
+ minimum: number;
205
+ };
202
206
  };
203
207
  additionalProperties: boolean;
204
208
  };
@@ -67,7 +67,8 @@ export const metadataOptionsSchema = {
67
67
  topics: { type: 'array', items: idProperty },
68
68
  autocreateTopics: { type: 'boolean' },
69
69
  forceUpdate: { type: 'boolean' },
70
- metadataMaxAge: { type: 'number', minimum: 0 }
70
+ metadataMaxAge: { type: 'number', minimum: 0 },
71
+ retries: { type: 'number', minimum: 0 }
71
72
  },
72
73
  additionalProperties: false
73
74
  };
@@ -48,4 +48,5 @@ export interface MetadataOptions {
48
48
  autocreateTopics?: boolean;
49
49
  forceUpdate?: boolean;
50
50
  metadataMaxAge?: number;
51
+ retries?: number;
51
52
  }
@@ -0,0 +1,32 @@
1
+ import { type GroupAssignment } from './types.ts';
2
+ export declare const CONSUMER_PROTOCOL_LOWEST_VERSION = 0;
3
+ export declare const CONSUMER_PROTOCOL_HIGHEST_VERSION = 3;
4
+ export interface ConsumerProtocolSubscriptionData {
5
+ version: number;
6
+ topics: string[];
7
+ userData: Buffer;
8
+ ownedPartitions: GroupAssignment[];
9
+ generationId: number;
10
+ rackId: string | null;
11
+ }
12
+ export interface ConsumerProtocolAssignmentData {
13
+ version: number;
14
+ assignedPartitions: GroupAssignment[];
15
+ userData: Buffer;
16
+ }
17
+ export declare function encodeConsumerProtocolSubscription(data: {
18
+ version: number;
19
+ topics: string[];
20
+ userData?: Buffer | string;
21
+ ownedPartitions?: GroupAssignment[];
22
+ generationId?: number;
23
+ rackId?: string | null;
24
+ }): Buffer;
25
+ export declare function decodeConsumerProtocolSubscription(buffer: Buffer): ConsumerProtocolSubscriptionData;
26
+ export declare function decodeCooperativeStickyGeneration(userData: Buffer): number;
27
+ export declare function encodeConsumerProtocolAssignment(data: {
28
+ version: number;
29
+ assignedPartitions: GroupAssignment[];
30
+ userData?: Buffer | string | null;
31
+ }): Buffer;
32
+ export declare function decodeConsumerProtocolAssignment(buffer: Buffer): ConsumerProtocolAssignmentData;
@@ -0,0 +1,116 @@
1
+ import { Reader } from "../../protocol/reader.js";
2
+ import { Writer } from "../../protocol/writer.js";
3
+ export const CONSUMER_PROTOCOL_LOWEST_VERSION = 0;
4
+ export const CONSUMER_PROTOCOL_HIGHEST_VERSION = 3;
5
+ function supportedVersion(version) {
6
+ if (version < CONSUMER_PROTOCOL_LOWEST_VERSION) {
7
+ return CONSUMER_PROTOCOL_LOWEST_VERSION;
8
+ }
9
+ if (version > CONSUMER_PROTOCOL_HIGHEST_VERSION) {
10
+ return CONSUMER_PROTOCOL_HIGHEST_VERSION;
11
+ }
12
+ return version;
13
+ }
14
+ function sortedAssignments(assignments) {
15
+ return assignments
16
+ .filter(({ partitions }) => partitions.length > 0)
17
+ .map(({ topic, partitions }) => ({ topic, partitions: [...partitions].sort((a, b) => a - b) }))
18
+ .sort((a, b) => a.topic.localeCompare(b.topic));
19
+ }
20
+ function appendTopicPartitions(writer, assignments) {
21
+ return writer.appendArray(sortedAssignments(assignments), (w, { topic, partitions }) => {
22
+ w.appendString(topic, false).appendArray(partitions, (w, partition) => w.appendInt32(partition), false, false);
23
+ }, false, false);
24
+ }
25
+ function readTopicPartitions(reader) {
26
+ return reader.readArray(r => ({
27
+ topic: r.readString(false),
28
+ partitions: r.readArray(r => r.readInt32(), false, false)
29
+ }), false, false);
30
+ }
31
+ export function encodeConsumerProtocolSubscription(data) {
32
+ const version = supportedVersion(data.version);
33
+ const userData = typeof data.userData === 'string' ? Buffer.from(data.userData) : data.userData;
34
+ const writer = Writer.create()
35
+ .appendInt16(version)
36
+ .appendArray([...data.topics].sort(), (w, topic) => w.appendString(topic, false), false, false)
37
+ .appendBytes(userData, false);
38
+ if (version >= 1) {
39
+ appendTopicPartitions(writer, data.ownedPartitions ?? []);
40
+ }
41
+ if (version >= 2) {
42
+ writer.appendInt32(data.generationId ?? -1);
43
+ }
44
+ if (version >= 3) {
45
+ writer.appendString(data.rackId || null, false);
46
+ }
47
+ return writer.buffer;
48
+ }
49
+ export function decodeConsumerProtocolSubscription(buffer) {
50
+ const reader = Reader.from(buffer);
51
+ const encodedVersion = reader.readInt16();
52
+ const version = supportedVersion(encodedVersion);
53
+ const subscription = {
54
+ version: encodedVersion,
55
+ topics: reader.readArray(r => r.readString(false), false, false),
56
+ userData: reader.readBytes(false),
57
+ ownedPartitions: [],
58
+ generationId: -1,
59
+ rackId: null
60
+ };
61
+ // KafkaJS and Platformatic <= 2.11 may advertise newer versions with a v0 body.
62
+ // Default absent trailing fields, but still reject partially encoded fields.
63
+ if (version >= 1 && reader.remaining > 0) {
64
+ subscription.ownedPartitions = readTopicPartitions(reader);
65
+ }
66
+ if (version >= 2 && reader.remaining > 0) {
67
+ subscription.generationId = reader.readInt32();
68
+ }
69
+ if (version >= 3 && reader.remaining > 0) {
70
+ subscription.rackId = reader.readNullableString(false);
71
+ }
72
+ return subscription;
73
+ }
74
+ export function decodeCooperativeStickyGeneration(userData) {
75
+ if (userData.length < 4) {
76
+ return -1;
77
+ }
78
+ try {
79
+ return Reader.from(userData).readInt32();
80
+ }
81
+ catch {
82
+ return -1;
83
+ }
84
+ }
85
+ export function encodeConsumerProtocolAssignment(data) {
86
+ const userData = typeof data.userData === 'string' ? Buffer.from(data.userData) : data.userData;
87
+ const writer = Writer.create().appendInt16(supportedVersion(data.version));
88
+ appendTopicPartitions(writer, data.assignedPartitions);
89
+ writer.appendBytes(userData ?? null, false);
90
+ return writer.buffer;
91
+ }
92
+ export function decodeConsumerProtocolAssignment(buffer) {
93
+ if (buffer.length === 0) {
94
+ return {
95
+ version: 0,
96
+ assignedPartitions: [],
97
+ userData: Buffer.alloc(0)
98
+ };
99
+ }
100
+ const reader = Reader.from(buffer);
101
+ const encodedVersion = reader.readInt16();
102
+ if (reader.remaining === 0) {
103
+ return {
104
+ version: encodedVersion,
105
+ assignedPartitions: [],
106
+ userData: Buffer.alloc(0)
107
+ };
108
+ }
109
+ const assignedPartitions = readTopicPartitions(reader);
110
+ const userData = reader.remaining > 0 ? reader.readBytes(false) : Buffer.alloc(0);
111
+ return {
112
+ version: encodedVersion,
113
+ assignedPartitions,
114
+ userData
115
+ };
116
+ }
@@ -6,12 +6,13 @@ import { Base, type BaseEvents, kCreateConnectionPool } from '../base/base.ts';
6
6
  import { type ClusterMetadata } from '../base/types.ts';
7
7
  import { MessagesStream } from './messages-stream.ts';
8
8
  import { TopicsMap } from './topics-map.ts';
9
- import { type CommitOptions, type ConsumeOptions, type ConsumerGroupJoinPayload, type ConsumerGroupLeavePayload, type ConsumerGroupRebalancePayload, type ConsumerHeartbeatErrorPayload, type ConsumerHeartbeatPayload, type ConsumerHeartbeatStalledPayload, type ConsumerOptions, type FetchOptions, type GetLagOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets, type OffsetsWithTimestamps } from './types.ts';
9
+ import { type CommitOptions, type ConsumeOptions, type ConsumerGroupJoinPayload, type ConsumerGroupLeavePayload, type ConsumerGroupAutocommitErrorPayload, type ConsumerGroupRebalancePayload, type ConsumerHeartbeatErrorPayload, type ConsumerHeartbeatPayload, type ConsumerHeartbeatStalledPayload, type ConsumerOptions, type FetchOptions, type GetLagOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets, type OffsetsWithTimestamps } from './types.ts';
10
10
  export interface ConsumerEvents extends BaseEvents {
11
11
  'consumer:group:join': (payload: ConsumerGroupJoinPayload) => void;
12
12
  'consumer:group:leave': (payload: ConsumerGroupLeavePayload) => void;
13
13
  'consumer:group:rejoin': () => void;
14
14
  'consumer:group:rebalance': (payload: ConsumerGroupRebalancePayload) => void;
15
+ 'consumer:group:autocommit:error': (payload: ConsumerGroupAutocommitErrorPayload) => void;
15
16
  'consumer:heartbeat:start': (payload?: ConsumerHeartbeatPayload) => void;
16
17
  'consumer:heartbeat:cancel': (payload: ConsumerHeartbeatPayload) => void;
17
18
  'consumer:heartbeat:end': (payload?: ConsumerHeartbeatPayload) => void;
@@ -33,8 +34,8 @@ export declare class Consumer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
33
34
  get lastHeartbeat(): Date | null;
34
35
  get coordinatorId(): number | null;
35
36
  get streamContext(): unknown;
36
- close(force: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
37
37
  close(force?: boolean): Promise<void>;
38
+ close(force: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
38
39
  isActive(): boolean;
39
40
  consume(options: ConsumeOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<MessagesStream<Key, Value, HeaderKey, HeaderValue>>): void;
40
41
  consume(options: ConsumeOptions<Key, Value, HeaderKey, HeaderValue>): Promise<MessagesStream<Key, Value, HeaderKey, HeaderValue>>;