kafka-crab-js 0.5.4 → 0.7.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.
@@ -34,7 +34,7 @@ async function produce() {
34
34
  const result = await producer.send(
35
35
  {
36
36
  topic,
37
- messages: [{ value: Buffer.from(`{"_id":"${i}","name":"Elizeu Drummond Sample js","phone":"555"}`) }],
37
+ messages: [{ payload: Buffer.from(`{"_id":"${i}","name":"Elizeu Drummond Sample js","phone":"555"}`) }],
38
38
  },
39
39
  );
40
40
  console.log('Js message sent. Offset:', result);
@@ -0,0 +1,103 @@
1
+ import { CommitMode, KafkaClient, KafkaStreamConsumer, PartitionPosition } from "../index.js";
2
+ import { setTimeout as sleep } from 'node:timers/promises';
3
+ import { fakerPT_BR } from '@faker-js/faker';
4
+ import { Readable } from "node:stream";
5
+
6
+ const kafkaClient = new KafkaClient({
7
+ brokers: 'localhost:29092',
8
+ clientId: 'my-js-group-11',
9
+ logLevel: 'debug',
10
+ brokerAddressFamily: 'v4',
11
+ });
12
+
13
+ /**
14
+ * Produces some messages to the topic
15
+ * @param {string} topic
16
+ * @param {number} messages
17
+ */
18
+ async function produce(topic, messages=1) {
19
+ const producer = kafkaClient.createProducer({ topic, configuration: { 'message.timeout.ms': '5000' } });
20
+ const index = Math.floor(Math.random() * 100_000)
21
+ for (let i = index ?? 0; i < index+messages; i++) {
22
+ try {
23
+ const result = await producer.send(
24
+ {
25
+ topic,
26
+ messages: [{ payload: Buffer.from(`{"_id":"${i}","name":"${fakerPT_BR.person.fullName()}","phone":"${fakerPT_BR.phone.number()}"}`) }],
27
+ },
28
+ );
29
+ console.log('Js message sent. Offset:', result);
30
+ } catch (error) {
31
+ console.error('Js Error on send', error);
32
+ }
33
+ }
34
+ }
35
+
36
+ class KafkaStreamReadable extends Readable {
37
+ /**
38
+ * Creates a new KafkaStream
39
+ * @param {KafkaStreamConsumer} kafkaStreamConsumer
40
+ */
41
+ constructor(kafkaStreamConsumer) {
42
+ super({ objectMode: true });
43
+ this.kafkaStreamConsumer = kafkaStreamConsumer;
44
+ }
45
+
46
+ async _read() {
47
+ let message = await this.kafkaStreamConsumer.recv();
48
+ this.push(message)
49
+ }
50
+ }
51
+
52
+ async function startConsumer() {
53
+ const kafkaStreamConsumer = kafkaClient.createStreamConsumer({
54
+ groupId: `crab-test`,
55
+ enableAutoCommit: true,
56
+ });
57
+
58
+ // await streamerConsumer.subscribe(
59
+ // [{
60
+ // topic: 'foo',
61
+ // partitionOffset: [
62
+ // { partition:0, offset: { position: PartitionPosition.Beginning }}
63
+ // ]
64
+ // },
65
+ // {
66
+ // topic: 'bar',
67
+ // partitionOffset: [
68
+ // { partition:0, offset: { position: PartitionPosition.Stored }},
69
+ // { partition:1, offset: { position: PartitionPosition.Beginning }}
70
+ // ]
71
+ // }]
72
+ // );
73
+ // await streamerConsumer.subscribe([ { topic: 'foo', allOffsets: { position: PartitionPosition.Stored } } ]);
74
+ // await streamerConsumer.subscribe('foo');
75
+ await kafkaStreamConsumer.subscribe([ { topic: 'foo' }, { topic: 'bar' } ]);
76
+
77
+ const maxMessages = 20;
78
+ let counter = 0;
79
+ const kafkaStream = new KafkaStreamReadable(kafkaStreamConsumer);
80
+ console.log('Starting consumer')
81
+ kafkaStream.on('data', (message) => {
82
+ counter++;
83
+ console.log('>>> Message received:', { payload: message.payload.toString(), offset: message.offset, partition: message.partition, topic: message.topic })
84
+ if (counter === maxMessages) {
85
+ kafkaStream.destroy();
86
+ }
87
+ // streamerConsumer.commit(message.partition, message.offset+1, CommitMode.Sync)
88
+ })
89
+
90
+ kafkaStream.on('close', () => {
91
+ kafkaStreamConsumer.unsubscribe();
92
+ console.log('Stream ended')
93
+ })
94
+ }
95
+
96
+ if (process.argv[2] === 'send') {
97
+ const topic = process.argv[3]
98
+ const messages = process.argv[4] ? Number(process.argv[4]) : 1
99
+ console.log('Sending', messages, 'messages to', topic)
100
+ await produce(topic, messages);
101
+ } else {
102
+ await startConsumer();
103
+ }
package/index.d.ts CHANGED
@@ -3,10 +3,20 @@
3
3
 
4
4
  /* auto-generated by NAPI-RS */
5
5
 
6
- export const enum CommitMode {
7
- AutoCommit = 'AutoCommit',
8
- Sync = 'Sync',
9
- Async = 'Async'
6
+ export const enum ConsumerResult {
7
+ Ok = 'Ok',
8
+ Retry = 'Retry'
9
+ }
10
+ export interface KafkaConsumerConfiguration {
11
+ topic: string
12
+ groupId: string
13
+ retryStrategy?: RetryStrategy
14
+ offset?: OffsetModel
15
+ createTopic?: boolean
16
+ commitMode?: CommitMode
17
+ enableAutoCommit?: boolean
18
+ configuration?: Record<string, string>
19
+ fecthMetadataTimeout?: number
10
20
  }
11
21
  export interface RetryStrategy {
12
22
  retries: number
@@ -16,19 +26,34 @@ export interface RetryStrategy {
16
26
  offset?: OffsetModel
17
27
  configuration?: Record<string, string>
18
28
  }
19
- export const enum ConsumerResult {
20
- Ok = 'Ok',
21
- Retry = 'Retry'
29
+ export const enum CommitMode {
30
+ Sync = 0,
31
+ Async = 1
22
32
  }
23
33
  export interface ConsumerConfiguration {
24
- topic: string
25
34
  groupId: string
26
- retryStrategy?: RetryStrategy
27
- offset?: OffsetModel
28
35
  createTopic?: boolean
29
- commitMode?: CommitMode
30
36
  enableAutoCommit?: boolean
31
37
  configuration?: Record<string, string>
38
+ fecthMetadataTimeout?: number
39
+ }
40
+ export const enum PartitionPosition {
41
+ Beginning = 'Beginning',
42
+ End = 'End',
43
+ Stored = 'Stored'
44
+ }
45
+ export interface OffsetModel {
46
+ offset?: number
47
+ position?: PartitionPosition
48
+ }
49
+ export interface PartitionOffset {
50
+ partition: number
51
+ offset: OffsetModel
52
+ }
53
+ export interface TopicPartitionConfig {
54
+ topic: string
55
+ allOffsets?: OffsetModel
56
+ partitionOffset?: Array<PartitionOffset>
32
57
  }
33
58
  export const enum SecurityProtocol {
34
59
  Plaintext = 'Plaintext',
@@ -49,19 +74,6 @@ export interface ProducerConfiguration {
49
74
  thrownOnError?: boolean
50
75
  configuration?: Record<string, string>
51
76
  }
52
- export interface ProducerRecord {
53
- topic: string
54
- messages: Array<MessageModel>
55
- }
56
- export const enum AutoOffsetReset {
57
- Smallest = 'Smallest',
58
- Earliest = 'Earliest',
59
- Beginning = 'Beginning',
60
- Largest = 'Largest',
61
- Latest = 'Latest',
62
- End = 'End',
63
- Error = 'Error'
64
- }
65
77
  export interface Payload {
66
78
  value: Buffer
67
79
  key?: Buffer
@@ -70,18 +82,13 @@ export interface Payload {
70
82
  partition: number
71
83
  offset: number
72
84
  }
73
- export const enum PartitionPosition {
74
- Beginning = 'Beginning',
75
- End = 'End',
76
- Stored = 'Stored'
77
- }
78
- export interface OffsetModel {
79
- offset?: number
80
- position?: PartitionPosition
81
- }
82
- export interface KafkaCrabError {
83
- code: number
84
- message: string
85
+ export interface Message {
86
+ payload: Buffer
87
+ key?: Buffer
88
+ headers?: Record<string, Buffer>
89
+ topic: string
90
+ partition: number
91
+ offset: number
85
92
  }
86
93
  export interface RecordMetadata {
87
94
  topic: string
@@ -89,19 +96,35 @@ export interface RecordMetadata {
89
96
  offset: number
90
97
  error?: KafkaCrabError
91
98
  }
92
- export interface MessageModel {
93
- value: Buffer
99
+ export interface MessageProducer {
100
+ payload: Buffer
94
101
  key?: Buffer
95
102
  headers?: Record<string, Buffer>
96
103
  }
104
+ export interface ProducerRecord {
105
+ topic: string
106
+ messages: Array<MessageProducer>
107
+ }
108
+ export interface KafkaCrabError {
109
+ code: number
110
+ message: string
111
+ }
97
112
  export class KafkaConsumer {
98
113
  startConsumer(callback: (error: Error | undefined, result: Payload) => Promise<ConsumerResult | undefined>): Promise<void>
99
114
  }
115
+ export class KafkaStreamConsumer {
116
+ subscribe(topicConfigs: string | Array<TopicPartitionConfig>): Promise<void>
117
+ unsubscribe(): void
118
+ seek(topic: string, partition: number, offsetModel: OffsetModel, timeout?: number | undefined | null): void
119
+ recv(): Promise<Message>
120
+ commit(topic: string, partition: number, offset: number, commit: CommitMode): void
121
+ }
100
122
  export class KafkaClient {
101
123
  readonly kafkaConfiguration: KafkaConfiguration
102
124
  constructor(kafkaConfiguration: KafkaConfiguration)
103
125
  createProducer(producerConfiguration: ProducerConfiguration): KafkaProducer
104
- createConsumer(consumerConfiguration: ConsumerConfiguration): KafkaConsumer
126
+ createConsumer(consumerConfiguration: KafkaConsumerConfiguration): KafkaConsumer
127
+ createStreamConsumer(consumerConfiguration: ConsumerConfiguration): KafkaStreamConsumer
105
128
  }
106
129
  export class KafkaProducer {
107
130
  send(producerRecord: ProducerRecord): Promise<Array<RecordMetadata>>
package/index.js CHANGED
@@ -224,17 +224,32 @@ switch (platform) {
224
224
  }
225
225
  break
226
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')
227
+ if (isMusl()) {
228
+ localFileExisted = existsSync(
229
+ join(__dirname, 'kafka-crab-js.linux-arm-musleabihf.node')
230
+ )
231
+ try {
232
+ if (localFileExisted) {
233
+ nativeBinding = require('./kafka-crab-js.linux-arm-musleabihf.node')
234
+ } else {
235
+ nativeBinding = require('kafka-crab-js-linux-arm-musleabihf')
236
+ }
237
+ } catch (e) {
238
+ loadError = e
239
+ }
240
+ } else {
241
+ localFileExisted = existsSync(
242
+ join(__dirname, 'kafka-crab-js.linux-arm-gnueabihf.node')
243
+ )
244
+ try {
245
+ if (localFileExisted) {
246
+ nativeBinding = require('./kafka-crab-js.linux-arm-gnueabihf.node')
247
+ } else {
248
+ nativeBinding = require('kafka-crab-js-linux-arm-gnueabihf')
249
+ }
250
+ } catch (e) {
251
+ loadError = e
235
252
  }
236
- } catch (e) {
237
- loadError = e
238
253
  }
239
254
  break
240
255
  case 'riscv64':
@@ -295,13 +310,13 @@ if (!nativeBinding) {
295
310
  throw new Error(`Failed to load native binding`)
296
311
  }
297
312
 
298
- const { CommitMode, ConsumerResult, KafkaConsumer, SecurityProtocol, KafkaClient, KafkaProducer, AutoOffsetReset, PartitionPosition } = nativeBinding
313
+ const { ConsumerResult, KafkaConsumer, KafkaStreamConsumer, CommitMode, PartitionPosition, SecurityProtocol, KafkaClient, KafkaProducer } = nativeBinding
299
314
 
300
- module.exports.CommitMode = CommitMode
301
315
  module.exports.ConsumerResult = ConsumerResult
302
316
  module.exports.KafkaConsumer = KafkaConsumer
317
+ module.exports.KafkaStreamConsumer = KafkaStreamConsumer
318
+ module.exports.CommitMode = CommitMode
319
+ module.exports.PartitionPosition = PartitionPosition
303
320
  module.exports.SecurityProtocol = SecurityProtocol
304
321
  module.exports.KafkaClient = KafkaClient
305
322
  module.exports.KafkaProducer = KafkaProducer
306
- module.exports.AutoOffsetReset = AutoOffsetReset
307
- module.exports.PartitionPosition = PartitionPosition
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kafka-crab-js",
3
- "version": "0.5.4",
3
+ "version": "0.7.0",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "napi": {
@@ -29,8 +29,12 @@
29
29
  "url": "https://github.com/inaiat/kafka-crab-js.git"
30
30
  },
31
31
  "devDependencies": {
32
- "@napi-rs/cli": "^2.18.0",
32
+ "@faker-js/faker": "^8.4.1",
33
+ "@napi-rs/cli": "^2.18.3",
34
+ "@types/node": "^20.12.12",
33
35
  "ava": "^6.1.2",
36
+ "nanoid": "^5.0.7",
37
+ "typescript": "^5.4.5",
34
38
  "xo": "^0.58.0"
35
39
  },
36
40
  "ava": {
@@ -55,10 +59,10 @@
55
59
  "universal": "napi universal",
56
60
  "version": "napi version"
57
61
  },
58
- "packageManager": "yarn@4.1.1",
62
+ "packageManager": "pnpm@9.1.1",
59
63
  "optionalDependencies": {
60
- "kafka-crab-js-darwin-arm64": "0.5.4",
61
- "kafka-crab-js-darwin-x64": "0.5.4",
62
- "kafka-crab-js-linux-x64-gnu": "0.5.4"
64
+ "kafka-crab-js-darwin-arm64": "0.7.0",
65
+ "kafka-crab-js-darwin-x64": "0.7.0",
66
+ "kafka-crab-js-linux-x64-gnu": "0.7.0"
63
67
  }
64
68
  }
@@ -0,0 +1,190 @@
1
+ use std::{collections::HashMap, time::Duration};
2
+
3
+ use rdkafka::{
4
+ config::RDKafkaLogLevel,
5
+ consumer::{Consumer, StreamConsumer},
6
+ ClientConfig, Offset, TopicPartitionList,
7
+ };
8
+ use tracing::{debug, error, info, warn};
9
+
10
+ use crate::kafka::{consumer::model::LoggingConsumer, kafka_admin::KafkaAdmin};
11
+
12
+ use super::model::{
13
+ ConsumerConfiguration, CustomContext, OffsetModel, PartitionOffset, PartitionPosition,
14
+ };
15
+
16
+ pub fn convert_to_rdkafka_offset(offset_model: &OffsetModel) -> Offset {
17
+ match offset_model.position {
18
+ Some(PartitionPosition::Beginning) => Offset::Beginning,
19
+ Some(PartitionPosition::End) => Offset::End,
20
+ Some(PartitionPosition::Stored) => Offset::Stored,
21
+ None => match offset_model.offset {
22
+ Some(value) => Offset::Offset(value),
23
+ None => Offset::Stored, // Default to stored
24
+ },
25
+ }
26
+ }
27
+
28
+ pub async fn create_stream_consumer_and_setup_everything(
29
+ client_config: &ClientConfig,
30
+ consumer_configuration: &ConsumerConfiguration,
31
+ topic: &str,
32
+ offset: &Option<OffsetModel>,
33
+ configuration: Option<HashMap<String, String>>,
34
+ timeout: Duration,
35
+ ) -> anyhow::Result<StreamConsumer<CustomContext>> {
36
+ let consumer = create_stream_consumer(client_config, consumer_configuration, configuration)?;
37
+
38
+ if consumer_configuration.create_topic.unwrap_or(true) {
39
+ try_create_topic(&vec![topic.to_owned()], client_config, timeout).await?;
40
+ }
41
+
42
+ if let Some(offset_model) = offset {
43
+ set_offset_of_all_partitions(offset_model, &consumer, topic, timeout)?;
44
+ }
45
+
46
+ try_subscribe(&consumer, &vec![topic.to_owned()])?;
47
+
48
+ Ok(consumer)
49
+ }
50
+
51
+ pub fn create_stream_consumer(
52
+ client_config: &ClientConfig,
53
+ consumer_configuration: &ConsumerConfiguration,
54
+ configuration: Option<HashMap<String, String>>,
55
+ ) -> anyhow::Result<StreamConsumer<CustomContext>> {
56
+ let context = CustomContext;
57
+
58
+ let ConsumerConfiguration {
59
+ group_id,
60
+ enable_auto_commit,
61
+ ..
62
+ } = consumer_configuration.clone();
63
+
64
+ let mut consumer_config: ClientConfig = client_config.clone();
65
+
66
+ if let Some(config) = configuration {
67
+ consumer_config.extend(config);
68
+ }
69
+
70
+ debug!(
71
+ "Creating consumer with configuration: {:?}",
72
+ consumer_config
73
+ );
74
+
75
+ let consumer: LoggingConsumer = consumer_config
76
+ .clone()
77
+ .set("group.id", group_id.clone())
78
+ .set(
79
+ "enable.auto.commit",
80
+ enable_auto_commit.unwrap_or(true).to_string(),
81
+ )
82
+ .set_log_level(RDKafkaLogLevel::Debug)
83
+ .create_with_context(context)?;
84
+
85
+ info!("Consumer created. Group id: {:?}", group_id);
86
+ Ok(consumer)
87
+ }
88
+
89
+ pub fn try_subscribe(consumer: &LoggingConsumer, topics: &Vec<String>) -> anyhow::Result<()> {
90
+ let topics_ref = topics.iter().map(|x| x.as_str()).collect::<Vec<&str>>();
91
+ consumer.subscribe(topics_ref.as_slice()).map_err(|e| {
92
+ anyhow::Error::msg(format!(
93
+ "Can't subscribe to specified topic(s): {:?}. Error: {:?}",
94
+ topics_ref, e
95
+ ))
96
+ })?;
97
+ debug!("Subscribed to topic(s): {:?}", topics_ref);
98
+ Ok(())
99
+ }
100
+
101
+ pub async fn try_create_topic(
102
+ topics: &Vec<String>,
103
+ client_config: &ClientConfig,
104
+ fetch_metadata_timeout: Duration,
105
+ ) -> anyhow::Result<()> {
106
+ let admin = KafkaAdmin::new(client_config, Some(fetch_metadata_timeout));
107
+ let result = admin.create_topic(topics).await;
108
+ if let Err(e) = result {
109
+ warn!("Fail to create topic {:?}", e);
110
+ return Err(anyhow::Error::msg(format!("Fail to create topic: {:?}", e)));
111
+ }
112
+ info!("Topic(s) created: {:?}", topics);
113
+ Ok(())
114
+ }
115
+
116
+ pub fn set_offset_of_all_partitions(
117
+ offset_model: &OffsetModel,
118
+ consumer: &StreamConsumer<CustomContext>,
119
+ topic: &str,
120
+ timeout: Duration,
121
+ ) -> anyhow::Result<()> {
122
+ let offset = convert_to_rdkafka_offset(offset_model);
123
+ debug!("Setting offset to: {:?}", offset);
124
+ let metadata = consumer.fetch_metadata(Some(topic), timeout)?;
125
+
126
+ metadata.topics().iter().for_each(|meta_topic| {
127
+ let mut tpl = TopicPartitionList::new();
128
+ meta_topic.partitions().iter().for_each(|meta_partition| {
129
+ info!("Adding partition: {:?}", meta_partition.id());
130
+ tpl.add_partition(topic, meta_partition.id());
131
+ });
132
+ match tpl.set_all_offsets(offset) {
133
+ Ok(_) => {
134
+ debug!("Offset set to: {:?}", offset);
135
+ }
136
+ Err(e) => {
137
+ error!("Fail to set offset: {:?}", e)
138
+ }
139
+ };
140
+ match consumer.assign(&tpl) {
141
+ Ok(_) => {
142
+ debug!("Assigning topic: {:?}", topic);
143
+ }
144
+ Err(e) => {
145
+ error!("Fail to assign topic: {:?}", e);
146
+ }
147
+ }
148
+ });
149
+
150
+ Ok(())
151
+ }
152
+
153
+ pub fn assign_offset_or_use_metadata(
154
+ topic: &str,
155
+ partition_offset: Option<Vec<PartitionOffset>>,
156
+ offset_model: Option<&OffsetModel>,
157
+ consumer: &StreamConsumer<CustomContext>,
158
+ timeout: Duration,
159
+ ) -> anyhow::Result<()> {
160
+ let mut tpl = TopicPartitionList::new();
161
+
162
+ if let Some(value) = partition_offset {
163
+ for item in value {
164
+ let offset = convert_to_rdkafka_offset(&item.offset);
165
+ debug!(
166
+ "Adding partition: {:?} with offset: {:?} for topic: {:?}",
167
+ item.partition, offset, topic
168
+ );
169
+ tpl.add_partition_offset(topic, item.partition, offset)?;
170
+ }
171
+ } else if let Some(offset_model) = offset_model {
172
+ let offset = convert_to_rdkafka_offset(offset_model);
173
+ let metadata = consumer.fetch_metadata(Some(topic), timeout)?;
174
+ for meta_topic in metadata.topics() {
175
+ for meta_partition in meta_topic.partitions() {
176
+ info!(
177
+ "Adding partition: {:?} with offset: {:?} for topic: {:?}",
178
+ meta_partition.id(),
179
+ offset,
180
+ topic
181
+ );
182
+ tpl.add_partition_offset(topic, meta_partition.id(), offset)?;
183
+ }
184
+ }
185
+ } else {
186
+ anyhow::bail!("At least one of partition_offset or offset_model should be provided");
187
+ }
188
+ consumer.assign(&tpl)?;
189
+ Ok(())
190
+ }
@@ -15,12 +15,13 @@ use crate::kafka::{
15
15
  kafka_util::{
16
16
  kakfa_headers_to_hashmap, kakfa_headers_to_hashmap_buffer, ExtractValueOnKafkaHashMap,
17
17
  },
18
- model::Payload,
18
+ producer::model::Payload,
19
19
  };
20
20
 
21
- use super::kafka_consumer::{ConsumerResult, ProducerHelper};
22
-
23
- use super::consumer_context::CustomContext;
21
+ use super::{
22
+ kafka_consumer::{ConsumerResult, ProducerHelper},
23
+ model::CustomContext,
24
+ };
24
25
 
25
26
  pub struct ConsumerThread {
26
27
  pub stream_consumer: StreamConsumer<CustomContext>,
@@ -181,7 +182,7 @@ impl ConsumerThread {
181
182
  }
182
183
  }
183
184
 
184
- fn create_payload(message: &BorrowedMessage<'_>, payload: &[u8]) -> Payload {
185
+ pub fn create_payload(message: &BorrowedMessage<'_>, payload: &[u8]) -> Payload {
185
186
  let key: Option<Buffer> = message.key().map(|bytes| bytes.into());
186
187
  let headers = Some(kakfa_headers_to_hashmap_buffer(message.headers()));
187
188
  let payload_js = Payload::new(