kafka-ts 0.0.3-beta → 0.0.3
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 +68 -8
- package/dist/api/api-versions.d.ts +9 -0
- package/dist/api/api-versions.js +24 -0
- package/dist/api/create-topics.d.ts +38 -0
- package/dist/api/create-topics.js +53 -0
- package/dist/api/delete-topics.d.ts +18 -0
- package/dist/api/delete-topics.js +33 -0
- package/dist/api/fetch.d.ts +84 -0
- package/dist/api/fetch.js +142 -0
- package/dist/api/find-coordinator.d.ts +21 -0
- package/dist/api/find-coordinator.js +39 -0
- package/dist/api/heartbeat.d.ts +11 -0
- package/dist/api/heartbeat.js +27 -0
- package/dist/api/index.d.ts +576 -0
- package/dist/api/index.js +165 -0
- package/dist/api/init-producer-id.d.ts +13 -0
- package/dist/api/init-producer-id.js +29 -0
- package/dist/api/join-group.d.ts +34 -0
- package/dist/api/join-group.js +51 -0
- package/dist/api/leave-group.d.ts +19 -0
- package/dist/api/leave-group.js +39 -0
- package/dist/api/list-offsets.d.ts +29 -0
- package/dist/api/list-offsets.js +48 -0
- package/dist/api/metadata.d.ts +40 -0
- package/dist/api/metadata.js +58 -0
- package/dist/api/offset-commit.d.ts +28 -0
- package/dist/api/offset-commit.js +48 -0
- package/dist/api/offset-fetch.d.ts +31 -0
- package/dist/api/offset-fetch.js +55 -0
- package/dist/api/produce.d.ts +54 -0
- package/dist/api/produce.js +126 -0
- package/dist/api/sasl-authenticate.d.ts +11 -0
- package/dist/api/sasl-authenticate.js +23 -0
- package/dist/api/sasl-handshake.d.ts +6 -0
- package/dist/api/sasl-handshake.js +19 -0
- package/dist/api/sync-group.d.ts +24 -0
- package/dist/api/sync-group.js +36 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/plain.d.ts +5 -0
- package/dist/auth/plain.js +12 -0
- package/dist/auth/scram.d.ts +9 -0
- package/dist/auth/scram.js +40 -0
- package/dist/broker.d.ts +30 -0
- package/dist/broker.js +55 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +36 -0
- package/dist/cluster.d.ts +27 -0
- package/dist/cluster.js +70 -0
- package/dist/cluster.test.d.ts +1 -0
- package/dist/cluster.test.js +343 -0
- package/dist/codecs/gzip.d.ts +2 -0
- package/dist/codecs/gzip.js +8 -0
- package/dist/codecs/index.d.ts +2 -0
- package/dist/codecs/index.js +17 -0
- package/dist/codecs/none.d.ts +2 -0
- package/dist/codecs/none.js +7 -0
- package/dist/codecs/types.d.ts +5 -0
- package/dist/codecs/types.js +2 -0
- package/dist/connection.d.ts +26 -0
- package/dist/connection.js +175 -0
- package/dist/consumer/consumer-group.d.ts +41 -0
- package/dist/consumer/consumer-group.js +215 -0
- package/dist/consumer/consumer-metadata.d.ts +7 -0
- package/dist/consumer/consumer-metadata.js +14 -0
- package/dist/consumer/consumer.d.ts +44 -0
- package/dist/consumer/consumer.js +225 -0
- package/dist/consumer/fetch-manager.d.ts +33 -0
- package/dist/consumer/fetch-manager.js +140 -0
- package/dist/consumer/fetcher.d.ts +25 -0
- package/dist/consumer/fetcher.js +64 -0
- package/dist/consumer/offset-manager.d.ts +22 -0
- package/dist/consumer/offset-manager.js +66 -0
- package/dist/consumer/processor.d.ts +19 -0
- package/dist/consumer/processor.js +59 -0
- package/dist/distributors/assignments-to-replicas.d.ts +16 -0
- package/dist/distributors/assignments-to-replicas.js +59 -0
- package/dist/distributors/assignments-to-replicas.test.d.ts +1 -0
- package/dist/distributors/assignments-to-replicas.test.js +40 -0
- package/dist/distributors/messages-to-topic-partition-leaders.d.ts +17 -0
- package/dist/distributors/messages-to-topic-partition-leaders.js +15 -0
- package/dist/distributors/messages-to-topic-partition-leaders.test.d.ts +1 -0
- package/dist/distributors/messages-to-topic-partition-leaders.test.js +30 -0
- package/dist/distributors/partitioner.d.ts +7 -0
- package/dist/distributors/partitioner.js +23 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +26 -0
- package/dist/metadata.d.ts +24 -0
- package/dist/metadata.js +106 -0
- package/dist/producer/producer.d.ts +24 -0
- package/dist/producer/producer.js +131 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +2 -0
- package/dist/utils/api.d.ts +9 -0
- package/dist/utils/api.js +5 -0
- package/dist/utils/crypto.d.ts +8 -0
- package/dist/utils/crypto.js +18 -0
- package/dist/utils/decoder.d.ts +30 -0
- package/dist/utils/decoder.js +152 -0
- package/dist/utils/delay.d.ts +1 -0
- package/dist/utils/delay.js +5 -0
- package/dist/utils/encoder.d.ts +28 -0
- package/dist/utils/encoder.js +125 -0
- package/dist/utils/error.d.ts +11 -0
- package/dist/utils/error.js +27 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/memo.d.ts +1 -0
- package/dist/utils/memo.js +16 -0
- package/dist/utils/murmur2.d.ts +3 -0
- package/dist/utils/murmur2.js +40 -0
- package/dist/utils/retrier.d.ts +10 -0
- package/dist/utils/retrier.js +22 -0
- package/dist/utils/tracer.d.ts +5 -0
- package/dist/utils/tracer.js +39 -0
- package/package.json +11 -2
- package/src/__snapshots__/{request-handler.test.ts.snap → cluster.test.ts.snap} +329 -26
- package/src/api/fetch.ts +84 -29
- package/src/api/index.ts +3 -1
- package/src/api/metadata.ts +1 -1
- package/src/api/offset-commit.ts +1 -1
- package/src/api/offset-fetch.ts +1 -5
- package/src/api/produce.ts +15 -18
- package/src/auth/index.ts +2 -0
- package/src/auth/plain.ts +10 -0
- package/src/auth/scram.ts +52 -0
- package/src/broker.ts +7 -9
- package/src/client.ts +2 -2
- package/src/cluster.test.ts +16 -14
- package/src/cluster.ts +38 -40
- package/src/codecs/gzip.ts +9 -0
- package/src/codecs/index.ts +16 -0
- package/src/codecs/none.ts +6 -0
- package/src/codecs/types.ts +4 -0
- package/src/connection.ts +31 -17
- package/src/consumer/consumer-group.ts +43 -23
- package/src/consumer/consumer.ts +64 -43
- package/src/consumer/fetch-manager.ts +43 -53
- package/src/consumer/fetcher.ts +20 -13
- package/src/consumer/offset-manager.ts +18 -7
- package/src/consumer/processor.ts +14 -8
- package/src/distributors/assignments-to-replicas.ts +1 -3
- package/src/distributors/partitioner.ts +27 -0
- package/src/index.ts +7 -2
- package/src/metadata.ts +4 -0
- package/src/producer/producer.ts +22 -12
- package/src/types.ts +3 -3
- package/src/utils/api.ts +1 -1
- package/src/utils/crypto.ts +15 -0
- package/src/utils/decoder.ts +11 -5
- package/src/utils/encoder.ts +29 -22
- package/src/utils/logger.ts +37 -0
- package/src/utils/murmur2.ts +44 -0
- package/src/utils/tracer.ts +40 -22
- package/.github/workflows/release.yml +0 -17
- package/certs/ca.crt +0 -29
- package/certs/ca.key +0 -52
- package/certs/ca.srl +0 -1
- package/certs/kafka.crt +0 -29
- package/certs/kafka.csr +0 -26
- package/certs/kafka.key +0 -52
- package/certs/kafka.keystore.jks +0 -0
- package/certs/kafka.truststore.jks +0 -0
- package/docker-compose.yml +0 -104
- package/examples/package-lock.json +0 -31
- package/examples/package.json +0 -14
- package/examples/src/client.ts +0 -9
- package/examples/src/consumer.ts +0 -18
- package/examples/src/create-topic.ts +0 -44
- package/examples/src/producer.ts +0 -24
- package/examples/src/replicator.ts +0 -25
- package/examples/src/utils/delay.ts +0 -1
- package/examples/src/utils/json.ts +0 -1
- package/examples/tsconfig.json +0 -7
- package/log4j.properties +0 -95
- package/scripts/generate-certs.sh +0 -24
- package/src/utils/debug.ts +0 -9
package/src/api/index.ts
CHANGED
|
@@ -37,7 +37,9 @@ export const API = {
|
|
|
37
37
|
SYNC_GROUP,
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
const apiNameByKey = Object.fromEntries(Object.entries(API).map(([k, v]) => [v.apiKey, k]));
|
|
41
|
+
|
|
42
|
+
export const getApiName = <Request, Response>(api: Api<Request, Response>) => apiNameByKey[api.apiKey];
|
|
41
43
|
|
|
42
44
|
export const API_ERROR = {
|
|
43
45
|
UNKNOWN_SERVER_ERROR: -1,
|
package/src/api/metadata.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createApi } from '../utils/api';
|
|
2
2
|
import { KafkaTSApiError } from '../utils/error';
|
|
3
3
|
|
|
4
|
-
export type Metadata = ReturnType<(typeof METADATA)['response']
|
|
4
|
+
export type Metadata = Awaited<ReturnType<(typeof METADATA)['response']>>;
|
|
5
5
|
|
|
6
6
|
export const METADATA = createApi({
|
|
7
7
|
apiKey: 3,
|
package/src/api/offset-commit.ts
CHANGED
package/src/api/offset-fetch.ts
CHANGED
|
@@ -3,14 +3,12 @@ import { KafkaTSApiError } from '../utils/error';
|
|
|
3
3
|
|
|
4
4
|
export const OFFSET_FETCH = createApi({
|
|
5
5
|
apiKey: 9,
|
|
6
|
-
apiVersion:
|
|
6
|
+
apiVersion: 8,
|
|
7
7
|
request: (
|
|
8
8
|
encoder,
|
|
9
9
|
data: {
|
|
10
10
|
groups: {
|
|
11
11
|
groupId: string;
|
|
12
|
-
memberId: string | null;
|
|
13
|
-
memberEpoch: number;
|
|
14
12
|
topics: {
|
|
15
13
|
name: string;
|
|
16
14
|
partitionIndexes: number[];
|
|
@@ -24,8 +22,6 @@ export const OFFSET_FETCH = createApi({
|
|
|
24
22
|
.writeCompactArray(data.groups, (encoder, group) =>
|
|
25
23
|
encoder
|
|
26
24
|
.writeCompactString(group.groupId)
|
|
27
|
-
.writeCompactString(group.memberId)
|
|
28
|
-
.writeInt32(group.memberEpoch)
|
|
29
25
|
.writeCompactArray(group.topics, (encoder, topic) =>
|
|
30
26
|
encoder
|
|
31
27
|
.writeCompactString(topic.name)
|
package/src/api/produce.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { KafkaTSApiError } from '../utils/error.js';
|
|
|
4
4
|
|
|
5
5
|
export const PRODUCE = createApi({
|
|
6
6
|
apiKey: 0,
|
|
7
|
-
apiVersion:
|
|
7
|
+
apiVersion: 9,
|
|
8
8
|
request: (
|
|
9
9
|
encoder,
|
|
10
10
|
data: {
|
|
@@ -28,11 +28,11 @@ export const PRODUCE = createApi({
|
|
|
28
28
|
attributes: number;
|
|
29
29
|
timestampDelta: bigint;
|
|
30
30
|
offsetDelta: number;
|
|
31
|
-
key:
|
|
32
|
-
value:
|
|
31
|
+
key: Buffer | null;
|
|
32
|
+
value: Buffer | null;
|
|
33
33
|
headers: {
|
|
34
|
-
key:
|
|
35
|
-
value:
|
|
34
|
+
key: Buffer;
|
|
35
|
+
value: Buffer;
|
|
36
36
|
}[];
|
|
37
37
|
}[];
|
|
38
38
|
}[];
|
|
@@ -61,14 +61,13 @@ export const PRODUCE = createApi({
|
|
|
61
61
|
.writeInt8(record.attributes)
|
|
62
62
|
.writeVarLong(record.timestampDelta)
|
|
63
63
|
.writeVarInt(record.offsetDelta)
|
|
64
|
-
.
|
|
65
|
-
.
|
|
64
|
+
.writeVarIntBuffer(record.key)
|
|
65
|
+
.writeVarIntBuffer(record.value)
|
|
66
66
|
.writeVarIntArray(record.headers, (encoder, header) =>
|
|
67
|
-
encoder.
|
|
68
|
-
)
|
|
69
|
-
.value();
|
|
67
|
+
encoder.writeVarIntBuffer(header.key).writeVarIntBuffer(header.value),
|
|
68
|
+
);
|
|
70
69
|
|
|
71
|
-
return encoder.writeVarInt(recordBody.
|
|
70
|
+
return encoder.writeVarInt(recordBody.getByteLength()).writeEncoder(recordBody);
|
|
72
71
|
})
|
|
73
72
|
.value();
|
|
74
73
|
|
|
@@ -76,19 +75,17 @@ export const PRODUCE = createApi({
|
|
|
76
75
|
.writeInt32(partition.partitionLeaderEpoch)
|
|
77
76
|
.writeInt8(2) // magic byte
|
|
78
77
|
.writeUInt32(unsigned(crc32C(batchBody)))
|
|
79
|
-
.write(batchBody)
|
|
80
|
-
.value();
|
|
78
|
+
.write(batchBody);
|
|
81
79
|
|
|
82
80
|
const batch = new Encoder()
|
|
83
81
|
.writeInt64(partition.baseOffset)
|
|
84
|
-
.writeInt32(batchHeader.
|
|
85
|
-
.
|
|
86
|
-
.value();
|
|
82
|
+
.writeInt32(batchHeader.getByteLength())
|
|
83
|
+
.writeEncoder(batchHeader);
|
|
87
84
|
|
|
88
85
|
return encoder
|
|
89
86
|
.writeInt32(partition.index)
|
|
90
|
-
.writeUVarInt(batch.
|
|
91
|
-
.
|
|
87
|
+
.writeUVarInt(batch.getByteLength() + 1) // batch size
|
|
88
|
+
.writeEncoder(batch)
|
|
92
89
|
.writeUVarInt(0);
|
|
93
90
|
})
|
|
94
91
|
.writeUVarInt(0),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { API } from "../api";
|
|
2
|
+
import { SASLProvider } from "../broker";
|
|
3
|
+
|
|
4
|
+
export const saslPlain = ({ username, password }: { username: string; password: string }): SASLProvider => ({
|
|
5
|
+
mechanism: 'PLAIN',
|
|
6
|
+
authenticate: async ({ sendRequest }) => {
|
|
7
|
+
const authBytes = [null, username, password].join('\u0000');
|
|
8
|
+
await sendRequest(API.SASL_AUTHENTICATE, { authBytes: Buffer.from(authBytes) });
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { API } from '../api';
|
|
2
|
+
import { SASLProvider } from '../broker';
|
|
3
|
+
import { base64Decode, base64Encode, generateNonce, hash, hmac, saltPassword, xor } from '../utils/crypto';
|
|
4
|
+
import { KafkaTSError } from '../utils/error';
|
|
5
|
+
|
|
6
|
+
const saslScram =
|
|
7
|
+
({ mechanism, keyLength, digest }: { mechanism: string; keyLength: number; digest: string }) =>
|
|
8
|
+
({ username, password }: { username: string; password: string }): SASLProvider => ({
|
|
9
|
+
mechanism,
|
|
10
|
+
authenticate: async ({ sendRequest }) => {
|
|
11
|
+
const nonce = generateNonce();
|
|
12
|
+
const firstMessage = `n=${username},r=${nonce}`;
|
|
13
|
+
|
|
14
|
+
const { authBytes } = await sendRequest(API.SASL_AUTHENTICATE, {
|
|
15
|
+
authBytes: Buffer.from(`n,,${firstMessage}`),
|
|
16
|
+
});
|
|
17
|
+
if (!authBytes) {
|
|
18
|
+
throw new KafkaTSError('No auth response');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const response = Object.fromEntries(
|
|
22
|
+
authBytes
|
|
23
|
+
.toString()
|
|
24
|
+
.split(',')
|
|
25
|
+
.map((pair) => pair.split('=')),
|
|
26
|
+
) as { r: string; s: string; i: string };
|
|
27
|
+
|
|
28
|
+
const rnonce = response.r;
|
|
29
|
+
if (!rnonce.startsWith(nonce)) {
|
|
30
|
+
throw new KafkaTSError('Invalid nonce');
|
|
31
|
+
}
|
|
32
|
+
const iterations = parseInt(response.i);
|
|
33
|
+
const salt = base64Decode(response.s);
|
|
34
|
+
|
|
35
|
+
const saltedPassword = await saltPassword(password, salt, iterations, keyLength, digest);
|
|
36
|
+
const clientKey = hmac(saltedPassword, 'Client Key', digest);
|
|
37
|
+
const clientKeyHash = hash(clientKey, digest);
|
|
38
|
+
|
|
39
|
+
let finalMessage = `c=${base64Encode('n,,')},r=${rnonce}`;
|
|
40
|
+
|
|
41
|
+
const fullMessage = `${firstMessage},${authBytes.toString()},${finalMessage}`;
|
|
42
|
+
const clientSignature = hmac(clientKeyHash, fullMessage, digest);
|
|
43
|
+
const clientProof = base64Encode(xor(clientKey, clientSignature));
|
|
44
|
+
|
|
45
|
+
finalMessage += `,p=${clientProof}`;
|
|
46
|
+
|
|
47
|
+
await sendRequest(API.SASL_AUTHENTICATE, { authBytes: Buffer.from(finalMessage) });
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const saslScramSha256 = saslScram({ mechanism: 'SCRAM-SHA-256', keyLength: 32, digest: 'sha256' });
|
|
52
|
+
export const saslScramSha512 = saslScram({ mechanism: 'SCRAM-SHA-512', keyLength: 64, digest: 'sha512' });
|
package/src/broker.ts
CHANGED
|
@@ -5,12 +5,15 @@ import { Connection, SendRequest } from './connection';
|
|
|
5
5
|
import { KafkaTSError } from './utils/error';
|
|
6
6
|
import { memo } from './utils/memo';
|
|
7
7
|
|
|
8
|
-
export type
|
|
8
|
+
export type SASLProvider = {
|
|
9
|
+
mechanism: string;
|
|
10
|
+
authenticate: (context: { sendRequest: SendRequest }) => Promise<void>;
|
|
11
|
+
};
|
|
9
12
|
|
|
10
13
|
type BrokerOptions = {
|
|
11
14
|
clientId: string | null;
|
|
12
15
|
options: TcpSocketConnectOpts;
|
|
13
|
-
sasl:
|
|
16
|
+
sasl: SASLProvider | null;
|
|
14
17
|
ssl: TLSSocketOptions | null;
|
|
15
18
|
};
|
|
16
19
|
|
|
@@ -51,7 +54,7 @@ export class Broker {
|
|
|
51
54
|
}
|
|
52
55
|
const { apiVersion } = apiByKey[apiKey];
|
|
53
56
|
if (apiVersion < minVersion || apiVersion > maxVersion) {
|
|
54
|
-
throw new KafkaTSError(`API ${apiKey} version ${apiVersion} is not supported by the broker`);
|
|
57
|
+
throw new KafkaTSError(`API ${apiKey} version ${apiVersion} is not supported by the broker (minVersion=${minVersion}, maxVersion=${maxVersion})`);
|
|
55
58
|
}
|
|
56
59
|
});
|
|
57
60
|
}
|
|
@@ -64,11 +67,6 @@ export class Broker {
|
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
private async saslAuthenticate() {
|
|
67
|
-
|
|
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) });
|
|
70
|
+
await this.options.sasl?.authenticate({ sendRequest: this.sendRequest });
|
|
73
71
|
}
|
|
74
72
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TcpSocketConnectOpts } from 'net';
|
|
2
2
|
import { TLSSocketOptions } from 'tls';
|
|
3
|
-
import {
|
|
3
|
+
import { SASLProvider } from './broker';
|
|
4
4
|
import { Cluster } from './cluster';
|
|
5
5
|
import { Consumer, ConsumerOptions } from './consumer/consumer';
|
|
6
6
|
import { Producer, ProducerOptions } from './producer/producer';
|
|
@@ -8,7 +8,7 @@ import { Producer, ProducerOptions } from './producer/producer';
|
|
|
8
8
|
type ClientOptions = {
|
|
9
9
|
clientId?: string | null;
|
|
10
10
|
bootstrapServers: TcpSocketConnectOpts[];
|
|
11
|
-
sasl?:
|
|
11
|
+
sasl?: SASLProvider | null;
|
|
12
12
|
ssl?: TLSSocketOptions | null;
|
|
13
13
|
};
|
|
14
14
|
|
package/src/cluster.test.ts
CHANGED
|
@@ -3,24 +3,26 @@ import { readFileSync } from 'fs';
|
|
|
3
3
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
4
4
|
import { API } from './api';
|
|
5
5
|
import { KEY_TYPE } from './api/find-coordinator';
|
|
6
|
+
import { saslPlain } from './auth';
|
|
6
7
|
import { createKafkaClient } from './client';
|
|
7
8
|
import { Cluster } from './cluster';
|
|
8
9
|
import { KafkaTSApiError } from './utils/error';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const kafka = createKafkaClient({
|
|
11
12
|
clientId: 'kafka-ts',
|
|
12
13
|
bootstrapServers: [{ host: 'localhost', port: 9092 }],
|
|
13
|
-
sasl: {
|
|
14
|
+
sasl: saslPlain({ username: 'admin', password: 'admin' }),
|
|
14
15
|
ssl: { ca: readFileSync('./certs/ca.crt').toString() },
|
|
15
16
|
});
|
|
16
17
|
|
|
17
|
-
describe.sequential('
|
|
18
|
+
describe.sequential('Low-level API', () => {
|
|
18
19
|
const groupId = randomBytes(16).toString('hex');
|
|
19
20
|
|
|
20
21
|
let cluster: Cluster;
|
|
21
22
|
|
|
22
23
|
beforeAll(async () => {
|
|
23
|
-
cluster = await kafka.createCluster()
|
|
24
|
+
cluster = await kafka.createCluster();
|
|
25
|
+
await cluster.connect();
|
|
24
26
|
|
|
25
27
|
const metadataResult = await cluster.sendRequest(API.METADATA, {
|
|
26
28
|
topics: null,
|
|
@@ -51,8 +53,8 @@ describe.sequential('Request handler', () => {
|
|
|
51
53
|
topics: [
|
|
52
54
|
{
|
|
53
55
|
name: 'kafka-ts-test-topic',
|
|
54
|
-
numPartitions:
|
|
55
|
-
replicationFactor:
|
|
56
|
+
numPartitions: 10,
|
|
57
|
+
replicationFactor: 3,
|
|
56
58
|
assignments: [],
|
|
57
59
|
configs: [],
|
|
58
60
|
},
|
|
@@ -88,6 +90,7 @@ describe.sequential('Request handler', () => {
|
|
|
88
90
|
expect(result).toMatchSnapshot();
|
|
89
91
|
});
|
|
90
92
|
|
|
93
|
+
let partitionIndex = 0;
|
|
91
94
|
let leaderId = 0;
|
|
92
95
|
|
|
93
96
|
it('should request metadata for a topic', async () => {
|
|
@@ -96,6 +99,7 @@ describe.sequential('Request handler', () => {
|
|
|
96
99
|
allowTopicAutoCreation: false,
|
|
97
100
|
includeTopicAuthorizedOperations: false,
|
|
98
101
|
});
|
|
102
|
+
partitionIndex = result.topics[0].partitions[0].partitionIndex;
|
|
99
103
|
leaderId = result.topics[0].partitions[0].leaderId;
|
|
100
104
|
result.controllerId = 0;
|
|
101
105
|
result.topics.forEach((topic) => {
|
|
@@ -133,7 +137,7 @@ describe.sequential('Request handler', () => {
|
|
|
133
137
|
name: 'kafka-ts-test-topic',
|
|
134
138
|
partitionData: [
|
|
135
139
|
{
|
|
136
|
-
index:
|
|
140
|
+
index: partitionIndex,
|
|
137
141
|
baseOffset: 0n,
|
|
138
142
|
partitionLeaderEpoch: 0,
|
|
139
143
|
attributes: 0,
|
|
@@ -148,12 +152,12 @@ describe.sequential('Request handler', () => {
|
|
|
148
152
|
attributes: 0,
|
|
149
153
|
offsetDelta: 0,
|
|
150
154
|
timestampDelta: 0n,
|
|
151
|
-
key: 'key',
|
|
152
|
-
value: 'value',
|
|
155
|
+
key: Buffer.from('key'),
|
|
156
|
+
value: Buffer.from('value'),
|
|
153
157
|
headers: [
|
|
154
158
|
{
|
|
155
|
-
key: 'header-key',
|
|
156
|
-
value: 'header-value',
|
|
159
|
+
key: Buffer.from('header-key'),
|
|
160
|
+
value: Buffer.from('header-value'),
|
|
157
161
|
},
|
|
158
162
|
],
|
|
159
163
|
},
|
|
@@ -179,7 +183,7 @@ describe.sequential('Request handler', () => {
|
|
|
179
183
|
topicId,
|
|
180
184
|
partitions: [
|
|
181
185
|
{
|
|
182
|
-
partition:
|
|
186
|
+
partition: partitionIndex,
|
|
183
187
|
currentLeaderEpoch: -1,
|
|
184
188
|
fetchOffset: 0n,
|
|
185
189
|
lastFetchedEpoch: 0,
|
|
@@ -317,8 +321,6 @@ describe.sequential('Request handler', () => {
|
|
|
317
321
|
groups: [
|
|
318
322
|
{
|
|
319
323
|
groupId,
|
|
320
|
-
memberId,
|
|
321
|
-
memberEpoch: 0,
|
|
322
324
|
topics: [
|
|
323
325
|
{
|
|
324
326
|
name: 'kafka-ts-test-topic',
|
package/src/cluster.ts
CHANGED
|
@@ -1,85 +1,83 @@
|
|
|
1
1
|
import { TcpSocketConnectOpts } from 'net';
|
|
2
2
|
import { TLSSocketOptions } from 'tls';
|
|
3
3
|
import { API } from './api';
|
|
4
|
-
import {
|
|
4
|
+
import { Metadata } from './api/metadata';
|
|
5
|
+
import { Broker, SASLProvider } from './broker';
|
|
5
6
|
import { SendRequest } from './connection';
|
|
6
|
-
import {
|
|
7
|
+
import { KafkaTSError } from './utils/error';
|
|
8
|
+
import { log } from './utils/logger';
|
|
7
9
|
|
|
8
10
|
type ClusterOptions = {
|
|
9
11
|
clientId: string | null;
|
|
10
12
|
bootstrapServers: TcpSocketConnectOpts[];
|
|
11
|
-
sasl:
|
|
13
|
+
sasl: SASLProvider | null;
|
|
12
14
|
ssl: TLSSocketOptions | null;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export class Cluster {
|
|
16
|
-
private seedBroker:
|
|
18
|
+
private seedBroker = new Broker({ clientId: null, sasl: null, ssl: null, options: { port: 9092 } });
|
|
17
19
|
private brokerById: Record<number, Broker> = {};
|
|
20
|
+
private brokerMetadata: Record<number, Metadata['brokers'][number]> = {};
|
|
18
21
|
|
|
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
|
-
}
|
|
22
|
+
constructor(private options: ClusterOptions) {}
|
|
27
23
|
|
|
28
24
|
public async connect() {
|
|
29
|
-
await this.
|
|
25
|
+
this.seedBroker = await this.findSeedBroker();
|
|
26
|
+
this.brokerById = {};
|
|
27
|
+
|
|
30
28
|
const metadata = await this.sendRequest(API.METADATA, {
|
|
31
29
|
allowTopicAutoCreation: false,
|
|
32
30
|
includeTopicAuthorizedOperations: false,
|
|
33
31
|
topics: [],
|
|
34
32
|
});
|
|
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;
|
|
33
|
+
this.brokerMetadata = Object.fromEntries(metadata.brokers.map((options) => [options.nodeId, options]));
|
|
48
34
|
}
|
|
49
35
|
|
|
50
36
|
public async disconnect() {
|
|
51
|
-
await Promise.all([
|
|
52
|
-
this.seedBroker.disconnect(),
|
|
53
|
-
...Object.values(this.brokerById).map((broker) => broker.disconnect()),
|
|
54
|
-
]);
|
|
37
|
+
await Promise.all([this.seedBroker.disconnect(), ...Object.values(this.brokerById).map((x) => x.disconnect())]);
|
|
55
38
|
}
|
|
56
39
|
|
|
40
|
+
public setSeedBroker = async (nodeId: number) => {
|
|
41
|
+
await this.seedBroker.disconnect();
|
|
42
|
+
this.seedBroker = await this.acquireBroker(nodeId);
|
|
43
|
+
};
|
|
44
|
+
|
|
57
45
|
public sendRequest: SendRequest = (...args) => this.seedBroker.sendRequest(...args);
|
|
58
46
|
|
|
59
47
|
public sendRequestToNode =
|
|
60
48
|
(nodeId: number): SendRequest =>
|
|
61
49
|
async (...args) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
throw new ConnectionError(`Broker ${nodeId} is not available`);
|
|
50
|
+
if (!this.brokerById[nodeId]) {
|
|
51
|
+
this.brokerById[nodeId] = await this.acquireBroker(nodeId);
|
|
65
52
|
}
|
|
66
|
-
|
|
67
|
-
return broker.sendRequest(...args);
|
|
53
|
+
return this.brokerById[nodeId].sendRequest(...args);
|
|
68
54
|
};
|
|
69
55
|
|
|
70
|
-
|
|
56
|
+
public async acquireBroker(nodeId: number) {
|
|
57
|
+
const broker = new Broker({
|
|
58
|
+
clientId: this.options.clientId,
|
|
59
|
+
sasl: this.options.sasl,
|
|
60
|
+
ssl: this.options.ssl,
|
|
61
|
+
options: this.brokerMetadata[nodeId],
|
|
62
|
+
});
|
|
63
|
+
await broker.connect();
|
|
64
|
+
return broker;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async findSeedBroker() {
|
|
71
68
|
const randomizedBrokers = this.options.bootstrapServers.toSorted(() => Math.random() - 0.5);
|
|
72
69
|
for (const options of randomizedBrokers) {
|
|
73
70
|
try {
|
|
74
|
-
|
|
71
|
+
const broker = await new Broker({
|
|
75
72
|
clientId: this.options.clientId,
|
|
76
73
|
sasl: this.options.sasl,
|
|
77
74
|
ssl: this.options.ssl,
|
|
78
75
|
options,
|
|
79
|
-
})
|
|
80
|
-
|
|
76
|
+
});
|
|
77
|
+
await broker.connect();
|
|
78
|
+
return broker;
|
|
81
79
|
} catch (error) {
|
|
82
|
-
|
|
80
|
+
log.warn(`Failed to connect to seed broker ${options.host}:${options.port}`, error);
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
throw new KafkaTSError('No seed brokers found');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { gzip, unzip } from 'zlib';
|
|
2
|
+
import { Codec } from './types';
|
|
3
|
+
|
|
4
|
+
export const GZIP: Codec = {
|
|
5
|
+
compress: async (data) =>
|
|
6
|
+
new Promise<Buffer>((resolve, reject) => gzip(data, (err, result) => (err ? reject(err) : resolve(result)))),
|
|
7
|
+
decompress: async (data) =>
|
|
8
|
+
new Promise<Buffer>((resolve, reject) => unzip(data, (err, result) => (err ? reject(err) : resolve(result)))),
|
|
9
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { GZIP } from './gzip';
|
|
2
|
+
import { NONE } from './none';
|
|
3
|
+
import { Codec } from './types';
|
|
4
|
+
|
|
5
|
+
const codecs: Record<number, Codec> = {
|
|
6
|
+
0: NONE,
|
|
7
|
+
1: GZIP,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const findCodec = (type: number) => {
|
|
11
|
+
const codec = codecs[type];
|
|
12
|
+
if (!codec) {
|
|
13
|
+
throw new Error(`Unsupported codec: ${type}`);
|
|
14
|
+
}
|
|
15
|
+
return codec;
|
|
16
|
+
};
|
package/src/connection.ts
CHANGED
|
@@ -6,11 +6,12 @@ import { Api } from './utils/api';
|
|
|
6
6
|
import { Decoder } from './utils/decoder';
|
|
7
7
|
import { Encoder } from './utils/encoder';
|
|
8
8
|
import { ConnectionError } from './utils/error';
|
|
9
|
+
import { log } from './utils/logger';
|
|
9
10
|
import { createTracer } from './utils/tracer';
|
|
10
11
|
|
|
11
12
|
const trace = createTracer('Connection');
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
type ConnectionOptions = {
|
|
14
15
|
clientId: string | null;
|
|
15
16
|
connection: TcpSocketConnectOpts;
|
|
16
17
|
ssl: TLSSocketOptions | null;
|
|
@@ -24,14 +25,14 @@ export class Connection {
|
|
|
24
25
|
[correlationId: number]: { resolve: (response: RawResonse) => void; reject: (error: Error) => void };
|
|
25
26
|
} = {};
|
|
26
27
|
private lastCorrelationId = 0;
|
|
27
|
-
private
|
|
28
|
+
private chunks: Buffer[] = [];
|
|
28
29
|
|
|
29
30
|
constructor(private options: ConnectionOptions) {}
|
|
30
31
|
|
|
31
32
|
@trace()
|
|
32
33
|
public async connect() {
|
|
33
34
|
this.queue = {};
|
|
34
|
-
this.
|
|
35
|
+
this.chunks = [];
|
|
35
36
|
|
|
36
37
|
await new Promise<void>((resolve, reject) => {
|
|
37
38
|
const { ssl, connection } = this.options;
|
|
@@ -59,6 +60,7 @@ export class Connection {
|
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
@trace()
|
|
62
64
|
public disconnect() {
|
|
63
65
|
this.socket.removeAllListeners();
|
|
64
66
|
return new Promise<void>((resolve) => {
|
|
@@ -69,9 +71,10 @@ export class Connection {
|
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
@trace((api, body) => ({
|
|
74
|
+
@trace((api, body) => ({ message: getApiName(api), body }))
|
|
73
75
|
public async sendRequest<Request, Response>(api: Api<Request, Response>, body: Request): Promise<Response> {
|
|
74
76
|
const correlationId = this.nextCorrelationId();
|
|
77
|
+
const apiName = getApiName(api);
|
|
75
78
|
|
|
76
79
|
const encoder = new Encoder()
|
|
77
80
|
.writeInt16(api.apiKey)
|
|
@@ -79,18 +82,25 @@ export class Connection {
|
|
|
79
82
|
.writeInt32(correlationId)
|
|
80
83
|
.writeString(this.options.clientId);
|
|
81
84
|
|
|
82
|
-
const request = api.request(encoder, body)
|
|
83
|
-
const requestEncoder = new Encoder().writeInt32(request.
|
|
85
|
+
const request = api.request(encoder, body);
|
|
86
|
+
const requestEncoder = new Encoder().writeInt32(request.getByteLength()).writeEncoder(request);
|
|
84
87
|
|
|
88
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
85
89
|
const { responseDecoder, responseSize } = await new Promise<RawResonse>(async (resolve, reject) => {
|
|
90
|
+
timeout = setTimeout(() => {
|
|
91
|
+
delete this.queue[correlationId];
|
|
92
|
+
reject(new ConnectionError(`${apiName} timed out`));
|
|
93
|
+
}, 30_000);
|
|
94
|
+
|
|
86
95
|
try {
|
|
87
|
-
await this.write(requestEncoder.value());
|
|
88
96
|
this.queue[correlationId] = { resolve, reject };
|
|
97
|
+
await this.write(requestEncoder.value());
|
|
89
98
|
} catch (error) {
|
|
90
99
|
reject(error);
|
|
91
100
|
}
|
|
92
101
|
});
|
|
93
|
-
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
const response = await api.response(responseDecoder);
|
|
94
104
|
|
|
95
105
|
assert(
|
|
96
106
|
responseDecoder.getOffset() - 4 === responseSize,
|
|
@@ -115,12 +125,13 @@ export class Connection {
|
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
private handleData(buffer: Buffer) {
|
|
118
|
-
this.
|
|
119
|
-
|
|
128
|
+
this.chunks.push(buffer);
|
|
129
|
+
|
|
130
|
+
const decoder = new Decoder(Buffer.concat(this.chunks));
|
|
131
|
+
if (decoder.getBufferLength() < 4) {
|
|
120
132
|
return;
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
const decoder = new Decoder(this.buffer);
|
|
124
135
|
const size = decoder.readInt32();
|
|
125
136
|
if (size !== decoder.getBufferLength() - 4) {
|
|
126
137
|
return;
|
|
@@ -128,15 +139,18 @@ export class Connection {
|
|
|
128
139
|
|
|
129
140
|
const correlationId = decoder.readInt32();
|
|
130
141
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
const context = this.queue[correlationId];
|
|
143
|
+
if (context) {
|
|
144
|
+
delete this.queue[correlationId];
|
|
145
|
+
context.resolve({ responseDecoder: decoder, responseSize: size });
|
|
146
|
+
} else {
|
|
147
|
+
log.debug('Could not find pending request for correlationId', { correlationId });
|
|
148
|
+
}
|
|
149
|
+
this.chunks = [];
|
|
136
150
|
}
|
|
137
151
|
|
|
138
152
|
private nextCorrelationId() {
|
|
139
|
-
return
|
|
153
|
+
return this.lastCorrelationId++;
|
|
140
154
|
}
|
|
141
155
|
}
|
|
142
156
|
|