rclnodejs 1.8.3 → 1.9.0-alpha.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.
Files changed (85) hide show
  1. package/README.md +46 -37
  2. package/index.js +29 -0
  3. package/lib/action/client.js +61 -3
  4. package/lib/message_info.js +94 -0
  5. package/lib/node.js +53 -3
  6. package/lib/parameter_event_handler.js +468 -0
  7. package/lib/parameter_watcher.js +12 -12
  8. package/lib/subscription.js +38 -5
  9. package/lib/timer.js +2 -1
  10. package/lib/wait_for_message.js +111 -0
  11. package/package.json +6 -3
  12. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  13. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  14. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  15. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  16. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  17. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  18. package/scripts/run_asan_test.sh +118 -0
  19. package/src/executor.cpp +36 -2
  20. package/src/executor.h +11 -0
  21. package/src/rcl_action_client_bindings.cpp +70 -1
  22. package/src/rcl_context_bindings.cpp +3 -3
  23. package/src/rcl_graph_bindings.cpp +2 -2
  24. package/src/rcl_subscription_bindings.cpp +70 -2
  25. package/src/rcl_utilities.cpp +2 -2
  26. package/tools/jsdoc/Makefile +5 -0
  27. package/tools/jsdoc/README.md +96 -0
  28. package/tools/jsdoc/build-index.js +610 -0
  29. package/tools/jsdoc/publish.js +854 -0
  30. package/tools/jsdoc/regenerate-published-docs.js +605 -0
  31. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.eot +0 -0
  32. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
  33. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.woff +0 -0
  34. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  35. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  36. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  37. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.eot +0 -0
  38. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
  39. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.woff +0 -0
  40. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.eot +0 -0
  41. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.svg +1831 -0
  42. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.woff +0 -0
  43. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  44. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  45. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  46. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.eot +0 -0
  47. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
  48. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.woff +0 -0
  49. package/tools/jsdoc/static/scripts/linenumber.js +25 -0
  50. package/tools/jsdoc/static/scripts/prettify/Apache-License-2.0.txt +202 -0
  51. package/tools/jsdoc/static/scripts/prettify/lang-css.js +36 -0
  52. package/tools/jsdoc/static/scripts/prettify/prettify.js +738 -0
  53. package/tools/jsdoc/static/styles/jsdoc-default.css +1012 -0
  54. package/tools/jsdoc/static/styles/prettify-jsdoc.css +111 -0
  55. package/tools/jsdoc/static/styles/prettify-tomorrow.css +132 -0
  56. package/tools/jsdoc/tmpl/augments.tmpl +10 -0
  57. package/tools/jsdoc/tmpl/container.tmpl +193 -0
  58. package/tools/jsdoc/tmpl/details.tmpl +143 -0
  59. package/tools/jsdoc/tmpl/example.tmpl +2 -0
  60. package/tools/jsdoc/tmpl/examples.tmpl +13 -0
  61. package/tools/jsdoc/tmpl/exceptions.tmpl +17 -0
  62. package/tools/jsdoc/tmpl/layout.tmpl +83 -0
  63. package/tools/jsdoc/tmpl/mainpage.tmpl +163 -0
  64. package/tools/jsdoc/tmpl/members.tmpl +43 -0
  65. package/tools/jsdoc/tmpl/method.tmpl +124 -0
  66. package/tools/jsdoc/tmpl/params.tmpl +133 -0
  67. package/tools/jsdoc/tmpl/properties.tmpl +110 -0
  68. package/tools/jsdoc/tmpl/returns.tmpl +12 -0
  69. package/tools/jsdoc/tmpl/source.tmpl +8 -0
  70. package/tools/jsdoc/tmpl/tutorial.tmpl +19 -0
  71. package/tools/jsdoc/tmpl/type.tmpl +7 -0
  72. package/types/action_client.d.ts +8 -0
  73. package/types/index.d.ts +34 -0
  74. package/types/message_info.d.ts +72 -0
  75. package/types/node.d.ts +21 -0
  76. package/types/parameter_event_handler.d.ts +139 -0
  77. package/types/subscription.d.ts +14 -2
  78. package/test_data_integrity.js +0 -108
  79. package/test_repro_exact.js +0 -57
  80. package/test_repro_hz.js +0 -86
  81. package/test_repro_pub.js +0 -36
  82. package/test_repro_stress.js +0 -83
  83. package/test_repro_sub.js +0 -64
  84. package/test_xproc_data.js +0 -64
  85. package/types/interfaces.d.ts +0 -8895
