pulsar-client 1.1.0 → 1.2.1

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/.asf.yaml ADDED
@@ -0,0 +1,46 @@
1
+ #
2
+ # Licensed to the Apache Software Foundation (ASF) under one
3
+ # or more contributor license agreements. See the NOTICE file
4
+ # distributed with this work for additional information
5
+ # regarding copyright ownership. The ASF licenses this file
6
+ # to you under the Apache License, Version 2.0 (the
7
+ # "License"); you may not use this file except in compliance
8
+ # with the License. You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing,
13
+ # software distributed under the License is distributed on an
14
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ # KIND, either express or implied. See the License for the
16
+ # specific language governing permissions and limitations
17
+ # under the License.
18
+ #
19
+
20
+ github:
21
+ description: "Apache Pulsar NodeJS Client"
22
+ homepage: https://pulsar.apache.org/
23
+ labels:
24
+ - pulsar
25
+ - pubsub
26
+ - messaging
27
+ - streaming
28
+ - queuing
29
+ - event-streaming
30
+ - node
31
+ - javascript
32
+ - nodejs
33
+ features:
34
+ # Enable wiki for documentation
35
+ wiki: true
36
+ # Enable issues management
37
+ issues: true
38
+ # Enable projects for project management boards
39
+ projects: true
40
+ enabled_merge_buttons:
41
+ # enable squash button:
42
+ squash: true
43
+ # disable merge button:
44
+ merge: false
45
+ # disable rebase button:
46
+ rebase: false
package/.eslintrc.json CHANGED
@@ -6,6 +6,14 @@
6
6
  "no-console": "off",
7
7
  "no-unused-vars": "warn"
8
8
  },
