kafka-ts 0.0.3-beta → 0.0.4

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 (176) hide show
  1. package/README.md +72 -8
  2. package/dist/api/api-versions.d.ts +9 -0
  3. package/{src/api/api-versions.ts → dist/api/api-versions.js} +8 -5
  4. package/dist/api/create-topics.d.ts +38 -0
  5. package/dist/api/create-topics.js +53 -0
  6. package/dist/api/delete-topics.d.ts +18 -0
  7. package/dist/api/delete-topics.js +33 -0
  8. package/dist/api/fetch.d.ts +84 -0
  9. package/dist/api/fetch.js +142 -0
  10. package/dist/api/find-coordinator.d.ts +21 -0
  11. package/{src/api/find-coordinator.ts → dist/api/find-coordinator.js} +14 -14
  12. package/dist/api/heartbeat.d.ts +11 -0
  13. package/dist/api/heartbeat.js +27 -0
  14. package/dist/api/index.d.ts +576 -0
  15. package/{src/api/index.ts → dist/api/index.js} +42 -41
  16. package/dist/api/init-producer-id.d.ts +13 -0
  17. package/dist/api/init-producer-id.js +29 -0
  18. package/dist/api/join-group.d.ts +34 -0
  19. package/dist/api/join-group.js +51 -0
  20. package/dist/api/leave-group.d.ts +19 -0
  21. package/dist/api/leave-group.js +39 -0
  22. package/dist/api/list-offsets.d.ts +29 -0
  23. package/dist/api/list-offsets.js +48 -0
  24. package/dist/api/metadata.d.ts +40 -0
  25. package/{src/api/metadata.ts → dist/api/metadata.js} +18 -26
  26. package/dist/api/offset-commit.d.ts +28 -0
  27. package/dist/api/offset-commit.js +48 -0
  28. package/dist/api/offset-fetch.d.ts +31 -0
  29. package/dist/api/offset-fetch.js +55 -0
  30. package/dist/api/produce.d.ts +54 -0
  31. package/{src/api/produce.ts → dist/api/produce.js} +55 -102
  32. package/dist/api/sasl-authenticate.d.ts +11 -0
  33. package/dist/api/sasl-authenticate.js +23 -0
  34. package/dist/api/sasl-handshake.d.ts +6 -0
  35. package/dist/api/sasl-handshake.js +19 -0
  36. package/dist/api/sync-group.d.ts +24 -0
  37. package/dist/api/sync-group.js +36 -0
  38. package/dist/auth/index.d.ts +2 -0
  39. package/dist/auth/index.js +8 -0
  40. package/dist/auth/plain.d.ts +5 -0
  41. package/dist/auth/plain.js +12 -0
  42. package/dist/auth/scram.d.ts +9 -0
  43. package/dist/auth/scram.js +40 -0
  44. package/dist/broker.d.ts +30 -0
  45. package/dist/broker.js +55 -0
  46. package/dist/client.d.ts +22 -0
  47. package/dist/client.js +36 -0
  48. package/dist/cluster.d.ts +27 -0
  49. package/dist/cluster.js +70 -0
  50. package/dist/cluster.test.d.ts +1 -0
  51. package/{src/cluster.test.ts → dist/cluster.test.js} +87 -113
  52. package/dist/codecs/gzip.d.ts +2 -0
  53. package/dist/codecs/gzip.js +8 -0
  54. package/dist/codecs/index.d.ts +2 -0
  55. package/dist/codecs/index.js +17 -0
  56. package/dist/codecs/none.d.ts +2 -0
  57. package/dist/codecs/none.js +7 -0
  58. package/dist/codecs/types.d.ts +5 -0
  59. package/dist/codecs/types.js +2 -0
  60. package/dist/connection.d.ts +26 -0
  61. package/dist/connection.js +175 -0
  62. package/dist/consumer/consumer-group.d.ts +41 -0
  63. package/dist/consumer/consumer-group.js +215 -0
  64. package/dist/consumer/consumer-metadata.d.ts +7 -0
  65. package/dist/consumer/consumer-metadata.js +14 -0
  66. package/dist/consumer/consumer.d.ts +44 -0
  67. package/dist/consumer/consumer.js +225 -0
  68. package/dist/consumer/fetch-manager.d.ts +33 -0
  69. package/dist/consumer/fetch-manager.js +140 -0
  70. package/dist/consumer/fetcher.d.ts +25 -0
  71. package/dist/consumer/fetcher.js +64 -0
  72. package/dist/consumer/offset-manager.d.ts +22 -0
  73. package/dist/consumer/offset-manager.js +66 -0
  74. package/dist/consumer/processor.d.ts +19 -0
  75. package/dist/consumer/processor.js +59 -0
  76. package/dist/distributors/assignments-to-replicas.d.ts +16 -0
  77. package/{src/distributors/assignments-to-replicas.ts → dist/distributors/assignments-to-replicas.js} +15 -41
  78. package/dist/distributors/assignments-to-replicas.test.d.ts +1 -0
  79. package/dist/distributors/assignments-to-replicas.test.js +40 -0
  80. package/dist/distributors/messages-to-topic-partition-leaders.d.ts +17 -0
  81. package/dist/distributors/messages-to-topic-partition-leaders.js +15 -0
  82. package/dist/distributors/messages-to-topic-partition-leaders.test.d.ts +1 -0
  83. package/dist/distributors/messages-to-topic-partition-leaders.test.js +30 -0
  84. package/dist/distributors/partitioner.d.ts +7 -0
  85. package/dist/distributors/partitioner.js +23 -0
  86. package/dist/index.d.ts +9 -0
  87. package/dist/index.js +26 -0
  88. package/dist/metadata.d.ts +24 -0
  89. package/dist/metadata.js +106 -0
  90. package/dist/producer/producer.d.ts +24 -0
  91. package/dist/producer/producer.js +131 -0
  92. package/{src/types.ts → dist/types.d.ts} +4 -4
  93. package/dist/types.js +2 -0
  94. package/{src/utils/api.ts → dist/utils/api.d.ts} +2 -4
  95. package/dist/utils/api.js +5 -0
  96. package/dist/utils/crypto.d.ts +8 -0
  97. package/dist/utils/crypto.js +18 -0
  98. package/dist/utils/decoder.d.ts +30 -0
  99. package/{src/utils/decoder.ts → dist/utils/decoder.js} +41 -57
  100. package/dist/utils/delay.d.ts +1 -0
  101. package/dist/utils/delay.js +5 -0
  102. package/dist/utils/encoder.d.ts +28 -0
  103. package/{src/utils/encoder.ts → dist/utils/encoder.js} +50 -66
  104. package/dist/utils/error.d.ts +11 -0
  105. package/dist/utils/error.js +27 -0
  106. package/dist/utils/logger.d.ts +9 -0
  107. package/dist/utils/logger.js +32 -0
  108. package/dist/utils/memo.d.ts +1 -0
  109. package/{src/utils/memo.ts → dist/utils/memo.js} +7 -3
  110. package/dist/utils/murmur2.d.ts +3 -0
  111. package/dist/utils/murmur2.js +40 -0
  112. package/dist/utils/retrier.d.ts +10 -0
  113. package/dist/utils/retrier.js +22 -0
  114. package/dist/utils/tracer.d.ts +5 -0
  115. package/dist/utils/tracer.js +39 -0
  116. package/package.json +11 -2
  117. package/.github/workflows/release.yml +0 -17
  118. package/.prettierrc +0 -8
  119. package/certs/ca.crt +0 -29
  120. package/certs/ca.key +0 -52
  121. package/certs/ca.srl +0 -1
  122. package/certs/kafka.crt +0 -29
  123. package/certs/kafka.csr +0 -26
  124. package/certs/kafka.key +0 -52
  125. package/certs/kafka.keystore.jks +0 -0
  126. package/certs/kafka.truststore.jks +0 -0
  127. package/docker-compose.yml +0 -104
  128. package/examples/package-lock.json +0 -31
  129. package/examples/package.json +0 -14
  130. package/examples/src/client.ts +0 -9
  131. package/examples/src/consumer.ts +0 -18
  132. package/examples/src/create-topic.ts +0 -44
  133. package/examples/src/producer.ts +0 -24
  134. package/examples/src/replicator.ts +0 -25
  135. package/examples/src/utils/delay.ts +0 -1
  136. package/examples/src/utils/json.ts +0 -1
  137. package/examples/tsconfig.json +0 -7
  138. package/log4j.properties +0 -95
  139. package/scripts/generate-certs.sh +0 -24
  140. package/src/__snapshots__/request-handler.test.ts.snap +0 -978
  141. package/src/api/create-topics.ts +0 -78
  142. package/src/api/delete-topics.ts +0 -42
  143. package/src/api/fetch.ts +0 -143
  144. package/src/api/heartbeat.ts +0 -33
  145. package/src/api/init-producer-id.ts +0 -35
  146. package/src/api/join-group.ts +0 -67
  147. package/src/api/leave-group.ts +0 -48
  148. package/src/api/list-offsets.ts +0 -65
  149. package/src/api/offset-commit.ts +0 -67
  150. package/src/api/offset-fetch.ts +0 -74
  151. package/src/api/sasl-authenticate.ts +0 -21
  152. package/src/api/sasl-handshake.ts +0 -16
  153. package/src/api/sync-group.ts +0 -54
  154. package/src/broker.ts +0 -74
  155. package/src/client.ts +0 -47
  156. package/src/cluster.ts +0 -87
  157. package/src/connection.ts +0 -143
  158. package/src/consumer/consumer-group.ts +0 -209
  159. package/src/consumer/consumer-metadata.ts +0 -14
  160. package/src/consumer/consumer.ts +0 -231
  161. package/src/consumer/fetch-manager.ts +0 -179
  162. package/src/consumer/fetcher.ts +0 -57
  163. package/src/consumer/offset-manager.ts +0 -93
  164. package/src/consumer/processor.ts +0 -47
  165. package/src/distributors/assignments-to-replicas.test.ts +0 -43
  166. package/src/distributors/messages-to-topic-partition-leaders.test.ts +0 -32
  167. package/src/distributors/messages-to-topic-partition-leaders.ts +0 -19
  168. package/src/index.ts +0 -4
  169. package/src/metadata.ts +0 -122
  170. package/src/producer/producer.ts +0 -132
  171. package/src/utils/debug.ts +0 -9
  172. package/src/utils/delay.ts +0 -1
  173. package/src/utils/error.ts +0 -21
  174. package/src/utils/retrier.ts +0 -39
  175. package/src/utils/tracer.ts +0 -31
  176. package/tsconfig.json +0 -17