@@ -0,0 +1,111 @@
1
+ // Copyright (c) 2026, The Robot Web Tools Contributors
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ 'use strict';
16
+
17
+ const { TimeoutError } = require('./errors.js');
18
+
19
+ /**
20
+ * Wait for a single message on a topic.
21
+ *
22
+ * Creates a temporary subscription, waits for the first message to arrive,
23
+ * and returns it. The temporary subscription is always cleaned up, even on
24
+ * timeout or error. The node must be spinning before calling this function.
25
+ *
26
+ * This is the rclnodejs equivalent of rclpy's `wait_for_message`.
27
+ *
28
+ * @param {function|string|object} typeClass - The ROS message type class.
29
+ * @param {Node} node - The node to create the temporary subscription on.
30
+ * @param {string} topic - The topic name to listen on.
31
+ * @param {object} [options] - Options.
32
+ * @param {number} [options.timeout] - Timeout in milliseconds. If omitted, waits indefinitely.
33
+ * @param {object} [options.qos] - QoS profile for the subscription.
34
+ * @returns {Promise<object>} - Resolves with the received message.
35
+ * @throws {Error} If timeout expires before a message arrives.
36
+ *
37
+ * @example
38
+ * node.spin();
39
+ * const msg = await waitForMessage(
40
+ * 'std_msgs/msg/String',
41
+ * node,
42
+ * '/my_topic',
43
+ * { timeout: 5000 }
44
+ * );
45
+ * console.log('Received:', msg.data);
46
+ */
47
+ function waitForMessage(typeClass, node, topic, options = {}) {
48
+ return new Promise((resolve, reject) => {
49
+ let subscription = null;
50
+ let timer = null;
51
+ let settled = false;
52
+
53
+ const cleanup = () => {
54
+ if (timer) {
55
+ clearTimeout(timer);
56
+ timer = null;
57
+ }
58
+ if (subscription) {
59
+ try {
60
+ node.destroySubscription(subscription);
61
+ } catch {
62
+ // Subscription may already be destroyed if node is shutting down
63
+ }
64
+ subscription = null;
65
+ }
66
+ };
67
+
68
+ const settle = (err, msg) => {
69
+ if (settled) return;
70
+ settled = true;
71
+ cleanup();
72
+ if (err) {
73
+ reject(err);
74
+ } else {
75
+ resolve(msg);
76
+ }
77
+ };
78
+
79
+ try {
80
+ const subOptions = {};
81
+ if (options.qos) {
82
+ subOptions.qos = options.qos;
83
+ }
84
+
85
+ subscription = node.createSubscription(
86
+ typeClass,
87
+ topic,
88
+ subOptions,
89
+ (msg) => {
90
+ settle(null, msg);
91
+ }
92
+ );
93
+
94
+ if (options.timeout != null && options.timeout >= 0) {
95
+ timer = setTimeout(() => {
96
+ settle(
97
+ new TimeoutError(
98
+ `waitForMessage timed out after ${options.timeout}ms on topic '${topic}'`,
99
+ { entityType: 'topic', entityName: topic }
100
+ )
101
+ );
102
+ }, options.timeout);
103
+ }
104
+ } catch (err) {
105
+ cleanup();
106
+ reject(err);
107
+ }
108
+ });
109
+ }
110
+
111
+ module.exports = waitForMessage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rclnodejs",
3
- "version": "1.8.3",
3
+ "version": "1.9.0-alpha.0",
4
4
  "description": "ROS2.0 JavaScript client with Node.js",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
@@ -24,10 +24,13 @@
24
24
  "clean": "node-gyp clean && npx rimraf ./generated",
25
25
  "install": "node scripts/install.js",
26
26
  "postinstall": "npm run generate-messages",
