rclnodejs 1.9.0-alpha.0 → 2.0.0-beta.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 (108) hide show
  1. package/.prettierignore +4 -0
  2. package/README.md +2 -2
  3. package/binding.gyp +6 -5
  4. package/index.js +10 -0
  5. package/lib/action/client.js +5 -4
  6. package/lib/action/server_goal_handle.js +26 -1
  7. package/lib/action/uuid.js +1 -1
  8. package/lib/client.js +0 -45
  9. package/lib/distro.js +11 -4
  10. package/lib/interface_loader.js +1 -1
  11. package/lib/message_introspector.js +1 -29
  12. package/lib/message_serialization.js +2 -2
  13. package/lib/native_loader.js +21 -9
  14. package/lib/node.js +209 -12
  15. package/lib/parameter_event_handler.js +98 -0
  16. package/lib/prebuilds.js +47 -0
  17. package/lib/qos_overriding_options.js +358 -0
  18. package/lib/rmw.js +6 -1
  19. package/lib/subscription.js +2 -2
  20. package/lib/timer.js +1 -1
  21. package/package.json +11 -6
  22. package/prebuilds/linux-arm64/humble-jammy-arm64-electron-rclnodejs.node +0 -0
  23. package/prebuilds/linux-arm64/humble-jammy-arm64-node-rclnodejs.node +0 -0
  24. package/prebuilds/linux-arm64/jazzy-noble-arm64-electron-rclnodejs.node +0 -0
  25. package/prebuilds/linux-arm64/jazzy-noble-arm64-node-rclnodejs.node +0 -0
  26. package/prebuilds/linux-arm64/kilted-noble-arm64-electron-rclnodejs.node +0 -0
  27. package/prebuilds/linux-arm64/kilted-noble-arm64-node-rclnodejs.node +0 -0
  28. package/prebuilds/linux-arm64/lyrical-resolute-arm64-electron-rclnodejs.node +0 -0
  29. package/prebuilds/linux-arm64/lyrical-resolute-arm64-node-rclnodejs.node +0 -0
  30. package/prebuilds/linux-x64/humble-jammy-x64-electron-rclnodejs.node +0 -0
  31. package/prebuilds/linux-x64/humble-jammy-x64-node-rclnodejs.node +0 -0
  32. package/prebuilds/linux-x64/jazzy-noble-x64-electron-rclnodejs.node +0 -0
  33. package/prebuilds/linux-x64/jazzy-noble-x64-node-rclnodejs.node +0 -0
  34. package/prebuilds/linux-x64/kilted-noble-x64-electron-rclnodejs.node +0 -0
  35. package/prebuilds/linux-x64/kilted-noble-x64-node-rclnodejs.node +0 -0
  36. package/prebuilds/linux-x64/lyrical-resolute-x64-electron-rclnodejs.node +0 -0
  37. package/prebuilds/linux-x64/lyrical-resolute-x64-node-rclnodejs.node +0 -0
  38. package/rosidl_gen/packages.js +4 -4
  39. package/rosidl_gen/templates/message-template.js +20 -6
  40. package/rosocket/README.md +152 -0
  41. package/rosocket/cli.js +168 -0
  42. package/rosocket/index.js +245 -0
  43. package/scripts/install.js +14 -3
  44. package/scripts/tag_prebuilds.js +26 -9
  45. package/src/rcl_action_client_bindings.cpp +4 -4
  46. package/src/rcl_graph_bindings.cpp +8 -8
  47. package/src/rcl_lifecycle_bindings.cpp +1 -1
  48. package/src/rcl_subscription_bindings.cpp +2 -2
  49. package/src/rcl_timer_bindings.cpp +21 -2
  50. package/src/rcl_utilities.cpp +4 -4
  51. package/src/rcl_utilities.h +2 -2
  52. package/types/distro.d.ts +15 -1
  53. package/types/node.d.ts +69 -5
  54. package/types/parameter_event_handler.d.ts +11 -0
  55. package/types/qos.d.ts +55 -0
  56. package/types/timer.d.ts +3 -2
  57. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  58. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  59. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  60. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  61. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  62. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  63. package/tools/jsdoc/Makefile +0 -5
  64. package/tools/jsdoc/README.md +0 -96
  65. package/tools/jsdoc/build-index.js +0 -610
  66. package/tools/jsdoc/publish.js +0 -854
  67. package/tools/jsdoc/regenerate-published-docs.js +0 -605
  68. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.eot +0 -0
  69. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.svg +0 -1830
  70. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.woff +0 -0
  71. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  72. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
  73. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  74. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.eot +0 -0
  75. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.svg +0 -1830
  76. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.woff +0 -0
  77. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.eot +0 -0
  78. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.svg +0 -1831
  79. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.woff +0 -0
  80. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  81. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
  82. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  83. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.eot +0 -0
  84. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.svg +0 -1831
  85. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.woff +0 -0
  86. package/tools/jsdoc/static/scripts/linenumber.js +0 -25
  87. package/tools/jsdoc/static/scripts/prettify/Apache-License-2.0.txt +0 -202
  88. package/tools/jsdoc/static/scripts/prettify/lang-css.js +0 -36
  89. package/tools/jsdoc/static/scripts/prettify/prettify.js +0 -738
  90. package/tools/jsdoc/static/styles/jsdoc-default.css +0 -1012
  91. package/tools/jsdoc/static/styles/prettify-jsdoc.css +0 -111
  92. package/tools/jsdoc/static/styles/prettify-tomorrow.css +0 -132
  93. package/tools/jsdoc/tmpl/augments.tmpl +0 -10
  94. package/tools/jsdoc/tmpl/container.tmpl +0 -193
  95. package/tools/jsdoc/tmpl/details.tmpl +0 -143
  96. package/tools/jsdoc/tmpl/example.tmpl +0 -2
  97. package/tools/jsdoc/tmpl/examples.tmpl +0 -13
  98. package/tools/jsdoc/tmpl/exceptions.tmpl +0 -17
  99. package/tools/jsdoc/tmpl/layout.tmpl +0 -83
  100. package/tools/jsdoc/tmpl/mainpage.tmpl +0 -163
  101. package/tools/jsdoc/tmpl/members.tmpl +0 -43
  102. package/tools/jsdoc/tmpl/method.tmpl +0 -124
  103. package/tools/jsdoc/tmpl/params.tmpl +0 -133
  104. package/tools/jsdoc/tmpl/properties.tmpl +0 -110
  105. package/tools/jsdoc/tmpl/returns.tmpl +0 -12
  106. package/tools/jsdoc/tmpl/source.tmpl +0 -8
  107. package/tools/jsdoc/tmpl/tutorial.tmpl +0 -19
  108. package/tools/jsdoc/tmpl/type.tmpl +0 -7
