rclnodejs 1.8.2 → 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 (112) hide show
  1. package/README.md +46 -37
  2. package/index.js +62 -23
  3. package/lib/action/client.js +67 -3
  4. package/lib/action/server.js +1 -3
  5. package/lib/distro.js +2 -1
  6. package/lib/lifecycle_publisher.js +2 -2
  7. package/lib/message_info.js +94 -0
  8. package/lib/node.js +90 -14
  9. package/lib/parameter.js +5 -9
  10. package/lib/parameter_event_handler.js +468 -0
  11. package/lib/parameter_watcher.js +12 -12
  12. package/lib/service.js +8 -4
  13. package/lib/subscription.js +38 -5
  14. package/lib/time_source.js +3 -20
  15. package/lib/timer.js +2 -1
  16. package/lib/wait_for_message.js +111 -0
  17. package/package.json +7 -4
  18. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  19. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  20. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  21. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  22. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  23. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  24. package/rosidl_gen/generate_worker.js +3 -13
  25. package/rosidl_gen/idl_generator.js +210 -0
  26. package/rosidl_gen/index.js +3 -12
  27. package/rosidl_gen/packages.js +1 -3
  28. package/rosidl_gen/primitive_types.js +2 -2
  29. package/rosidl_parser/idl_parser.py +437 -0
  30. package/rosidl_parser/parser.py +2 -4
  31. package/rosidl_parser/rosidl_parser.js +27 -0
  32. package/scripts/run_asan_test.sh +118 -0
  33. package/src/executor.cpp +37 -2
  34. package/src/executor.h +11 -0
  35. package/src/macros.h +2 -2
  36. package/src/rcl_action_client_bindings.cpp +88 -12
  37. package/src/rcl_action_server_bindings.cpp +24 -13
  38. package/src/rcl_client_bindings.cpp +13 -5
  39. package/src/rcl_context_bindings.cpp +10 -11
  40. package/src/rcl_graph_bindings.cpp +2 -2
  41. package/src/rcl_guard_condition_bindings.cpp +12 -3
  42. package/src/rcl_lifecycle_bindings.cpp +34 -15
  43. package/src/rcl_node_bindings.cpp +11 -4
  44. package/src/rcl_publisher_bindings.cpp +12 -3
  45. package/src/rcl_service_bindings.cpp +12 -3
  46. package/src/rcl_subscription_bindings.cpp +92 -21
  47. package/src/rcl_timer_bindings.cpp +24 -9
  48. package/src/rcl_type_description_service_bindings.cpp +9 -1
  49. package/src/rcl_utilities.cpp +2 -2
  50. package/tools/jsdoc/Makefile +5 -0
  51. package/tools/jsdoc/README.md +96 -0
  52. package/tools/jsdoc/build-index.js +610 -0
  53. package/tools/jsdoc/publish.js +854 -0
  54. package/tools/jsdoc/regenerate-published-docs.js +605 -0
  55. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.eot +0 -0
  56. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
  57. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.woff +0 -0
  58. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  59. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  60. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  61. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.eot +0 -0
  62. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
  63. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.woff +0 -0
  64. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.eot +0 -0
  65. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.svg +1831 -0
  66. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.woff +0 -0
  67. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  68. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  69. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  70. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.eot +0 -0
  71. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
  72. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.woff +0 -0
  73. package/tools/jsdoc/static/scripts/linenumber.js +25 -0
  74. package/tools/jsdoc/static/scripts/prettify/Apache-License-2.0.txt +202 -0
  75. package/tools/jsdoc/static/scripts/prettify/lang-css.js +36 -0
  76. package/tools/jsdoc/static/scripts/prettify/prettify.js +738 -0
  77. package/tools/jsdoc/static/styles/jsdoc-default.css +1012 -0
  78. package/tools/jsdoc/static/styles/prettify-jsdoc.css +111 -0
  79. package/tools/jsdoc/static/styles/prettify-tomorrow.css +132 -0
  80. package/tools/jsdoc/tmpl/augments.tmpl +10 -0
  81. package/tools/jsdoc/tmpl/container.tmpl +193 -0
  82. package/tools/jsdoc/tmpl/details.tmpl +143 -0
  83. package/tools/jsdoc/tmpl/example.tmpl +2 -0
  84. package/tools/jsdoc/tmpl/examples.tmpl +13 -0
  85. package/tools/jsdoc/tmpl/exceptions.tmpl +17 -0
  86. package/tools/jsdoc/tmpl/layout.tmpl +83 -0
  87. package/tools/jsdoc/tmpl/mainpage.tmpl +163 -0
  88. package/tools/jsdoc/tmpl/members.tmpl +43 -0
  89. package/tools/jsdoc/tmpl/method.tmpl +124 -0
  90. package/tools/jsdoc/tmpl/params.tmpl +133 -0
  91. package/tools/jsdoc/tmpl/properties.tmpl +110 -0
  92. package/tools/jsdoc/tmpl/returns.tmpl +12 -0
  93. package/tools/jsdoc/tmpl/source.tmpl +8 -0
  94. package/tools/jsdoc/tmpl/tutorial.tmpl +19 -0
  95. package/tools/jsdoc/tmpl/type.tmpl +7 -0
  96. package/types/action_client.d.ts +8 -0
  97. package/types/index.d.ts +34 -0
  98. package/types/message_info.d.ts +72 -0
  99. package/types/node.d.ts +21 -0
  100. package/types/parameter_event_handler.d.ts +139 -0
  101. package/types/subscription.d.ts +14 -2
  102. package/rosidl_convertor/README.md +0 -298
  103. package/rosidl_convertor/idl_convertor.js +0 -50
  104. package/rosidl_convertor/idl_convertor.py +0 -1250
  105. package/test_data_integrity.js +0 -108
  106. package/test_repro_exact.js +0 -57
  107. package/test_repro_hz.js +0 -86
  108. package/test_repro_pub.js +0 -36
  109. package/test_repro_stress.js +0 -83
  110. package/test_repro_sub.js +0 -64
  111. package/test_xproc_data.js +0 -64
  112. package/types/interfaces.d.ts +0 -8895