9
+ "overrides": [
10
+ {
11
+ "files": ["tests/*.js"],
12
+ "rules": {
13
+ "no-await-in-loop": "off"
14
+ }
15
+ }
16
+ ],
9
17
  "globals": {
10
18
  "describe": true,
11
19
  "test": true,
package/README.md CHANGED
@@ -44,6 +44,7 @@ Compatibility between each version of the Node.js client and the C++ client is a
44
44
  |----------------|----------------|
45
45
  | 1.0.0 | 2.3.0 or later |
46
46
  | 1.1.0 | 2.4.0 or later |
47
+ | 1.2.0 | 2.5.0 or later |
47
48
 
48
49
  If an incompatible version of the C++ client is installed, you may fail to build or run this library.
49
50
 
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+
20
+ const Pulsar = require('pulsar-client');
21
+
22
+ (async () => {
23
+ // Create a client
24
+ const client = new Pulsar.Client({
25
+ serviceUrl: 'pulsar://localhost:6650',
26
+ operationTimeoutSeconds: 30,
27
+ });
28
+
29
+ // Create a consumer with listener
30
+ const consumer = await client.subscribe({
31
+ topic: 'persistent://public/default/my-topic',
32
+ subscription: 'sub1',
33
+ subscriptionType: 'Shared',
34
+ ackTimeoutMs: 10000,
35
+ listener: (msg, msgConsumer) => {
36
+ console.log(msg.getData().toString());
37
+ msgConsumer.acknowledge(msg);
38
+ },
39
+ });
40
+ })();
package/index.js CHANGED
@@ -22,6 +22,13 @@ const AuthenticationTls = require('./src/AuthenticationTls.js');
22
22
  const AuthenticationAthenz = require('./src/AuthenticationAthenz.js');
23
23
  const AuthenticationToken = require('./src/AuthenticationToken.js');
24
24
 
25
+ const LogLevel = {
26
+ DEBUG: 0,
27
+ INFO: 1,
28
+ WARN: 2,
29
+ ERROR: 3,
30
+ };
31
+
25
32
  const Pulsar = {
26
33
  Client: PulsarBinding.Client,
27
34
  Message: PulsarBinding.Message,
@@ -29,6 +36,7 @@ const Pulsar = {
29
36
  AuthenticationTls,
30
37
  AuthenticationAthenz,
31
38
  AuthenticationToken,
39
+ LogLevel,
32
40
  };
33
41
 
34
42
  module.exports = Pulsar;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulsar-client",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Pulsar Node.js client",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -11,8 +11,8 @@
11
11
  "install": "node-pre-gyp install --fallback-to-build",
12
12
  "clean": "node-gyp clean",
13
13
  "configure": "node-gyp configure",
14
- "lint": "eslint --ext .js .",
15
- "format": "clang-format -i --verbose src/* && eslint --fix --ext .js .",
14
+ "lint": "clang-format-lint src/*.cc src/*.h && eslint --ext .js .",
15
+ "format": "clang-format-lint --fix src/*.cc src/*.h && eslint --fix --ext .js .",
16
16
  "build": "npm run format && node-gyp rebuild",
17
17
  "build:debug": "npm run format && node-gyp rebuild --debug",
18
18
  "license:report": "mkdir -p report && grunt grunt-license-report",
@@ -30,6 +30,7 @@
30
30
  "node": ">=10.16.0"
31
31
  },
32
32
  "devDependencies": {
33
+ "@seadub/clang-format-lint": "0.0.2",
33
34
  "clang-format": "^1.2.4",
34
35
  "commander": "^2.20.0",
35
36
  "delay": "^4.3.0",
@@ -41,7 +42,7 @@
41
42
  "hdr-histogram-js": "^1.1.4",
42
43
  "jest": "^24.8.0",
43
44
  "license-check-and-add": "^2.3.6",
44
- "lodash": "^4.17.15"
45
+ "lodash": "^4.17.19"
45
46
  },
46
47
  "dependencies": {
47
48
  "bindings": "^1.3.1",
@@ -1 +1 @@
1
- 2.4.0
1
+ 2.5.0
package/run-unit-tests.sh CHANGED
@@ -33,7 +33,7 @@ done;
33
33
  apt install $PULSAR_PKG_DIR/apache-pulsar-client*.deb
34
34
 
35
35
  ./pulsar-test-service-start.sh
36
- npm install && npm run build && npm run test
36
+ npm install && npm run lint && npm run build && npm run test
37
37
  RES=$?
38
38
  ./pulsar-test-service-stop.sh
39
39
 
package/src/Client.cc CHANGED
@@ -38,6 +38,7 @@ static const std::string CFG_TLS_TRUST_CERT = "tlsTrustCertsFilePath";
38
38
  static const std::string CFG_TLS_VALIDATE_HOSTNAME = "tlsValidateHostname";
39
39
  static const std::string CFG_TLS_ALLOW_INSECURE = "tlsAllowInsecureConnection";
40
40
  static const std::string CFG_STATS_INTERVAL = "statsIntervalInSeconds";
41
+ static const std::string CFG_LOG = "log";
41
42
 
42
43
  Napi::FunctionReference Client::constructor;
43
44
 
@@ -73,6 +74,17 @@ Client::Client(const Napi::CallbackInfo &info) : Napi::ObjectWrap<Client>(info)
73
74
 
74
75
  pulsar_client_configuration_t *cClientConfig = pulsar_client_configuration_create();
75
76
 
77
+ if (clientConfig.Has(CFG_LOG) && clientConfig.Get(CFG_LOG).IsFunction()) {
78
+ Napi::ThreadSafeFunction logFunction = Napi::ThreadSafeFunction::New(
79
+ env, clientConfig.Get(CFG_LOG).As<Napi::Function>(), "Pulsar Logging", 0, 1);
80
+ this->logCallback = new LogCallback();
81
+ this->logCallback->callback = logFunction;
82
+
83
+ pulsar_client_configuration_set_logger(cClientConfig, &LogMessage, this->logCallback);
84
+ } else {
85
+ this->logCallback = nullptr;
86
+ }
87
+
76
88
  if (clientConfig.Has(CFG_AUTH) && clientConfig.Get(CFG_AUTH).IsObject()) {
77
89
  Napi::Object obj = clientConfig.Get(CFG_AUTH).ToObject();
78
90
  if (obj.Has(CFG_AUTH_PROP) && obj.Get(CFG_AUTH_PROP).IsObject()) {
@@ -139,14 +151,20 @@ Client::Client(const Napi::CallbackInfo &info) : Napi::ObjectWrap<Client>(info)
139
151
 
140
152
  this->cClient = pulsar_client_create(serviceUrl.Utf8Value().c_str(), cClientConfig);
141
153
  pulsar_client_configuration_free(cClientConfig);
154
+ this->Ref();
142
155
  }
143
156
 
144
- Client::~Client() { pulsar_client_free(this->cClient); }
157
+ Client::~Client() {
158
+ pulsar_client_free(this->cClient);
159
+ if (this->logCallback != nullptr) {
160
+ this->logCallback->callback.Release();
161
+ this->logCallback = nullptr;
162
+ }
163
+ }
145
164
 
146
165
  Napi::Value Client::CreateProducer(const Napi::CallbackInfo &info) {
147
166
  return Producer::NewInstance(info, this->cClient);
148
167
  }
149
-
150
168
  Napi::Value Client::Subscribe(const Napi::CallbackInfo &info) {
151
169
  return Consumer::NewInstance(info, this->cClient);
152
170
  }
@@ -155,6 +173,29 @@ Napi::Value Client::CreateReader(const Napi::CallbackInfo &info) {
155
173
  return Reader::NewInstance(info, this->cClient);
156
174
  }
157
175
 
176
+ void LogMessageProxy(Napi::Env env, Napi::Function jsCallback, struct LogMessage *logMessage) {
177
+ Napi::Number logLevel = Napi::Number::New(env, static_cast<double>(logMessage->level));
178
+ Napi::String file = Napi::String::New(env, logMessage->file);
179
+ Napi::Number line = Napi::Number::New(env, static_cast<double>(logMessage->line));
180
+ Napi::String message = Napi::String::New(env, logMessage->message);
181
+
182
+ delete logMessage;
183
+ jsCallback.Call({logLevel, file, line, message});
184
+ }
185
+
186
+ void LogMessage(pulsar_logger_level_t level, const char *file, int line, const char *message, void *ctx) {
187
+ LogCallback *logCallback = (LogCallback *)ctx;
188
+
189
+ if (logCallback->callback.Acquire() != napi_ok) {
190
+ return;
191
+ }
192
+
193
+ struct LogMessage *logMessage = new struct LogMessage(level, std::string(file), line, std::string(message));
194
+
195
+ logCallback->callback.BlockingCall(logMessage, LogMessageProxy);
196
+ logCallback->callback.Release();
197
+ }
198
+
158
199
  class ClientCloseWorker : public Napi::AsyncWorker {
159
200
  public:
160
201
  ClientCloseWorker(const Napi::Promise::Deferred &deferred, pulsar_client_t *cClient)
@@ -178,6 +219,8 @@ class ClientCloseWorker : public Napi::AsyncWorker {
178
219
  };
179
220
 
180
221
  Napi::Value Client::Close(const Napi::CallbackInfo &info) {
222
+ this->Unref();
223
+
181
224
  Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(info.Env());
182
225
  ClientCloseWorker *wk = new ClientCloseWorker(deferred, this->cClient);
183
226
  wk->Queue();
package/src/Client.h CHANGED
@@ -23,15 +23,33 @@
23
23
  #include <napi.h>
24
24
  #include <pulsar/c/client.h>
25
25
 
26
+ struct LogMessage {
27
+ pulsar_logger_level_t level;
28
+ std::string file;
29
+ int line;
30
+ std::string message;
31
+
32
+ LogMessage(pulsar_logger_level_t level, std::string file, int line, std::string message)
33
+ : level(level), file(file), line(line), message(message) {}
34
+ };
35
+
36
+ struct LogCallback {
37
+ Napi::ThreadSafeFunction callback;
38
+ };
39
+
40
+ void LogMessage(pulsar_logger_level_t level, const char *file, int line, const char *message, void *ctx);
41
+
26
42
  class Client : public Napi::ObjectWrap<Client> {
27
43
  public:
28
44
  static Napi::Object Init(Napi::Env env, Napi::Object exports);
45
+
29
46
  Client(const Napi::CallbackInfo &info);
30
47
  ~Client();
31
48
 
32
49
  private:
33
50
  static Napi::FunctionReference constructor;
34
51
  pulsar_client_t *cClient;
52
+ LogCallback *logCallback;
35
53
 
36
54
  Napi::Value CreateProducer(const Napi::CallbackInfo &info);
37
55
  Napi::Value Subscribe(const Napi::CallbackInfo &info);
package/src/Consumer.cc CHANGED
@@ -45,8 +45,49 @@ void Consumer::Init(Napi::Env env, Napi::Object exports) {
45
45
  constructor.SuppressDestruct();
46
46
  }
47
47
 
48
+ struct MessageListenerProxyData {
49
+ pulsar_message_t *cMessage;
50
+ Consumer *consumer;
51
+
52
+ MessageListenerProxyData(pulsar_message_t *cMessage, Consumer *consumer)
53
+ : cMessage(cMessage), consumer(consumer) {}
54
+ };
55
+
56
+ void MessageListenerProxy(Napi::Env env, Napi::Function jsCallback, MessageListenerProxyData *data) {
57
+ Napi::Object msg = Message::NewInstance({}, data->cMessage);
58
+ Consumer *consumer = data->consumer;
59
+ delete data;
60
+
61
+ jsCallback.Call({msg, consumer->Value()});
62
+ }
63
+
64
+ void MessageListener(pulsar_consumer_t *cConsumer, pulsar_message_t *cMessage, void *ctx) {
65
+ ListenerCallback *listenerCallback = (ListenerCallback *)ctx;
66
+
67
+ Consumer *consumer = (Consumer *)listenerCallback->consumer;
68
+
69
+ if (listenerCallback->callback.Acquire() != napi_ok) {
70
+ return;
71
+ }
72
+
73
+ MessageListenerProxyData *dataPtr = new MessageListenerProxyData(cMessage, consumer);
74
+ listenerCallback->callback.BlockingCall(dataPtr, MessageListenerProxy);
75
+ listenerCallback->callback.Release();
76
+ }
77
+
48
78
  void Consumer::SetCConsumer(std::shared_ptr<CConsumerWrapper> cConsumer) { this->wrapper = cConsumer; }
49
- void Consumer::SetListenerCallback(ListenerCallback *listener) { this->listener = listener; }
79
+ void Consumer::SetListenerCallback(ListenerCallback *listener) {
80
+ if (listener) {
81
+ // Maintain reference to consumer, so it won't get garbage collected
82
+ // since, when we have a listener, we don't have to maintain reference to consumer (in js code)
83
+ this->Ref();
84
+
85
+ // Pass consumer as argument
86
+ listener->consumer = this;
87
+ }
88
+
89
+ this->listener = listener;
90
+ }
50
91
 
51
92
  Consumer::Consumer(const Napi::CallbackInfo &info) : Napi::ObjectWrap<Consumer>(info), listener(nullptr) {}
52
93
 
@@ -100,6 +141,7 @@ class ConsumerNewInstanceWorker : public Napi::AsyncWorker {
100
141
  void OnOK() {
101
142
  Napi::Object obj = Consumer::constructor.New({});
102
143
  Consumer *consumer = Consumer::Unwrap(obj);
144
+
103
145
  consumer->SetCConsumer(this->consumerWrapper);
104
146
  consumer->SetListenerCallback(this->listener);
105
147
  this->deferred.Resolve(obj);
@@ -118,8 +160,10 @@ class ConsumerNewInstanceWorker : public Napi::AsyncWorker {
118
160
  Napi::Value Consumer::NewInstance(const Napi::CallbackInfo &info, pulsar_client_t *cClient) {
119
161
  Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(info.Env());
120
162
  Napi::Object config = info[0].As<Napi::Object>();
163
+
121
164
  std::shared_ptr<CConsumerWrapper> consumerWrapper = std::make_shared<CConsumerWrapper>();
122
- ConsumerConfig *consumerConfig = new ConsumerConfig(config, consumerWrapper);
165
+
166
+ ConsumerConfig *consumerConfig = new ConsumerConfig(config, consumerWrapper, &MessageListener);
123
167
  ConsumerNewInstanceWorker *wk =
124
168
  new ConsumerNewInstanceWorker(deferred, cClient, consumerConfig, consumerWrapper);
125
169
  wk->Queue();
@@ -213,17 +257,25 @@ void Consumer::AcknowledgeCumulativeId(const Napi::CallbackInfo &info) {
213
257
 
214
258
  class ConsumerCloseWorker : public Napi::AsyncWorker {
215
259
  public:
216
- ConsumerCloseWorker(const Napi::Promise::Deferred &deferred, pulsar_consumer_t *cConsumer)
260
+ ConsumerCloseWorker(const Napi::Promise::Deferred &deferred, pulsar_consumer_t *cConsumer,
261
+ Consumer *consumer)
217
262
  : AsyncWorker(Napi::Function::New(deferred.Promise().Env(), [](const Napi::CallbackInfo &info) {})),
218
263
  deferred(deferred),
219
- cConsumer(cConsumer) {}
264
+ cConsumer(cConsumer),
265
+ consumer(consumer) {}
266
+
220
267
  ~ConsumerCloseWorker() {}
221
268
  void Execute() {
222
269
  pulsar_consumer_pause_message_listener(this->cConsumer);
223
270
  pulsar_result result = pulsar_consumer_close(this->cConsumer);
224
- if (result != pulsar_result_Ok) SetError(pulsar_result_str(result));
271
+ if (result != pulsar_result_Ok) {
272
+ SetError(pulsar_result_str(result));
273
+ }
274
+ }
275
+ void OnOK() {
276
+ this->consumer->Cleanup();
277
+ this->deferred.Resolve(Env().Null());
225
278
  }
226
- void OnOK() { this->deferred.Resolve(Env().Null()); }
227
279
  void OnError(const Napi::Error &e) {
228
280
  this->deferred.Reject(
229
281
  Napi::Error::New(Env(), std::string("Failed to close consumer: ") + e.Message()).Value());
@@ -232,18 +284,31 @@ class ConsumerCloseWorker : public Napi::AsyncWorker {
232
284
  private:
233
285
  Napi::Promise::Deferred deferred;
234
286
  pulsar_consumer_t *cConsumer;
287
+ Consumer *consumer;
235
288
  };
236
289
 
290
+ void Consumer::Cleanup() {
291
+ if (this->listener) {
292
+ this->CleanupListener();
293
+ }
294
+ }
295
+
296
+ void Consumer::CleanupListener() {
297
+ pulsar_consumer_pause_message_listener(this->wrapper->cConsumer);
298
+ this->Unref();
299
+ this->listener->callback.Release();
300
+ this->listener = nullptr;
301
+ }
302
+
237
303
  Napi::Value Consumer::Close(const Napi::CallbackInfo &info) {
238
304
  Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(info.Env());
239
- ConsumerCloseWorker *wk = new ConsumerCloseWorker(deferred, this->wrapper->cConsumer);
305
+ ConsumerCloseWorker *wk = new ConsumerCloseWorker(deferred, this->wrapper->cConsumer, this);
240
306
  wk->Queue();
241
307
  return deferred.Promise();
242
308
  }
243
309
 
244
310
  Consumer::~Consumer() {
245
311
  if (this->listener) {
246
- pulsar_consumer_pause_message_listener(this->wrapper->cConsumer);
247
- this->listener->callback.Release();
312
+ this->CleanupListener();
248
313
  }
249
314
  }
package/src/Consumer.h CHANGED
@@ -23,6 +23,7 @@
23
23
  #include <napi.h>
24
24
  #include <pulsar/c/client.h>
25
25
  #include "ConsumerConfig.h"
26
+ #include "MessageListener.h"
26
27
 
27
28
  class Consumer : public Napi::ObjectWrap<Consumer> {
28
29
  public:
@@ -33,6 +34,8 @@ class Consumer : public Napi::ObjectWrap<Consumer> {
33
34
  ~Consumer();
34
35
  void SetCConsumer(std::shared_ptr<CConsumerWrapper> cConsumer);
35
36
  void SetListenerCallback(ListenerCallback *listener);
37
+ void Cleanup();
38
+ void CleanupListener();
36
39
 
37
40
  private:
38
41
  std::shared_ptr<CConsumerWrapper> wrapper;
@@ -35,47 +35,22 @@ static const std::string CFG_RECV_QUEUE_ACROSS_PARTITIONS = "receiverQueueSizeAc
35
35
  static const std::string CFG_CONSUMER_NAME = "consumerName";
36
36
  static const std::string CFG_PROPS = "properties";
37
37
  static const std::string CFG_LISTENER = "listener";
38
+ static const std::string CFG_READ_COMPACTED = "readCompacted";
38
39
 
39
40
  static const std::map<std::string, pulsar_consumer_type> SUBSCRIPTION_TYPE = {
40
41
  {"Exclusive", pulsar_ConsumerExclusive},
41
42
  {"Shared", pulsar_ConsumerShared},
43
+ {"KeyShared", pulsar_ConsumerKeyShared},
42
44
  {"Failover", pulsar_ConsumerFailover}};
43
45
 
44
46
  static const std::map<std::string, initial_position> INIT_POSITION = {
45
47
  {"Latest", initial_position_latest}, {"Earliest", initial_position_earliest}};
46
48
 
47
- struct MessageListenerProxyData {
48
- std::shared_ptr<CConsumerWrapper> consumerWrapper;
49
- pulsar_message_t *cMessage;
50
-
51
- MessageListenerProxyData(std::shared_ptr<CConsumerWrapper> consumerWrapper, pulsar_message_t *cMessage)
52
- : consumerWrapper(consumerWrapper), cMessage(cMessage) {}
53
- };
54
-
55
- void MessageListenerProxy(Napi::Env env, Napi::Function jsCallback, MessageListenerProxyData *data) {
56
- Napi::Object msg = Message::NewInstance({}, data->cMessage);
57
- Napi::Object consumerObj = Consumer::constructor.New({});
58
- Consumer *consumer = Consumer::Unwrap(consumerObj);
59
- consumer->SetCConsumer(std::move(data->consumerWrapper));
60
- delete data;
61
- jsCallback.Call({msg, consumerObj});
62
- }
63
-
64
- void MessageListener(pulsar_consumer_t *cConsumer, pulsar_message_t *cMessage, void *ctx) {
65
- ListenerCallback *listenerCallback = (ListenerCallback *)ctx;
66
- if (listenerCallback->callback.Acquire() != napi_ok) {
67
- return;
68
- };
69
- MessageListenerProxyData *dataPtr =
70
- new MessageListenerProxyData(listenerCallback->consumerWrapper, cMessage);
71
- listenerCallback->callback.BlockingCall(dataPtr, MessageListenerProxy);
72
- listenerCallback->callback.Release();
73
- }
74
-
75
49
  void FinalizeListenerCallback(Napi::Env env, ListenerCallback *cb, void *) { delete cb; }
76
50
 
77
51
  ConsumerConfig::ConsumerConfig(const Napi::Object &consumerConfig,
78
- std::shared_ptr<CConsumerWrapper> consumerWrapper)
52
+ std::shared_ptr<CConsumerWrapper> consumerWrapper,
53
+ pulsar_message_listener messageListener)
79
54
  : topic(""), subscription(""), ackTimeoutMs(0), nAckRedeliverTimeoutMs(60000), listener(nullptr) {
80
55
  this->cConsumerConfig = pulsar_consumer_configuration_create();
81
56
 
@@ -154,14 +129,20 @@ ConsumerConfig::ConsumerConfig(const Napi::Object &consumerConfig,
154
129
 
155
130
  if (consumerConfig.Has(CFG_LISTENER) && consumerConfig.Get(CFG_LISTENER).IsFunction()) {
156
131
  this->listener = new ListenerCallback();
157
- this->listener->consumerWrapper = consumerWrapper;
158
132
  Napi::ThreadSafeFunction callback = Napi::ThreadSafeFunction::New(
159
133
  consumerConfig.Env(), consumerConfig.Get(CFG_LISTENER).As<Napi::Function>(), "Listener Callback", 1,
160
134
  1, (void *)NULL, FinalizeListenerCallback, listener);
161
135
  this->listener->callback = std::move(callback);
162
- pulsar_consumer_configuration_set_message_listener(this->cConsumerConfig, &MessageListener,
136
+ pulsar_consumer_configuration_set_message_listener(this->cConsumerConfig, messageListener,
163
137
  this->listener);
164
138
  }
139
+
140
+ if (consumerConfig.Has(CFG_READ_COMPACTED) && consumerConfig.Get(CFG_READ_COMPACTED).IsBoolean()) {
141
+ bool readCompacted = consumerConfig.Get(CFG_READ_COMPACTED).ToBoolean();
142
+ if (readCompacted) {
143
+ pulsar_consumer_set_read_compacted(this->cConsumerConfig, 1);
144
+ }
145
+ }
165
146
  }
166
147
 
167
148
  ConsumerConfig::~ConsumerConfig() {
@@ -20,31 +20,22 @@
20
20
  #ifndef CONSUMER_CONFIG_H
21
21
  #define CONSUMER_CONFIG_H
22
22
 
23
- #include <napi.h>
24
23
  #include <pulsar/c/consumer_configuration.h>
24
+ #include "MessageListener.h"
25
25
 
26
26
  #define MIN_ACK_TIMEOUT_MILLIS 10000
27
27
 
28
- struct CConsumerWrapper {
29
- pulsar_consumer_t *cConsumer;
30
- CConsumerWrapper();
31
- ~CConsumerWrapper();
32
- };
33
-
34
- struct ListenerCallback {
35
- Napi::ThreadSafeFunction callback;
36
- std::shared_ptr<CConsumerWrapper> consumerWrapper;
37
- };
38
-
39
28
  class ConsumerConfig {
40
29
  public:
41
- ConsumerConfig(const Napi::Object &consumerConfig, std::shared_ptr<CConsumerWrapper> consumerWrapper);
30
+ ConsumerConfig(const Napi::Object &consumerConfig, std::shared_ptr<CConsumerWrapper> consumerWrapper,
31
+ pulsar_message_listener messageListener);
42
32
  ~ConsumerConfig();
43
33
  pulsar_consumer_configuration_t *GetCConsumerConfig();
44
34
  std::string GetTopic();
45
35
  std::string GetSubscription();
46
36
  int64_t GetAckTimeoutMs();
47
37
  int64_t GetNAckRedeliverTimeoutMs();
38
+
48
39
  ListenerCallback *GetListenerCallback();
49
40
 
50
41
  private:
package/src/Message.cc CHANGED
@@ -40,6 +40,7 @@ Napi::Object Message::Init(Napi::Env env, Napi::Object exports) {
40
40
  InstanceMethod("getMessageId", &Message::GetMessageId),
41
41
  InstanceMethod("getPublishTimestamp", &Message::GetPublishTimestamp),
42
42
  InstanceMethod("getEventTimestamp", &Message::GetEventTimestamp),
43
+ InstanceMethod("getRedeliveryCount", &Message::GetRedeliveryCount),
43
44
  InstanceMethod("getPartitionKey", &Message::GetPartitionKey)});
44
45
 
45
46
  constructor = Napi::Persistent(func);
@@ -68,6 +69,14 @@ Napi::Value Message::GetTopicName(const Napi::CallbackInfo &info) {
68
69
  return Napi::String::New(env, pulsar_message_get_topic_name(this->cMessage));
69
70
  }
70
71
 
72
+ Napi::Value Message::GetRedeliveryCount(const Napi::CallbackInfo &info) {
73
+ Napi::Env env = info.Env();
74
+ if (!ValidateCMessage(env)) {
75
+ return env.Null();
76
+ }
77
+ return Napi::Number::New(env, pulsar_message_get_redelivery_count(this->cMessage));
78
+ }
79
+
71
80
  Napi::Value Message::GetProperties(const Napi::CallbackInfo &info) {
72
81
  Napi::Env env = info.Env();
73
82
  if (!ValidateCMessage(env)) {
package/src/Message.h CHANGED
@@ -44,6 +44,7 @@ class Message : public Napi::ObjectWrap<Message> {
44
44
  Napi::Value GetPublishTimestamp(const Napi::CallbackInfo &info);
45
45
  Napi::Value GetEventTimestamp(const Napi::CallbackInfo &info);
46
46
  Napi::Value GetPartitionKey(const Napi::CallbackInfo &info);
47
+ Napi::Value GetRedeliveryCount(const Napi::CallbackInfo &info);
47
48
  bool ValidateCMessage(Napi::Env env);
48
49
 
49
50
  static char **NewStringArray(int size) { return (char **)calloc(sizeof(char *), size); }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+
20
+ #ifndef MESSAGELISTENER_H
21
+ #define MESSAGELISTENER_H
22
+
23
+ #include <napi.h>
24
+ #include <pulsar/c/client.h>
25
+
26
+ struct CConsumerWrapper {
27
+ pulsar_consumer_t *cConsumer;
28
+ CConsumerWrapper();
29
+ ~CConsumerWrapper();
30
+ };
31
+
32
+ struct ListenerCallback {
33
+ Napi::ThreadSafeFunction callback;
34
+
35
+ // Using consumer as void* since the ListenerCallback is shared between Config and Consumer.
36
+ void *consumer;
37
+ };
38
+
39
+ #endif
@@ -26,6 +26,7 @@ static const std::string CFG_START_MESSAGE_ID = "startMessageId";
26
26
  static const std::string CFG_RECV_QUEUE = "receiverQueueSize";
27
27
  static const std::string CFG_READER_NAME = "readerName";
28
28
  static const std::string CFG_SUBSCRIPTION_ROLE_PREFIX = "subscriptionRolePrefix";
29
+ static const std::string CFG_READ_COMPACTED = "readCompacted";
29
30
 
30
31
  ReaderConfig::ReaderConfig(const Napi::Object &readerConfig) : topic(""), cStartMessageId(NULL) {
31
32
  this->cReaderConfig = pulsar_reader_configuration_create();
@@ -59,6 +60,13 @@ ReaderConfig::ReaderConfig(const Napi::Object &readerConfig) : topic(""), cStart
59
60
  if (!subscriptionRolePrefix.empty())
60
61
  pulsar_reader_configuration_set_reader_name(this->cReaderConfig, subscriptionRolePrefix.c_str());
61
62
  }
63
+
64
+ if (readerConfig.Has(CFG_READ_COMPACTED) && readerConfig.Get(CFG_READ_COMPACTED).IsBoolean()) {
65
+ bool readCompacted = readerConfig.Get(CFG_READ_COMPACTED).ToBoolean();
66
+ if (readCompacted) {
67
+ pulsar_reader_configuration_set_read_compacted(this->cReaderConfig, 1);
68
+ }
69
+ }
62
70
  }
63
71
 
64
72
  ReaderConfig::~ReaderConfig() { pulsar_reader_configuration_free(this->cReaderConfig); }
@@ -78,6 +78,8 @@ statusFilePath=/usr/local/apache/htdocs
78
78
  # Using a value of 0, is disabling unackeMessage limit check and consumer can receive messages without any restriction
79
79
  maxUnackedMessagesPerConsumer=50000
80
80
 
81
+ subscriptionRedeliveryTrackerEnabled=true
82
+
81
83
  ### --- Authentication --- ###
82
84
 
83
85
  # Enable authentication
@@ -87,5 +87,22 @@ const Pulsar = require('../index.js');
87
87
  })).rejects.toThrow('NAck timeout should be greater than or equal to zero');
88
88
  });
89
89
  });
90
+
91
+ describe('Close', () => {
92
+ test('throws error on subsequent calls to close', async () => {
93
+ const consumer = await client.subscribe({
94
+ topic: 'persistent://public/default/my-topic',
95
+ subscription: 'sub1',
96
+ subscriptionType: 'Shared',
97
+ // Test with listener since it changes the flow of close
98
+ // and reproduces an issue
99
+ listener() {},
100
+ });
101
+
102
+ await expect(consumer.close()).resolves.toEqual(null);
103
+
104
+ await expect(consumer.close()).rejects.toThrow('Failed to close consumer: AlreadyClosed');
105
+ });
106
+ });
90
107
  });
91
108
  })();
@@ -116,6 +116,52 @@ const Pulsar = require('../index.js');
116
116
  await client.close();
117
117
  });
118
118
 
119
+ test('getRedeliveryCount', async () => {
120
+ const client = new Pulsar.Client({
121
+ serviceUrl: 'pulsar://localhost:6650',
122
+ operationTimeoutSeconds: 30,
123
+ });
124
+
125
+ const topic = 'persistent://public/default/produce-consume';
126
+ const producer = await client.createProducer({
127
+ topic,
128
+ sendTimeoutMs: 30000,
129
+ batchingEnabled: true,
130
+ });
131
+ expect(producer).not.toBeNull();
132
+
133
+ const consumer = await client.subscribe({
134
+ topic,
135
+ subscriptionType: 'Shared',
136
+ subscription: 'sub1',
137
+ ackTimeoutMs: 10000,
138
+ nAckRedeliverTimeoutMs: 100,
139
+ });
140
+
141
+ expect(consumer).not.toBeNull();
142
+
143
+ const message = 'my-message';
144
+ producer.send({
145
+ data: Buffer.from(message),
146
+ });
147
+ await producer.flush();
148
+
149
+ let redeliveryCount;
150
+ let msg;
151
+ for (let index = 0; index < 3; index += 1) {
152
+ msg = await consumer.receive();
153
+ redeliveryCount = msg.getRedeliveryCount();
154
+ consumer.negativeAcknowledge(msg);
155
+ }
156
+ expect(redeliveryCount).toBe(2);
157
+ consumer.acknowledge(msg);
158
+
159
+ await producer.close();
160
+ await consumer.close();
161
+ await client.close();
162
+ });
163
+
164
+
119
165
  test('Produce/Consume Listener', async () => {
120
166
  const client = new Pulsar.Client({
121
167
  serviceUrl: 'pulsar://localhost:6650',