27
- "docs": "cd docs && make",
27
+ "docs": "make -C tools/jsdoc",
28
+ "docs:gh-pages": "node tools/jsdoc/regenerate-published-docs.js --branch origin/gh-pages --preserve-published",
29
+ "docs:gh-pages:full": "node tools/jsdoc/regenerate-published-docs.js --branch origin/gh-pages --full-rebuild",
28
30
  "test": "nyc node --expose-gc ./scripts/run_test.js && tsd && npm install --no-save electron && node test/electron/run_test.js",
29
31
  "test-idl": "nyc node --expose-gc ./scripts/run_test.js --idl",
30
32
  "lint": "eslint && node ./scripts/cpplint.js",
33
+ "test:asan": "bash scripts/run_asan_test.sh",
31
34
  "format": "clang-format -i -style=file ./src/*.cpp ./src/*.h && npx --yes prettier --write \"{lib,rosidl_gen,rostsd_gen,rosidl_parser,types,example,test,scripts,benchmark,rostsd_gen}/**/*.{js,md,ts}\" ./*.{js,md,ts}",
32
35
  "prepare": "husky",
33
36
  "coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
@@ -71,7 +74,7 @@
71
74
  "sinon": "^21.0.0",
72
75
  "tree-kill": "^1.2.2",
73
76
  "tsd": "^0.33.0",
74
- "typescript": "^5.7.2"
77
+ "typescript": "^6.0.2"
75
78
  },