package/lib/node.js CHANGED
@@ -48,6 +48,10 @@ const Service = require('./service.js');
48
48
  const Subscription = require('./subscription.js');
49
49
  const ObservableSubscription = require('./observable_subscription.js');
50
50
  const MessageInfo = require('./message_info.js');
51
+ const {
52
+ declareQosParameters,
53
+ _resolveQoS,
54
+ } = require('./qos_overriding_options.js');
51
55
  const TimeSource = require('./time_source.js');
52
56
  const Timer = require('./timer.js');
53
57
  const TypeDescriptionService = require('./type_description_service.js');
@@ -157,7 +161,9 @@ class Node extends rclnodejs.ShadowNode {
157
161
  this._parameterService = null;
158
162
  this._typeDescriptionService = null;
159
163
  this._parameterEventPublisher = null;
164
+ this._preSetParametersCallbacks = [];
160
165
  this._setParametersCallbacks = [];
166
+ this._postSetParametersCallbacks = [];
161
167
  this._logger = new Logging(rclnodejs.getNodeLoggerName(this.handle));
162
168
  this._spinning = false;
163
169
  this._enableRosout = options.enableRosout;
@@ -250,8 +256,14 @@ class Node extends rclnodejs.ShadowNode {
250
256
 
251
257
  timersReady.forEach((timer) => {
252
258
  if (timer.isReady()) {
253
- rclnodejs.callTimer(timer.handle);
254
- timer.callback();
259
+ let timerInfo;
260
+ if (typeof rclnodejs.callTimerWithInfo === 'function') {
261
+ timerInfo = rclnodejs.callTimerWithInfo(timer.handle);
262
+ timer.callback(timerInfo);
263
+ } else {
264
+ rclnodejs.callTimer(timer.handle);
265
+ timer.callback();
266
+ }
255
267
  }
256
268
  });
257
269
 
@@ -622,15 +634,43 @@ class Node extends rclnodejs.ShadowNode {
622
634
  /**
623
635
  * Create a Timer.
624
636
  * @param {bigint} period - The number representing period in nanoseconds.
625
- * @param {function} callback - The callback to be called when timeout.
626
- * @param {Clock} [clock] - The clock which the timer gets time from.
637
+ * @param {function} callback - The callback to be called when the timer fires.
638
+ * On distros with native support, the callback receives a `TimerInfo` object
639
+ * describing the expected and actual call time.
640
+ * @param {object|Clock} [optionsOrClock] - Timer options or the clock which the timer gets time from.
641
+ * Supported options: `{ autostart?: boolean }`.
642
+ * @param {Clock} [clock] - The clock which the timer gets time from when options are provided.
627
643
  * @return {Timer} - An instance of Timer.
628
644
  */
629
- createTimer(period, callback, clock = null) {
630
- if (arguments.length === 3 && !(arguments[2] instanceof Clock)) {
631
- clock = null;
632
- } else if (arguments.length === 4) {
633
- clock = arguments[3];
645
+ createTimer(period, callback, optionsOrClock = null, clock = null) {
646
+ let options = {};
647
+
648
+ if (optionsOrClock instanceof Clock.Clock) {
649
+ clock = optionsOrClock;
650
+ } else if (optionsOrClock === null || optionsOrClock === undefined) {
651
+ // Keep the 4th argument as the clock when the 3rd argument is omitted or explicitly null.
652
+ } else {
653
+ if (typeof optionsOrClock !== 'object' || Array.isArray(optionsOrClock)) {
654
+ throw new TypeValidationError(
655
+ 'options',
656
+ optionsOrClock,
657
+ 'object or Clock',
658
+ {
659
+ nodeName: this.name(),
660
+ }
661
+ );
662
+ }
663
+ options = optionsOrClock;
664
+ }
665
+
666
+ if (
667
+ arguments.length === 4 &&
668
+ clock !== null &&
669
+ !(clock instanceof Clock.Clock)
670
+ ) {
671
+ throw new TypeValidationError('clock', clock, 'Clock', {
672
+ nodeName: this.name(),
673
+ });
634
674
  }
635
675
 
636
676
  if (typeof period !== 'bigint') {
@@ -643,12 +683,27 @@ class Node extends rclnodejs.ShadowNode {
643
683
  nodeName: this.name(),
644
684
  });
645
685
  }
686
+ if (
687
+ options.autostart !== undefined &&
688
+ typeof options.autostart !== 'boolean'
689
+ ) {
690
+ throw new TypeValidationError(
691
+ 'options.autostart',
692
+ options.autostart,
693
+ 'boolean',
694
+ {
695
+ nodeName: this.name(),
696
+ }
697
+ );
698
+ }
646
699
 
647
700
  const timerClock = clock || this._clock;
701
+ const autostart = options.autostart ?? true;
648
702
  let timerHandle = rclnodejs.createTimer(
649
703
  timerClock.handle,
650
704
  this.context.handle,
651
- period
705
+ period,
706
+ autostart
652
707
  );
653
708
  let timer = new Timer(timerHandle, period, callback);
654
709
  debug('Finish creating timer, period = %d.', period);
@@ -705,6 +760,10 @@ class Node extends rclnodejs.ShadowNode {
705
760
  * @param {object} options - The options argument used to parameterize the publisher.
706
761
  * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
707
762
  * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the publisher, default: QoS.profileDefault.
763
+ * @param {QoSOverridingOptions} [options.qosOverridingOptions] - If provided, declares read-only ROS parameters
764
+ * for the specified QoS policies (e.g. `qos_overrides./topic.publisher.depth`). These can be overridden at
765
+ * startup via `--ros-args -p` or `--params-file`. If qos is a profile string, it will be resolved to a
766
+ * mutable QoS object before overrides are applied.
708
767
  * @param {PublisherEventCallbacks} eventCallbacks - The event callbacks for the publisher.
709
768
  * @return {Publisher} - An instance of Publisher.
710
769
  */
@@ -752,6 +811,21 @@ class Node extends rclnodejs.ShadowNode {
752
811
  );
753
812
  }
754
813
 
814
+ // Apply QoS overriding options if provided
815
+ if (options.qosOverridingOptions) {
816
+ const resolvedTopic = this.resolveTopicName(topic);
817
+ if (typeof options.qos === 'string' || !(options.qos instanceof QoS)) {
818
+ options.qos = _resolveQoS(options.qos);
819
+ }
820
+ declareQosParameters(
821
+ 'publisher',
822
+ this,
823
+ resolvedTopic,
824
+ options.qos,
825
+ options.qosOverridingOptions
826
+ );
827
+ }
828
+
755
829
  let publisher = publisherClass.createPublisher(
756
830
  this,
757
831
  typeClass,
@@ -795,6 +869,10 @@ class Node extends rclnodejs.ShadowNode {
795
869
  * @param {string[]} [options.contentFilter.parameters=undefined] - Array of strings that give values to
796
870
  * the ‘parameters’ (i.e., "%n" tokens) in the filter_expression. The number of supplied parameters must
797
871
  * fit with the requested values in the filter_expression (i.e., the number of %n tokens). default: undefined.
872
+ * @param {QoSOverridingOptions} [options.qosOverridingOptions] - If provided, declares read-only ROS parameters
873
+ * for the specified QoS policies (e.g. `qos_overrides./topic.subscription.depth`). These can be overridden at
874
+ * startup via `--ros-args -p` or `--params-file`. If qos is a profile string, it will be resolved to a
875
+ * mutable QoS object before overrides are applied.
798
876
  * @param {SubscriptionCallback} callback - The callback to be call when receiving the topic subscribed. The topic will be an instance of null-terminated Buffer when options.isRaw is true.
799
877
  * @param {SubscriptionEventCallbacks} eventCallbacks - The event callbacks for the subscription.
800
878
  * @return {Subscription} - An instance of Subscription.
@@ -848,6 +926,21 @@ class Node extends rclnodejs.ShadowNode {
848
926
  );
849
927
  }
850
928
 
929
+ // Apply QoS overriding options if provided
930
+ if (options.qosOverridingOptions) {
931
+ const resolvedTopic = this.resolveTopicName(topic);
932
+ if (typeof options.qos === 'string' || !(options.qos instanceof QoS)) {
933
+ options.qos = _resolveQoS(options.qos);
934
+ }
935
+ declareQosParameters(
936
+ 'subscription',
937
+ this,
938
+ resolvedTopic,
939
+ options.qos,
940
+ options.qosOverridingOptions
941
+ );
942
+ }
943
+
851
944
  let subscription = Subscription.createSubscription(
852
945
  this,
853
946
  typeClass,
@@ -1494,7 +1587,7 @@ class Node extends rclnodejs.ShadowNode {
1494
1587
  * @returns {Array} - list of clients
1495
1588
  */
1496
1589
  getClientsInfoByService(service, noDemangle = false) {
1497
- if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
1590
+ if (DistroUtils.getDistroId() < DistroUtils.DistroId.LYRICAL) {
1498
1591
  console.warn(
1499
1592
  'getClientsInfoByService is not supported by this version of ROS 2'
1500
1593
  );
@@ -1528,7 +1621,7 @@ class Node extends rclnodejs.ShadowNode {
1528
1621
  * @returns {Array} - list of servers
1529
1622
  */
1530
1623
  getServersInfoByService(service, noDemangle = false) {
1531
- if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
1624
+ if (DistroUtils.getDistroId() < DistroUtils.DistroId.LYRICAL) {
1532
1625
  console.warn(
1533
1626
  'getServersInfoByService is not supported by this version of ROS 2'
1534
1627
  );
@@ -2015,6 +2108,29 @@ class Node extends rclnodejs.ShadowNode {
2015
2108
  * @return {SetParameterResult} - A single collective result.
2016
2109
  */
2017
2110
  _setParametersAtomically(parameters = [], declareParameterMode = false) {
2111
+ // 1) PRE callbacks — pipeline: each callback receives the output of the previous
2112
+ if (this._preSetParametersCallbacks.length > 0) {
2113
+ for (const callback of this._preSetParametersCallbacks) {
2114
+ const result = callback(parameters);
2115
+ if (!Array.isArray(result)) {
2116
+ return {
2117
+ successful: false,
2118
+ reason:
2119
+ 'pre-set parameters callback must return an array of Parameters',
2120
+ };
2121
+ }
2122
+ parameters = result;
2123
+ if (parameters.length === 0) {
2124
+ return {
2125
+ successful: false,
2126
+ reason:
2127
+ 'parameter list is empty after pre-set callback; set rejected',
2128
+ };
2129
+ }
2130
+ }
2131
+ }
2132
+
2133
+ // 2) Validate
2018
2134
  let result = this._validateParameters(parameters, declareParameterMode);
2019
2135
  if (!result.successful) {
2020
2136
  return result;
@@ -2084,6 +2200,11 @@ class Node extends rclnodejs.ShadowNode {
2084
2200
  // Publish ParameterEvent.
2085
2201
  this._parameterEventPublisher.publish(parameterEvent);
2086
2202
 
2203
+ // POST callbacks — for side effects after successful set
2204
+ for (const callback of this._postSetParametersCallbacks) {
2205
+ callback(parameters);
2206
+ }
2207
+
2087
2208
  return {
2088
2209
  successful: true,
2089
2210
  reason: '',
@@ -2128,6 +2249,82 @@ class Node extends rclnodejs.ShadowNode {
2128
2249
  }
2129
2250
  }
2130
2251
 
2252
+ /**
2253
+ * A callback invoked before parameter validation and setting.
2254
+ * It receives the parameter list and must return a (possibly modified) parameter list.
2255
+ *
2256
+ * @callback PreSetParametersCallback
2257
+ * @param {Parameter[]} parameters - The parameters about to be set.
2258
+ * @returns {Parameter[]} - The modified parameter list to proceed with.
2259
+ *
2260
+ * @see [Node.addPreSetParametersCallback]{@link Node#addPreSetParametersCallback}
2261
+ * @see [Node.removePreSetParametersCallback]{@link Node#removePreSetParametersCallback}
2262
+ */
2263
+
2264
+ /**
2265
+ * Add a callback invoked before parameter validation.
2266
+ * The callback receives the parameter list and must return a (possibly modified)
2267
+ * parameter list. This can be used to coerce, add, or remove parameters before
2268
+ * they are validated and applied. If any pre-set callback returns an empty list,
2269
+ * the set is rejected.
2270
+ *
2271
+ * @param {PreSetParametersCallback} callback - The callback to add.
2272
+ * @returns {undefined}
2273
+ */
2274
+ addPreSetParametersCallback(callback) {
2275
+ this._preSetParametersCallbacks.unshift(callback);
2276
+ }
2277
+
2278
+ /**
2279
+ * Remove a pre-set parameters callback.
2280
+ *
2281
+ * @param {PreSetParametersCallback} callback - The callback to remove.
2282
+ * @returns {undefined}
2283
+ */
2284
+ removePreSetParametersCallback(callback) {
2285
+ const idx = this._preSetParametersCallbacks.indexOf(callback);
2286
+ if (idx > -1) {
2287
+ this._preSetParametersCallbacks.splice(idx, 1);
2288
+ }
2289
+ }
2290
+
2291
+ /**
2292
+ * A callback invoked after parameters have been successfully set.
2293
+ * It receives the final parameter list. For side effects only (return value is ignored).
2294
+ *
2295
+ * @callback PostSetParametersCallback
2296
+ * @param {Parameter[]} parameters - The parameters that were set.
2297
+ * @returns {undefined}
2298
+ *
2299
+ * @see [Node.addPostSetParametersCallback]{@link Node#addPostSetParametersCallback}
2300
+ * @see [Node.removePostSetParametersCallback]{@link Node#removePostSetParametersCallback}
2301
+ */
2302
+
2303
+ /**
2304
+ * Add a callback invoked after parameters are successfully set.
2305
+ * The callback receives the final parameter list. Useful for triggering
2306
+ * side effects (e.g., reconfiguring a component when a parameter changes).
2307
+ *
2308
+ * @param {PostSetParametersCallback} callback - The callback to add.
2309
+ * @returns {undefined}
2310
+ */
2311
+ addPostSetParametersCallback(callback) {
2312
+ this._postSetParametersCallbacks.unshift(callback);
2313
+ }
2314
+
2315
+ /**
2316
+ * Remove a post-set parameters callback.
2317
+ *
2318
+ * @param {PostSetParametersCallback} callback - The callback to remove.
2319
+ * @returns {undefined}
2320
+ */
2321
+ removePostSetParametersCallback(callback) {
2322
+ const idx = this._postSetParametersCallbacks.indexOf(callback);
2323
+ if (idx > -1) {
2324
+ this._postSetParametersCallbacks.splice(idx, 1);
2325
+ }
2326
+ }
2327
+
2131
2328
  /**
2132
2329
  * Get the fully qualified name of the node.
2133
2330
  *
@@ -16,6 +16,7 @@
16
16
 
17
17
  const { TypeValidationError, OperationError } = require('./errors');
18
18
  const { normalizeNodeName } = require('./utils');
19
+ const validator = require('./validator');
19
20
  const debug = require('debug')('rclnodejs:parameter_event_handler');
20
21
 
21
22
  const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent';
@@ -210,6 +211,64 @@ class ParameterEventHandler {
210
211
  return handle;
211
212
  }
212
213
 
214
+ /**
215
+ * Configure which node parameter events will be received.
216
+ *
217
+ * If nodeNames is omitted or empty, the current node filter is cleared.
218
+ * When a filter is active, parameter and event callbacks only receive
219
+ * events from the specified nodes.
220
+ *
221
+ * @param {string[]} [nodeNames] - Node names to filter parameter events from.
222
+ * Relative names are resolved against the handler node namespace.
223
+ * @returns {boolean} True if the filter is active or was successfully cleared.
224
+ */
225
+ configureNodesFilter(nodeNames) {
226
+ this.#checkNotDestroyed();
227
+
228
+ if (nodeNames === undefined || nodeNames === null) {
229
+ this.#subscription.clearContentFilter();
230
+ return !this.#subscription.hasContentFilter();
231
+ }
232
+
233
+ if (!Array.isArray(nodeNames)) {
234
+ throw new TypeValidationError('nodeNames', nodeNames, 'string[]', {
235
+ entityType: 'parameter event handler',
236
+ });
237
+ }
238
+
239
+ if (nodeNames.length === 0) {
240
+ this.#subscription.clearContentFilter();
241
+ return !this.#subscription.hasContentFilter();
242
+ }
243
+
244
+ const resolvedNodeNames = nodeNames.map((nodeName, index) => {
245
+ if (typeof nodeName !== 'string' || nodeName.trim() === '') {
246
+ throw new TypeValidationError(
247
+ `nodeNames[${index}]`,
248
+ nodeName,
249
+ 'non-empty string',
250
+ {
251
+ entityType: 'parameter event handler',
252
+ }
253
+ );
254
+ }
255
+
256
+ const resolvedNodeName = this.#resolvePath(nodeName.trim());
257
+ this.#validateFullyQualifiedNodePath(resolvedNodeName);
258
+ return resolvedNodeName;
259
+ });
260
+
261
+ const contentFilter = {
262
+ expression: resolvedNodeNames
263
+ .map((_, index) => `node = %${index}`)
264
+ .join(' OR '),
265
+ parameters: resolvedNodeNames.map((nodeName) => `'${nodeName}'`),
266
+ };
267
+
268
+ this.#subscription.setContentFilter(contentFilter);
269
+ return this.#subscription.hasContentFilter();
270
+ }
271
+
213
272
  /**
214
273
  * Remove a previously added parameter callback.
215
274
  *
@@ -450,6 +509,45 @@ class ParameterEventHandler {
450
509
  return `${paramName}\0${nodeName}`;
451
510
  }
452
511
 
512
+ /**
513
+ * Resolve a node path to the fully qualified name used in ParameterEvent.node.
514
+ * @private
515
+ */
516
+ #resolvePath(nodePath) {
517
+ // Absolute node paths are already rooted. Relative names are resolved
518
+ // against the handler node namespace before building the content filter.
519
+ const unresolvedPath = nodePath.startsWith('/')
520
+ ? nodePath
521
+ : `${this.#node.namespace().replace(/\/+$/, '')}/${nodePath}`;
522
+
523
+ // Collapse repeated separators for inputs like '/ns//node/' or 'nested//node'.
524
+ const resolvedPath = unresolvedPath.replace(/\/+/g, '/');
525
+
526
+ // Preserve the root namespace as '/' and strip trailing slashes everywhere
527
+ // else so the filter matches the canonical ParameterEvent.node format.
528
+ if (resolvedPath === '/') {
529
+ return resolvedPath;
530
+ }
531
+
532
+ return resolvedPath.replace(/\/+$/, '');
533
+ }
534
+
535
+ /**
536
+ * Validate a fully qualified node path before using it in a content filter.
537
+ * @private
538
+ */
539
+ #validateFullyQualifiedNodePath(nodePath) {
540
+ const normalizedPath =
541
+ nodePath.length > 1 ? nodePath.replace(/\/+$/, '') : nodePath;
542
+ const separatorIndex = normalizedPath.lastIndexOf('/');
543
+ const nodeNamespace =
544
+ separatorIndex === 0 ? '/' : normalizedPath.slice(0, separatorIndex);
545
+ const nodeName = normalizedPath.slice(separatorIndex + 1);
546
+
547
+ validator.validateNamespace(nodeNamespace);
548
+ validator.validateNodeName(nodeName);
549
+ }
550
+
453
551
  /**
454
552
  * Check if the handler has been destroyed and throw if so.
455
553
  * @private
@@ -0,0 +1,47 @@
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 PREBUILD_PACKAGE_NAME = 'rclnodejs';
18
+ const SUPPORTED_PREBUILD_RUNTIMES = new Set(['node', 'electron']);
19
+
20
+ function detectPrebuildRuntime() {
21
+ if (process.env.npm_config_runtime === 'electron') {
22
+ return 'electron';
23
+ }
24
+
25
+ return process.versions.electron ? 'electron' : 'node';
26
+ }
27
+
28
+ function getTaggedPrebuildFilename({
29
+ rosDistro,
30
+ ubuntuCodename,
31
+ arch,
32
+ runtime,
33
+ }) {
34
+ return `${rosDistro}-${ubuntuCodename}-${arch}-${runtime}-${PREBUILD_PACKAGE_NAME}.node`;
35
+ }
36
+
37
+ function getRuntimeFromGeneratedPrebuild(fileName) {
38
+ const runtime = fileName.split('.')[0];
39
+ return SUPPORTED_PREBUILD_RUNTIMES.has(runtime) ? runtime : null;
40
+ }
41
+
42
+ module.exports = {
43
+ detectPrebuildRuntime,
44
+ getRuntimeFromGeneratedPrebuild,
45
+ getTaggedPrebuildFilename,
46
+ PREBUILD_PACKAGE_NAME,
47
+ };