package/lib/node.js CHANGED
@@ -40,12 +40,14 @@ const {
40
40
  const ParameterService = require('./parameter_service.js');
41
41
  const ParameterClient = require('./parameter_client.js');
42
42
  const ParameterWatcher = require('./parameter_watcher.js');
43
+ const ParameterEventHandler = require('./parameter_event_handler.js');
43
44
  const Publisher = require('./publisher.js');
44
45
  const QoS = require('./qos.js');
45
46
  const Rates = require('./rate.js');
46
47
  const Service = require('./service.js');
47
48
  const Subscription = require('./subscription.js');
48
49
  const ObservableSubscription = require('./observable_subscription.js');
50
+ const MessageInfo = require('./message_info.js');
49
51
  const TimeSource = require('./time_source.js');
50
52
  const Timer = require('./timer.js');
51
53
  const TypeDescriptionService = require('./type_description_service.js');
@@ -97,7 +99,30 @@ class Node extends rclnodejs.ShadowNode {
97
99
  );
98
100
  }
99
101
 
102
+ static _normalizeOptions(options) {
103
+ if (options instanceof NodeOptions) {
104
+ return options;
105
+ }
106
+ const defaults = NodeOptions.defaultOptions;
107
+ return {
108
+ startParameterServices:
109
+ options.startParameterServices ?? defaults.startParameterServices,
110
+ parameterOverrides:
111
+ options.parameterOverrides ?? defaults.parameterOverrides,
112
+ automaticallyDeclareParametersFromOverrides:
113
+ options.automaticallyDeclareParametersFromOverrides ??
114
+ defaults.automaticallyDeclareParametersFromOverrides,
115
+ startTypeDescriptionService:
116
+ options.startTypeDescriptionService ??
117
+ defaults.startTypeDescriptionService,
118
+ enableRosout: options.enableRosout ?? defaults.enableRosout,
119
+ rosoutQos: options.rosoutQos ?? defaults.rosoutQos,
120
+ };
121
+ }
122
+
100
123
  _init(name, namespace, options, context, args, useGlobalArguments) {
124
+ options = Node._normalizeOptions(options);
125
+
101
126
  this.handle = rclnodejs.createNode(
102
127
  name,
103
128
  namespace,
@@ -125,6 +150,7 @@ class Node extends rclnodejs.ShadowNode {
125
150
  this._actionServers = [];
126
151
  this._parameterClients = [];
127
152
  this._parameterWatchers = [];
153
+ this._parameterEventHandlers = [];
128
154
  this._rateTimerServer = null;
129
155
  this._parameterDescriptors = new Map();
130
156
  this._parameters = new Map();
@@ -151,7 +177,7 @@ class Node extends rclnodejs.ShadowNode {
151
177
  // override cli parameterOverrides with those specified in options
152
178
  if (options.parameterOverrides.length > 0) {
153
179
  for (const parameter of options.parameterOverrides) {
154
- if ((!parameter) instanceof Parameter) {
180
+ if (!(parameter instanceof Parameter)) {
155
181
  throw new TypeValidationError(
156
182
  'parameterOverride',
157
183
  parameter,
@@ -246,9 +272,22 @@ class Node extends rclnodejs.ShadowNode {
246
272
  this._runWithMessageType(
247
273
  subscription.typeClass,
248
274
  (message, deserialize) => {
249
- let success = rclnodejs.rclTake(subscription.handle, message);
250
- if (success) {
251
- subscription.processResponse(deserialize());
275
+ if (subscription.wantsMessageInfo) {
276
+ let rawInfo = rclnodejs.rclTakeWithInfo(
277
+ subscription.handle,
278
+ message
279
+ );
280
+ if (rawInfo) {
281
+ subscription.processResponse(
282
+ deserialize(),
283
+ new MessageInfo(rawInfo)
284
+ );
285
+ }
286
+ } else {
287
+ let success = rclnodejs.rclTake(subscription.handle, message);
288
+ if (success) {
289
+ subscription.processResponse(deserialize());
290
+ }
252
291
  }
253
292
  }
254
293
  );
@@ -425,17 +464,20 @@ class Node extends rclnodejs.ShadowNode {
425
464
  }
426
465
 
427
466
  if (properties.isGoalExpired) {
428
- let GoalInfoArray = ActionInterfaces.GoalInfo.ArrayType;
429
- let message = new GoalInfoArray(actionServer._goalHandles.size);
430
- let count = rclnodejs.actionExpireGoals(
431
- actionServer.handle,
432
- actionServer._goalHandles.size,
433
- message._refArray.buffer
434
- );
435
- if (count > 0) {
436
- actionServer.processGoalExpired(message, count);
467
+ let numGoals = actionServer._goalHandles.size;
468
+ if (numGoals > 0) {
469
+ let GoalInfoArray = ActionInterfaces.GoalInfo.ArrayType;
470
+ let message = new GoalInfoArray(numGoals);
471
+ let count = rclnodejs.actionExpireGoals(
472
+ actionServer.handle,
473
+ numGoals,
474
+ message._refArray.buffer
475
+ );
476
+ if (count > 0) {
477
+ actionServer.processGoalExpired(message, count);
478
+ }
479
+ GoalInfoArray.freeArray(message);
437
480
  }
438
- GoalInfoArray.freeArray(message);
439
481
  }
440
482
  }
441
483
 
@@ -1056,6 +1098,7 @@ class Node extends rclnodejs.ShadowNode {
1056
1098
 
1057
1099
  this._parameterClients.forEach((paramClient) => paramClient.destroy());
1058
1100
  this._parameterWatchers.forEach((watcher) => watcher.destroy());
1101
+ this._parameterEventHandlers.forEach((handler) => handler.destroy());
1059
1102
 
1060
1103
  this.context.onNodeDestroyed(this);
1061
1104
 
@@ -1076,6 +1119,7 @@ class Node extends rclnodejs.ShadowNode {
1076
1119
  this._actionServers = [];
1077
1120
  this._parameterClients = [];
1078
1121
  this._parameterWatchers = [];
1122
+ this._parameterEventHandlers = [];
1079
1123
 
1080
1124
  if (this._rateTimerServer) {
1081
1125
  this._rateTimerServer.shutdown();
@@ -1188,6 +1232,38 @@ class Node extends rclnodejs.ShadowNode {
1188
1232
  watcher.destroy();
1189
1233
  }
1190
1234
 
1235
+ /**
1236
+ * Create a ParameterEventHandler that monitors parameter changes on any node.
1237
+ *
1238
+ * Unlike {@link ParameterWatcher} which watches specific parameters on a single
1239
+ * remote node, ParameterEventHandler can register callbacks for parameters on
1240
+ * any node in the ROS 2 graph by subscribing to /parameter_events.
1241
+ *
1242
+ * @param {object} [options] - Options for the handler
1243
+ * @param {object} [options.qos] - QoS profile for the parameter_events subscription
1244
+ * @return {ParameterEventHandler} - An instance of ParameterEventHandler
1245
+ * @see {@link ParameterEventHandler}
1246
+ */
1247
+ createParameterEventHandler(options = {}) {
1248
+ const handler = new ParameterEventHandler(this, options);
1249
+ debug('Created ParameterEventHandler on node=%s', this.name());
1250
+ this._parameterEventHandlers.push(handler);
1251
+ return handler;
1252
+ }
1253
+
1254
+ /**
1255
+ * Destroy a ParameterEventHandler.
1256
+ * @param {ParameterEventHandler} handler - The handler to be destroyed.
1257
+ * @return {undefined}
1258
+ */
1259
+ destroyParameterEventHandler(handler) {
1260
+ if (!(handler instanceof ParameterEventHandler)) {
1261
+ throw new TypeError('Invalid argument');
1262
+ }
1263
+ this._removeEntityFromArray(handler, this._parameterEventHandlers);
1264
+ handler.destroy();
1265
+ }
1266
+
1191
1267
  /**
1192
1268
  * Destroy a Timer.
1193
1269
  * @param {Timer} timer - The Timer to be destroyed.
package/lib/parameter.js CHANGED
@@ -864,9 +864,8 @@ function parameterTypeFromValue(value) {
864
864
  function validType(parameterType) {
865
865
  let result =
866
866
  typeof parameterType === 'number' &&
867
- ParameterType.PARAMETER_NOT_SET <=
868
- parameterType <=
869
- ParameterType.PARAMETER_STRING_ARRAY;
867
+ parameterType >= ParameterType.PARAMETER_NOT_SET &&
868
+ parameterType <= ParameterType.PARAMETER_STRING_ARRAY;
870
869
 
871
870
  return result;
872
871
  }
@@ -923,14 +922,11 @@ function _validArray(values, type) {
923
922
  arrayElementType = ParameterType.PARAMETER_BOOL;
924
923
  } else if (type === ParameterType.PARAMETER_BYTE_ARRAY) {
925
924
  arrayElementType = PARAMETER_BYTE;
926
- }
927
- if (type === ParameterType.PARAMETER_INTEGER_ARRAY) {
925
+ } else if (type === ParameterType.PARAMETER_INTEGER_ARRAY) {
928
926
  arrayElementType = ParameterType.PARAMETER_INTEGER;
929
- }
930
- if (type === ParameterType.PARAMETER_DOUBLE_ARRAY) {
927
+ } else if (type === ParameterType.PARAMETER_DOUBLE_ARRAY) {
931
928
  arrayElementType = ParameterType.PARAMETER_DOUBLE;
932
- }
933
- if (type === ParameterType.PARAMETER_STRING_ARRAY) {
929
+ } else if (type === ParameterType.PARAMETER_STRING_ARRAY) {
934
930
  arrayElementType = ParameterType.PARAMETER_STRING;
935
931
  }
936
932
 
@@ -0,0 +1,468 @@
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 { TypeValidationError, OperationError } = require('./errors');
18
+ const { normalizeNodeName } = require('./utils');
19
+ const debug = require('debug')('rclnodejs:parameter_event_handler');
20
+
21
+ const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent';
22
+ const PARAMETER_EVENT_TOPIC = '/parameter_events';
23
+
24
+ /**
25
+ * @class ParameterCallbackHandle
26
+ * Opaque handle returned when adding a parameter callback.
27
+ * Used to remove the callback later.
28
+ */
29
+ class ParameterCallbackHandle {
30
+ /**
31
+ * @param {string} parameterName - The parameter name
32
+ * @param {string} nodeName - The fully qualified node name
33
+ * @param {Function} callback - The callback function
34
+ * @hideconstructor
35
+ */
36
+ constructor(parameterName, nodeName, callback) {
37
+ this.parameterName = parameterName;
38
+ this.nodeName = nodeName;
39
+ this.callback = callback;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @class ParameterEventCallbackHandle
45
+ * Opaque handle returned when adding a parameter event callback.
46
+ * Used to remove the callback later.
47
+ */
48
+ class ParameterEventCallbackHandle {
49
+ /**
50
+ * @param {Function} callback - The callback function
51
+ * @hideconstructor
52
+ */
53
+ constructor(callback) {
54
+ this.callback = callback;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * @class ParameterEventHandler
60
+ *
61
+ * Monitors and responds to parameter changes on any node in the ROS 2 graph
62
+ * by subscribing to the `/parameter_events` topic.
63
+ *
64
+ * Unlike {@link ParameterWatcher}, which is tied to a single remote node and
65
+ * requires waiting for that node's parameter services, ParameterEventHandler
66
+ * responds to parameter events from any node without needing service availability.
67
+ *
68
+ * Two types of callbacks are supported:
69
+ * - **Parameter callbacks**: fired when a specific parameter on a specific node
70
+ * is added or changed (new_parameters + changed_parameters).
71
+ * Note: deleted parameters are not dispatched to parameter callbacks;
72
+ * use event callbacks to observe deletions.
73
+ * - **Event callbacks**: fired for every ParameterEvent message received,
74
+ * including deletions.
75
+ *
76
+ * @example
77
+ * const handler = node.createParameterEventHandler();
78
+ *
79
+ * // Watch a specific parameter on a specific node
80
+ * const handle = handler.addParameterCallback(
81
+ * 'my_param',
82
+ * '/my_node',
83
+ * (parameter) => {
84
+ * console.log(`Parameter changed: ${parameter.name} = ${parameter.value}`);
85
+ * }
86
+ * );
87
+ *
88
+ * // Watch all parameter events
89
+ * const eventHandle = handler.addParameterEventCallback((event) => {
90
+ * console.log(`Event from node: ${event.node}`);
91
+ * });
92
+ *
93
+ * // Remove callbacks when done
94
+ * handler.removeParameterCallback(handle);
95
+ * handler.removeParameterEventCallback(eventHandle);
96
+ *
97
+ * // Destroy when no longer needed
98
+ * handler.destroy();
99
+ */
100
+ class ParameterEventHandler {
101
+ #node;
102
+ #subscription;
103
+ #parameterCallbacks; // Map<string, ParameterCallbackHandle[]> keyed by "paramName\0nodeName"
104
+ #eventCallbacks; // ParameterEventCallbackHandle[]
105
+ #destroyed;
106
+
107
+ /**
108
+ * Create a ParameterEventHandler.
109
+ *
110
+ * @param {object} node - The rclnodejs Node used to create the subscription
111
+ * @param {object} [options] - Options
112
+ * @param {object} [options.qos] - QoS profile for the parameter_events subscription
113
+ */
114
+ constructor(node, options = {}) {
115
+ if (!node || typeof node.createSubscription !== 'function') {
116
+ throw new TypeValidationError('node', node, 'Node instance', {
117
+ entityType: 'parameter event handler',
118
+ });
119
+ }
120
+
121
+ if (
122
+ options !== undefined &&
123
+ options !== null &&
124
+ typeof options !== 'object'
125
+ ) {
126
+ throw new TypeValidationError('options', options, 'object or undefined', {
127
+ entityType: 'parameter event handler',
128
+ });
129
+ }
130
+
131
+ const opts = options || {};
132
+
133
+ this.#node = node;
134
+ this.#parameterCallbacks = new Map();
135
+ this.#eventCallbacks = [];
136
+ this.#destroyed = false;
137
+
138
+ const subscriptionOptions = opts.qos ? { qos: opts.qos } : undefined;
139
+
140
+ this.#subscription = node.createSubscription(
141
+ PARAMETER_EVENT_MSG_TYPE,
142
+ PARAMETER_EVENT_TOPIC,
143
+ subscriptionOptions,
144
+ (event) => this.#handleEvent(event)
145
+ );
146
+
147
+ debug('Created ParameterEventHandler on node=%s', node.name());
148
+ }
149
+
150
+ /**
151
+ * Add a callback for a specific parameter on a specific node.
152
+ *
153
+ * The callback is invoked whenever the named parameter is added or changed
154
+ * on the specified node. The callback receives the parameter message object
155
+ * (rcl_interfaces/msg/Parameter) with `name` and `value` fields.
156
+ *
157
+ * @param {string} parameterName - Name of the parameter to monitor
158
+ * @param {string} nodeName - Fully qualified name of the node (e.g., '/my_node')
159
+ * @param {Function} callback - Called with (parameter) when the parameter changes
160
+ * @returns {ParameterCallbackHandle} Handle for removing this callback later
161
+ * @throws {Error} If the handler has been destroyed
162
+ * @throws {TypeError} If arguments are invalid
163
+ */
164
+ addParameterCallback(parameterName, nodeName, callback) {
165
+ this.#checkNotDestroyed();
166
+
167
+ if (typeof parameterName !== 'string' || parameterName.trim() === '') {
168
+ throw new TypeValidationError(
169
+ 'parameterName',
170
+ parameterName,
171
+ 'non-empty string',
172
+ { entityType: 'parameter event handler' }
173
+ );
174
+ }
175
+
176
+ if (typeof nodeName !== 'string' || nodeName.trim() === '') {
177
+ throw new TypeValidationError('nodeName', nodeName, 'non-empty string', {
178
+ entityType: 'parameter event handler',
179
+ });
180
+ }
181
+
182
+ if (typeof callback !== 'function') {
183
+ throw new TypeValidationError('callback', callback, 'function', {
184
+ entityType: 'parameter event handler',
185
+ });
186
+ }
187
+
188
+ const resolvedNodeName = normalizeNodeName(nodeName);
189
+ const resolvedParamName = parameterName.trim();
190
+ const handle = new ParameterCallbackHandle(
191
+ resolvedParamName,
192
+ resolvedNodeName,
193
+ callback
194
+ );
195
+ const key = this.#makeKey(resolvedParamName, resolvedNodeName);
196
+
197
+ if (!this.#parameterCallbacks.has(key)) {
198
+ this.#parameterCallbacks.set(key, []);
199
+ }
200
+
201
+ // Insert at front (FILO order, matching rclpy behavior)
202
+ this.#parameterCallbacks.get(key).unshift(handle);
203
+
204
+ debug(
205
+ 'Added parameter callback: param=%s node=%s',
206
+ resolvedParamName,
207
+ resolvedNodeName
208
+ );
209
+
210
+ return handle;
211
+ }
212
+
213
+ /**
214
+ * Remove a previously added parameter callback.
215
+ *
216
+ * @param {ParameterCallbackHandle} handle - The handle returned by addParameterCallback
217
+ * @throws {Error} If the handle is not found or handler is destroyed
218
+ */
219
+ removeParameterCallback(handle) {
220
+ this.#checkNotDestroyed();
221
+
222
+ if (!(handle instanceof ParameterCallbackHandle)) {
223
+ throw new TypeValidationError(
224
+ 'handle',
225
+ handle,
226
+ 'ParameterCallbackHandle',
227
+ { entityType: 'parameter event handler' }
228
+ );
229
+ }
230
+
231
+ const key = this.#makeKey(handle.parameterName, handle.nodeName);
232
+ const callbacks = this.#parameterCallbacks.get(key);
233
+
234
+ if (!callbacks) {
235
+ throw new OperationError(
236
+ `No callbacks registered for parameter '${handle.parameterName}' on node '${handle.nodeName}'`,
237
+ { entityType: 'parameter event handler' }
238
+ );
239
+ }
240
+
241
+ const index = callbacks.indexOf(handle);
242
+ if (index === -1) {
243
+ throw new OperationError("Callback doesn't exist", {
244
+ entityType: 'parameter event handler',
245
+ });
246
+ }
247
+
248
+ callbacks.splice(index, 1);
249
+
250
+ if (callbacks.length === 0) {
251
+ this.#parameterCallbacks.delete(key);
252
+ }
253
+
254
+ debug(
255
+ 'Removed parameter callback: param=%s node=%s',
256
+ handle.parameterName,
257
+ handle.nodeName
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Add a callback that is invoked for every parameter event.
263
+ *
264
+ * The callback receives the full ParameterEvent message
265
+ * (rcl_interfaces/msg/ParameterEvent) with `node`, `new_parameters`,
266
+ * `changed_parameters`, and `deleted_parameters` fields.
267
+ *
268
+ * @param {Function} callback - Called with (event) for every ParameterEvent
269
+ * @returns {ParameterEventCallbackHandle} Handle for removing this callback later
270
+ * @throws {Error} If the handler has been destroyed
271
+ * @throws {TypeError} If callback is not a function
272
+ */
273
+ addParameterEventCallback(callback) {
274
+ this.#checkNotDestroyed();
275
+
276
+ if (typeof callback !== 'function') {
277
+ throw new TypeValidationError('callback', callback, 'function', {
278
+ entityType: 'parameter event handler',
279
+ });
280
+ }
281
+
282
+ const handle = new ParameterEventCallbackHandle(callback);
283
+
284
+ // Insert at front (FILO order)
285
+ this.#eventCallbacks.unshift(handle);
286
+
287
+ debug('Added parameter event callback');
288
+
289
+ return handle;
290
+ }
291
+
292
+ /**
293
+ * Remove a previously added parameter event callback.
294
+ *
295
+ * @param {ParameterEventCallbackHandle} handle - The handle returned by addParameterEventCallback
296
+ * @throws {Error} If the handle is not found or handler is destroyed
297
+ */
298
+ removeParameterEventCallback(handle) {
299
+ this.#checkNotDestroyed();
300
+
301
+ if (!(handle instanceof ParameterEventCallbackHandle)) {
302
+ throw new TypeValidationError(
303
+ 'handle',
304
+ handle,
305
+ 'ParameterEventCallbackHandle',
306
+ { entityType: 'parameter event handler' }
307
+ );
308
+ }
309
+
310
+ const index = this.#eventCallbacks.indexOf(handle);
311
+ if (index === -1) {
312
+ throw new OperationError("Callback doesn't exist", {
313
+ entityType: 'parameter event handler',
314
+ });
315
+ }
316
+
317
+ this.#eventCallbacks.splice(index, 1);
318
+
319
+ debug('Removed parameter event callback');
320
+ }
321
+
322
+ /**
323
+ * Check if the handler has been destroyed.
324
+ *
325
+ * @returns {boolean} True if destroyed
326
+ */
327
+ isDestroyed() {
328
+ return this.#destroyed;
329
+ }
330
+
331
+ /**
332
+ * Destroy the handler and clean up resources.
333
+ * Removes the subscription and clears all callbacks.
334
+ */
335
+ destroy() {
336
+ if (this.#destroyed) {
337
+ return;
338
+ }
339
+
340
+ debug('Destroying ParameterEventHandler');
341
+
342
+ if (this.#subscription) {
343
+ try {
344
+ this.#node.destroySubscription(this.#subscription);
345
+ } catch (error) {
346
+ debug('Error destroying subscription: %s', error.message);
347
+ }
348
+ this.#subscription = null;
349
+ }
350
+
351
+ this.#parameterCallbacks.clear();
352
+ this.#eventCallbacks.length = 0;
353
+ this.#destroyed = true;
354
+ }
355
+
356
+ /**
357
+ * Get a specific parameter from a ParameterEvent message.
358
+ *
359
+ * @param {object} event - A ParameterEvent message
360
+ * @param {string} parameterName - The parameter name to look for
361
+ * @param {string} nodeName - The node name to match
362
+ * @returns {object|null} The matching parameter message, or null
363
+ * @static
364
+ */
365
+ static getParameterFromEvent(event, parameterName, nodeName) {
366
+ const resolvedNodeName = normalizeNodeName(nodeName);
367
+ const resolvedParamName = (parameterName || '').trim();
368
+
369
+ if (normalizeNodeName(event.node) !== resolvedNodeName) {
370
+ return null;
371
+ }
372
+
373
+ const allParams = [
374
+ ...(event.new_parameters || []),
375
+ ...(event.changed_parameters || []),
376
+ ];
377
+
378
+ for (const param of allParams) {
379
+ if (param.name === resolvedParamName) {
380
+ return param;
381
+ }
382
+ }
383
+
384
+ return null;
385
+ }
386
+
387
+ /**
388
+ * Get all parameters from a ParameterEvent message (new + changed).
389
+ *
390
+ * @param {object} event - A ParameterEvent message
391
+ * @returns {object[]} Array of parameter messages
392
+ * @static
393
+ */
394
+ static getParametersFromEvent(event) {
395
+ return [
396
+ ...(event.new_parameters || []),
397
+ ...(event.changed_parameters || []),
398
+ ];
399
+ }
400
+
401
+ /**
402
+ * Handle incoming parameter event.
403
+ * @private
404
+ */
405
+ #handleEvent(event) {
406
+ const eventNodeName = normalizeNodeName(event.node);
407
+
408
+ // Dispatch parameter-specific callbacks by iterating event params
409
+ // and doing direct Map lookups (O(event_params) instead of O(registered_callbacks))
410
+ const allParams = [
411
+ ...(event.new_parameters || []),
412
+ ...(event.changed_parameters || []),
413
+ ];
414
+
415
+ for (const parameter of allParams) {
416
+ const key = this.#makeKey(parameter.name, eventNodeName);
417
+ const callbacks = this.#parameterCallbacks.get(key);
418
+
419
+ if (callbacks) {
420
+ for (const handle of callbacks.slice()) {
421
+ try {
422
+ handle.callback(parameter);
423
+ } catch (err) {
424
+ debug(
425
+ 'Error in parameter callback for %s on %s: %s',
426
+ parameter.name,
427
+ eventNodeName,
428
+ err.message
429
+ );
430
+ }
431
+ }
432
+ }
433
+ }
434
+
435
+ // Dispatch event-level callbacks
436
+ for (const handle of this.#eventCallbacks.slice()) {
437
+ try {
438
+ handle.callback(event);
439
+ } catch (err) {
440
+ debug('Error in parameter event callback: %s', err.message);
441
+ }
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Create a map key from parameter name and node name.
447
+ * @private
448
+ */
449
+ #makeKey(paramName, nodeName) {
450
+ return `${paramName}\0${nodeName}`;
451
+ }
452
+
453
+ /**
454
+ * Check if the handler has been destroyed and throw if so.
455
+ * @private
456
+ */
457
+ #checkNotDestroyed() {
458
+ if (this.#destroyed) {
459
+ throw new OperationError('ParameterEventHandler has been destroyed', {
460
+ entityType: 'parameter event handler',
461
+ });
462
+ }
463
+ }
464
+ }
465
+
466
+ module.exports = ParameterEventHandler;
467
+ module.exports.ParameterCallbackHandle = ParameterCallbackHandle;
468
+ module.exports.ParameterEventCallbackHandle = ParameterEventCallbackHandle;