76
79
  "dependencies": {
77
80
  "@rclnodejs/ref-array-di": "^1.2.2",
@@ -0,0 +1,118 @@
1
+ #!/bin/bash
2
+ # Run tests with AddressSanitizer (ASan/LSan) to detect memory leaks and errors
3
+ # in the native N-API addon.
4
+ #
5
+ # Requires: ROS2 sourced, g++ with libasan
6
+ #
7
+ # Usage:
8
+ # bash scripts/run_asan_test.sh # rebuild + run all tests
9
+ # bash scripts/run_asan_test.sh test/test-node.js # rebuild + run specific test(s)
10
+ # bash scripts/run_asan_test.sh --no-build test/test-node.js # skip rebuild
11
+ # bash scripts/run_asan_test.sh --exclude test/test-serialization.js # exclude specific test(s)
12
+
13
+ set -e
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
16
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
17
+ cd "$PROJECT_DIR"
18
+
19
+ # Parse flags
20
+ DO_BUILD=1
21
+ ARGS=()
22
+ EXCLUDES=()
23
+ NEXT_IS_EXCLUDE=0
24
+ for arg in "$@"; do
25
+ if [[ $NEXT_IS_EXCLUDE -eq 1 ]]; then
26
+ EXCLUDES+=("$arg")
27
+ NEXT_IS_EXCLUDE=0
28
+ elif [[ "$arg" == "--no-build" ]]; then
29
+ DO_BUILD=0
30
+ elif [[ "$arg" == "--exclude" ]]; then
31
+ NEXT_IS_EXCLUDE=1
32
+ else
33
+ ARGS+=("$arg")
34
+ fi
35
+ done
36
+
37
+ if [[ $NEXT_IS_EXCLUDE -eq 1 ]]; then
38
+ echo "Error: --exclude requires a test file argument"
39
+ echo "Usage: bash scripts/run_asan_test.sh --exclude test/test-foo.js"
40
+ exit 1
41
+ fi
42
+
43
+ # Step 1: Build with ASan
44
+ if [[ $DO_BUILD -eq 1 ]]; then
45
+ echo "=== Building with AddressSanitizer ==="
46
+ CXXFLAGS="-fsanitize=address" npx node-gyp -j 16 rebuild --debug
47
+ fi
48
+
49
+ # Step 2: Locate libasan
50
+ LIBASAN=$(g++ -print-file-name=libasan.so)
51
+ if [[ "$LIBASAN" == "libasan.so" ]]; then
52
+ # g++ returned just the name, try to find the full path
53
+ LIBASAN=$(find /usr/lib* -name "libasan.so*" -type f 2>/dev/null | head -1)
54
+ fi
55
+
56
+ if [[ -z "$LIBASAN" || ! -f "$LIBASAN" ]]; then
57
+ echo "Warning: Could not find libasan.so, proceeding without LD_PRELOAD"
58
+ echo "If you see 'ASan not the first DSO' errors, install libasan: sudo apt install libasan6"
59
+ LIBASAN=""
60
+ fi
61
+
62
+ # Step 3: Set up environment
63
+ export LSAN_OPTIONS="suppressions=$PROJECT_DIR/suppr.txt"
64
+ if [[ -n "$LIBASAN" ]]; then
65
+ export LD_PRELOAD="$LIBASAN"
66
+ fi
67
+
68
+ # Step 3.5: Temporarily hide prebuilds so the debug build is loaded
69
+ PREBUILDS_DIR="$PROJECT_DIR/prebuilds"
70
+ PREBUILDS_BAK="$PROJECT_DIR/.prebuilds_asan_bak"
71
+ MOVED_PREBUILDS=0
72
+ if [[ -d "$PREBUILDS_DIR" ]]; then
73
+ if [[ -d "$PREBUILDS_BAK" ]]; then
74
+ echo "Error: $PREBUILDS_BAK already exists (previous interrupted run?)."
75
+ echo "Please remove it manually and retry: rm -rf $PREBUILDS_BAK"
76
+ exit 1
77
+ fi
78
+ mv "$PREBUILDS_DIR" "$PREBUILDS_BAK"
79
+ MOVED_PREBUILDS=1
80
+ fi
81
+
82
+ # Restore prebuilds on exit (even on failure)
83
+ cleanup() {
84
+ if [[ $MOVED_PREBUILDS -eq 1 && -d "$PREBUILDS_BAK" ]]; then
85
+ mv "$PREBUILDS_BAK" "$PREBUILDS_DIR"
86
+ fi
87
+ }
88
+ trap cleanup EXIT
89
+
90
+ # Step 4: Build mocha exclude args from --exclude flags, blocklist.json, and asan_blocklist.json
91
+ MOCHA_EXCLUDES=()
92
+ for exc in "${EXCLUDES[@]}"; do
93
+ MOCHA_EXCLUDES+=(--ignore "$exc")
94
+ done
95
+
96
+ # Auto-exclude tests from blocklist.json (Linux entries) and asan_blocklist.json
97
+ for blocklist in "$PROJECT_DIR/test/blocklist.json" "$PROJECT_DIR/test/asan_blocklist.json"; do
98
+ if [[ -f "$blocklist" ]]; then
99
+ while IFS= read -r test_file; do
100
+ MOCHA_EXCLUDES+=(--ignore "test/$test_file")
101
+ done < <(node -e "
102
+ const bl = require('$blocklist');
103
+ const entries = bl.Linux || bl;
104
+ if (Array.isArray(entries)) entries.forEach(t => console.log(t));
105
+ " 2>/dev/null)
106
+ fi
107
+ done
108
+
109
+ # Step 5: Run tests
110
+ if [[ ${#ARGS[@]} -gt 0 ]]; then
111
+ echo "=== Running ASan test: ${ARGS[*]} ==="
112
+ node --expose-gc node_modules/.bin/mocha -r test/gc-on-exit.js "${MOCHA_EXCLUDES[@]}" "${ARGS[@]}"
113
+ else
114
+ echo "=== Running all ASan tests ==="
115
+ node --expose-gc node_modules/.bin/mocha -r test/gc-on-exit.js "${MOCHA_EXCLUDES[@]}" 'test/test-*.js'
116
+ fi
117
+
118
+ echo "=== ASan test complete ==="
package/src/executor.cpp CHANGED
@@ -48,7 +48,8 @@ Executor::Executor(Napi::Env env, HandleManager* handle_manager,
48
48
  handle_manager_(handle_manager),
49
49
  delegate_(delegate),
50
50
  context_(nullptr),
51
- env_(env) {
51
+ env_(env),
52
+ work_pending_(false) {
52
53
  running_.store(false);
53
54
  }
54
55
 
@@ -105,6 +106,8 @@ void Executor::Stop() {
105
106
  // Stop thread first, and then uv_close
106
107
  // Make sure async_ is not used anymore
107
108
  running_.store(false);
109
+ // Wake the background thread in case it is waiting on the condvar.
110
+ work_done_cv_.notify_all();
108
111
  handle_manager_->StopWaitingHandles();
109
112
  uv_thread_join(&background_thread_);
110
113
 
@@ -116,7 +119,7 @@ void Executor::Stop() {
116
119
  // Important Notice:
117
120
  // This might be called after Executor::~Executor()
118
121
  // Don't free Executor::async_ in Executor's dtor
119
- delete async;
122
+ delete reinterpret_cast<uv_async_t*>(async);
120
123
  handle_closed = true;
121
124
  });
122
125
  while (!handle_closed) uv_run(uv_default_loop(), UV_RUN_ONCE);
@@ -133,6 +136,21 @@ bool Executor::IsMainThread() {
133
136
 
134
137
  void Executor::DoWork(uv_async_t* handle) {
135
138
  Executor* executor = reinterpret_cast<Executor*>(handle->data);
139
+
140
+ // RAII guard: always clear work_pending_ and notify the background thread,
141
+ // even if ExecuteReadyHandles() throws (e.g. from N-API callbacks).
142
+ // Without this, the background thread would block forever on work_done_cv_.
143
+ struct WorkDoneGuard {
144
+ Executor* exec;
145
+ ~WorkDoneGuard() {
146
+ {
147
+ std::lock_guard<std::mutex> lock(exec->work_done_mutex_);
148
+ exec->work_pending_ = false;
149
+ }
150
+ exec->work_done_cv_.notify_one();
151
+ }
152
+ } guard{executor};
153
+
136
154
  executor->ExecuteReadyHandles();
137
155
  }
138
156
 
@@ -159,7 +177,23 @@ void Executor::Run(void* arg) {
159
177
 
160
178
  if (!uv_is_closing(reinterpret_cast<uv_handle_t*>(executor->async_)) &&
161
179
  handle_manager->ready_handles_count() > 0) {
180
+ // Tell the main thread there is work to do, then wait for it to
181
+ // finish before re-entering rcl_wait. This prevents a data race
182
+ // where the background thread holds subscriptions in the wait set
183
+ // while the main thread modifies their state (e.g. content filter).
184
+ {
185
+ std::lock_guard<std::mutex> lock(executor->work_done_mutex_);
186
+ executor->work_pending_ = true;
187
+ }
162
188
  uv_async_send(executor->async_);
189
+
190
+ // Wait until DoWork() signals completion.
191
+ {
192
+ std::unique_lock<std::mutex> lock(executor->work_done_mutex_);
193
+ executor->work_done_cv_.wait(lock, [executor] {
194
+ return !executor->work_pending_ || !executor->running_.load();
195
+ });
196
+ }
163
197
  }
164
198
  }
165
199
 
package/src/executor.h CHANGED
@@ -20,7 +20,9 @@
20
20
  #include <uv.h>
21
21
 
22
22
  #include <atomic>
23
+ #include <condition_variable>
23
24
  #include <exception>
25
+ #include <mutex>
24
26
  #include <vector>
25
27
 
26
28
  #include "rcl_handle.h"
@@ -72,6 +74,15 @@ class Executor {
72
74
  Napi::Env env_;
73
75
 
74
76
  std::atomic_bool running_;
77
+
78
+ // Synchronization: the background thread waits after uv_async_send until
79
+ // the main thread finishes ExecuteReadyHandles. This prevents the
80
+ // background thread from re-entering rcl_wait (which holds a reference to
81
+ // subscriptions) while the main thread modifies subscription state (e.g.
82
+ // content filter changes).
83
+ std::mutex work_done_mutex_;
84
+ std::condition_variable work_done_cv_;
85
+ bool work_pending_; // true while the main thread is processing handles
75
86
  };
76
87
 
77
88
  } // namespace rclnodejs
@@ -250,6 +250,67 @@ Napi::Value ActionSendCancelRequest(const Napi::CallbackInfo& info) {
250
250
  return Napi::Number::New(env, static_cast<double>(sequence_number));
251
251
  }
252
252
 
253
+ #if ROS_VERSION >= 5000 // ROS2 Rolling
254
+ Napi::Value ActionConfigureFeedbackSubFilterAddGoalId(
255
+ const Napi::CallbackInfo& info) {
256
+ Napi::Env env = info.Env();
257
+
258
+ RclHandle* action_client_handle =
259
+ RclHandle::Unwrap(info[0].As<Napi::Object>());
260
+ rcl_action_client_t* action_client =
261
+ reinterpret_cast<rcl_action_client_t*>(action_client_handle->ptr());
262
+
263
+ auto goal_id_buffer = info[1].As<Napi::Buffer<uint8_t>>();
264
+ const uint8_t* goal_id_array = goal_id_buffer.Data();
265
+ size_t goal_id_size = goal_id_buffer.Length();
266
+
267
+ rcl_ret_t ret =
268
+ rcl_action_client_configure_feedback_subscription_filter_add_goal_id(
269
+ action_client, goal_id_array, goal_id_size);
270
+
271
+ if (RCL_RET_OK != ret) {
272
+ std::string error_text{
273
+ "Failed to add goal id to feedback subscription content filter: "};
274
+ error_text += rcl_get_error_string().str;
275
+ rcl_reset_error();
276
+ Napi::Error::New(env, error_text).ThrowAsJavaScriptException();
277
+ return Napi::Boolean::New(env, false);
278
+ }
279
+
280
+ return Napi::Boolean::New(env, true);
281
+ }
282
+
283
+ Napi::Value ActionConfigureFeedbackSubFilterRemoveGoalId(
284
+ const Napi::CallbackInfo& info) {
285
+ Napi::Env env = info.Env();
286
+
287
+ RclHandle* action_client_handle =
288
+ RclHandle::Unwrap(info[0].As<Napi::Object>());
289
+ rcl_action_client_t* action_client =
290
+ reinterpret_cast<rcl_action_client_t*>(action_client_handle->ptr());
291
+
292
+ auto goal_id_buffer = info[1].As<Napi::Buffer<uint8_t>>();
293
+ const uint8_t* goal_id_array = goal_id_buffer.Data();
294
+ size_t goal_id_size = goal_id_buffer.Length();
295
+
296
+ rcl_ret_t ret =
297
+ rcl_action_client_configure_feedback_subscription_filter_remove_goal_id(
298
+ action_client, goal_id_array, goal_id_size);
299
+
300
+ if (RCL_RET_OK != ret) {
301
+ std::string error_text{
302
+ "Failed to remove goal id from feedback subscription content "
303
+ "filter: "};
304
+ error_text += rcl_get_error_string().str;
305
+ rcl_reset_error();
306
+ Napi::Error::New(env, error_text).ThrowAsJavaScriptException();
307
+ return Napi::Boolean::New(env, false);
308
+ }
309
+
310
+ return Napi::Boolean::New(env, true);
311
+ }
312
+ #endif // ROS_VERSION >= 5000
313
+
253
314
  #if ROS_VERSION >= 2505 // ROS2 >= Kilted
254
315
  Napi::Value ConfigureActionClientIntrospection(const Napi::CallbackInfo& info) {
255
316
  Napi::Env env = info.Env();
@@ -307,7 +368,15 @@ Napi::Object InitActionClientBindings(Napi::Env env, Napi::Object exports) {
307
368
  #if ROS_VERSION >= 2505 // ROS2 >= Kilted
308
369
  exports.Set("configureActionClientIntrospection",
309
370
  Napi::Function::New(env, ConfigureActionClientIntrospection));
310
- #endif // ROS_VERSION >= 2505
371
+ #endif // ROS_VERSION >= 2505
372
+ #if ROS_VERSION >= 5000 // ROS2 Rolling
373
+ exports.Set(
374
+ "actionConfigureFeedbackSubFilterAddGoalId",
375
+ Napi::Function::New(env, ActionConfigureFeedbackSubFilterAddGoalId));
376
+ exports.Set(
377
+ "actionConfigureFeedbackSubFilterRemoveGoalId",
378
+ Napi::Function::New(env, ActionConfigureFeedbackSubFilterRemoveGoalId));
379
+ #endif // ROS_VERSION >= 5000
311
380
  return exports;
312
381
  }
313
382
 
@@ -17,10 +17,10 @@
17
17
  #include <rcl/logging.h>
18
18
  #include <rcl/rcl.h>
19
19
 
20
- #include <rcpputils/scope_exit.hpp>
21
- // NOLINTNEXTLINE
22
20
  #include <string>
23
21
 
22
+ #include <rcpputils/scope_exit.hpp>
23
+
24
24
  #include "macros.h"
25
25
  #include "rcl_handle.h"
26
26
  #include "rcl_utilities.h"
@@ -54,6 +54,7 @@ Napi::Value Init(const Napi::CallbackInfo& info) {
54
54
  Napi::Array jsArgv = info[1].As<Napi::Array>();
55
55
  size_t argc = jsArgv.Length();
56
56
  char** argv = AbstractArgsFromNapiArray(jsArgv);
57
+ RCPPUTILS_SCOPE_EXIT({ FreeArgs(argv, argc); });
57
58
 
58
59
  // Set up the domain id.
59
60
  size_t domain_id = RCL_DEFAULT_DOMAIN_ID;
@@ -82,7 +83,6 @@ Napi::Value Init(const Napi::CallbackInfo& info) {
82
83
  RCL_RET_OK, rcl_logging_configure(&context->global_arguments, &allocator),
83
84
  rcl_get_error_string().str);
84
85
 
85
- RCPPUTILS_SCOPE_EXIT({ FreeArgs(argv, argc); });
86
86
  return env.Undefined();
87
87
  }
88
88
 
@@ -18,10 +18,10 @@
18
18
  #include <rcl/graph.h>
19
19
  #include <rcl/rcl.h>
20
20
 
21
- #include <rcpputils/scope_exit.hpp>
22
- // NOLINTNEXTLINE
23
21
  #include <string>
24
22
 
23
+ #include <rcpputils/scope_exit.hpp>
24
+
25
25
  #include "macros.h"
26
26
  #include "rcl_handle.h"
27
27
  #include "rcl_utilities.h"
@@ -16,13 +16,15 @@
16
16
 
17
17
  #include <rcl/error_handling.h>
18
18
  #include <rcl/rcl.h>
19
+ #include <rcl/subscription.h>
20
+ #include <rmw/types.h>
19
21
 
20
22
  #include <cstdio>
21
23
  #include <memory>
22
- #include <rcpputils/scope_exit.hpp>
23
- // NOLINTNEXTLINE
24
24
  #include <string>
25
25
 
26
+ #include <rcpputils/scope_exit.hpp>
27
+
26
28
  #include "macros.h"
27
29
  #include "rcl_handle.h"
28
30
  #include "rcl_utilities.h"
@@ -53,6 +55,53 @@ Napi::Value RclTake(const Napi::CallbackInfo& info) {
53
55
  return env.Undefined();
54
56
  }
55
57
 
58
+ Napi::Value RclTakeWithInfo(const Napi::CallbackInfo& info) {
59
+ Napi::Env env = info.Env();
60
+
61
+ RclHandle* subscription_handle =
62
+ RclHandle::Unwrap(info[0].As<Napi::Object>());
63
+ rcl_subscription_t* subscription =
64
+ reinterpret_cast<rcl_subscription_t*>(subscription_handle->ptr());
65
+ void* msg_taken = info[1].As<Napi::Buffer<char>>().Data();
66
+
67
+ rmw_message_info_t message_info = rmw_get_zero_initialized_message_info();
68
+ rcl_ret_t ret = rcl_take(subscription, msg_taken, &message_info, nullptr);
69
+
70
+ if (ret != RCL_RET_OK && ret != RCL_RET_SUBSCRIPTION_TAKE_FAILED) {
71
+ std::string error_string = rcl_get_error_string().str;
72
+ rcl_reset_error();
73
+ Napi::Error::New(env, error_string).ThrowAsJavaScriptException();
74
+ return env.Undefined();
75
+ }
76
+
77
+ if (ret == RCL_RET_SUBSCRIPTION_TAKE_FAILED) {
78
+ return env.Undefined();
79
+ }
80
+
81
+ // Build JS object with message info fields
82
+ Napi::Object js_info = Napi::Object::New(env);
83
+ js_info.Set("source_timestamp",
84
+ Napi::BigInt::New(env, message_info.source_timestamp));
85
+ js_info.Set("received_timestamp",
86
+ Napi::BigInt::New(env, message_info.received_timestamp));
87
+ js_info.Set(
88
+ "publication_sequence_number",
89
+ Napi::BigInt::New(
90
+ env, static_cast<int64_t>(message_info.publication_sequence_number)));
91
+ js_info.Set(
92
+ "reception_sequence_number",
93
+ Napi::BigInt::New(
94
+ env, static_cast<int64_t>(message_info.reception_sequence_number)));
95
+
96
+ // Publisher GID as Buffer
97
+ auto gid_buf =
98
+ Napi::Buffer<uint8_t>::Copy(env, message_info.publisher_gid.data,
99
+ sizeof(message_info.publisher_gid.data));
100
+ js_info.Set("publisher_gid", gid_buf);
101
+
102
+ return js_info;
103
+ }
104
+
56
105
  Napi::Value CreateSubscription(const Napi::CallbackInfo& info) {
57
106
  Napi::Env env = info.Env();
58
107
 
@@ -216,6 +265,20 @@ Napi::Value GetSubscriptionTopic(const Napi::CallbackInfo& info) {
216
265
  return Napi::String::New(env, topic);
217
266
  }
218
267
 
268
+ #if ROS_VERSION > 2505 // Rolling only
269
+ Napi::Value IsContentFilterSupported(const Napi::CallbackInfo& info) {
270
+ Napi::Env env = info.Env();
271
+
272
+ RclHandle* subscription_handle =
273
+ RclHandle::Unwrap(info[0].As<Napi::Object>());
274
+ rcl_subscription_t* subscription =
275
+ reinterpret_cast<rcl_subscription_t*>(subscription_handle->ptr());
276
+
277
+ bool is_supported = rcl_subscription_is_cft_supported(subscription);
278
+ return Napi::Boolean::New(env, is_supported);
279
+ }
280
+ #endif
281
+
219
282
  Napi::Value HasContentFilter(const Napi::CallbackInfo& info) {
220
283
  Napi::Env env = info.Env();
221
284
 
@@ -422,11 +485,16 @@ Napi::Value GetPublisherCount(const Napi::CallbackInfo& info) {
422
485
 
423
486
  Napi::Object InitSubscriptionBindings(Napi::Env env, Napi::Object exports) {
424
487
  exports.Set("rclTake", Napi::Function::New(env, RclTake));
488
+ exports.Set("rclTakeWithInfo", Napi::Function::New(env, RclTakeWithInfo));
425
489
  exports.Set("createSubscription",
426
490
  Napi::Function::New(env, CreateSubscription));
427
491
  exports.Set("rclTakeRaw", Napi::Function::New(env, RclTakeRaw));
428
492
  exports.Set("getSubscriptionTopic",
429
493
  Napi::Function::New(env, GetSubscriptionTopic));
494
+ #if ROS_VERSION > 2505 // Rolling only
495
+ exports.Set("isContentFilterSupported",
496
+ Napi::Function::New(env, IsContentFilterSupported));
497
+ #endif
430
498
  exports.Set("hasContentFilter", Napi::Function::New(env, HasContentFilter));
431
499
  exports.Set("setContentFilter", Napi::Function::New(env, SetContentFilter));
432
500
  exports.Set("getContentFilter", Napi::Function::New(env, GetContentFilter));
@@ -21,10 +21,10 @@
21
21
 
22
22
  #include <cstdio>
23
23
  #include <memory>
24
- #include <rcpputils/scope_exit.hpp>
25
- // NOLINTNEXTLINE
26
24
  #include <string>
27
25
 
26
+ #include <rcpputils/scope_exit.hpp>
27
+
28
28
  namespace {
29
29
 
30
30
  const rmw_qos_profile_t* GetQoSProfileFromString(const std::string& profile) {
@@ -0,0 +1,5 @@
1
+ .PHONY: docs
2
+
3
+ docs:
4
+ npx jsdoc --package ../../package.json ../../index.js ../../lib/*.js ../../lib/action/*.js -t . -d ../../docs
5
+ node ./build-index.js