rclnodejs 1.8.3 → 1.9.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 (90) hide show
  1. package/README.md +46 -37
  2. package/index.js +39 -0
  3. package/lib/action/client.js +61 -3
  4. package/lib/action/server_goal_handle.js +26 -1
  5. package/lib/message_info.js +94 -0
  6. package/lib/node.js +260 -13
  7. package/lib/parameter_event_handler.js +566 -0
  8. package/lib/parameter_watcher.js +12 -12
  9. package/lib/qos_overriding_options.js +358 -0
  10. package/lib/subscription.js +38 -5
  11. package/lib/timer.js +3 -2
  12. package/lib/wait_for_message.js +111 -0
  13. package/package.json +7 -4
  14. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  15. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  16. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  17. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  18. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  19. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  20. package/scripts/run_asan_test.sh +118 -0
  21. package/src/executor.cpp +36 -2
  22. package/src/executor.h +11 -0
  23. package/src/rcl_action_client_bindings.cpp +70 -1
  24. package/src/rcl_context_bindings.cpp +3 -3
  25. package/src/rcl_graph_bindings.cpp +2 -2
  26. package/src/rcl_subscription_bindings.cpp +70 -2
  27. package/src/rcl_timer_bindings.cpp +21 -2
  28. package/src/rcl_utilities.cpp +2 -2
  29. package/tools/jsdoc/Makefile +5 -0
  30. package/tools/jsdoc/README.md +96 -0
  31. package/tools/jsdoc/build-index.js +610 -0
  32. package/tools/jsdoc/publish.js +854 -0
  33. package/tools/jsdoc/regenerate-published-docs.js +605 -0
  34. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.eot +0 -0
  35. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
  36. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.woff +0 -0
  37. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  38. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  39. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  40. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.eot +0 -0
  41. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
  42. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.woff +0 -0
  43. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.eot +0 -0
  44. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.svg +1831 -0
  45. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.woff +0 -0
  46. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  47. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  48. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  49. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.eot +0 -0
  50. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
  51. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.woff +0 -0
  52. package/tools/jsdoc/static/scripts/linenumber.js +25 -0
  53. package/tools/jsdoc/static/scripts/prettify/Apache-License-2.0.txt +202 -0
  54. package/tools/jsdoc/static/scripts/prettify/lang-css.js +36 -0
  55. package/tools/jsdoc/static/scripts/prettify/prettify.js +738 -0
  56. package/tools/jsdoc/static/styles/jsdoc-default.css +1012 -0
  57. package/tools/jsdoc/static/styles/prettify-jsdoc.css +111 -0
  58. package/tools/jsdoc/static/styles/prettify-tomorrow.css +132 -0
  59. package/tools/jsdoc/tmpl/augments.tmpl +10 -0
  60. package/tools/jsdoc/tmpl/container.tmpl +193 -0
  61. package/tools/jsdoc/tmpl/details.tmpl +143 -0
  62. package/tools/jsdoc/tmpl/example.tmpl +2 -0
  63. package/tools/jsdoc/tmpl/examples.tmpl +13 -0
  64. package/tools/jsdoc/tmpl/exceptions.tmpl +17 -0
  65. package/tools/jsdoc/tmpl/layout.tmpl +83 -0
  66. package/tools/jsdoc/tmpl/mainpage.tmpl +163 -0
  67. package/tools/jsdoc/tmpl/members.tmpl +43 -0
  68. package/tools/jsdoc/tmpl/method.tmpl +124 -0
  69. package/tools/jsdoc/tmpl/params.tmpl +133 -0
  70. package/tools/jsdoc/tmpl/properties.tmpl +110 -0
  71. package/tools/jsdoc/tmpl/returns.tmpl +12 -0
  72. package/tools/jsdoc/tmpl/source.tmpl +8 -0
  73. package/tools/jsdoc/tmpl/tutorial.tmpl +19 -0
  74. package/tools/jsdoc/tmpl/type.tmpl +7 -0
  75. package/types/action_client.d.ts +8 -0
  76. package/types/index.d.ts +34 -0
  77. package/types/message_info.d.ts +72 -0
  78. package/types/node.d.ts +90 -5
  79. package/types/parameter_event_handler.d.ts +150 -0
  80. package/types/qos.d.ts +55 -0
  81. package/types/subscription.d.ts +14 -2
  82. package/types/timer.d.ts +3 -2
  83. package/test_data_integrity.js +0 -108
  84. package/test_repro_exact.js +0 -57
  85. package/test_repro_hz.js +0 -86
  86. package/test_repro_pub.js +0 -36
  87. package/test_repro_stress.js +0 -83
  88. package/test_repro_sub.js +0 -64
  89. package/test_xproc_data.js +0 -64
  90. package/types/interfaces.d.ts +0 -8895
@@ -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));
@@ -78,6 +78,15 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
78
78
 
79
79
  bool lossless;
80
80
  int64_t period_nsec = info[2].As<Napi::BigInt>().Int64Value(&lossless);
81
+ bool autostart = true;
82
+ if (info.Length() > 3) {
83
+ if (!info[3].IsBoolean()) {
84
+ Napi::TypeError::New(env, "Timer autostart must be a boolean")
85
+ .ThrowAsJavaScriptException();
86
+ return env.Undefined();
87
+ }
88
+ autostart = info[3].As<Napi::Boolean>().Value();
89
+ }
81
90
  rcl_timer_t* timer =
82
91
  reinterpret_cast<rcl_timer_t*>(malloc(sizeof(rcl_timer_t)));
