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 +3 -0
- package/Cargo.toml +29 -0
- package/README.md +5 -0
- package/build.rs +5 -0
- package/example/sample.mjs +50 -0
- package/index.d.ts +82 -0
- package/index.js +265 -0
- package/package.json +44 -0
- package/src/kafka/kafka_admin.rs +129 -0
- package/src/kafka/kafka_client.rs +131 -0
- package/src/kafka/kafka_consumer.rs +188 -0
- package/src/kafka/kafka_producer.rs +57 -0
- package/src/kafka/kafka_util.rs +78 -0
- package/src/kafka/mod.rs +6 -0
- package/src/kafka/model.rs +86 -0
- package/src/lib.rs +6 -0
package/.yarnrc.yml
ADDED
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
package/build.rs
ADDED
|
@@ -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
|
+
}
|
package/src/kafka/mod.rs
ADDED
|
@@ -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
|
+
}
|