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,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMetadata = void 0;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const createMetadata = ({ cluster, topics, isolationLevel = 0 /* IsolationLevel.READ_UNCOMMITTED */, allowTopicAutoCreation = true, fromBeginning = false, offsetManager, }) => {
|
|
6
|
+
let topicPartitions = {};
|
|
7
|
+
let topicNameById = {};
|
|
8
|
+
let topicIdByName = {};
|
|
9
|
+
let leaderIdByTopicPartition = {};
|
|
10
|
+
let isrNodesByTopicPartition;
|
|
11
|
+
let assignment = {};
|
|
12
|
+
const fetchMetadata = async () => {
|
|
13
|
+
const response = await cluster.sendRequest(api_1.API.METADATA, {
|
|
14
|
+
allowTopicAutoCreation,
|
|
15
|
+
includeTopicAuthorizedOperations: false,
|
|
16
|
+
topics: topics?.map((name) => ({ id: null, name })) ?? null,
|
|
17
|
+
});
|
|
18
|
+
topicPartitions = Object.fromEntries(response.topics.map((topic) => [topic.name, topic.partitions.map((partition) => partition.partitionIndex)]));
|
|
19
|
+
topicNameById = Object.fromEntries(response.topics.map((topic) => [topic.topicId, topic.name]));
|
|
20
|
+
topicIdByName = Object.fromEntries(response.topics.map((topic) => [topic.name, topic.topicId]));
|
|
21
|
+
leaderIdByTopicPartition = Object.fromEntries(response.topics.map((topic) => [
|
|
22
|
+
topic.name,
|
|
23
|
+
Object.fromEntries(topic.partitions.map((partition) => [partition.partitionIndex, partition.leaderId])),
|
|
24
|
+
]));
|
|
25
|
+
isrNodesByTopicPartition = Object.fromEntries(response.topics.map((topic) => [
|
|
26
|
+
topic.name,
|
|
27
|
+
Object.fromEntries(topic.partitions.map((partition) => [partition.partitionIndex, partition.isrNodes])),
|
|
28
|
+
]));
|
|
29
|
+
assignment = topicPartitions;
|
|
30
|
+
};
|
|
31
|
+
const listOffsets = async () => {
|
|
32
|
+
const offsets = await cluster.sendRequest(api_1.API.LIST_OFFSETS, {
|
|
33
|
+
replicaId: -1,
|
|
34
|
+
isolationLevel,
|
|
35
|
+
topics: Object.entries(assignment)
|
|
36
|
+
.flatMap(([topic, partitions]) => partitions.map((partition) => ({ topic, partition })))
|
|
37
|
+
.map(({ topic, partition }) => ({
|
|
38
|
+
name: topic,
|
|
39
|
+
partitions: [{ partitionIndex: partition, currentLeaderEpoch: -1, timestamp: -1n }],
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
42
|
+
offsets.topics.forEach(({ name, partitions }) => {
|
|
43
|
+
partitions.forEach(({ partitionIndex, offset }) => {
|
|
44
|
+
offsetManager?.resolve(name, partitionIndex, fromBeginning ? 0n : offset);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
init: async () => {
|
|
50
|
+
await fetchMetadata();
|
|
51
|
+
await listOffsets();
|
|
52
|
+
},
|
|
53
|
+
getTopicPartitions: () => topicPartitions,
|
|
54
|
+
getTopicIdByName: (name) => topicIdByName[name],
|
|
55
|
+
getTopicNameById: (id) => topicNameById[id],
|
|
56
|
+
getAssignment: () => assignment,
|
|
57
|
+
setAssignment: (newAssignment) => {
|
|
58
|
+
assignment = newAssignment;
|
|
59
|
+
},
|
|
60
|
+
getLeaderIdByTopicPartition: (topic, partition) => leaderIdByTopicPartition[topic][partition],
|
|
61
|
+
getIsrNodeIdsByTopicPartition: (topic, partition) => isrNodesByTopicPartition[topic][partition],
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
exports.createMetadata = createMetadata;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { IsolationLevel } from "../api/fetch";
|
|
2
|
+
import { Cluster } from "../cluster";
|
|
3
|
+
import { ConsumerMetadata } from "./consumer-metadata";
|
|
4
|
+
type OffsetManagerOptions = {
|
|
5
|
+
cluster: Cluster;
|
|
6
|
+
metadata: ConsumerMetadata;
|
|
7
|
+
isolationLevel: IsolationLevel;
|
|
8
|
+
};
|
|
9
|
+
export declare class OffsetManager {
|
|
10
|
+
private options;
|
|
11
|
+
private currentOffsets;
|
|
12
|
+
pendingOffsets: Record<string, Record<number, bigint>>;
|
|
13
|
+
constructor(options: OffsetManagerOptions);
|
|
14
|
+
getCurrentOffset(topic: string, partition: number): bigint;
|
|
15
|
+
resolve(topic: string, partition: number, offset: bigint): void;
|
|
16
|
+
flush(): void;
|
|
17
|
+
fetchOffsets(options: {
|
|
18
|
+
fromBeginning: boolean;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
private listOffsets;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OffsetManager = void 0;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const messages_to_topic_partition_leaders_1 = require("../distributors/messages-to-topic-partition-leaders");
|
|
6
|
+
class OffsetManager {
|
|
7
|
+
options;
|
|
8
|
+
currentOffsets = {};
|
|
9
|
+
pendingOffsets = {};
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
getCurrentOffset(topic, partition) {
|
|
14
|
+
return this.currentOffsets[topic]?.[partition] ?? 0n;
|
|
15
|
+
}
|
|
16
|
+
resolve(topic, partition, offset) {
|
|
17
|
+
this.pendingOffsets[topic] ??= {};
|
|
18
|
+
this.pendingOffsets[topic][partition] = offset;
|
|
19
|
+
this.currentOffsets[topic] ??= {};
|
|
20
|
+
this.currentOffsets[topic][partition] = offset;
|
|
21
|
+
}
|
|
22
|
+
flush() {
|
|
23
|
+
this.pendingOffsets = {};
|
|
24
|
+
}
|
|
25
|
+
async fetchOffsets(options) {
|
|
26
|
+
const { metadata } = this.options;
|
|
27
|
+
const topicPartitions = Object.entries(metadata.getAssignment()).flatMap(([topic, partitions]) => partitions.map((partition) => ({ topic, partition })));
|
|
28
|
+
const nodeTopicPartitions = (0, messages_to_topic_partition_leaders_1.distributeMessagesToTopicPartitionLeaders)(topicPartitions, metadata.getTopicPartitionLeaderIds());
|
|
29
|
+
await Promise.all(Object.entries(nodeTopicPartitions).map(([nodeId, topicPartitions]) => this.listOffsets({
|
|
30
|
+
...options,
|
|
31
|
+
nodeId: parseInt(nodeId),
|
|
32
|
+
nodeAssignment: Object.fromEntries(Object.entries(topicPartitions).map(([topicName, partitions]) => [topicName, Object.keys(partitions).map(Number)])),
|
|
33
|
+
})));
|
|
34
|
+
this.flush();
|
|
35
|
+
}
|
|
36
|
+
async listOffsets({ nodeId, nodeAssignment, fromBeginning, }) {
|
|
37
|
+
const { cluster, isolationLevel } = this.options;
|
|
38
|
+
const offsets = await cluster.sendRequestToNode(nodeId)(api_1.API.LIST_OFFSETS, {
|
|
39
|
+
replicaId: -1,
|
|
40
|
+
isolationLevel,
|
|
41
|
+
topics: Object.entries(nodeAssignment)
|
|
42
|
+
.flatMap(([topic, partitions]) => partitions.map((partition) => ({ topic, partition })))
|
|
43
|
+
.map(({ topic, partition }) => ({
|
|
44
|
+
name: topic,
|
|
45
|
+
partitions: [{ partitionIndex: partition, currentLeaderEpoch: -1, timestamp: -1n }],
|
|
46
|
+
})),
|
|
47
|
+
});
|
|
48
|
+
offsets.topics.forEach(({ name, partitions }) => {
|
|
49
|
+
partitions.forEach(({ partitionIndex, offset }) => {
|
|
50
|
+
this.resolve(name, partitionIndex, fromBeginning ? 0n : offset);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
this.flush();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.OffsetManager = OffsetManager;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type Assignment = {
|
|
2
|
+
[topicName: string]: number[];
|
|
3
|
+
};
|
|
4
|
+
type TopicPartitionReplicaIds = {
|
|
5
|
+
[topicName: string]: {
|
|
6
|
+
[partition: number]: number[];
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export type NodeAssignment = {
|
|
10
|
+
[replicaId: number]: Assignment;
|
|
11
|
+
};
|
|
12
|
+
/** From replica ids pick the one with fewest assignments to balance the load across brokers */
|
|
13
|
+
export declare const distributeAssignmentsToNodesBalanced: (assignment: Assignment, topicPartitionReplicaIds: TopicPartitionReplicaIds) => NodeAssignment;
|
|
14
|
+
/** Minimize the total number of replicas in the result to reduce the number of requests to different brokers */
|
|
15
|
+
export declare const distributeAssignmentsToNodesOptimized: (assignment: Assignment, topicPartitionReplicaIds: TopicPartitionReplicaIds) => NodeAssignment;
|
|
16
|
+
export declare const distributeAssignmentsToNodes: (assignment: Assignment, topicPartitionReplicaIds: TopicPartitionReplicaIds) => NodeAssignment;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.distributeAssignmentsToNodes = exports.distributeAssignmentsToNodesOptimized = exports.distributeAssignmentsToNodesBalanced = void 0;
|
|
4
|
+
/** From replica ids pick the one with fewest assignments to balance the load across brokers */
|
|
5
|
+
const distributeAssignmentsToNodesBalanced = (assignment, topicPartitionReplicaIds) => {
|
|
6
|
+
const replicaPartitions = getPartitionsByReplica(assignment, topicPartitionReplicaIds);
|
|
7
|
+
const result = {};
|
|
8
|
+
for (const [topicName, partitions] of Object.entries(assignment)) {
|
|
9
|
+
for (const partition of partitions) {
|
|
10
|
+
const replicaIds = topicPartitionReplicaIds[topicName][partition];
|
|
11
|
+
const replicaId = replicaIds.reduce((prev, curr) => {
|
|
12
|
+
if (!prev) {
|
|
13
|
+
return curr;
|
|
14
|
+
}
|
|
15
|
+
return (replicaPartitions[prev]?.length ?? 0) < (replicaPartitions[curr]?.length ?? 0) ? prev : curr;
|
|
16
|
+
});
|
|
17
|
+
result[replicaId] ??= {};
|
|
18
|
+
result[replicaId][topicName] ??= [];
|
|
19
|
+
result[replicaId][topicName].push(partition);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
};
|
|
24
|
+
exports.distributeAssignmentsToNodesBalanced = distributeAssignmentsToNodesBalanced;
|
|
25
|
+
/** Minimize the total number of replicas in the result to reduce the number of requests to different brokers */
|
|
26
|
+
const distributeAssignmentsToNodesOptimized = (assignment, topicPartitionReplicaIds) => {
|
|
27
|
+
const result = {};
|
|
28
|
+
const sortFn = ([, partitionsA], [, partitionsB]) => partitionsB.length - partitionsA.length;
|
|
29
|
+
let replicaPartitions = getPartitionsByReplica(assignment, topicPartitionReplicaIds);
|
|
30
|
+
while (replicaPartitions.length) {
|
|
31
|
+
replicaPartitions.sort(sortFn);
|
|
32
|
+
const [replicaId, partitions] = replicaPartitions.shift();
|
|
33
|
+
if (!partitions.length) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
result[parseInt(replicaId)] = partitions.reduce((acc, partition) => {
|
|
37
|
+
const [topicName, partitionId] = partition.split(":");
|
|
38
|
+
acc[topicName] ??= [];
|
|
39
|
+
acc[topicName].push(parseInt(partitionId));
|
|
40
|
+
return acc;
|
|
41
|
+
}, {});
|
|
42
|
+
replicaPartitions = replicaPartitions.map(([replicaId, replicaPartitions]) => [replicaId, replicaPartitions.filter((partition) => !partitions.includes(partition))]);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
exports.distributeAssignmentsToNodesOptimized = distributeAssignmentsToNodesOptimized;
|
|
47
|
+
const getPartitionsByReplica = (assignment, topicPartitionReplicaIds) => {
|
|
48
|
+
const partitionsByReplicaId = {};
|
|
49
|
+
for (const [topicName, partitions] of Object.entries(assignment)) {
|
|
50
|
+
for (const partition of partitions) {
|
|
51
|
+
const replicaIds = topicPartitionReplicaIds[topicName][partition];
|
|
52
|
+
for (const replicaId of replicaIds) {
|
|
53
|
+
partitionsByReplicaId[replicaId] ??= [];
|
|
54
|
+
partitionsByReplicaId[replicaId].push(`${topicName}:${partition}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Object.entries(partitionsByReplicaId);
|
|
59
|
+
};
|
|
60
|
+
exports.distributeAssignmentsToNodes = exports.distributeAssignmentsToNodesBalanced;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const assignments_to_replicas_1 = require("./assignments-to-replicas");
|
|
5
|
+
(0, vitest_1.describe)("Distribute assignments to replica ids", () => {
|
|
6
|
+
(0, vitest_1.describe)("distributeAssignmentsToNodesBalanced", () => {
|
|
7
|
+
(0, vitest_1.it)("smoke", () => {
|
|
8
|
+
const result = (0, assignments_to_replicas_1.distributeAssignmentsToNodesBalanced)({ topic: [0, 1] }, { topic: { 0: [0, 1], 1: [1, 2] } });
|
|
9
|
+
(0, vitest_1.expect)(result).toMatchInlineSnapshot(`
|
|
10
|
+
{
|
|
11
|
+
"1": {
|
|
12
|
+
"topic": [
|
|
13
|
+
0,
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
"2": {
|
|
17
|
+
"topic": [
|
|
18
|
+
1,
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
`);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.describe)("distributeAssignmentsToNodesOptimized", () => {
|
|
26
|
+
(0, vitest_1.it)("smoke", () => {
|
|
27
|
+
const result = (0, assignments_to_replicas_1.distributeAssignmentsToNodesOptimized)({ topic: [0, 1] }, { topic: { 0: [0, 1], 1: [1, 2] } });
|
|
28
|
+
(0, vitest_1.expect)(result).toMatchInlineSnapshot(`
|
|
29
|
+
{
|
|
30
|
+
"1": {
|
|
31
|
+
"topic": [
|
|
32
|
+
0,
|
|
33
|
+
1,
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
`);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type TopicPartitionLeader = {
|
|
2
|
+
[topicName: string]: {
|
|
3
|
+
[partitionId: number]: number;
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
type MessagesByNodeTopicPartition<T> = {
|
|
7
|
+
[nodeId: number]: {
|
|
8
|
+
[topicName: string]: {
|
|
9
|
+
[partitionId: number]: T[];
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export declare const distributeMessagesToTopicPartitionLeaders: <T extends {
|
|
14
|
+
topic: string;
|
|
15
|
+
partition: number;
|
|
16
|
+
}>(messages: T[], topicPartitionLeader: TopicPartitionLeader) => MessagesByNodeTopicPartition<T>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.distributeMessagesToTopicPartitionLeaders = void 0;
|
|
4
|
+
const distributeMessagesToTopicPartitionLeaders = (messages, topicPartitionLeader) => {
|
|
5
|
+
const result = {};
|
|
6
|
+
messages.forEach((message) => {
|
|
7
|
+
const leaderId = topicPartitionLeader[message.topic][message.partition];
|
|
8
|
+
result[leaderId] ??= {};
|
|
9
|
+
result[leaderId][message.topic] ??= {};
|
|
10
|
+
result[leaderId][message.topic][message.partition] ??= [];
|
|
11
|
+
result[leaderId][message.topic][message.partition].push(message);
|
|
12
|
+
});
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
exports.distributeMessagesToTopicPartitionLeaders = distributeMessagesToTopicPartitionLeaders;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const messages_to_topic_partition_leaders_1 = require("./messages-to-topic-partition-leaders");
|
|
5
|
+
(0, vitest_1.describe)("Distribute messages to partition leader ids", () => {
|
|
6
|
+
(0, vitest_1.describe)("distributeMessagesToTopicPartitionLeaders", () => {
|
|
7
|
+
(0, vitest_1.it)("snoke", () => {
|
|
8
|
+
const result = (0, messages_to_topic_partition_leaders_1.distributeMessagesToTopicPartitionLeaders)([{ topic: "topic", partition: 0, key: null, value: null, offset: 0n, timestamp: 0n, headers: {} }], { topic: { 0: 1 } });
|
|
9
|
+
(0, vitest_1.expect)(result).toMatchInlineSnapshot(`
|
|
10
|
+
{
|
|
11
|
+
"1": {
|
|
12
|
+
"topic": {
|
|
13
|
+
"0": [
|
|
14
|
+
{
|
|
15
|
+
"headers": {},
|
|
16
|
+
"key": null,
|
|
17
|
+
"offset": 0n,
|
|
18
|
+
"partition": 0,
|
|
19
|
+
"timestamp": 0n,
|
|
20
|
+
"topic": "topic",
|
|
21
|
+
"value": null,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
`);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const kafkats_1 = require("kafkats");
|
|
4
|
+
const json_1 = require("./utils/json");
|
|
5
|
+
(async () => {
|
|
6
|
+
const brokers = [{ host: "localhost", port: 9092 }];
|
|
7
|
+
const topic = "playground-topic";
|
|
8
|
+
// const producer = createProducer({ brokers });
|
|
9
|
+
// const producerInterval = setInterval(async () => {
|
|
10
|
+
// await producer.send([
|
|
11
|
+
// {
|
|
12
|
+
// topic,
|
|
13
|
+
// partition: 0,
|
|
14
|
+
// offset: 1n,
|
|
15
|
+
// timestamp: BigInt(Date.now()),
|
|
16
|
+
// key: null,
|
|
17
|
+
// value: `PING ${Math.random()}`,
|
|
18
|
+
// headers: { timestamp: Date.now().toString() }
|
|
19
|
+
// }
|
|
20
|
+
// ])
|
|
21
|
+
// }, 5000);
|
|
22
|
+
const consumer = await (0, kafkats_1.startConsumer)({
|
|
23
|
+
topics: [topic],
|
|
24
|
+
brokers,
|
|
25
|
+
onBatch: (messages) => {
|
|
26
|
+
console.log(JSON.stringify(messages, json_1.serializer, 2));
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
process.on("SIGINT", async () => {
|
|
30
|
+
await consumer.close();
|
|
31
|
+
// clearInterval(producerInterval);
|
|
32
|
+
// await producer.close();
|
|
33
|
+
});
|
|
34
|
+
})();
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./client"), exports);
|
|
18
|
+
__exportStar(require("./api"), exports);
|
|
19
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Cluster } from "./cluster";
|
|
2
|
+
type MetadataOptions = {
|
|
3
|
+
cluster: Cluster;
|
|
4
|
+
};
|
|
5
|
+
export declare class Metadata {
|
|
6
|
+
private options;
|
|
7
|
+
private topicPartitions;
|
|
8
|
+
private topicNameById;
|
|
9
|
+
private topicIdByName;
|
|
10
|
+
private leaderIdByTopicPartition;
|
|
11
|
+
private isrNodesByTopicPartition;
|
|
12
|
+
constructor(options: MetadataOptions);
|
|
13
|
+
getTopicPartitionLeaderIds(): Record<string, Record<number, number>>;
|
|
14
|
+
getTopicPartitionReplicaIds(): Record<string, Record<number, number[]>>;
|
|
15
|
+
getTopicPartitions(): Record<string, number[]>;
|
|
16
|
+
getTopicIdByName(name: string): string;
|
|
17
|
+
getTopicNameById(id: string): string;
|
|
18
|
+
fetchMetadataIfNecessary({ topics, allowTopicAutoCreation, }: {
|
|
19
|
+
topics: string[];
|
|
20
|
+
allowTopicAutoCreation: boolean;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
private fetchMetadata;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Metadata = void 0;
|
|
4
|
+
const api_1 = require("./api");
|
|
5
|
+
const delay_1 = require("./utils/delay");
|
|
6
|
+
const error_1 = require("./utils/error");
|
|
7
|
+
class Metadata {
|
|
8
|
+
options;
|
|
9
|
+
topicPartitions = {};
|
|
10
|
+
topicNameById = {};
|
|
11
|
+
topicIdByName = {};
|
|
12
|
+
leaderIdByTopicPartition = {};
|
|
13
|
+
isrNodesByTopicPartition = {};
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
getTopicPartitionLeaderIds() {
|
|
18
|
+
return this.leaderIdByTopicPartition;
|
|
19
|
+
}
|
|
20
|
+
getTopicPartitionReplicaIds() {
|
|
21
|
+
return this.isrNodesByTopicPartition;
|
|
22
|
+
}
|
|
23
|
+
getTopicPartitions() {
|
|
24
|
+
return this.topicPartitions;
|
|
25
|
+
}
|
|
26
|
+
getTopicIdByName(name) {
|
|
27
|
+
return this.topicIdByName[name];
|
|
28
|
+
}
|
|
29
|
+
getTopicNameById(id) {
|
|
30
|
+
return this.topicNameById[id];
|
|
31
|
+
}
|
|
32
|
+
async fetchMetadataIfNecessary({ topics, allowTopicAutoCreation, }) {
|
|
33
|
+
const missingTopics = topics.filter((topic) => !this.topicPartitions[topic]);
|
|
34
|
+
if (!missingTopics.length) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return await this.fetchMetadata({ topics: missingTopics, allowTopicAutoCreation });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof error_1.KafkaTSApiError &&
|
|
42
|
+
error.errorCode === api_1.API_ERROR.UNKNOWN_TOPIC_OR_PARTITION &&
|
|
43
|
+
allowTopicAutoCreation) {
|
|
44
|
+
// TODO: investigate if we can avoid the delay
|
|
45
|
+
await (0, delay_1.delay)(1000);
|
|
46
|
+
return await this.fetchMetadata({ topics: missingTopics, allowTopicAutoCreation });
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async fetchMetadata({ topics, allowTopicAutoCreation, }) {
|
|
52
|
+
const { cluster } = this.options;
|
|
53
|
+
const response = await cluster.sendRequest(api_1.API.METADATA, {
|
|
54
|
+
allowTopicAutoCreation,
|
|
55
|
+
includeTopicAuthorizedOperations: false,
|
|
56
|
+
topics: topics?.map((name) => ({ id: null, name })) ?? null,
|
|
57
|
+
});
|
|
58
|
+
this.topicPartitions = {
|
|
59
|
+
...this.topicPartitions,
|
|
60
|
+
...Object.fromEntries(response.topics.map((topic) => [
|
|
61
|
+
topic.name,
|
|
62
|
+
topic.partitions.map((partition) => partition.partitionIndex),
|
|
63
|
+
])),
|
|
64
|
+
};
|
|
65
|
+
this.topicNameById = {
|
|
66
|
+
...this.topicNameById,
|
|
67
|
+
...Object.fromEntries(response.topics.map((topic) => [topic.topicId, topic.name])),
|
|
68
|
+
};
|
|
69
|
+
this.topicIdByName = {
|
|
70
|
+
...this.topicIdByName,
|
|
71
|
+
...Object.fromEntries(response.topics.map((topic) => [topic.name, topic.topicId])),
|
|
72
|
+
};
|
|
73
|
+
this.leaderIdByTopicPartition = {
|
|
74
|
+
...this.leaderIdByTopicPartition,
|
|
75
|
+
...Object.fromEntries(response.topics.map((topic) => [
|
|
76
|
+
topic.name,
|
|
77
|
+
Object.fromEntries(topic.partitions.map((partition) => [partition.partitionIndex, partition.leaderId])),
|
|
78
|
+
])),
|
|
79
|
+
};
|
|
80
|
+
this.isrNodesByTopicPartition = {
|
|
81
|
+
...this.isrNodesByTopicPartition,
|
|
82
|
+
...Object.fromEntries(response.topics.map((topic) => [
|
|
83
|
+
topic.name,
|
|
84
|
+
Object.fromEntries(topic.partitions.map((partition) => [partition.partitionIndex, partition.isrNodes])),
|
|
85
|
+
])),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.Metadata = Metadata;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Cluster } from "../cluster";
|
|
2
|
+
import { Message } from "../types";
|
|
3
|
+
export type ProducerOptions = {
|
|
4
|
+
allowTopicAutoCreation?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare class Producer {
|
|
7
|
+
private cluster;
|
|
8
|
+
private options;
|
|
9
|
+
private metadata;
|
|
10
|
+
private producerId;
|
|
11
|
+
private producerEpoch;
|
|
12
|
+
private sequences;
|
|
13
|
+
constructor(cluster: Cluster, options: ProducerOptions);
|
|
14
|
+
send(messages: Message[]): Promise<void>;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
private ensureConnected;
|
|
17
|
+
private initProducerId;
|
|
18
|
+
private nextSequence;
|
|
19
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Producer = void 0;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const messages_to_topic_partition_leaders_1 = require("../distributors/messages-to-topic-partition-leaders");
|
|
6
|
+
const metadata_1 = require("../metadata");
|
|
7
|
+
const delay_1 = require("../utils/delay");
|
|
8
|
+
const memo_1 = require("../utils/memo");
|
|
9
|
+
class Producer {
|
|
10
|
+
cluster;
|
|
11
|
+
options;
|
|
12
|
+
metadata;
|
|
13
|
+
producerId = 0n;
|
|
14
|
+
producerEpoch = 0;
|
|
15
|
+
sequences = {};
|
|
16
|
+
constructor(cluster, options) {
|
|
17
|
+
this.cluster = cluster;
|
|
18
|
+
this.options = {
|
|
19
|
+
...options,
|
|
20
|
+
allowTopicAutoCreation: options.allowTopicAutoCreation ?? false,
|
|
21
|
+
};
|
|
22
|
+
this.metadata = new metadata_1.Metadata({ cluster });
|
|
23
|
+
}
|
|
24
|
+
async send(messages) {
|
|
25
|
+
await this.ensureConnected();
|
|
26
|
+
const { allowTopicAutoCreation } = this.options;
|
|
27
|
+
const defaultTimestamp = BigInt(Date.now());
|
|
28
|
+
const topics = Array.from(new Set(messages.map((message) => message.topic)));
|
|
29
|
+
await this.metadata.fetchMetadataIfNecessary({ topics, allowTopicAutoCreation });
|
|
30
|
+
const nodeTopicPartitionMessages = (0, messages_to_topic_partition_leaders_1.distributeMessagesToTopicPartitionLeaders)(messages, this.metadata.getTopicPartitionLeaderIds());
|
|
31
|
+
await Promise.all(Object.entries(nodeTopicPartitionMessages).map(async ([nodeId, topicPartitionMessages]) => {
|
|
32
|
+
await this.cluster.sendRequestToNode(parseInt(nodeId))(api_1.API.PRODUCE, {
|
|
33
|
+
transactionalId: null,
|
|
34
|
+
acks: 1,
|
|
35
|
+
timeoutMs: 5000,
|
|
36
|
+
topicData: Object.entries(topicPartitionMessages).map(([topic, partitionMessages]) => ({
|
|
37
|
+
name: topic,
|
|
38
|
+
partitionData: Object.entries(partitionMessages).map(([partition, messages]) => {
|
|
39
|
+
let baseTimestamp;
|
|
40
|
+
let maxTimestamp;
|
|
41
|
+
messages.forEach(({ timestamp = defaultTimestamp }) => {
|
|
42
|
+
if (!baseTimestamp || timestamp < baseTimestamp) {
|
|
43
|
+
baseTimestamp = timestamp;
|
|
44
|
+
}
|
|
45
|
+
if (!maxTimestamp || timestamp > maxTimestamp) {
|
|
46
|
+
maxTimestamp = timestamp;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const baseSequence = this.nextSequence(topic, parseInt(partition), messages.length);
|
|
50
|
+
return {
|
|
51
|
+
index: parseInt(partition),
|
|
52
|
+
baseOffset: 0n,
|
|
53
|
+
partitionLeaderEpoch: -1,
|
|
54
|
+
attributes: 0,
|
|
55
|
+
lastOffsetDelta: messages.length - 1,
|
|
56
|
+
baseTimestamp: baseTimestamp ?? 0n,
|
|
57
|
+
maxTimestamp: maxTimestamp ?? 0n,
|
|
58
|
+
producerId: this.producerId,
|
|
59
|
+
producerEpoch: 0,
|
|
60
|
+
baseSequence,
|
|
61
|
+
records: messages.map((message, index) => ({
|
|
62
|
+
attributes: 0,
|
|
63
|
+
timestampDelta: (message.timestamp ?? defaultTimestamp) - (baseTimestamp ?? 0n),
|
|
64
|
+
offsetDelta: index,
|
|
65
|
+
key: message.key,
|
|
66
|
+
value: message.value,
|
|
67
|
+
headers: Object.entries(message.headers ?? {}).map(([key, value]) => ({
|
|
68
|
+
key,
|
|
69
|
+
value,
|
|
70
|
+
})),
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
})),
|
|
75
|
+
});
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
async close() {
|
|
79
|
+
await this.cluster.disconnect();
|
|
80
|
+
}
|
|
81
|
+
ensureConnected = (0, memo_1.memo)(async () => {
|
|
82
|
+
await this.cluster.connect();
|
|
83
|
+
await this.initProducerId();
|
|
84
|
+
});
|
|
85
|
+
async initProducerId() {
|
|
86
|
+
try {
|
|
87
|
+
const result = await this.cluster.sendRequest(api_1.API.INIT_PRODUCER_ID, {
|
|
88
|
+
transactionalId: null,
|
|
89
|
+
transactionTimeoutMs: 0,
|
|
90
|
+
producerId: this.producerId,
|
|
91
|
+
producerEpoch: this.producerEpoch,
|
|
92
|
+
});
|
|
93
|
+
this.producerId = result.producerId;
|
|
94
|
+
this.producerEpoch = result.producerEpoch;
|
|
95
|
+
this.sequences = {};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.errorCode === api_1.API_ERROR.COORDINATOR_LOAD_IN_PROGRESS) {
|
|
99
|
+
await (0, delay_1.delay)(100);
|
|
100
|
+
return this.initProducerId();
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
nextSequence(topic, partition, messagesCount) {
|
|
106
|
+
this.sequences[topic] ??= {};
|
|
107
|
+
this.sequences[topic][partition] ??= 0;
|
|
108
|
+
return (this.sequences[topic][partition] += messagesCount || 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.Producer = Producer;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Connection } from "./connection";
|
|
2
|
+
import { Api } from "./utils/api";
|
|
3
|
+
type RequestHandlerOptions = {
|
|
4
|
+
clientId: string | null;
|
|
5
|
+
};
|
|
6
|
+
export declare class RequestHandler {
|
|
7
|
+
private connection;
|
|
8
|
+
private options;
|
|
9
|
+
private queue;
|
|
10
|
+
private currentBuffer;
|
|
11
|
+
constructor(connection: Connection, options: RequestHandlerOptions);
|
|
12
|
+
private handleData;
|
|
13
|
+
sendRequest<Request, Response>(api: Api<Request, Response>, args: Request): Promise<Response>;
|
|
14
|
+
}
|
|
15
|
+
export type SendRequest = typeof RequestHandler.prototype.sendRequest;
|
|
16
|
+
export {};
|