83
92
  *timer = rcl_get_zero_initialized_timer();
@@ -85,8 +94,7 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
85
94
  #if ROS_VERSION > 2305 // After Iron.
86
95
  {
87
96
  rcl_ret_t ret = rcl_timer_init2(timer, clock, context, period_nsec, nullptr,
88
- rcl_get_default_allocator(),
89
- /*autostart=*/true);
97
+ rcl_get_default_allocator(), autostart);
90
98
  if (RCL_RET_OK != ret) {
91
99
  std::string error_msg = rcl_get_error_string().str;
92
100
  rcl_reset_error();
@@ -106,6 +114,17 @@ Napi::Value CreateTimer(const Napi::CallbackInfo& info) {
106
114
  Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
107
115
  return env.Undefined();
108
116
  }
117
+ if (!autostart) {
118
+ rcl_ret_t cancel_ret = rcl_timer_cancel(timer);
119
+ if (RCL_RET_OK != cancel_ret) {
120
+ std::string error_msg = rcl_get_error_string().str;
121
+ rcl_reset_error();
122
+ rcl_timer_fini(timer);
123
+ free(timer);
124
+ Napi::Error::New(env, error_msg).ThrowAsJavaScriptException();
125
+ return env.Undefined();
126
+ }
127
+ }
109
128
  }
110
129
  #endif
111
130
 
@@ -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
@@ -0,0 +1,96 @@
1
+ # JSDoc Workflow
2
+
3
+ This directory contains the custom JSDoc template, the landing-page generator,
4
+ and the staging script used to prepare the docs content that is published to the
5
+ `gh-pages` branch.
6
+
7
+ ## Commands
8
+
9
+ ### `npm run docs`
10
+
11
+ Build local docs for the current workspace version.
12
+
13
+ Output:
14
+
15
+ - `docs/<current-version>/`
16
+ - `docs/index.html`
17
+
18
+ Use this to verify the docs for the version currently declared in
19
+ `package.json`.
20
+
21
+ ### `npm run docs:gh-pages`
22
+
23
+ Stage the publishable docs tree under `build/gh-pages-docs/`.
24
+
25
+ Behavior:
26
+
27
+ - reads the currently published version set from `origin/gh-pages`
28
+ - preserves that published history
29
+ - regenerates docs for the current workspace version
30
+ - rebuilds the staged landing page index
31
+
32
+ This is the normal command to use for a new release.
33
+
34
+ If you delete `build/` and rerun `npm run docs:gh-pages`, the staged tree will
35
+ still contain all currently published versions. That command recreates
36
+ `build/gh-pages-docs/` by copying the published docs snapshot from
37
+ `origin/gh-pages`, then regenerating only the current workspace version.
38
+
39
+ ### `npm run docs:gh-pages:full`
40
+
41
+ Fully rebuild the currently published docs history under
42
+ `build/gh-pages-docs/`.
43
+
44
+ Behavior:
45
+
46
+ - reads the published version set from `origin/gh-pages`
47
+ - rebuilds only those published versions from tags
48
+ - regenerates docs for the current workspace version
49
+ - rebuilds the staged landing page index
50
+
51
+ This does **not** rebuild docs for every historical `rclnodejs` tag. It only
52
+ rebuilds the subset that is actually published online.
53
+
54
+ ## New Release Example
55
+
56
+ For a new release such as `1.9.0`:
57
+
58
+ 1. Update `package.json` to `1.9.0`.
59
+ 2. Run `npm run docs`.
60
+ 3. Verify the local output in `docs/1.9.0/` and `docs/index.html`.
61
+ 4. Run `npm run docs:gh-pages`.
62
+ 5. Verify the staged output in:
63
+ - `build/gh-pages-docs/docs/1.9.0/`
64
+ - `build/gh-pages-docs/docs/index.html`
65
+ - `build/gh-pages-docs/.nojekyll`
66
+ 6. Publish the contents of `build/gh-pages-docs/` to the `gh-pages` branch.
67
+
68
+ ## Manual Landing Page Rebuild
69
+
70
+ If the staged docs tree already exists and you only want to rebuild
71
+ `build/gh-pages-docs/docs/index.html`, run `tools/jsdoc/build-index.js` against
72
+ that docs root and point it at the package metadata for the latest published
73
+ version.
74
+
75
+ Example for published version `1.8.0`:
76
+
77
+ ```bash
78
+ mkdir -p build/gh-pages-docs/.tmp
79
+ git show 1.8.0:package.json > build/gh-pages-docs/.tmp/package-1.8.0.json
80
+
81
+ export RCLNODEJS_DOCS_ROOT="$PWD/build/gh-pages-docs/docs"
82
+ export RCLNODEJS_DOCS_INDEX_PATH="$PWD/build/gh-pages-docs/docs/index.html"
83
+ export RCLNODEJS_LOCAL_INDEX_PATH=''
84
+ export RCLNODEJS_PACKAGE_JSON_PATH="$PWD/build/gh-pages-docs/.tmp/package-1.8.0.json"
85
+
86
+ node tools/jsdoc/build-index.js
87
+ rm -rf build/gh-pages-docs/.tmp
88
+ ```
89
+
90
+ ## Notes
91
+
92
+ - The staged publish output keeps shared assets in `build/gh-pages-docs/docs/_static/`.
93
+ - `.nojekyll` must remain in the staged output because the published docs tree
94
+ uses an underscore-prefixed directory.
95
+ - The live docs index is the source of truth for which versions should remain
96
+ published.