kafka-crab-js 0.0.0

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/.yarnrc.yml ADDED
@@ -0,0 +1,3 @@
1
+ nodeLinker: node-modules
2
+
3
+ yarnPath: .yarn/releases/yarn-3.6.0.cjs
package/Cargo.toml ADDED
@@ -0,0 +1,29 @@
1
+ [package]
2
+ edition = "2021"
3
+ name = "kafka-crab-js"
4
+ version = "0.0.0"
5
+
6
+ [lib]
7
+ crate-type = ["cdylib"]
8
+
9
+ [dependencies]
10
+ # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
11
+ napi = { version = "2.13", default-features = false, features = ["napi4", "tokio_rt"] }
12
+ napi-derive = "2.13"
13
+
14
+ anyhow = { version= "1.0", features = ["backtrace"]}
15
+
16
+ rdkafka = { version = "0.32", features = ["libz-static", "cmake-build", "tokio", "ssl-vendored"] }
17
+ tokio = { version = "1", features = ["full"] }
18
+
19
+ # log = "0.4"
20
+ # env_logger = "0.10.0"
21
+
22
+ tracing = "0.1"
23
+ tracing-subscriber = "0.3"
24
+
25
+ [build-dependencies]
26
+ napi-build = "2.0.1"
27
+
28
+ [profile.release]
29
+ lto = true
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ Kafka Crab Js
2
+ -------------
3
+
4
+ Blazingly Fast Kafka Client for NodeJs
5
+
package/build.rs ADDED
@@ -0,0 +1,5 @@
1
+ extern crate napi_build;
2
+
3
+ fn main() {
4
+ napi_build::setup();
5
+ }
@@ -0,0 +1,50 @@
1
+ import { ConsumerResult, KafkaClient, KafkaCommitMode, PartitionPosition } from '../index.js'
2
+ import timersPromises from 'node:timers/promises'
3
+ import { Buffer } from 'node:buffer';
4
+
5
+ const kafkaClient = new KafkaClient("localhost:29092", "my-id")
6
+ const topic = "my-js-topic"
7
+
8
+ let counter = 50;
9
+
10
+ async function process(message) {
11
+ await timersPromises.setTimeout(1)
12
+ return "message received: " + message
13
+ }
14
+
15
+ const consumer = kafkaClient.createConsumer({
16
+ topic,
17
+ groupId: "my-js-group",
18
+ commitMode: KafkaCommitMode.Sync,
19
+ offset: { position: PartitionPosition.Stored },
20
+ })
21
+
22
+ console.time("consumer")
23
+
24
+ consumer.startConsumer(async (err, value)=> {
25
+ if (err) {
26
+ console.error("Consumer error", err)
27
+ return
28
+ }
29
+ const message = value.toString()
30
+ const content = await process(message)
31
+ counter++
32
+ if (counter >= 0) {
33
+ console.timeEnd("consumer")
34
+ console.log("Counter:", counter, " Content:", content)
35
+ console.time("consumer")
36
+ }
37
+ return ConsumerResult.Ok
38
+ })
39
+
40
+ await timersPromises.setTimeout(5_000)
41
+
42
+ const result = await kafkaClient.createProducer().send(
43
+ topic,
44
+ {
45
+ key: Buffer.from("abc"),
46
+ value: Buffer.from(`{"_id":"${counter}","name":"Elizeu Drummond Sample js","phone":"555"}`),
47
+ headers: {"key": Buffer.from("value1")}
48
+ }
49
+ )
50
+ console.log(result)
package/index.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /* auto-generated by NAPI-RS */
5
+
6
+ export const enum SecurityProtocol {
7
+ Plaintext = 'Plaintext',
8
+ Ssl = 'Ssl',
9
+ SaslPlaintext = 'SaslPlaintext',
10
+ SaslSsl = 'SaslSsl'
11
+ }
12
+ export interface OwnedDelivery {
13
+ partition: number
14
+ offset: number
15
+ }
16
+ export interface MessageModel {
17
+ key: Buffer
18
+ value: Buffer
19
+ headers?: Record<string, Buffer>
20
+ }
21
+ export interface ProduceRecord {
22
+ topic: string
23
+ messages: Array<MessageModel>
24
+ }
25
+ export const enum AutoOffsetReset {
26
+ Smallest = 'Smallest',
27
+ Earliest = 'Earliest',
28
+ Beginning = 'Beginning',
29
+ Largest = 'Largest',
30
+ Latest = 'Latest',
31
+ End = 'End',
32
+ Error = 'Error'
33
+ }
34
+ export const enum KafkaCommitMode {
35
+ AutoCommit = 'AutoCommit',
36
+ Sync = 'Sync',
37
+ Async = 'Async'
38
+ }
39
+ export interface ConsumerConfiguration {
40
+ topic: string
41
+ groupId: string
42
+ retryStrategy?: RetryStrategy
43
+ offset?: OffsetModel
44
+ createTopic?: boolean
45
+ commitMode?: KafkaCommitMode
46
+ }
47
+ export const enum PartitionPosition {
48
+ Beginning = 0,
49
+ End = 1,
50
+ Stored = 2
51
+ }
52
+ export interface OffsetModel {
53
+ offset?: number
54
+ position?: PartitionPosition
55
+ }
56
+ export interface RetryStrategy {
57
+ retries: number
58
+ nextTopicOnFail: string
59
+ pauseConsumerDuration?: number
60
+ }
61
+ export const enum ConsumerResult {
62
+ Ok = 'Ok',
63
+ Retry = 'Retry'
64
+ }
65
+ export class KafkaConfiguration {
66
+ brokers: string
67
+ clientId: string
68
+ securityProtocol?: SecurityProtocol
69
+ allowAutoCreateTopic?: boolean
70
+ }
71
+ export class KafkaClient {
72
+ readonly kafkaConfiguration: KafkaConfiguration
73
+ constructor(brokers: string, clientId: string)
74
+ createProducer(): KafkaProducer
75
+ createConsumer(consumerConfiguration: ConsumerConfiguration): KafkaConsumer
76
+ }
77
+ export class KafkaConsumer {
78
+ startConsumer(callback: (err: Error | null, result: Buffer) => Promise<ConsumerResult>): Promise<void>
79
+ }
80
+ export class KafkaProducer {
81
+ send(topic: string, message: MessageModel): Promise<OwnedDelivery>
82
+ }
package/index.js ADDED
@@ -0,0 +1,265 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ /* prettier-ignore */
4
+
5
+ /* auto-generated by NAPI-RS */
6
+
7
+ const { existsSync, readFileSync } = require('fs')
8
+ const { join } = require('path')
9
+
10
+ const { platform, arch } = process
11
+
12
+ let nativeBinding = null
13
+ let localFileExisted = false
14
+ let loadError = null
15
+
16
+ function isMusl() {
17
+ // For Node 10
18
+ if (!process.report || typeof process.report.getReport !== 'function') {
19
+ try {
20
+ const lddPath = require('child_process').execSync('which ldd').toString().trim()
21
+ return readFileSync(lddPath, 'utf8').includes('musl')
22
+ } catch (e) {
23
+ return true
24
+ }
25
+ } else {
26
+ const { glibcVersionRuntime } = process.report.getReport().header
27
+ return !glibcVersionRuntime
28
+ }
29
+ }
30
+
31
+ switch (platform) {
32
+ case 'android':
33
+ switch (arch) {
34
+ case 'arm64':
35
+ localFileExisted = existsSync(join(__dirname, 'kafka-crab-js.android-arm64.node'))
36
+ try {
37
+ if (localFileExisted) {
38
+ nativeBinding = require('./kafka-crab-js.android-arm64.node')
39
+ } else {
40
+ nativeBinding = require('kafka-crab-js-android-arm64')
41
+ }
42
+ } catch (e) {
43
+ loadError = e
44
+ }
45
+ break
46
+ case 'arm':
47
+ localFileExisted = existsSync(join(__dirname, 'kafka-crab-js.android-arm-eabi.node'))
48
+ try {
49
+ if (localFileExisted) {
50
+ nativeBinding = require('./kafka-crab-js.android-arm-eabi.node')
51
+ } else {
52
+ nativeBinding = require('kafka-crab-js-android-arm-eabi')
53
+ }
54
+ } catch (e) {
55
+ loadError = e
56
+ }
57
+ break
58
+ default:
59
+ throw new Error(`Unsupported architecture on Android ${arch}`)
60
+ }
61
+ break
62
+ case 'win32':
63
+ switch (arch) {
64
+ case 'x64':
65
+ localFileExisted = existsSync(
66
+ join(__dirname, 'kafka-crab-js.win32-x64-msvc.node')
67
+ )
68
+ try {
69
+ if (localFileExisted) {
70
+ nativeBinding = require('./kafka-crab-js.win32-x64-msvc.node')
71
+ } else {
72
+ nativeBinding = require('kafka-crab-js-win32-x64-msvc')
73
+ }
74
+ } catch (e) {
75
+ loadError = e
76
+ }
77
+ break
78
+ case 'ia32':
79
+ localFileExisted = existsSync(
80
+ join(__dirname, 'kafka-crab-js.win32-ia32-msvc.node')
81
+ )
82
+ try {
83
+ if (localFileExisted) {
84
+ nativeBinding = require('./kafka-crab-js.win32-ia32-msvc.node')
85
+ } else {
86
+ nativeBinding = require('kafka-crab-js-win32-ia32-msvc')
87
+ }
88
+ } catch (e) {
89
+ loadError = e
90
+ }
91
+ break
92
+ case 'arm64':
93
+ localFileExisted = existsSync(
94
+ join(__dirname, 'kafka-crab-js.win32-arm64-msvc.node')
95
+ )
96
+ try {
97
+ if (localFileExisted) {
98
+ nativeBinding = require('./kafka-crab-js.win32-arm64-msvc.node')
99
+ } else {
100
+ nativeBinding = require('kafka-crab-js-win32-arm64-msvc')
101
+ }
102
+ } catch (e) {
103
+ loadError = e
104
+ }
105
+ break
106
+ default:
107
+ throw new Error(`Unsupported architecture on Windows: ${arch}`)
108
+ }
109
+ break
110
+ case 'darwin':
111
+ localFileExisted = existsSync(join(__dirname, 'kafka-crab-js.darwin-universal.node'))
112
+ try {
113
+ if (localFileExisted) {
114
+ nativeBinding = require('./kafka-crab-js.darwin-universal.node')
115
+ } else {
116
+ nativeBinding = require('kafka-crab-js-darwin-universal')
117
+ }
118
+ break
119
+ } catch {}
120
+ switch (arch) {
121
+ case 'x64':
122
+ localFileExisted = existsSync(join(__dirname, 'kafka-crab-js.darwin-x64.node'))
123
+ try {
124
+ if (localFileExisted) {
125
+ nativeBinding = require('./kafka-crab-js.darwin-x64.node')
126
+ } else {
127
+ nativeBinding = require('kafka-crab-js-darwin-x64')
128
+ }
129
+ } catch (e) {
130
+ loadError = e
131
+ }
132
+ break
133
+ case 'arm64':
134
+ localFileExisted = existsSync(
135
+ join(__dirname, 'kafka-crab-js.darwin-arm64.node')
136
+ )
137
+ try {
138
+ if (localFileExisted) {
139
+ nativeBinding = require('./kafka-crab-js.darwin-arm64.node')
140
+ } else {
141
+ nativeBinding = require('kafka-crab-js-darwin-arm64')
142
+ }
143
+ } catch (e) {
144
+ loadError = e
145
+ }
146
+ break
147
+ default:
148
+ throw new Error(`Unsupported architecture on macOS: ${arch}`)
149
+ }
150
+ break
151
+ case 'freebsd':
152
+ if (arch !== 'x64') {
153
+ throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
154
+ }
155
+ localFileExisted = existsSync(join(__dirname, 'kafka-crab-js.freebsd-x64.node'))
156
+ try {
157
+ if (localFileExisted) {
158
+ nativeBinding = require('./kafka-crab-js.freebsd-x64.node')
159
+ } else {
160
+ nativeBinding = require('kafka-crab-js-freebsd-x64')
161
+ }
162
+ } catch (e) {
163
+ loadError = e
164
+ }
165
+ break
166
+ case 'linux':
167
+ switch (arch) {
168
+ case 'x64':
169
+ if (isMusl()) {
170
+ localFileExisted = existsSync(
171
+ join(__dirname, 'kafka-crab-js.linux-x64-musl.node')
172
+ )
173
+ try {
174
+ if (localFileExisted) {
175
+ nativeBinding = require('./kafka-crab-js.linux-x64-musl.node')
176
+ } else {
177
+ nativeBinding = require('kafka-crab-js-linux-x64-musl')
178
+ }
179
+ } catch (e) {
180
+ loadError = e
181
+ }
182
+ } else {
183
+ localFileExisted = existsSync(
184
+ join(__dirname, 'kafka-crab-js.linux-x64-gnu.node')
185
+ )
186
+ try {
187
+ if (localFileExisted) {
188
+ nativeBinding = require('./kafka-crab-js.linux-x64-gnu.node')
189
+ } else {
190
+ nativeBinding = require('kafka-crab-js-linux-x64-gnu')
191
+ }
192
+ } catch (e) {
193
+ loadError = e
194
+ }
195
+ }
196
+ break
197
+ case 'arm64':
198
+ if (isMusl()) {
199
+ localFileExisted = existsSync(
200
+ join(__dirname, 'kafka-crab-js.linux-arm64-musl.node')
201
+ )
202
+ try {
203
+ if (localFileExisted) {
204
+ nativeBinding = require('./kafka-crab-js.linux-arm64-musl.node')
205
+ } else {
206
+ nativeBinding = require('kafka-crab-js-linux-arm64-musl')
207
+ }
208
+ } catch (e) {
209
+ loadError = e
210
+ }
211
+ } else {
212
+ localFileExisted = existsSync(
213
+ join(__dirname, 'kafka-crab-js.linux-arm64-gnu.node')
214
+ )
215
+ try {
216
+ if (localFileExisted) {
217
+ nativeBinding = require('./kafka-crab-js.linux-arm64-gnu.node')
218
+ } else {
219
+ nativeBinding = require('kafka-crab-js-linux-arm64-gnu')
220
+ }
221
+ } catch (e) {
222
+ loadError = e
223
+ }
224
+ }
225
+ break
226
+ case 'arm':
227
+ localFileExisted = existsSync(
228
+ join(__dirname, 'kafka-crab-js.linux-arm-gnueabihf.node')
229
+ )
230
+ try {
231
+ if (localFileExisted) {
232
+ nativeBinding = require('./kafka-crab-js.linux-arm-gnueabihf.node')
233
+ } else {
234
+ nativeBinding = require('kafka-crab-js-linux-arm-gnueabihf')
235
+ }
236
+ } catch (e) {
237
+ loadError = e
238
+ }
239
+ break
240
+ default:
241
+ throw new Error(`Unsupported architecture on Linux: ${arch}`)
242
+ }
243
+ break
244
+ default:
245
+ throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
246
+ }
247
+
248
+ if (!nativeBinding) {
249
+ if (loadError) {
250
+ throw loadError
251
+ }
252
+ throw new Error(`Failed to load native binding`)
253
+ }
254
+
255
+ const { SecurityProtocol, KafkaConfiguration, KafkaClient, KafkaConsumer, KafkaProducer, AutoOffsetReset, KafkaCommitMode, PartitionPosition, ConsumerResult } = nativeBinding
256
+
257
+ module.exports.SecurityProtocol = SecurityProtocol
258
+ module.exports.KafkaConfiguration = KafkaConfiguration
259
+ module.exports.KafkaClient = KafkaClient
260
+ module.exports.KafkaConsumer = KafkaConsumer
261
+ module.exports.KafkaProducer = KafkaProducer
262
+ module.exports.AutoOffsetReset = AutoOffsetReset
263
+ module.exports.KafkaCommitMode = KafkaCommitMode
264
+ module.exports.PartitionPosition = PartitionPosition
265
+ module.exports.ConsumerResult = ConsumerResult
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "kafka-crab-js",
3
+ "version": "0.0.0",
4
+ "main": "index.js",
5
+ "types": "index.d.ts",
6
+ "napi": {
7
+ "name": "kafka-crab-js",
8
+ "triples": {
9
+ "additional": [
10
+ "aarch64-apple-darwin",
11
+ "x86_64-unknown-linux-musl"
12
+ ]
13
+ }
14
+ },
15
+ "license": "MIT",
16
+ "devDependencies": {
17
+ "@napi-rs/cli": "^2.16.1",
18
+ "ava": "^5.1.1",
19
+ "nanoid": "^4.0.2"
20
+ },
21
+ "ava": {
22
+ "timeout": "3m"
23
+ },
24
+ "engines": {
25
+ "node": ">= 10"
26
+ },
27
+ "scripts": {
28
+ "artifacts": "napi artifacts",
29
+ "build": "napi build --platform --release",
30
+ "build:debug": "napi build --platform",
31
+ "prepublishOnly": "napi prepublish -t npm",
32
+ "test": "ava",
33
+ "universal": "napi universal",
34
+ "version": "napi version"
35
+ },
36
+ "packageManager": "yarn@3.6.0",
37
+ "optionalDependencies": {
38
+ "kafka-crab-js-win32-x64-msvc": "0.0.0",
39
+ "kafka-crab-js-darwin-x64": "0.0.0",
40
+ "kafka-crab-js-linux-x64-gnu": "0.0.0",
41
+ "kafka-crab-js-darwin-arm64": "0.0.0",
42
+ "kafka-crab-js-linux-x64-musl": "0.0.0"
43
+ }
44
+ }
@@ -0,0 +1,129 @@
1
+ use rdkafka::{
2
+ admin::{
3
+ AdminClient, AdminOptions, ConfigResource, NewTopic, ResourceSpecifier, TopicReplication,
4
+ },
5
+ client::DefaultClientContext,
6
+ config::ClientConfig,
7
+ consumer::{BaseConsumer, Consumer},
8
+ error::KafkaError,
9
+ types::RDKafkaErrorCode,
10
+ };
11
+ use tracing::{debug, info};
12
+
13
+ use std::{collections::HashMap, str::FromStr, time::Duration};
14
+
15
+ const DEFAULT_NUM_PARTITIONS: i32 = 3;
16
+ const DEFAULT_REPLICATION: i32 = 3;
17
+
18
+ pub struct KafkaAdmin<'a> {
19
+ client_config: &'a ClientConfig,
20
+ admin_client: AdminClient<DefaultClientContext>,
21
+ }
22
+
23
+ impl<'a> KafkaAdmin<'a> {
24
+ pub fn new(client_config: &'a ClientConfig) -> Self {
25
+ let admin_client: AdminClient<DefaultClientContext> = client_config
26
+ .create()
27
+ .expect("admin client creation failed");
28
+
29
+ KafkaAdmin {
30
+ client_config,
31
+ admin_client,
32
+ }
33
+ }
34
+
35
+ async fn fetch_config_resource(&self) -> Result<HashMap<String, String>, KafkaError> {
36
+ let consumer: BaseConsumer = self
37
+ .client_config
38
+ .create()
39
+ .expect("Consumer creation failed");
40
+
41
+ let metadata = consumer
42
+ .fetch_metadata(None, Duration::from_secs(5))
43
+ .expect("Failed to fetch metadata");
44
+
45
+ for broker in metadata.brokers() {
46
+ info!(
47
+ "Id: {} Host: {}:{} ",
48
+ broker.id(),
49
+ broker.host(),
50
+ broker.port()
51
+ );
52
+ }
53
+
54
+ let config_resource = self
55
+ .admin_client
56
+ .describe_configs(
57
+ &[ResourceSpecifier::Broker(metadata.orig_broker_id())],
58
+ &AdminOptions::new(),
59
+ )
60
+ .await?;
61
+
62
+ Ok(extract_config_resource(config_resource))
63
+ }
64
+
65
+ pub async fn create_topic(&self, topic_name: &str) -> anyhow::Result<()> {
66
+ let broker_properties = self.fetch_config_resource().await?.clone();
67
+
68
+ debug!("Broker properties {:?}", broker_properties);
69
+
70
+ self
71
+ .admin_client
72
+ .create_topics(
73
+ &[NewTopic {
74
+ name: topic_name,
75
+ num_partitions: broker_properties
76
+ .get("num.partitions")
77
+ .get_parsed_or_default_value(DEFAULT_NUM_PARTITIONS),
78
+ replication: TopicReplication::Fixed(
79
+ broker_properties
80
+ .get("default.replication.factor")
81
+ .get_parsed_or_default_value(DEFAULT_REPLICATION),
82
+ ),
83
+ config: vec![],
84
+ }],
85
+ &AdminOptions::default(),
86
+ )
87
+ .await
88
+ .map_err(anyhow::Error::new)?;
89
+
90
+ info!("Topic {} was created successfully", topic_name);
91
+ Ok(())
92
+ }
93
+ }
94
+
95
+ fn extract_config_resource(
96
+ config_resource: Vec<Result<ConfigResource, RDKafkaErrorCode>>,
97
+ ) -> HashMap<String, String> {
98
+ let mut properties: HashMap<String, String> = HashMap::new();
99
+ for config_resource_list in config_resource {
100
+ match config_resource_list {
101
+ Ok(v) => {
102
+ for config_entry_list in v.entries {
103
+ if let Some(value) = config_entry_list.value {
104
+ properties.insert(config_entry_list.name, value);
105
+ }
106
+ }
107
+ }
108
+ Err(e) => {
109
+ debug!("Error on fetching config entry {:?}", e)
110
+ }
111
+ }
112
+ }
113
+
114
+ properties
115
+ }
116
+
117
+ trait ParsedOrDefaultValue {
118
+ fn get_parsed_or_default_value<T: FromStr>(self, default_value: T) -> T;
119
+ }
120
+
121
+ impl ParsedOrDefaultValue for Option<&String> {
122
+ fn get_parsed_or_default_value<T: FromStr>(self, default_value: T) -> T {
123
+ if let Some(ret) = self {
124
+ ret.parse().unwrap_or(default_value)
125
+ } else {
126
+ default_value
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,131 @@
1
+ use std::fmt;
2
+
3
+ use napi::bindgen_prelude::*;
4
+ use rdkafka::{
5
+ client::ClientContext,
6
+ config::{ClientConfig, RDKafkaLogLevel},
7
+ consumer::{ConsumerContext, Rebalance},
8
+ error::KafkaResult,
9
+ topic_partition_list::TopicPartitionList,
10
+ };
11
+ use tracing::info;
12
+
13
+ use super::{
14
+ kafka_consumer::KafkaConsumer, kafka_producer::KafkaProducer, model::ConsumerConfiguration,
15
+ };
16
+
17
+ struct CustomContext;
18
+
19
+ impl ClientContext for CustomContext {}
20
+
21
+ impl ConsumerContext for CustomContext {
22
+ fn pre_rebalance(&self, rebalance: &Rebalance) {
23
+ info!("Pre rebalance {:?}", rebalance);
24
+ }
25
+
26
+ fn post_rebalance(&self, rebalance: &Rebalance) {
27
+ info!("Post rebalance {:?}", rebalance);
28
+ }
29
+
30
+ fn commit_callback(&self, result: KafkaResult<()>, _offsets: &TopicPartitionList) {
31
+ info!("Committing offsets: {:?}. Offset: {:?}", result, _offsets);
32
+ }
33
+ }
34
+
35
+ #[derive(Debug)]
36
+ #[napi(string_enum)]
37
+ pub enum SecurityProtocol {
38
+ Plaintext,
39
+ Ssl,
40
+ SaslPlaintext,
41
+ SaslSsl,
42
+ }
43
+
44
+ impl fmt::Display for SecurityProtocol {
45
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
46
+ match self {
47
+ SecurityProtocol::Plaintext => write!(f, "plaintext"),
48
+ SecurityProtocol::Ssl => write!(f, "ssl"),
49
+ SecurityProtocol::SaslPlaintext => write!(f, "sasl_plaintext"),
50
+ SecurityProtocol::SaslSsl => write!(f, "sasl_ssl"),
51
+ }
52
+ }
53
+ }
54
+
55
+ #[derive(Clone, Debug)]
56
+ #[napi]
57
+ pub struct KafkaConfiguration {
58
+ pub brokers: String,
59
+ pub client_id: String,
60
+ pub security_protocol: Option<SecurityProtocol>,
61
+ pub allow_auto_create_topic: Option<bool>,
62
+ }
63
+
64
+ #[derive(Clone, Debug)]
65
+ #[napi]
66
+ pub struct KafkaClient {
67
+ rdkafka_client_config: ClientConfig,
68
+ #[napi(readonly)]
69
+ pub kafka_configuration: KafkaConfiguration,
70
+ }
71
+
72
+ #[napi]
73
+ impl KafkaClient {
74
+ #[napi(constructor)]
75
+ pub fn new(brokers: String, client_id: String) -> Self {
76
+ match tracing_subscriber::fmt::try_init() {
77
+ Ok(_) => {}
78
+ Err(e) => println!("Failed to initialize tracing: {:?}", e),
79
+ }
80
+
81
+ KafkaClient::with_kafka_configuration(KafkaConfiguration {
82
+ brokers,
83
+ client_id,
84
+ security_protocol: None,
85
+ allow_auto_create_topic: None,
86
+ })
87
+ }
88
+
89
+ pub fn get_client_config(&self) -> &ClientConfig {
90
+ &self.rdkafka_client_config
91
+ }
92
+
93
+ pub fn with_kafka_configuration(kafka_configuration: KafkaConfiguration) -> Self {
94
+ let KafkaConfiguration {
95
+ brokers,
96
+ security_protocol,
97
+ client_id,
98
+ allow_auto_create_topic,
99
+ } = kafka_configuration.clone();
100
+
101
+ let mut rdkafka_client_config = ClientConfig::new();
102
+
103
+ rdkafka_client_config.set_log_level(RDKafkaLogLevel::Info);
104
+ rdkafka_client_config.set("bootstrap.servers", brokers);
105
+ rdkafka_client_config.set("client.id", client_id);
106
+ if let Some(security_protocol) = security_protocol {
107
+ rdkafka_client_config.set("security.protocol", security_protocol.to_string());
108
+ }
109
+ if let Some(allow_auto_create_topic) = allow_auto_create_topic {
110
+ rdkafka_client_config.set(
111
+ "allow.auto.create.topics",
112
+ allow_auto_create_topic.to_string(),
113
+ );
114
+ }
115
+
116
+ KafkaClient {
117
+ rdkafka_client_config,
118
+ kafka_configuration,
119
+ }
120
+ }
121
+
122
+ #[napi]
123
+ pub fn create_producer(&self) -> KafkaProducer {
124
+ KafkaProducer::new(self.rdkafka_client_config.clone())
125
+ }
126
+
127
+ #[napi]
128
+ pub fn create_consumer(&self, consumer_configuration: ConsumerConfiguration) -> KafkaConsumer {
129
+ KafkaConsumer::new(self.clone(), consumer_configuration)
130
+ }
131
+ }
@@ -0,0 +1,188 @@
1
+ use napi::{
2
+ bindgen_prelude::{Buffer, Promise},
3
+ threadsafe_function::ThreadsafeFunction,
4
+ Result,
5
+ };
6
+ use rdkafka::{
7
+ client::ClientContext,
8
+ config::RDKafkaLogLevel,
9
+ consumer::{stream_consumer::StreamConsumer, CommitMode, Consumer, ConsumerContext, Rebalance},
10
+ error::KafkaResult,
11
+ message::BorrowedMessage,
12
+ topic_partition_list::TopicPartitionList,
13
+ Message, Offset,
14
+ };
15
+
16
+ use std::{time::Duration, vec};
17
+ use tracing::{debug, error, info};
18
+
19
+ use super::{
20
+ kafka_admin::KafkaAdmin,
21
+ kafka_client::KafkaClient,
22
+ model::{ConsumerConfiguration, ConsumerResult, KafkaCommitMode, OffsetModel, PartitionPosition},
23
+ };
24
+
25
+ type LoggingConsumer = StreamConsumer<CustomContext>;
26
+
27
+ pub struct CustomContext;
28
+
29
+ impl ClientContext for CustomContext {}
30
+
31
+ impl ConsumerContext for CustomContext {
32
+ fn pre_rebalance(&self, rebalance: &Rebalance) {
33
+ info!("Pre rebalance {:?}", rebalance);
34
+ }
35
+
36
+ fn post_rebalance(&self, rebalance: &Rebalance) {
37
+ info!("Post rebalance {:?}", rebalance);
38
+ }
39
+
40
+ fn commit_callback(&self, result: KafkaResult<()>, _offsets: &TopicPartitionList) {
41
+ info!("Committing offsets: {:?}. Offset: {:?}", result, _offsets);
42
+ }
43
+ }
44
+
45
+ #[derive(Clone, Debug)]
46
+ #[napi]
47
+ pub struct KafkaConsumer {
48
+ kafka_client: KafkaClient,
49
+ consumer_configuration: ConsumerConfiguration,
50
+ }
51
+
52
+ #[napi]
53
+ impl KafkaConsumer {
54
+ pub fn new(kafka_client: KafkaClient, consumer_configuration: ConsumerConfiguration) -> Self {
55
+ KafkaConsumer {
56
+ kafka_client,
57
+ consumer_configuration,
58
+ }
59
+ }
60
+
61
+ #[napi(ts_args_type = "callback: (err: Error | null, result: Buffer) => Promise<ConsumerResult>")]
62
+ pub async fn start_consumer(&self, func: ThreadsafeFunction<Buffer>) -> Result<()> {
63
+ let ConsumerConfiguration { commit_mode, .. } = self.consumer_configuration.clone();
64
+
65
+ let commit_mode = match commit_mode {
66
+ None | Some(KafkaCommitMode::AutoCommit) => None, //Default AutoCommit
67
+ Some(KafkaCommitMode::Sync) => Some(CommitMode::Sync),
68
+ Some(KafkaCommitMode::Async) => Some(CommitMode::Async),
69
+ };
70
+
71
+ let stream_consumer: StreamConsumer<CustomContext> = self
72
+ .setup_consumer()
73
+ .await
74
+ .expect("Consumer creation failed");
75
+
76
+ tokio::spawn(async move {
77
+ loop {
78
+ match stream_consumer.recv().await {
79
+ Err(e) => error!("Error while receiving from stream consumer: {:?}", e),
80
+ Ok(message) => process_message(commit_mode, &stream_consumer, &func, &message).await,
81
+ };
82
+ }
83
+ });
84
+
85
+ Ok(())
86
+ }
87
+
88
+ async fn setup_consumer(&self) -> anyhow::Result<StreamConsumer<CustomContext>> {
89
+ let context = CustomContext;
90
+
91
+ let ConsumerConfiguration {
92
+ topic,
93
+ offset,
94
+ create_topic,
95
+ group_id,
96
+ ..
97
+ } = self.consumer_configuration.clone();
98
+
99
+ let consumer: LoggingConsumer = self
100
+ .kafka_client
101
+ .get_client_config()
102
+ .clone()
103
+ .set("group.id", group_id.clone())
104
+ .set("enable.auto.commit", "false")
105
+ .set_log_level(RDKafkaLogLevel::Debug)
106
+ .create_with_context(context)
107
+ .expect("Consumer creation failed");
108
+
109
+ if create_topic.unwrap_or(true) {
110
+ let admin = KafkaAdmin::new(self.kafka_client.get_client_config());
111
+ admin.create_topic(&topic).await?;
112
+ }
113
+
114
+ if let Some(offset) = convert_to_rdkafka_offset(offset) {
115
+ let metadata = consumer
116
+ .fetch_metadata(Some(&topic), Duration::from_millis(1500))
117
+ .expect("Fail to retrive metadata from consumer");
118
+
119
+ metadata.topics().iter().for_each(|meta_topic| {
120
+ let mut tpl = TopicPartitionList::new();
121
+ meta_topic.partitions().iter().for_each(|meta_partition| {
122
+ tpl.add_partition(&topic, meta_partition.id());
123
+ });
124
+ tpl.set_all_offsets(offset).expect("Fail to set offset");
125
+ consumer
126
+ .assign(&tpl)
127
+ .expect("Assign topic partition list failed");
128
+ });
129
+ }
130
+
131
+ consumer
132
+ .subscribe(vec![&*topic.to_string()].as_slice())
133
+ .expect("Can't subscribe to specified topics");
134
+
135
+ info!(
136
+ "Consumer created. Group id: {:?}, Topic: {:?}",
137
+ group_id, topic
138
+ );
139
+ Ok(consumer)
140
+ }
141
+ }
142
+
143
+ fn convert_to_rdkafka_offset(offset_model: Option<OffsetModel>) -> Option<Offset> {
144
+ match offset_model {
145
+ Some(model) => Some(match model.position {
146
+ Some(PartitionPosition::Beginning) => Offset::Beginning,
147
+ Some(PartitionPosition::End) => Offset::End,
148
+ Some(PartitionPosition::Stored) => Offset::Stored,
149
+ None => match model.offset {
150
+ Some(value) => Offset::Offset(value),
151
+ None => Offset::Stored, // Default to stored
152
+ },
153
+ }),
154
+ None => None,
155
+ }
156
+ }
157
+
158
+ async fn process_message(
159
+ commit_mode: Option<CommitMode>,
160
+ stream_consumer: &StreamConsumer<CustomContext>,
161
+ func: &ThreadsafeFunction<Buffer>,
162
+ message: &BorrowedMessage<'_>,
163
+ ) {
164
+ match message.payload_view::<[u8]>() {
165
+ None => {}
166
+ Some(Ok(payload)) => match func
167
+ .call_async::<Promise<ConsumerResult>>(Ok(payload.into()))
168
+ .await
169
+ {
170
+ Ok(js_result) => match js_result.await {
171
+ Ok(value) => {
172
+ debug!("Message processed: {:?}", value);
173
+ if let Some(commit_mode) = commit_mode {
174
+ match stream_consumer.commit_message(&message, commit_mode) {
175
+ Ok(_) => debug!("Message committed"),
176
+ Err(e) => error!("Error on commit: {:?}", e),
177
+ }
178
+ }
179
+ }
180
+ Err(e) => error!("Error on promise: {:?}", e),
181
+ },
182
+ Err(js_error) => error!("Error on js call: {:?}", js_error),
183
+ },
184
+ Some(Err(e)) => {
185
+ error!("Error while deserializing message payload: {:?}", e);
186
+ }
187
+ }
188
+ }
@@ -0,0 +1,57 @@
1
+ use std::time::Duration;
2
+
3
+ use napi::{Error, Result, Status};
4
+ use rdkafka::{
5
+ message::ToBytes,
6
+ producer::{FutureProducer, FutureRecord},
7
+ ClientConfig,
8
+ };
9
+
10
+ use super::model::MessageModel;
11
+
12
+ #[derive(Clone)]
13
+ #[napi]
14
+ pub struct KafkaProducer {
15
+ future_producer: FutureProducer,
16
+ }
17
+
18
+ #[napi(object)]
19
+ pub struct OwnedDelivery {
20
+ pub partition: i32,
21
+ pub offset: i64,
22
+ }
23
+
24
+ #[napi]
25
+ impl KafkaProducer {
26
+ pub fn new(client_config: ClientConfig) -> Self {
27
+ let future_producer = client_config
28
+ .clone()
29
+ .set("message.timeout.ms", "5000")
30
+ .create::<FutureProducer>()
31
+ .expect("Producer creation error");
32
+
33
+ KafkaProducer { future_producer }
34
+ }
35
+
36
+ #[napi]
37
+ pub async fn send(&self, topic: String, message: MessageModel) -> Result<OwnedDelivery> {
38
+ let MessageModel {
39
+ key,
40
+ value,
41
+ headers: _,
42
+ } = message;
43
+
44
+ let record = FutureRecord::to(&topic)
45
+ .key(key.to_bytes())
46
+ .payload(value.to_bytes());
47
+
48
+ match self
49
+ .future_producer
50
+ .send(record, Duration::from_secs(5000))
51
+ .await
52
+ {
53
+ Ok((partition, offset)) => Ok(OwnedDelivery { partition, offset }),
54
+ Err((kafka_error, _)) => Err(Error::new(Status::GenericFailure, kafka_error)),
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,78 @@
1
+ use std::collections::HashMap;
2
+
3
+ use rdkafka::message::{BorrowedHeaders, Headers};
4
+
5
+ pub fn convert_kakfa_headers_to_hashmap(headers: Option<&BorrowedHeaders>) -> HashMap<&str, &[u8]> {
6
+ match headers {
7
+ Some(value) => value
8
+ .iter()
9
+ .filter(|it| it.value.is_some())
10
+ .map(|it| (it.key, it.value.unwrap()))
11
+ .collect::<HashMap<&str, &[u8]>>(),
12
+ _ => HashMap::new(),
13
+ }
14
+ }
15
+
16
+ pub trait ExtractValueOnKafkaHashMap<T> {
17
+ fn get_value(&self, key: &str) -> Option<T>;
18
+ }
19
+
20
+ impl ExtractValueOnKafkaHashMap<usize> for HashMap<&str, &[u8]> {
21
+ fn get_value(&self, key: &str) -> Option<usize> {
22
+ match self.get(key) {
23
+ Some(it) => {
24
+ let parsed = match String::from_utf8(it.to_vec()) {
25
+ Ok(it) => Some(it.parse::<usize>()),
26
+ Err(_) => None,
27
+ };
28
+ match parsed {
29
+ Some(value) => value.ok(),
30
+ _ => None,
31
+ }
32
+ }
33
+ None => None,
34
+ }
35
+ }
36
+ }
37
+
38
+ #[cfg(test)]
39
+ mod tests {
40
+ use std::collections::HashMap;
41
+
42
+ use rdkafka::message::{Header, OwnedHeaders};
43
+
44
+ use crate::kafka::kafka_util::{convert_kakfa_headers_to_hashmap, ExtractValueOnKafkaHashMap};
45
+
46
+ fn create_header<'a>(key: &'a str, value: Option<&'a str>) -> Header<'a, &'a str> {
47
+ Header { key, value }
48
+ }
49
+
50
+ #[test]
51
+ fn filter_out_nones() {
52
+ let _x = Header {
53
+ key: "header_key",
54
+ value: Some("header_value"),
55
+ };
56
+ let headers = OwnedHeaders::new()
57
+ .insert(create_header("key1", Some("1")))
58
+ .insert(create_header("key2", None))
59
+ .insert(create_header("key4", None))
60
+ .insert(create_header("key5", Some("5")));
61
+
62
+ let hash_map = convert_kakfa_headers_to_hashmap(Some(headers.as_borrowed()));
63
+
64
+ assert_eq!(hash_map.len(), 2);
65
+ }
66
+
67
+ #[test]
68
+ fn extract_usize() {
69
+ let hash_map = HashMap::from([("key1", "32".as_bytes())]);
70
+ assert_eq!(hash_map.get_value("key1"), Some(32));
71
+ }
72
+
73
+ #[test]
74
+ fn extract_empty_value() {
75
+ let hash_map = HashMap::from([("key1", "32".as_bytes())]);
76
+ assert_eq!(hash_map.get_value("key2"), None);
77
+ }
78
+ }
@@ -0,0 +1,6 @@
1
+ pub mod kafka_admin;
2
+ pub mod kafka_client;
3
+ pub mod kafka_consumer;
4
+ pub mod kafka_producer;
5
+ pub mod kafka_util;
6
+ pub mod model;
@@ -0,0 +1,86 @@
1
+ use napi::bindgen_prelude::*;
2
+
3
+ use std::{collections::HashMap, fmt};
4
+
5
+ use napi::bindgen_prelude::Buffer;
6
+
7
+ #[napi(object)]
8
+ #[derive(Clone)]
9
+ pub struct MessageModel {
10
+ pub key: Buffer,
11
+ pub value: Buffer,
12
+ pub headers: Option<HashMap<String, Buffer>>,
13
+ }
14
+
15
+ #[napi(object)]
16
+ #[derive(Clone)]
17
+ pub struct ProduceRecord {
18
+ pub topic: String,
19
+ pub messages: Vec<MessageModel>,
20
+ }
21
+
22
+ #[derive(Debug)]
23
+ #[napi(string_enum)]
24
+ pub enum AutoOffsetReset {
25
+ Smallest,
26
+ Earliest,
27
+ Beginning,
28
+ Largest,
29
+ Latest,
30
+ End,
31
+ Error,
32
+ }
33
+
34
+ impl fmt::Display for AutoOffsetReset {
35
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36
+ write!(f, "{}", format!("{:?}", self).to_lowercase())
37
+ }
38
+ }
39
+
40
+ #[napi(string_enum)]
41
+ #[derive(Debug)]
42
+ pub enum KafkaCommitMode {
43
+ AutoCommit,
44
+ Sync,
45
+ Async,
46
+ }
47
+
48
+ #[napi(object)]
49
+ #[derive(Clone, Debug)]
50
+ pub struct ConsumerConfiguration {
51
+ pub topic: String,
52
+ pub group_id: String,
53
+ pub retry_strategy: Option<RetryStrategy>,
54
+ pub offset: Option<OffsetModel>,
55
+ pub create_topic: Option<bool>,
56
+ pub commit_mode: Option<KafkaCommitMode>,
57
+ }
58
+
59
+ #[napi(object)]
60
+ #[derive(Debug)]
61
+ pub enum PartitionPosition {
62
+ Beginning,
63
+ End,
64
+ Stored,
65
+ }
66
+ #[napi(object)]
67
+ #[derive(Clone, Debug)]
68
+ pub struct OffsetModel {
69
+ pub offset: Option<i64>,
70
+ pub position: Option<PartitionPosition>,
71
+ }
72
+
73
+ #[napi(object)]
74
+ #[derive(Clone, Debug)]
75
+ pub struct RetryStrategy {
76
+ pub retries: i32,
77
+ pub next_topic_on_fail: String,
78
+ pub pause_consumer_duration: Option<i64>,
79
+ }
80
+
81
+ #[napi(string_enum)]
82
+ #[derive(Debug)]
83
+ pub enum ConsumerResult {
84
+ Ok,
85
+ Retry,
86
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,6 @@
1
+ #![deny(clippy::all)]
2
+
3
+ mod kafka;
4
+
5
+ #[macro_use]
6
+ extern crate napi_derive;