kafka-ts 0.0.1-beta
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/.prettierrc +7 -0
- package/LICENSE +24 -0
- package/README.md +88 -0
- package/certs/ca.crt +29 -0
- package/certs/ca.key +52 -0
- package/certs/ca.srl +1 -0
- package/certs/kafka.crt +29 -0
- package/certs/kafka.csr +26 -0
- package/certs/kafka.key +52 -0
- package/certs/kafka.keystore.jks +0 -0
- package/certs/kafka.truststore.jks +0 -0
- 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 +77 -0
- package/dist/api/fetch.js +106 -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 +573 -0
- package/dist/api/index.js +164 -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 +33 -0
- package/dist/api/offset-fetch.js +57 -0
- package/dist/api/produce.d.ts +53 -0
- package/dist/api/produce.js +129 -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/broker.d.ts +29 -0
- package/dist/broker.js +60 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +36 -0
- package/dist/cluster.d.ts +24 -0
- package/dist/cluster.js +72 -0
- package/dist/connection.d.ts +25 -0
- package/dist/connection.js +155 -0
- package/dist/consumer/consumer-group.d.ts +36 -0
- package/dist/consumer/consumer-group.js +182 -0
- package/dist/consumer/consumer-metadata.d.ts +7 -0
- package/dist/consumer/consumer-metadata.js +14 -0
- package/dist/consumer/consumer.d.ts +37 -0
- package/dist/consumer/consumer.js +178 -0
- package/dist/consumer/metadata.d.ts +24 -0
- package/dist/consumer/metadata.js +64 -0
- package/dist/consumer/offset-manager.d.ts +22 -0
- package/dist/consumer/offset-manager.js +56 -0
- package/dist/distributors/assignments-to-replicas.d.ts +17 -0
- package/dist/distributors/assignments-to-replicas.js +60 -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/examples/src/replicator.js +34 -0
- package/dist/examples/src/utils/json.js +5 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +19 -0
- package/dist/metadata.d.ts +24 -0
- package/dist/metadata.js +89 -0
- package/dist/producer/producer.d.ts +19 -0
- package/dist/producer/producer.js +111 -0
- package/dist/request-handler.d.ts +16 -0
- package/dist/request-handler.js +67 -0
- package/dist/request-handler.test.d.ts +1 -0
- package/dist/request-handler.test.js +340 -0
- package/dist/src/api/api-versions.js +18 -0
- package/dist/src/api/create-topics.js +46 -0
- package/dist/src/api/delete-topics.js +26 -0
- package/dist/src/api/fetch.js +95 -0
- package/dist/src/api/find-coordinator.js +34 -0
- package/dist/src/api/heartbeat.js +22 -0
- package/dist/src/api/index.js +38 -0
- package/dist/src/api/init-producer-id.js +24 -0
- package/dist/src/api/join-group.js +48 -0
- package/dist/src/api/leave-group.js +30 -0
- package/dist/src/api/list-offsets.js +39 -0
- package/dist/src/api/metadata.js +47 -0
- package/dist/src/api/offset-commit.js +39 -0
- package/dist/src/api/offset-fetch.js +44 -0
- package/dist/src/api/produce.js +119 -0
- package/dist/src/api/sync-group.js +31 -0
- package/dist/src/broker.js +35 -0
- package/dist/src/connection.js +21 -0
- package/dist/src/consumer/consumer-group.js +131 -0
- package/dist/src/consumer/consumer.js +103 -0
- package/dist/src/consumer/metadata.js +52 -0
- package/dist/src/consumer/offset-manager.js +23 -0
- package/dist/src/index.js +19 -0
- package/dist/src/producer/producer.js +84 -0
- package/dist/src/request-handler.js +57 -0
- package/dist/src/request-handler.test.js +321 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/api.js +5 -0
- package/dist/src/utils/decoder.js +161 -0
- package/dist/src/utils/encoder.js +137 -0
- package/dist/src/utils/error.js +10 -0
- package/dist/types.d.ts +9 -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/debug.d.ts +2 -0
- package/dist/utils/debug.js +11 -0
- package/dist/utils/decoder.d.ts +29 -0
- package/dist/utils/decoder.js +147 -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 +122 -0
- package/dist/utils/error.d.ts +11 -0
- package/dist/utils/error.js +27 -0
- package/dist/utils/memo.d.ts +1 -0
- package/dist/utils/memo.js +16 -0
- package/dist/utils/retrier.d.ts +10 -0
- package/dist/utils/retrier.js +22 -0
- package/dist/utils/tracer.d.ts +1 -0
- package/dist/utils/tracer.js +26 -0
- package/docker-compose.yml +104 -0
- package/examples/node_modules/.package-lock.json +22 -0
- package/examples/package-lock.json +30 -0
- package/examples/package.json +14 -0
- package/examples/src/client.ts +9 -0
- package/examples/src/consumer.ts +17 -0
- package/examples/src/create-topic.ts +37 -0
- package/examples/src/producer.ts +24 -0
- package/examples/src/replicator.ts +25 -0
- package/examples/src/utils/json.ts +1 -0
- package/examples/tsconfig.json +7 -0
- package/log4j.properties +95 -0
- package/package.json +17 -0
- package/scripts/generate-certs.sh +24 -0
- package/src/__snapshots__/request-handler.test.ts.snap +1687 -0
- package/src/api/api-versions.ts +21 -0
- package/src/api/create-topics.ts +78 -0
- package/src/api/delete-topics.ts +42 -0
- package/src/api/fetch.ts +143 -0
- package/src/api/find-coordinator.ts +39 -0
- package/src/api/heartbeat.ts +33 -0
- package/src/api/index.ts +164 -0
- package/src/api/init-producer-id.ts +35 -0
- package/src/api/join-group.ts +67 -0
- package/src/api/leave-group.ts +48 -0
- package/src/api/list-offsets.ts +65 -0
- package/src/api/metadata.ts +66 -0
- package/src/api/offset-commit.ts +67 -0
- package/src/api/offset-fetch.ts +74 -0
- package/src/api/produce.ts +173 -0
- package/src/api/sasl-authenticate.ts +21 -0
- package/src/api/sasl-handshake.ts +16 -0
- package/src/api/sync-group.ts +54 -0
- package/src/broker.ts +74 -0
- package/src/client.ts +47 -0
- package/src/cluster.ts +87 -0
- package/src/connection.ts +141 -0
- package/src/consumer/consumer-group.ts +209 -0
- package/src/consumer/consumer-metadata.ts +14 -0
- package/src/consumer/consumer.ts +229 -0
- package/src/consumer/offset-manager.ts +93 -0
- package/src/distributors/assignments-to-replicas.test.ts +43 -0
- package/src/distributors/assignments-to-replicas.ts +85 -0
- package/src/distributors/messages-to-topic-partition-leaders.test.ts +32 -0
- package/src/distributors/messages-to-topic-partition-leaders.ts +19 -0
- package/src/index.ts +3 -0
- package/src/metadata.ts +122 -0
- package/src/producer/producer.ts +132 -0
- package/src/request-handler.test.ts +366 -0
- package/src/types.ts +9 -0
- package/src/utils/api.ts +11 -0
- package/src/utils/debug.ts +9 -0
- package/src/utils/decoder.ts +168 -0
- package/src/utils/delay.ts +1 -0
- package/src/utils/encoder.ts +141 -0
- package/src/utils/error.ts +21 -0
- package/src/utils/memo.ts +12 -0
- package/src/utils/retrier.ts +39 -0
- package/src/utils/tracer.ts +28 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
4
|
+
import { API } from "./api";
|
|
5
|
+
import { KEY_TYPE } from "./api/find-coordinator";
|
|
6
|
+
import { createKafkaClient } from "./client";
|
|
7
|
+
import { Cluster } from "./cluster";
|
|
8
|
+
import { KafkaTSApiError } from "./utils/error";
|
|
9
|
+
|
|
10
|
+
export const kafka = createKafkaClient({
|
|
11
|
+
clientId: "kafkats",
|
|
12
|
+
bootstrapServers: [{ host: "localhost", port: 9092 }],
|
|
13
|
+
sasl: { mechanism: "PLAIN", username: "admin", password: "admin" },
|
|
14
|
+
ssl: { ca: readFileSync("./certs/ca.crt").toString() },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe.sequential("Request handler", () => {
|
|
18
|
+
const groupId = randomBytes(16).toString("hex");
|
|
19
|
+
|
|
20
|
+
let cluster: Cluster;
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
cluster = await kafka.createCluster().connect();
|
|
24
|
+
|
|
25
|
+
const metadataResult = await cluster.sendRequest(API.METADATA, {
|
|
26
|
+
topics: null,
|
|
27
|
+
allowTopicAutoCreation: false,
|
|
28
|
+
includeTopicAuthorizedOperations: false,
|
|
29
|
+
});
|
|
30
|
+
if (metadataResult.topics.some((topic) => topic.name === "kafkats-test-topic")) {
|
|
31
|
+
await cluster.sendRequest(API.DELETE_TOPICS, {
|
|
32
|
+
topics: [{ name: "kafkats-test-topic", topicId: null }],
|
|
33
|
+
timeoutMs: 10000,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterAll(async () => {
|
|
39
|
+
await cluster.disconnect();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should request api versions", async () => {
|
|
43
|
+
const result = await cluster.sendRequest(API.API_VERSIONS, {});
|
|
44
|
+
expect(result).toMatchSnapshot();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let topicId: string = "d6718d178e1b47c886441ad2d19faea5";
|
|
48
|
+
|
|
49
|
+
it("should create topics", async () => {
|
|
50
|
+
const result = await cluster.sendRequest(API.CREATE_TOPICS, {
|
|
51
|
+
topics: [
|
|
52
|
+
{
|
|
53
|
+
name: "kafkats-test-topic",
|
|
54
|
+
numPartitions: 1,
|
|
55
|
+
replicationFactor: 1,
|
|
56
|
+
assignments: [],
|
|
57
|
+
configs: [],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
timeoutMs: 10000,
|
|
61
|
+
validateOnly: false,
|
|
62
|
+
});
|
|
63
|
+
topicId = result.topics[0].topicId;
|
|
64
|
+
result.topics.forEach((topic) => {
|
|
65
|
+
topic.topicId = "Any<UUID>";
|
|
66
|
+
});
|
|
67
|
+
expect(result).toMatchSnapshot();
|
|
68
|
+
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should request metadata for all topics", async () => {
|
|
73
|
+
const result = await cluster.sendRequest(API.METADATA, {
|
|
74
|
+
topics: null,
|
|
75
|
+
allowTopicAutoCreation: false,
|
|
76
|
+
includeTopicAuthorizedOperations: false,
|
|
77
|
+
});
|
|
78
|
+
result.controllerId = 0;
|
|
79
|
+
result.topics.forEach((topic) => {
|
|
80
|
+
topic.topicId = "Any<UUID>";
|
|
81
|
+
topic.partitions.forEach((partition) => {
|
|
82
|
+
partition.leaderId = 0;
|
|
83
|
+
partition.isrNodes = [0];
|
|
84
|
+
partition.replicaNodes = [0];
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
expect(result).toMatchSnapshot();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let leaderId = 0;
|
|
91
|
+
|
|
92
|
+
it("should request metadata for a topic", async () => {
|
|
93
|
+
const result = await cluster.sendRequest(API.METADATA, {
|
|
94
|
+
topics: [{ id: topicId, name: "kafkats-test-topic" }],
|
|
95
|
+
allowTopicAutoCreation: false,
|
|
96
|
+
includeTopicAuthorizedOperations: false,
|
|
97
|
+
});
|
|
98
|
+
leaderId = result.topics[0].partitions[0].leaderId;
|
|
99
|
+
result.controllerId = 0;
|
|
100
|
+
result.topics.forEach((topic) => {
|
|
101
|
+
topic.topicId = "Any<UUID>";
|
|
102
|
+
topic.partitions.forEach((partition) => {
|
|
103
|
+
partition.leaderId = 0;
|
|
104
|
+
partition.isrNodes = [0];
|
|
105
|
+
partition.replicaNodes = [0];
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
expect(result).toMatchSnapshot();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should init producer id", async () => {
|
|
112
|
+
const result = await cluster.sendRequest(API.INIT_PRODUCER_ID, {
|
|
113
|
+
transactionalId: null,
|
|
114
|
+
transactionTimeoutMs: 0,
|
|
115
|
+
producerId: 0n,
|
|
116
|
+
producerEpoch: 0,
|
|
117
|
+
});
|
|
118
|
+
result.producerId = 0n;
|
|
119
|
+
expect(result).toMatchSnapshot();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should produce messages", async () => {
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const result = await cluster.sendRequestToNode(leaderId)(API.PRODUCE, {
|
|
125
|
+
transactionalId: null,
|
|
126
|
+
timeoutMs: 10000,
|
|
127
|
+
acks: 1,
|
|
128
|
+
topicData: [
|
|
129
|
+
{
|
|
130
|
+
name: "kafkats-test-topic",
|
|
131
|
+
partitionData: [
|
|
132
|
+
{
|
|
133
|
+
index: 0,
|
|
134
|
+
baseOffset: 0n,
|
|
135
|
+
partitionLeaderEpoch: 0,
|
|
136
|
+
attributes: 0,
|
|
137
|
+
baseSequence: 0,
|
|
138
|
+
baseTimestamp: BigInt(now),
|
|
139
|
+
lastOffsetDelta: 0,
|
|
140
|
+
maxTimestamp: BigInt(now),
|
|
141
|
+
producerEpoch: 0,
|
|
142
|
+
producerId: 9n,
|
|
143
|
+
records: [
|
|
144
|
+
{
|
|
145
|
+
attributes: 0,
|
|
146
|
+
offsetDelta: 0,
|
|
147
|
+
timestampDelta: 0n,
|
|
148
|
+
key: "key",
|
|
149
|
+
value: "value",
|
|
150
|
+
headers: [
|
|
151
|
+
{
|
|
152
|
+
key: "header-key",
|
|
153
|
+
value: "header-value",
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
expect(result).toMatchSnapshot();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should fetch messages", async () => {
|
|
167
|
+
const result = await cluster.sendRequestToNode(leaderId)(API.FETCH, {
|
|
168
|
+
maxWaitMs: 100,
|
|
169
|
+
minBytes: 1,
|
|
170
|
+
maxBytes: 10485760,
|
|
171
|
+
isolationLevel: 1,
|
|
172
|
+
sessionId: 0,
|
|
173
|
+
sessionEpoch: -1,
|
|
174
|
+
topics: [
|
|
175
|
+
{
|
|
176
|
+
topicId,
|
|
177
|
+
partitions: [
|
|
178
|
+
{
|
|
179
|
+
partition: 0,
|
|
180
|
+
currentLeaderEpoch: -1,
|
|
181
|
+
fetchOffset: 0n,
|
|
182
|
+
lastFetchedEpoch: 0,
|
|
183
|
+
logStartOffset: -1n,
|
|
184
|
+
partitionMaxBytes: 10485760,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
forgottenTopicsData: [],
|
|
190
|
+
rackId: "",
|
|
191
|
+
});
|
|
192
|
+
result.responses.forEach((response) => {
|
|
193
|
+
response.topicId = "Any<UUID>";
|
|
194
|
+
response.partitions.forEach((partition) => {
|
|
195
|
+
partition.records.forEach((record) => {
|
|
196
|
+
expect(record.baseTimestamp).toBeGreaterThan(1721926744730n);
|
|
197
|
+
expect(record.maxTimestamp).toBeGreaterThan(1721926744730n);
|
|
198
|
+
expect(record.crc).toBeGreaterThan(0);
|
|
199
|
+
|
|
200
|
+
record.baseTimestamp = 0n;
|
|
201
|
+
record.maxTimestamp = 0n;
|
|
202
|
+
record.crc = 0;
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
expect(result).toMatchSnapshot();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
let coordinatorId = -1;
|
|
210
|
+
|
|
211
|
+
it("should find coordinator", async () => {
|
|
212
|
+
const result = await cluster.sendRequest(API.FIND_COORDINATOR, { keyType: KEY_TYPE.GROUP, keys: [groupId] });
|
|
213
|
+
result.coordinators.forEach((coordinator) => {
|
|
214
|
+
coordinator.key = "Any<String>";
|
|
215
|
+
});
|
|
216
|
+
coordinatorId = result.coordinators[0].nodeId;
|
|
217
|
+
result.coordinators.forEach((coordinator) => {
|
|
218
|
+
coordinator.nodeId = 1;
|
|
219
|
+
coordinator.port = 9093;
|
|
220
|
+
});
|
|
221
|
+
expect(result).toMatchSnapshot();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
let memberId = "";
|
|
225
|
+
|
|
226
|
+
it("should fail join group request with new memberId", async () => {
|
|
227
|
+
try {
|
|
228
|
+
const result = await cluster.sendRequestToNode(coordinatorId)(API.JOIN_GROUP, {
|
|
229
|
+
groupId,
|
|
230
|
+
sessionTimeoutMs: 30000,
|
|
231
|
+
rebalanceTimeoutMs: 60000,
|
|
232
|
+
memberId,
|
|
233
|
+
groupInstanceId: null,
|
|
234
|
+
protocolType: "consumer",
|
|
235
|
+
protocols: [
|
|
236
|
+
{
|
|
237
|
+
name: "RoundRobinAssigner",
|
|
238
|
+
metadata: { version: 0, topics: ["kafkats-test-topic"] },
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
reason: null,
|
|
242
|
+
});
|
|
243
|
+
expect(false, "Should throw an error").toBe(true);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
const { response } = error as KafkaTSApiError;
|
|
246
|
+
memberId = response.memberId;
|
|
247
|
+
response.memberId = "Any<UUID>";
|
|
248
|
+
expect(response).toMatchSnapshot();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("should join group", async () => {
|
|
253
|
+
const result = await cluster.sendRequestToNode(coordinatorId)(API.JOIN_GROUP, {
|
|
254
|
+
groupId,
|
|
255
|
+
sessionTimeoutMs: 30000,
|
|
256
|
+
rebalanceTimeoutMs: 60000,
|
|
257
|
+
memberId,
|
|
258
|
+
groupInstanceId: null,
|
|
259
|
+
protocolType: "consumer",
|
|
260
|
+
protocols: [
|
|
261
|
+
{
|
|
262
|
+
name: "RoundRobinAssigner",
|
|
263
|
+
metadata: { version: 0, topics: ["kafkats-test-topic"] },
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
reason: null,
|
|
267
|
+
});
|
|
268
|
+
result.memberId = "Any<UUID>";
|
|
269
|
+
result.leader = "Any<UUID>";
|
|
270
|
+
result.members.forEach((member) => {
|
|
271
|
+
member.memberId = "Any<UUID>";
|
|
272
|
+
});
|
|
273
|
+
expect(result).toMatchSnapshot();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should sync group", async () => {
|
|
277
|
+
const result = await cluster.sendRequestToNode(coordinatorId)(API.SYNC_GROUP, {
|
|
278
|
+
groupId,
|
|
279
|
+
generationId: 1,
|
|
280
|
+
memberId,
|
|
281
|
+
groupInstanceId: null,
|
|
282
|
+
protocolType: "consumer",
|
|
283
|
+
protocolName: "RoundRobinAssigner",
|
|
284
|
+
assignments: [
|
|
285
|
+
{
|
|
286
|
+
memberId,
|
|
287
|
+
assignment: { "kafka-test-topic": [0] },
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
});
|
|
291
|
+
expect(result).toMatchSnapshot();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should commit offsets", async () => {
|
|
295
|
+
const result = await cluster.sendRequestToNode(coordinatorId)(API.OFFSET_COMMIT, {
|
|
296
|
+
groupId,
|
|
297
|
+
generationIdOrMemberEpoch: 1,
|
|
298
|
+
memberId,
|
|
299
|
+
groupInstanceId: null,
|
|
300
|
+
topics: [
|
|
301
|
+
{
|
|
302
|
+
name: "kafkats-test-topic",
|
|
303
|
+
partitions: [
|
|
304
|
+
{ partitionIndex: 0, committedOffset: 1n, committedLeaderEpoch: 0, committedMetadata: null },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
});
|
|
309
|
+
expect(result).toMatchSnapshot();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should fetch offsets", async () => {
|
|
313
|
+
const result = await cluster.sendRequestToNode(coordinatorId)(API.OFFSET_FETCH, {
|
|
314
|
+
groups: [
|
|
315
|
+
{
|
|
316
|
+
groupId,
|
|
317
|
+
memberId,
|
|
318
|
+
memberEpoch: 0,
|
|
319
|
+
topics: [
|
|
320
|
+
{
|
|
321
|
+
name: "kafkats-test-topic",
|
|
322
|
+
partitionIndexes: [0],
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
requireStable: false,
|
|
328
|
+
});
|
|
329
|
+
result.groups.forEach((group) => {
|
|
330
|
+
group.groupId = "Any<String>";
|
|
331
|
+
});
|
|
332
|
+
expect(result).toMatchSnapshot();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should heartbeat", async () => {
|
|
336
|
+
const result = await cluster.sendRequestToNode(coordinatorId)(API.HEARTBEAT, {
|
|
337
|
+
groupId,
|
|
338
|
+
generationId: 1,
|
|
339
|
+
memberId,
|
|
340
|
+
groupInstanceId: null,
|
|
341
|
+
});
|
|
342
|
+
expect(result).toMatchSnapshot();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should leave group", async () => {
|
|
346
|
+
const result = await cluster.sendRequestToNode(coordinatorId)(API.LEAVE_GROUP, {
|
|
347
|
+
groupId,
|
|
348
|
+
members: [{ memberId, groupInstanceId: null, reason: null }],
|
|
349
|
+
});
|
|
350
|
+
result.members.forEach((member) => {
|
|
351
|
+
member.memberId = "Any<UUID>";
|
|
352
|
+
});
|
|
353
|
+
expect(result).toMatchSnapshot();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("should delete topics", async () => {
|
|
357
|
+
const result = await cluster.sendRequest(API.DELETE_TOPICS, {
|
|
358
|
+
topics: [{ name: "kafkats-test-topic", topicId: null }],
|
|
359
|
+
timeoutMs: 10000,
|
|
360
|
+
});
|
|
361
|
+
result.responses.forEach((response) => {
|
|
362
|
+
response.topicId = "Any<UUID>";
|
|
363
|
+
});
|
|
364
|
+
expect(result).toMatchSnapshot();
|
|
365
|
+
});
|
|
366
|
+
});
|
package/src/types.ts
ADDED
package/src/utils/api.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Decoder } from "./decoder";
|
|
2
|
+
import { Encoder } from "./encoder";
|
|
3
|
+
|
|
4
|
+
export type Api<Request, Response> = {
|
|
5
|
+
apiKey: number;
|
|
6
|
+
apiVersion: number;
|
|
7
|
+
request: (encoder: Encoder, body: Request) => Encoder;
|
|
8
|
+
response: (buffer: Decoder) => Response;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const createApi = <Request, Response>(api: Api<Request, Response>) => api;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const serializer = (_: string, value: unknown) => (typeof value === "bigint" ? value.toString() : value);
|
|
2
|
+
|
|
3
|
+
export const createDebugger = (module: string) => (func: string, message: string, data?: unknown) => {
|
|
4
|
+
if (!process.env.DEBUG?.includes("kafkats")) return;
|
|
5
|
+
console.debug(
|
|
6
|
+
`[${module}] ${func}: ${message}`,
|
|
7
|
+
data && `(${data instanceof Error ? data : JSON.stringify(data, serializer, 4)})`,
|
|
8
|
+
);
|
|
9
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export class Decoder {
|
|
2
|
+
private offset = 0;
|
|
3
|
+
|
|
4
|
+
constructor(private buffer: Buffer) {}
|
|
5
|
+
|
|
6
|
+
public getOffset() {
|
|
7
|
+
return this.offset;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public getBufferLength() {
|
|
11
|
+
return this.buffer.length;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public readInt8() {
|
|
15
|
+
const value = this.buffer.readInt8(this.offset);
|
|
16
|
+
this.offset += 1;
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public readInt16() {
|
|
21
|
+
const value = this.buffer.readInt16BE(this.offset);
|
|
22
|
+
this.offset += 2;
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public readInt32() {
|
|
27
|
+
const value = this.buffer.readInt32BE(this.offset);
|
|
28
|
+
this.offset += 4;
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public readUInt32() {
|
|
33
|
+
const value = this.buffer.readUInt32BE(this.offset);
|
|
34
|
+
this.offset += 4;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public readInt64() {
|
|
39
|
+
const value = this.buffer.readBigInt64BE(this.offset);
|
|
40
|
+
this.offset += 8;
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public readUVarInt() {
|
|
45
|
+
let result = 0;
|
|
46
|
+
let shift = 0;
|
|
47
|
+
let currentByte;
|
|
48
|
+
do {
|
|
49
|
+
currentByte = this.buffer[this.offset++];
|
|
50
|
+
result |= (currentByte & 0x7f) << shift;
|
|
51
|
+
shift += 7;
|
|
52
|
+
} while ((currentByte & 0x80) !== 0);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public readVarInt() {
|
|
57
|
+
const decodedValue = this.readUVarInt();
|
|
58
|
+
return (decodedValue >>> 1) ^ -(decodedValue & 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public readUVarLong() {
|
|
62
|
+
let result = BigInt(0);
|
|
63
|
+
let shift = BigInt(0);
|
|
64
|
+
let currentByte;
|
|
65
|
+
do {
|
|
66
|
+
currentByte = BigInt(this.buffer[this.offset++]);
|
|
67
|
+
result |= (currentByte & BigInt(0x7f)) << shift;
|
|
68
|
+
shift += BigInt(7);
|
|
69
|
+
} while ((currentByte & BigInt(0x80)) !== BigInt(0));
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public readVarLong() {
|
|
74
|
+
const decodedValue = this.readUVarLong();
|
|
75
|
+
return (decodedValue >> BigInt(1)) ^ -(decodedValue & BigInt(1));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public readString() {
|
|
79
|
+
const length = this.readInt16();
|
|
80
|
+
if (length < 0) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const value = this.buffer.toString("utf-8", this.offset, this.offset + length);
|
|
85
|
+
this.offset += length;
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public readCompactString() {
|
|
90
|
+
const length = this.readUVarInt() - 1;
|
|
91
|
+
if (length < 0) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const value = this.buffer.toString("utf-8", this.offset, this.offset + length);
|
|
96
|
+
this.offset += length;
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public readVarIntString() {
|
|
101
|
+
const length = this.readVarInt();
|
|
102
|
+
if (length < 0) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const value = this.buffer.toString("utf-8", this.offset, this.offset + length);
|
|
107
|
+
this.offset += length;
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public readUUID() {
|
|
112
|
+
const value = this.buffer.toString("hex", this.offset, this.offset + 16);
|
|
113
|
+
this.offset += 16;
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public readBoolean() {
|
|
118
|
+
const value = this.buffer.readInt8(this.offset) === 1;
|
|
119
|
+
this.offset += 1;
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public readArray<T>(callback: (opts: Decoder) => T): T[] {
|
|
124
|
+
const length = this.readInt32();
|
|
125
|
+
const results = Array.from({ length }).map(() => callback(this));
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public readCompactArray<T>(callback: (opts: Decoder) => T): T[] {
|
|
130
|
+
const length = this.readUVarInt() - 1;
|
|
131
|
+
const results = Array.from({ length }).map(() => callback(this));
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public readRecords<T>(callback: (opts: Decoder) => T): T[] {
|
|
136
|
+
const length = this.readInt32();
|
|
137
|
+
|
|
138
|
+
return Array.from({ length }).map(() => {
|
|
139
|
+
const size = this.readVarInt();
|
|
140
|
+
const child = new Decoder(this.buffer.subarray(this.offset, this.offset + size));
|
|
141
|
+
this.offset += size;
|
|
142
|
+
return callback(child);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public read(length: number) {
|
|
147
|
+
const value = this.buffer.subarray(this.offset, this.offset + length);
|
|
148
|
+
this.offset += length;
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public readBytes() {
|
|
153
|
+
const length = this.readInt32();
|
|
154
|
+
return this.read(length);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public readCompactBytes() {
|
|
158
|
+
const length = this.readUVarInt() - 1;
|
|
159
|
+
if (length < 0) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return this.read(length);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public readTagBuffer() {
|
|
166
|
+
this.readUVarInt();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const delay = (delayMs: number) => new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|