@@ -1,16 +0,0 @@
1
- import { createApi } from '../utils/api';
2
- import { KafkaTSApiError } from '../utils/error';
3
-
4
- export const SASL_HANDSHAKE = createApi({
5
- apiKey: 17,
6
- apiVersion: 1,
7
- request: (encoder, data: { mechanism: string }) => encoder.writeString(data.mechanism),
8
- response: (decoder) => {
9
- const result = {
10
- errorCode: decoder.readInt16(),
11
- mechanisms: decoder.readArray((mechanism) => mechanism.readString()),
12
- };
13
- if (result.errorCode) throw new KafkaTSApiError(result.errorCode, null, result);
14
- return result;
15
- },
16
- });
@@ -1,54 +0,0 @@
1
- import { createApi } from '../utils/api';
2
- import { KafkaTSApiError } from '../utils/error';
3
-
4
- export type Assignment = { [topic: string]: number[] };
5
-
6
- export type MemberAssignment = {
7
- memberId: string;
8
- assignment: Assignment;
9
- };
10
-
11
- export const SYNC_GROUP = createApi({
12
- apiKey: 14,
13
- apiVersion: 5,
14
- request: (
15
- encoder,
16
- data: {
17
- groupId: string;
18
- generationId: number;
19
- memberId: string;
20
- groupInstanceId: string | null;
21
- protocolType: string | null;
22
- protocolName: string | null;
23
- assignments: MemberAssignment[];
24
- },
25
- ) =>
26
- encoder
27
- .writeUVarInt(0)
28
- .writeCompactString(data.groupId)
29
- .writeInt32(data.generationId)
30
- .writeCompactString(data.memberId)
31
- .writeCompactString(data.groupInstanceId)
32
- .writeCompactString(data.protocolType)
33
- .writeCompactString(data.protocolName)
34
- .writeCompactArray(data.assignments, (encoder, assignment) =>
35
- encoder
36
- .writeCompactString(assignment.memberId)
37
- .writeCompactString(JSON.stringify(assignment.assignment))
38
- .writeUVarInt(0),
39
- )
40
- .writeUVarInt(0),
41
- response: (decoder) => {
42
- const result = {
43
- _tag: decoder.readTagBuffer(),
44
- throttleTimeMs: decoder.readInt32(),
45
- errorCode: decoder.readInt16(),
46
- protocolType: decoder.readCompactString(),
47
- protocolName: decoder.readCompactString(),
48
- assignments: decoder.readCompactString()!,
49
- _tag2: decoder.readTagBuffer(),
50
- };
51
- if (result.errorCode) throw new KafkaTSApiError(result.errorCode, null, result);
52
- return result;
53
- },
54
- });
package/src/broker.ts DELETED
@@ -1,74 +0,0 @@
1
- import { TcpSocketConnectOpts } from 'net';
2
- import { TLSSocketOptions } from 'tls';
3
- import { API } from './api';
4
- import { Connection, SendRequest } from './connection';
5
- import { KafkaTSError } from './utils/error';
6
- import { memo } from './utils/memo';
7
-
8
- export type SASLOptions = { mechanism: 'PLAIN'; username: string; password: string };
9
-
10
- type BrokerOptions = {
11
- clientId: string | null;
12
- options: TcpSocketConnectOpts;
13
- sasl: SASLOptions | null;
14
- ssl: TLSSocketOptions | null;
15
- };
16
-
17
- export class Broker {
18
- private connection: Connection;
19
- public sendRequest: SendRequest;
20
-
21
- constructor(private options: BrokerOptions) {
22
- this.connection = new Connection({
23
- clientId: this.options.clientId,
24
- connection: this.options.options,
25
- ssl: this.options.ssl,
26
- });
27
- this.sendRequest = this.connection.sendRequest.bind(this.connection);
28
- }
29
-
30
- public async connect() {
31
- await this.connection.connect();
32
- await this.validateApiVersions();
33
- await this.saslHandshake();
34
- await this.saslAuthenticate();
35
- return this;
36
- }
37
-
38
- public ensureConnected = memo(() => this.connect());
39
-
40
- public async disconnect() {
41
- await this.connection.disconnect();
42
- }
43
-
44
- private async validateApiVersions() {
45
- const { versions } = await this.sendRequest(API.API_VERSIONS, {});
46
-
47
- const apiByKey = Object.fromEntries(Object.values(API).map((api) => [api.apiKey, api]));
48
- versions.forEach(({ apiKey, minVersion, maxVersion }) => {
49
- if (!apiByKey[apiKey]) {
50
- return;
51
- }
52
- const { apiVersion } = apiByKey[apiKey];
53
- if (apiVersion < minVersion || apiVersion > maxVersion) {
54
- throw new KafkaTSError(`API ${apiKey} version ${apiVersion} is not supported by the broker`);
55
- }
56
- });
57
- }
58
-
59
- private async saslHandshake() {
60
- if (!this.options.sasl) {
61
- return;
62
- }
63
- await this.sendRequest(API.SASL_HANDSHAKE, { mechanism: this.options.sasl.mechanism });
64
- }
65
-
66
- private async saslAuthenticate() {
67
- if (this.options.sasl?.mechanism !== 'PLAIN') {
68
- return;
69
- }
70
- const { username, password } = this.options.sasl;
71
- const authBytes = [null, username, password].join('\u0000');
72
- await this.sendRequest(API.SASL_AUTHENTICATE, { authBytes: Buffer.from(authBytes) });
73
- }
74
- }
package/src/client.ts DELETED
@@ -1,47 +0,0 @@
1
- import { TcpSocketConnectOpts } from 'net';
2
- import { TLSSocketOptions } from 'tls';
3
- import { SASLOptions } from './broker';
4
- import { Cluster } from './cluster';
5
- import { Consumer, ConsumerOptions } from './consumer/consumer';
6
- import { Producer, ProducerOptions } from './producer/producer';
7
-
8
- type ClientOptions = {
9
- clientId?: string | null;
10
- bootstrapServers: TcpSocketConnectOpts[];
11
- sasl?: SASLOptions | null;
12
- ssl?: TLSSocketOptions | null;
13
- };
14
-
15
- export class Client {
16
- private options: Required<ClientOptions>;
17
-
18
- constructor(options: ClientOptions) {
19
- this.options = {
20
- ...options,
21
- clientId: options.clientId ?? null,
22
- sasl: options.sasl ?? null,
23
- ssl: options.ssl ?? null,
24
- };
25
- }
26
-
27
- public async startConsumer(options: ConsumerOptions) {
28
- const consumer = new Consumer(this.createCluster(), options);
29
- await consumer.start();
30
- return consumer;
31
- }
32
-
33
- public createProducer(options: ProducerOptions) {
34
- return new Producer(this.createCluster(), options);
35
- }
36
-
37
- public createCluster() {
38
- return new Cluster({
39
- clientId: this.options.clientId,
40
- bootstrapServers: this.options.bootstrapServers,
41
- sasl: this.options.sasl,
42
- ssl: this.options.ssl,
43
- });
44
- }
45
- }
46
-
47
- export const createKafkaClient = (options: ClientOptions) => new Client(options);
package/src/cluster.ts DELETED
@@ -1,87 +0,0 @@
1
- import { TcpSocketConnectOpts } from 'net';
2
- import { TLSSocketOptions } from 'tls';
3
- import { API } from './api';
4
- import { Broker, SASLOptions } from './broker';
5
- import { SendRequest } from './connection';
6
- import { ConnectionError, KafkaTSError } from './utils/error';
7
-
8
- type ClusterOptions = {
9
- clientId: string | null;
10
- bootstrapServers: TcpSocketConnectOpts[];
11
- sasl: SASLOptions | null;
12
- ssl: TLSSocketOptions | null;
13
- };
14
-
15
- export class Cluster {
16
- private seedBroker: Broker;
17
- private brokerById: Record<number, Broker> = {};
18
-
19
- constructor(private options: ClusterOptions) {
20
- this.seedBroker = new Broker({
21
- clientId: this.options.clientId,
22
- sasl: this.options.sasl,
23
- ssl: this.options.ssl,
24
- options: this.options.bootstrapServers[0],
25
- });
26
- }
27
-
28
- public async connect() {
29
- await this.connectSeedBroker();
30
- const metadata = await this.sendRequest(API.METADATA, {
31
- allowTopicAutoCreation: false,
32
- includeTopicAuthorizedOperations: false,
33
- topics: [],
34
- });
35
-
36
- this.brokerById = Object.fromEntries(
37
- metadata.brokers.map(({ nodeId, ...options }) => [
38
- nodeId,
39
- new Broker({
40
- clientId: this.options.clientId,
41
- sasl: this.options.sasl,
42
- ssl: this.options.ssl,
43
- options,
44
- }),
45
- ]),
46
- );
47
- return this;
48
- }
49
-
50
- public async disconnect() {
51
- await Promise.all([
52
- this.seedBroker.disconnect(),
53
- ...Object.values(this.brokerById).map((broker) => broker.disconnect()),
54
- ]);
55
- }
56
-
57
- public sendRequest: SendRequest = (...args) => this.seedBroker.sendRequest(...args);
58
-
59
- public sendRequestToNode =
60
- (nodeId: number): SendRequest =>
61
- async (...args) => {
62
- const broker = this.brokerById[nodeId];
63
- if (!broker) {
64
- throw new ConnectionError(`Broker ${nodeId} is not available`);
65
- }
66
- await broker.ensureConnected();
67
- return broker.sendRequest(...args);
68
- };
69
-
70
- private async connectSeedBroker() {
71
- const randomizedBrokers = this.options.bootstrapServers.toSorted(() => Math.random() - 0.5);
72
- for (const options of randomizedBrokers) {
73
- try {
74
- this.seedBroker = await new Broker({
75
- clientId: this.options.clientId,
76
- sasl: this.options.sasl,
77
- ssl: this.options.ssl,
78
- options,
79
- }).connect();
80
- return;
81
- } catch (error) {
82
- console.warn(`Failed to connect to seed broker ${options.host}:${options.port}`, error);
83
- }
84
- }
85
- throw new KafkaTSError('No seed brokers found');
86
- }
87
- }
package/src/connection.ts DELETED
@@ -1,143 +0,0 @@
1
- import assert from 'assert';
2
- import net, { isIP, Socket, TcpSocketConnectOpts } from 'net';
3
- import tls, { TLSSocketOptions } from 'tls';
4
- import { getApiName } from './api';
5
- import { Api } from './utils/api';
6
- import { Decoder } from './utils/decoder';
7
- import { Encoder } from './utils/encoder';
8
- import { ConnectionError } from './utils/error';
9
- import { createTracer } from './utils/tracer';
10
-
11
- const trace = createTracer('Connection');
12
-
13
- export type ConnectionOptions = {
14
- clientId: string | null;
15
- connection: TcpSocketConnectOpts;
16
- ssl: TLSSocketOptions | null;
17
- };
18
-
19
- type RawResonse = { responseDecoder: Decoder; responseSize: number };
20
-
21
- export class Connection {
22
- private socket = new Socket();
23
- private queue: {
24
- [correlationId: number]: { resolve: (response: RawResonse) => void; reject: (error: Error) => void };
25
- } = {};
26
- private lastCorrelationId = 0;
27
- private buffer: Buffer | null = null;
28
-
29
- constructor(private options: ConnectionOptions) {}
30
-
31
- @trace()
32
- public async connect() {
33
- this.queue = {};
34
- this.buffer = null;
35
-
36
- await new Promise<void>((resolve, reject) => {
37
- const { ssl, connection } = this.options;
38
-
39
- this.socket = ssl
40
- ? tls.connect(
41
- {
42
- ...connection,
43
- ...ssl,
44
- ...(connection.host && !isIP(connection.host) && { servername: connection.host }),
45
- },
46
- resolve,
47
- )
48
- : net.connect(connection, resolve);
49
- this.socket.once('error', reject);
50
- });
51
- this.socket.removeAllListeners('error');
52
-
53
- this.socket.on('data', (data) => this.handleData(data));
54
- this.socket.once('close', async () => {
55
- Object.values(this.queue).forEach(({ reject }) => {
56
- reject(new ConnectionError('Socket closed unexpectedly'));
57
- });
58
- this.queue = {};
59
- });
60
- }
61
-
62
- public disconnect() {
63
- this.socket.removeAllListeners();
64
- return new Promise<void>((resolve) => {
65
- if (this.socket.pending) {
66
- return resolve();
67
- }
68
- this.socket.end(resolve);
69
- });
70
- }
71
-
72
- @trace((api, body) => ({ apiName: getApiName(api), body }))
73
- public async sendRequest<Request, Response>(api: Api<Request, Response>, body: Request): Promise<Response> {
74
- const correlationId = this.nextCorrelationId();
75
-
76
- const encoder = new Encoder()
77
- .writeInt16(api.apiKey)
78
- .writeInt16(api.apiVersion)
79
- .writeInt32(correlationId)
80
- .writeString(this.options.clientId);
81
-
82
- const request = api.request(encoder, body).value();
83
- const requestEncoder = new Encoder().writeInt32(request.length).write(request);
84
-
85
- const { responseDecoder, responseSize } = await new Promise<RawResonse>(async (resolve, reject) => {
86
- try {
87
- await this.write(requestEncoder.value());
88
- this.queue[correlationId] = { resolve, reject };
89
- } catch (error) {
90
- reject(error);
91
- }
92
- });
93
- const response = api.response(responseDecoder);
94
-
95
- assert(
96
- responseDecoder.getOffset() - 4 === responseSize,
97
- `Buffer not correctly consumed: ${responseDecoder.getOffset() - 4} !== ${responseSize}`,
98
- );
99
-
100
- return response;
101
- }
102
-
103
- private write(buffer: Buffer) {
104
- return new Promise<void>((resolve, reject) => {
105
- const { stack } = new Error('Write error');
106
- this.socket.write(buffer, (error) => {
107
- if (error) {
108
- const err = new ConnectionError(error.message);
109
- err.stack += `\n${stack}`;
110
- return reject(err);
111
- }
112
- resolve();
113
- });
114
- });
115
- }
116
-
117
- private handleData(buffer: Buffer) {
118
- this.buffer = this.buffer ? Buffer.concat([this.buffer, buffer]) : buffer;
119
- if (this.buffer.length < 4) {
120
- return;
121
- }
122
-
123
- const decoder = new Decoder(this.buffer);
124
- const size = decoder.readInt32();
125
- if (size !== decoder.getBufferLength() - 4) {
126
- return;
127
- }
128
-
129
- const correlationId = decoder.readInt32();
130
-
131
- const { resolve } = this.queue[correlationId];
132
- delete this.queue[correlationId];
133
-
134
- resolve({ responseDecoder: decoder, responseSize: size });
135
- this.buffer = null;
136
- }
137
-
138
- private nextCorrelationId() {
139
- return (this.lastCorrelationId = (this.lastCorrelationId + 1) % 2 ** 31);
140
- }
141
- }
142
-
143
- export type SendRequest = typeof Connection.prototype.sendRequest;
@@ -1,209 +0,0 @@
1
- import { API, API_ERROR } from '../api';
2
- import { KEY_TYPE } from '../api/find-coordinator';
3
- import { Assignment, MemberAssignment } from '../api/sync-group';
4
- import { Cluster } from '../cluster';
5
- import { KafkaTSApiError, KafkaTSError } from '../utils/error';
6
- import { ConsumerMetadata } from './consumer-metadata';
7
- import { OffsetManager } from './offset-manager';
8
-
9
- type ConsumerGroupOptions = {
10
- cluster: Cluster;
11
- topics: string[];
12
- groupId: string;
13
- groupInstanceId: string | null;
14
- sessionTimeoutMs: number;
15
- rebalanceTimeoutMs: number;
16
- metadata: ConsumerMetadata;
17
- offsetManager: OffsetManager;
18
- };
19
-
20
- export class ConsumerGroup {
21
- private coordinatorId = -1;
22
- private memberId = '';
23
- private generationId = -1;
24
- private leaderId = '';
25
- private memberIds: string[] = [];
26
- private heartbeatInterval: NodeJS.Timeout | null = null;
27
- private heartbeatError: KafkaTSError | null = null;
28
-
29
- constructor(private options: ConsumerGroupOptions) {}
30
-
31
- public async join() {
32
- await this.findCoordinator();
33
- await this.joinGroup();
34
- await this.syncGroup();
35
- await this.offsetFetch();
36
- this.startHeartbeater();
37
- }
38
-
39
- private async startHeartbeater() {
40
- this.heartbeatInterval = setInterval(async () => {
41
- try {
42
- await this.heartbeat();
43
- } catch (error) {
44
- this.heartbeatError = error as KafkaTSError;
45
- }
46
- }, 5000);
47
- }
48
-
49
- private async stopHeartbeater() {
50
- if (this.heartbeatInterval) {
51
- clearInterval(this.heartbeatInterval);
52
- this.heartbeatInterval = null;
53
- }
54
- }
55
-
56
- public async handleLastHeartbeat() {
57
- if (this.heartbeatError) {
58
- throw this.heartbeatError;
59
- }
60
- }
61
-
62
- private async findCoordinator() {
63
- const { coordinators } = await this.options.cluster.sendRequest(API.FIND_COORDINATOR, {
64
- keyType: KEY_TYPE.GROUP,
65
- keys: [this.options.groupId],
66
- });
67
- this.coordinatorId = coordinators[0].nodeId;
68
- }
69
-
70
- private async joinGroup(): Promise<void> {
71
- const { cluster, groupId, groupInstanceId, sessionTimeoutMs, rebalanceTimeoutMs, topics } = this.options;
72
- try {
73
- const response = await cluster.sendRequestToNode(this.coordinatorId)(API.JOIN_GROUP, {
74
- groupId,
75
- groupInstanceId,
76
- memberId: this.memberId,
77
- sessionTimeoutMs,
78
- rebalanceTimeoutMs,
79
- protocolType: 'consumer',
80
- protocols: [{ name: 'RoundRobinAssigner', metadata: { version: 0, topics } }],
81
- reason: null,
82
- });
83
- this.memberId = response.memberId;
84
- this.generationId = response.generationId;
85
- this.leaderId = response.leader;
86
- this.memberIds = response.members.map((member) => member.memberId);
87
- } catch (error) {
88
- if ((error as KafkaTSApiError).errorCode === API_ERROR.MEMBER_ID_REQUIRED) {
89
- this.memberId = (error as KafkaTSApiError).response.memberId;
90
- return this.joinGroup();
91
- }
92
- throw error;
93
- }
94
- }
95
-
96
- private async syncGroup() {
97
- const { cluster, metadata, groupId, groupInstanceId } = this.options;
98
-
99
- let assignments: MemberAssignment[] = [];
100
- if (this.memberId === this.leaderId) {
101
- const memberAssignments = Object.entries(metadata.getTopicPartitions())
102
- .flatMap(([topic, partitions]) => partitions.map((partition) => ({ topic, partition })))
103
- .reduce(
104
- (acc, { topic, partition }, index) => {
105
- const memberId = this.memberIds[index % this.memberIds.length];
106
- acc[memberId] ??= {};
107
- acc[memberId][topic] ??= [];
108
- acc[memberId][topic].push(partition);
109
- return acc;
110
- },
111
- {} as Record<string, Record<string, number[]>>,
112
- );
113
- assignments = Object.entries(memberAssignments).map(([memberId, assignment]) => ({ memberId, assignment }));
114
- }
115
-
116
- const response = await cluster.sendRequestToNode(this.coordinatorId)(API.SYNC_GROUP, {
117
- groupId,
118
- groupInstanceId,
119
- memberId: this.memberId,
120
- generationId: this.generationId,
121
- protocolType: 'consumer',
122
- protocolName: 'RoundRobinAssigner',
123
- assignments,
124
- });
125
- metadata.setAssignment(JSON.parse(response.assignments || '{}') as Assignment);
126
- }
127
-
128
- private async offsetFetch() {
129
- const { cluster, groupId, topics, metadata, offsetManager } = this.options;
130
-
131
- const assignment = metadata.getAssignment();
132
- const request = {
133
- groups: [
134
- {
135
- groupId,
136
- memberId: this.memberId,
137
- memberEpoch: -1,
138
- topics: topics
139
- .map((topic) => ({ name: topic, partitionIndexes: assignment[topic] ?? [] }))
140
- .filter(({ partitionIndexes }) => partitionIndexes.length),
141
- },
142
- ].filter(({ topics }) => topics.length),
143
- requireStable: true,
144
- };
145
- if (!request.groups.length) return;
146
-
147
- const response = await cluster.sendRequestToNode(this.coordinatorId)(API.OFFSET_FETCH, request);
148
- response.groups.forEach((group) => {
149
- group.topics.forEach((topic) => {
150
- topic.partitions
151
- .filter(({ committedOffset }) => committedOffset >= 0)
152
- .forEach(({ partitionIndex, committedOffset }) =>
153
- offsetManager.resolve(topic.name, partitionIndex, committedOffset),
154
- );
155
- });
156
- });
157
- offsetManager.flush();
158
- }
159
-
160
- public async offsetCommit() {
161
- const { cluster, groupId, groupInstanceId, offsetManager } = this.options;
162
- const request = {
163
- groupId,
164
- groupInstanceId,
165
- memberId: this.memberId,
166
- generationIdOrMemberEpoch: this.generationId,
167
- topics: Object.entries(offsetManager.pendingOffsets).map(([topic, partitions]) => ({
168
- name: topic,
169
- partitions: Object.entries(partitions).map(([partition, offset]) => ({
170
- partitionIndex: parseInt(partition),
171
- committedOffset: offset,
172
- committedLeaderEpoch: -1,
173
- committedMetadata: null,
174
- })),
175
- })),
176
- };
177
- if (!request.topics.length) {
178
- return;
179
- }
180
- await cluster.sendRequestToNode(this.coordinatorId)(API.OFFSET_COMMIT, request);
181
- offsetManager.flush();
182
- }
183
-
184
- public async heartbeat() {
185
- const { cluster, groupId, groupInstanceId } = this.options;
186
- await cluster.sendRequestToNode(this.coordinatorId)(API.HEARTBEAT, {
187
- groupId,
188
- groupInstanceId,
189
- memberId: this.memberId,
190
- generationId: this.generationId,
191
- });
192
- }
193
-
194
- public async leaveGroup() {
195
- const { cluster, groupId, groupInstanceId } = this.options;
196
- this.stopHeartbeater();
197
- try {
198
- await cluster.sendRequestToNode(this.coordinatorId)(API.LEAVE_GROUP, {
199
- groupId,
200
- members: [{ memberId: this.memberId, groupInstanceId, reason: null }],
201
- });
202
- } catch (error) {
203
- if ((error as KafkaTSApiError).errorCode === API_ERROR.FENCED_INSTANCE_ID) {
204
- return;
205
- }
206
- throw error;
207
- }
208
- }
209
- }
@@ -1,14 +0,0 @@
1
- import { Assignment } from '../api/sync-group';
2
- import { Metadata } from '../metadata';
3
-
4
- export class ConsumerMetadata extends Metadata {
5
- private assignment: Assignment = {};
6
-
7
- public getAssignment() {
8
- return this.assignment;
9
- }
10
-
11
- public setAssignment(newAssignment: Assignment) {
12
- this.assignment = newAssignment;
13
- }
14
- }