rclnodejs 1.7.0 → 1.8.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 (65) hide show
  1. package/binding.gyp +2 -0
  2. package/index.js +93 -0
  3. package/lib/action/client.js +54 -1
  4. package/lib/client.js +66 -1
  5. package/lib/clock.js +178 -0
  6. package/lib/clock_change.js +49 -0
  7. package/lib/clock_event.js +88 -0
  8. package/lib/errors.js +50 -0
  9. package/lib/logging.js +78 -0
  10. package/lib/message_introspector.js +123 -0
  11. package/lib/message_validation.js +512 -0
  12. package/lib/node.js +133 -1
  13. package/lib/node_options.js +40 -1
  14. package/lib/observable_subscription.js +105 -0
  15. package/lib/publisher.js +56 -1
  16. package/lib/qos.js +57 -0
  17. package/lib/subscription.js +8 -0
  18. package/lib/timer.js +42 -0
  19. package/lib/validator.js +63 -7
  20. package/package.json +4 -2
  21. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  22. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  23. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  24. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  25. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  26. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  27. package/rosidl_gen/message_translator.js +0 -61
  28. package/scripts/config.js +1 -0
  29. package/src/addon.cpp +2 -0
  30. package/src/clock_event.cpp +268 -0
  31. package/src/clock_event.hpp +62 -0
  32. package/src/macros.h +2 -4
  33. package/src/rcl_action_server_bindings.cpp +21 -3
  34. package/src/rcl_bindings.cpp +59 -0
  35. package/src/rcl_context_bindings.cpp +5 -0
  36. package/src/rcl_graph_bindings.cpp +73 -0
  37. package/src/rcl_logging_bindings.cpp +158 -0
  38. package/src/rcl_node_bindings.cpp +14 -2
  39. package/src/rcl_publisher_bindings.cpp +12 -0
  40. package/src/rcl_service_bindings.cpp +7 -6
  41. package/src/rcl_subscription_bindings.cpp +51 -14
  42. package/src/rcl_time_point_bindings.cpp +135 -0
  43. package/src/rcl_timer_bindings.cpp +140 -0
  44. package/src/rcl_utilities.cpp +103 -2
  45. package/src/rcl_utilities.h +7 -1
  46. package/types/action_client.d.ts +27 -2
  47. package/types/base.d.ts +3 -0
  48. package/types/client.d.ts +29 -1
  49. package/types/clock.d.ts +86 -0
  50. package/types/clock_change.d.ts +27 -0
  51. package/types/clock_event.d.ts +51 -0
  52. package/types/errors.d.ts +49 -0
  53. package/types/index.d.ts +10 -0
  54. package/types/interfaces.d.ts +1 -1910
  55. package/types/logging.d.ts +32 -0
  56. package/types/message_introspector.d.ts +75 -0
  57. package/types/message_validation.d.ts +183 -0
  58. package/types/node.d.ts +67 -0
  59. package/types/node_options.d.ts +13 -0
  60. package/types/observable_subscription.d.ts +39 -0
  61. package/types/publisher.d.ts +28 -1
  62. package/types/qos.d.ts +18 -0
  63. package/types/subscription.d.ts +6 -0
  64. package/types/timer.d.ts +18 -0
  65. package/types/validator.d.ts +86 -0
@@ -0,0 +1,512 @@
1
+ // Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
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 { MessageValidationError, TypeValidationError } = require('./errors.js');
18
+ const interfaceLoader = require('./interface_loader.js');
19
+
20
+ /**
21
+ * Validation issue problem types
22
+ * @enum {string}
23
+ */
24
+ const ValidationProblem = {
25
+ /** Field exists in object but not in message schema */
26
+ UNKNOWN_FIELD: 'UNKNOWN_FIELD',
27
+ /** Field type doesn't match expected type */
28
+ TYPE_MISMATCH: 'TYPE_MISMATCH',
29
+ /** Required field is missing */
30
+ MISSING_FIELD: 'MISSING_FIELD',
31
+ /** Array length constraint violated */
32
+ ARRAY_LENGTH: 'ARRAY_LENGTH',
33
+ /** Value is out of valid range */
34
+ OUT_OF_RANGE: 'OUT_OF_RANGE',
35
+ /** Nested message validation failed */
36
+ NESTED_ERROR: 'NESTED_ERROR',
37
+ };
38
+
39
+ /**
40
+ * Map ROS primitive types to JavaScript types
41
+ */
42
+ const PRIMITIVE_TYPE_MAP = {
43
+ bool: 'boolean',
44
+ int8: 'number',
45
+ uint8: 'number',
46
+ int16: 'number',
47
+ uint16: 'number',
48
+ int32: 'number',
49
+ uint32: 'number',
50
+ int64: 'bigint',
51
+ uint64: 'bigint',
52
+ float32: 'number',
53
+ float64: 'number',
54
+ char: 'number',
55
+ byte: 'number',
56
+ string: 'string',
57
+ wstring: 'string',
58
+ };
59
+
60
+ /**
61
+ * Check if value is a TypedArray
62
+ * @param {any} value - Value to check
63
+ * @returns {boolean} True if TypedArray
64
+ */
65
+ function isTypedArray(value) {
66
+ return ArrayBuffer.isView(value) && !(value instanceof DataView);
67
+ }
68
+
69
+ /**
70
+ * Get the JavaScript type string for a value
71
+ * @param {any} value - Value to get type of
72
+ * @returns {string} Type description
73
+ */
74
+ function getValueType(value) {
75
+ if (value === null) return 'null';
76
+ if (value === undefined) return 'undefined';
77
+ if (Array.isArray(value)) return 'array';
78
+ if (isTypedArray(value)) return 'TypedArray';
79
+ return typeof value;
80
+ }
81
+
82
+ /**
83
+ * Resolve a type class from various input formats
84
+ * @param {string|object|function} typeClass - Type identifier
85
+ * @returns {function|null} The resolved type class or null
86
+ */
87
+ function resolveTypeClass(typeClass) {
88
+ if (typeof typeClass === 'function') {
89
+ return typeClass;
90
+ }
91
+
92
+ try {
93
+ return interfaceLoader.loadInterface(typeClass);
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Get message type string from type class
101
+ * @param {function} typeClass - Message type class
102
+ * @returns {string} Message type string (e.g., 'std_msgs/msg/String')
103
+ */
104
+ function getMessageTypeString(typeClass) {
105
+ if (typeof typeClass.type === 'function') {
106
+ const t = typeClass.type();
107
+ return `${t.pkgName}/${t.subFolder}/${t.interfaceName}`;
108
+ }
109
+ return 'unknown';
110
+ }
111
+
112
+ /**
113
+ * Get the schema definition for a message type
114
+ * @param {function|string|object} typeClass - Message type class or identifier
115
+ * @returns {object|null} Schema definition with fields and constants, or null if not found
116
+ * @example
117
+ * const schema = getMessageSchema(StringClass);
118
+ * // Returns: {
119
+ * // fields: [{name: 'data', type: {type: 'string', isPrimitiveType: true, ...}}],
120
+ * // constants: [],
121
+ * // messageType: 'std_msgs/msg/String'
122
+ * // }
123
+ */
124
+ function getMessageSchema(typeClass) {
125
+ const resolved = resolveTypeClass(typeClass);
126
+ if (!resolved || !resolved.ROSMessageDef) {
127
+ return null;
128
+ }
129
+
130
+ const def = resolved.ROSMessageDef;
131
+ return {
132
+ fields: def.fields || [],
133
+ constants: def.constants || [],
134
+ messageType: getMessageTypeString(resolved),
135
+ baseType: def.baseType,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Get field names for a message type
141
+ * @param {function|string|object} typeClass - Message type class or identifier
142
+ * @returns {string[]} Array of field names
143
+ */
144
+ function getFieldNames(typeClass) {
145
+ const schema = getMessageSchema(typeClass);
146
+ if (!schema) return [];
147
+ return schema.fields.map((f) => f.name);
148
+ }
149
+
150
+ /**
151
+ * Get type information for a specific field
152
+ * @param {function|string|object} typeClass - Message type class or identifier
153
+ * @param {string} fieldName - Name of the field
154
+ * @returns {object|null} Field type information or null if not found
155
+ */
156
+ function getFieldType(typeClass, fieldName) {
157
+ const schema = getMessageSchema(typeClass);
158
+ if (!schema) return null;
159
+
160
+ const field = schema.fields.find((f) => f.name === fieldName);
161
+ return field ? field.type : null;
162
+ }
163
+
164
+ /**
165
+ * Validate a primitive value against its expected type
166
+ * @param {any} value - Value to validate
167
+ * @param {object} fieldType - Field type definition
168
+ * @returns {object|null} Validation issue or null if valid
169
+ */
170
+ function validatePrimitiveValue(value, fieldType) {
171
+ const expectedJsType = PRIMITIVE_TYPE_MAP[fieldType.type];
172
+ const actualType = typeof value;
173
+
174
+ if (!expectedJsType) {
175
+ return null; // Unknown primitive type, skip validation
176
+ }
177
+
178
+ // Allow number for bigint fields (will be converted)
179
+ if (expectedJsType === 'bigint' && actualType === 'number') {
180
+ return null;
181
+ }
182
+
183
+ if (actualType !== expectedJsType) {
184
+ return {
185
+ problem: ValidationProblem.TYPE_MISMATCH,
186
+ expected: expectedJsType,
187
+ received: actualType,
188
+ };
189
+ }
190
+
191
+ return null;
192
+ }
193
+
194
+ /**
195
+ * Validate array constraints
196
+ * @param {any} value - Array value to validate
197
+ * @param {object} fieldType - Field type definition
198
+ * @returns {object|null} Validation issue or null if valid
199
+ */
200
+ function validateArrayConstraints(value, fieldType) {
201
+ if (!Array.isArray(value) && !isTypedArray(value)) {
202
+ return {
203
+ problem: ValidationProblem.TYPE_MISMATCH,
204
+ expected: 'array',
205
+ received: getValueType(value),
206
+ };
207
+ }
208
+
209
+ const length = value.length;
210
+
211
+ // Fixed size array
212
+ if (fieldType.isFixedSizeArray && length !== fieldType.arraySize) {
213
+ return {
214
+ problem: ValidationProblem.ARRAY_LENGTH,
215
+ expected: `exactly ${fieldType.arraySize} elements`,
216
+ received: `${length} elements`,
217
+ };
218
+ }
219
+
220
+ // Upper bound array
221
+ if (fieldType.isUpperBound && length > fieldType.arraySize) {
222
+ return {
223
+ problem: ValidationProblem.ARRAY_LENGTH,
224
+ expected: `at most ${fieldType.arraySize} elements`,
225
+ received: `${length} elements`,
226
+ };
227
+ }
228
+
229
+ return null;
230
+ }
231
+
232
+ /**
233
+ * Validate a message object against its schema
234
+ * @param {object} obj - Plain object to validate
235
+ * @param {function|string|object} typeClass - Message type class or identifier
236
+ * @param {object} [options] - Validation options
237
+ * @param {boolean} [options.strict=false] - If true, unknown fields cause validation failure
238
+ * @param {boolean} [options.checkTypes=true] - If true, validate field types
239
+ * @param {boolean} [options.checkRequired=false] - If true, check for missing fields
240
+ * @param {string} [options.path=''] - Current path for nested validation (internal use)
241
+ * @returns {{valid: boolean, issues: Array<object>}} Validation result
242
+ */
243
+ function validateMessage(obj, typeClass, options = {}) {
244
+ const {
245
+ strict = false,
246
+ checkTypes = true,
247
+ checkRequired = false,
248
+ path = '',
249
+ } = options;
250
+
251
+ const issues = [];
252
+ const resolved = resolveTypeClass(typeClass);
253
+
254
+ if (!resolved) {
255
+ issues.push({
256
+ field: path || '(root)',
257
+ problem: 'INVALID_TYPE_CLASS',
258
+ expected: 'valid message type class',
259
+ received: typeof typeClass,
260
+ });
261
+ return { valid: false, issues };
262
+ }
263
+
264
+ const schema = getMessageSchema(resolved);
265
+ if (!schema) {
266
+ issues.push({
267
+ field: path || '(root)',
268
+ problem: 'NO_SCHEMA',
269
+ expected: 'message with ROSMessageDef',
270
+ received: 'class without schema',
271
+ });
272
+ return { valid: false, issues };
273
+ }
274
+
275
+ if (obj === null || obj === undefined) {
276
+ issues.push({
277
+ field: path || '(root)',
278
+ problem: ValidationProblem.TYPE_MISMATCH,
279
+ expected: 'object',
280
+ received: String(obj),
281
+ });
282
+ return { valid: false, issues };
283
+ }
284
+
285
+ const type = typeof obj;
286
+ if (
287
+ type === 'string' ||
288
+ type === 'number' ||
289
+ type === 'boolean' ||
290
+ type === 'bigint'
291
+ ) {
292
+ if (schema.fields.length === 1 && schema.fields[0].name === 'data') {
293
+ const fieldType = schema.fields[0].type;
294
+ if (checkTypes && fieldType.isPrimitiveType) {
295
+ const typeIssue = validatePrimitiveValue(obj, fieldType);
296
+ if (typeIssue) {
297
+ issues.push({
298
+ field: path ? `${path}.data` : 'data',
299
+ ...typeIssue,
300
+ });
301
+ }
302
+ }
303
+ return { valid: issues.length === 0, issues };
304
+ }
305
+ }
306
+
307
+ if (type !== 'object') {
308
+ issues.push({
309
+ field: path || '(root)',
310
+ problem: ValidationProblem.TYPE_MISMATCH,
311
+ expected: 'object',
312
+ received: type,
313
+ });
314
+ return { valid: false, issues };
315
+ }
316
+
317
+ const fieldNames = new Set(schema.fields.map((f) => f.name));
318
+ const objKeys = Object.keys(obj);
319
+
320
+ if (strict) {
321
+ for (const key of objKeys) {
322
+ if (!fieldNames.has(key)) {
323
+ issues.push({
324
+ field: path ? `${path}.${key}` : key,
325
+ problem: ValidationProblem.UNKNOWN_FIELD,
326
+ });
327
+ }
328
+ }
329
+ }
330
+
331
+ for (const field of schema.fields) {
332
+ const fieldPath = path ? `${path}.${field.name}` : field.name;
333
+ const value = obj[field.name];
334
+ const fieldType = field.type;
335
+
336
+ if (field.name.startsWith('_')) continue;
337
+
338
+ if (value === undefined) {
339
+ if (checkRequired) {
340
+ issues.push({
341
+ field: fieldPath,
342
+ problem: ValidationProblem.MISSING_FIELD,
343
+ expected: fieldType.type,
344
+ });
345
+ }
346
+ continue;
347
+ }
348
+
349
+ if (fieldType.isArray) {
350
+ const arrayIssue = validateArrayConstraints(value, fieldType);
351
+ if (arrayIssue) {
352
+ issues.push({ field: fieldPath, ...arrayIssue });
353
+ continue;
354
+ }
355
+
356
+ if (checkTypes && Array.isArray(value) && value.length > 0) {
357
+ if (fieldType.isPrimitiveType) {
358
+ for (let i = 0; i < value.length; i++) {
359
+ const elemIssue = validatePrimitiveValue(value[i], fieldType);
360
+ if (elemIssue) {
361
+ issues.push({
362
+ field: `${fieldPath}[${i}]`,
363
+ ...elemIssue,
364
+ });
365
+ }
366
+ }
367
+ } else {
368
+ for (let i = 0; i < value.length; i++) {
369
+ const nestedResult = validateMessage(
370
+ value[i],
371
+ getNestedTypeClass(resolved, field.name),
372
+ {
373
+ strict,
374
+ checkTypes,
375
+ checkRequired,
376
+ path: `${fieldPath}[${i}]`,
377
+ }
378
+ );
379
+ if (!nestedResult.valid) {
380
+ issues.push(...nestedResult.issues);
381
+ }
382
+ }
383
+ }
384
+ }
385
+ } else if (fieldType.isPrimitiveType) {
386
+ if (checkTypes) {
387
+ const typeIssue = validatePrimitiveValue(value, fieldType);
388
+ if (typeIssue) {
389
+ issues.push({ field: fieldPath, ...typeIssue });
390
+ }
391
+ }
392
+ } else {
393
+ if (value !== null && typeof value === 'object') {
394
+ const nestedTypeClass = getNestedTypeClass(resolved, field.name);
395
+ if (nestedTypeClass) {
396
+ const nestedResult = validateMessage(value, nestedTypeClass, {
397
+ strict,
398
+ checkTypes,
399
+ checkRequired,
400
+ path: fieldPath,
401
+ });
402
+ if (!nestedResult.valid) {
403
+ issues.push(...nestedResult.issues);
404
+ }
405
+ }
406
+ } else if (checkTypes && value !== null) {
407
+ issues.push({
408
+ field: fieldPath,
409
+ problem: ValidationProblem.TYPE_MISMATCH,
410
+ expected: 'object',
411
+ received: getValueType(value),
412
+ });
413
+ }
414
+ }
415
+ }
416
+
417
+ return { valid: issues.length === 0, issues };
418
+ }
419
+
420
+ /**
421
+ * Get the type class for a nested field
422
+ * @param {function} parentTypeClass - Parent message type class
423
+ * @param {string} fieldName - Field name
424
+ * @returns {function|null} Nested type class or null
425
+ */
426
+ function getNestedTypeClass(parentTypeClass, fieldName) {
427
+ try {
428
+ const instance = new parentTypeClass();
429
+ const fieldValue = instance[fieldName];
430
+
431
+ if (
432
+ fieldValue &&
433
+ fieldValue.constructor &&
434
+ fieldValue.constructor.ROSMessageDef
435
+ ) {
436
+ return fieldValue.constructor;
437
+ }
438
+
439
+ if (
440
+ fieldValue &&
441
+ fieldValue.classType &&
442
+ fieldValue.classType.elementType
443
+ ) {
444
+ return fieldValue.classType.elementType;
445
+ }
446
+ } catch {
447
+ const schema = getMessageSchema(parentTypeClass);
448
+ if (schema) {
449
+ const field = schema.fields.find((f) => f.name === fieldName);
450
+ if (field && !field.type.isPrimitiveType) {
451
+ const typeName = `${field.type.pkgName}/msg/${field.type.type}`;
452
+ return resolveTypeClass(typeName);
453
+ }
454
+ }
455
+ }
456
+ return null;
457
+ }
458
+
459
+ /**
460
+ * Validate a message and throw if invalid
461
+ * @param {object} obj - Plain object to validate
462
+ * @param {function|string|object} typeClass - Message type class or identifier
463
+ * @param {object} [options] - Validation options (same as validateMessage)
464
+ * @throws {MessageValidationError} If validation fails
465
+ * @returns {void}
466
+ */
467
+ function assertValidMessage(obj, typeClass, options = {}) {
468
+ const result = validateMessage(obj, typeClass, options);
469
+
470
+ if (!result.valid) {
471
+ const resolved = resolveTypeClass(typeClass);
472
+ const messageType = resolved
473
+ ? getMessageTypeString(resolved)
474
+ : String(typeClass);
475
+ throw new MessageValidationError(messageType, result.issues);
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Create a validator function for a specific message type
481
+ * @param {function|string|object} typeClass - Message type class or identifier
482
+ * @param {object} [defaultOptions] - Default validation options
483
+ * @returns {function} Validator function that takes (obj, options?) and returns validation result
484
+ */
485
+ function createMessageValidator(typeClass, defaultOptions = {}) {
486
+ const resolved = resolveTypeClass(typeClass);
487
+ if (!resolved) {
488
+ throw new TypeValidationError(
489
+ 'typeClass',
490
+ typeClass,
491
+ 'valid message type class'
492
+ );
493
+ }
494
+
495
+ return function validator(obj, options = {}) {
496
+ return validateMessage(obj, resolved, {
497
+ ...defaultOptions,
498
+ ...options,
499
+ });
500
+ };
501
+ }
502
+
503
+ module.exports = {
504
+ ValidationProblem,
505
+ getMessageSchema,
506
+ getFieldNames,
507
+ getFieldType,
508
+ validateMessage,
509
+ assertValidMessage,
510
+ createMessageValidator,
511
+ getMessageTypeString,
512
+ };
package/lib/node.js CHANGED
@@ -45,6 +45,7 @@ const QoS = require('./qos.js');
45
45
  const Rates = require('./rate.js');
46
46
  const Service = require('./service.js');
47
47
  const Subscription = require('./subscription.js');
48
+ const ObservableSubscription = require('./observable_subscription.js');
48
49
  const TimeSource = require('./time_source.js');
49
50
  const Timer = require('./timer.js');
50
51
  const TypeDescriptionService = require('./type_description_service.js');
@@ -102,7 +103,8 @@ class Node extends rclnodejs.ShadowNode {
102
103
  namespace,
103
104
  context.handle,
104
105
  args,
105
- useGlobalArguments
106
+ useGlobalArguments,
107
+ options.rosoutQos
106
108
  );
107
109
  Object.defineProperty(this, 'handle', {
108
110
  configurable: false,
@@ -132,6 +134,11 @@ class Node extends rclnodejs.ShadowNode {
132
134
  this._setParametersCallbacks = [];
133
135
  this._logger = new Logging(rclnodejs.getNodeLoggerName(this.handle));
134
136
  this._spinning = false;
137
+ this._enableRosout = options.enableRosout;
138
+
139
+ if (this._enableRosout) {
140
+ rclnodejs.initRosoutPublisherForNode(this.handle);
141
+ }
135
142
 
136
143
  this._parameterEventPublisher = this.createPublisher(
137
144
  PARAMETER_EVENT_MSG_TYPE,
@@ -814,6 +821,42 @@ class Node extends rclnodejs.ShadowNode {
814
821
  return subscription;
815
822
  }
816
823
 
824
+ /**
825
+ * Create a Subscription that returns an RxJS Observable.
826
+ * This allows using reactive programming patterns with ROS 2 messages.
827
+ *
828
+ * @param {function|string|object} typeClass - The ROS message class,
829
+ * OR a string representing the message class, e.g. 'std_msgs/msg/String',
830
+ * OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
831
+ * @param {string} topic - The name of the topic.
832
+ * @param {object} [options] - The options argument used to parameterize the subscription.
833
+ * @param {boolean} [options.enableTypedArray=true] - The topic will use TypedArray if necessary.
834
+ * @param {QoS} [options.qos=QoS.profileDefault] - ROS Middleware "quality of service" settings.
835
+ * @param {boolean} [options.isRaw=false] - The topic is serialized when true.
836
+ * @param {string} [options.serializationMode='default'] - Controls message serialization format.
837
+ * @param {object} [options.contentFilter] - The content-filter (if supported by RMW).
838
+ * @param {SubscriptionEventCallbacks} [eventCallbacks] - The event callbacks for the subscription.
839
+ * @return {ObservableSubscription} - An ObservableSubscription with an RxJS Observable.
840
+ */
841
+ createObservableSubscription(typeClass, topic, options, eventCallbacks) {
842
+ let observableSubscription = null;
843
+
844
+ const subscription = this.createSubscription(
845
+ typeClass,
846
+ topic,
847
+ options,
848
+ (message) => {
849
+ if (observableSubscription) {
850
+ observableSubscription._emit(message);
851
+ }
852
+ },
853
+ eventCallbacks
854
+ );
855
+
856
+ observableSubscription = new ObservableSubscription(subscription);
857
+ return observableSubscription;
858
+ }
859
+
817
860
  /**
818
861
  * Create a Client.
819
862
  * @param {function|string|object} typeClass - The ROS message class,
@@ -1016,6 +1059,11 @@ class Node extends rclnodejs.ShadowNode {
1016
1059
 
1017
1060
  this.context.onNodeDestroyed(this);
1018
1061
 
1062
+ if (this._enableRosout) {
1063
+ rclnodejs.finiRosoutPublisherForNode(this.handle);
1064
+ this._enableRosout = false;
1065
+ }
1066
+
1019
1067
  this.handle.release();
1020
1068
  this._clock = null;
1021
1069
  this._timers = [];
@@ -1349,6 +1397,74 @@ class Node extends rclnodejs.ShadowNode {
1349
1397
  );
1350
1398
  }
1351
1399
 
1400
+ /**
1401
+ * Return a list of clients on a given service.
1402
+ *
1403
+ * The returned parameter is a list of ServiceEndpointInfo objects, where each will contain
1404
+ * the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
1405
+ *
1406
+ * When the `no_mangle` parameter is `true`, the provided `service` should be a valid
1407
+ * service name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
1408
+ * apps). When the `no_mangle` parameter is `false`, the provided `service` should
1409
+ * follow ROS service name conventions.
1410
+ *
1411
+ * `service` may be a relative, private, or fully qualified service name.
1412
+ * A relative or private service will be expanded using this node's namespace and name.
1413
+ * The queried `service` is not remapped.
1414
+ *
1415
+ * @param {string} service - The service on which to find the clients.
1416
+ * @param {boolean} [noDemangle=false] - If `true`, `service` needs to be a valid middleware service
1417
+ * name, otherwise it should be a valid ROS service name. Defaults to `false`.
1418
+ * @returns {Array} - list of clients
1419
+ */
1420
+ getClientsInfoByService(service, noDemangle = false) {
1421
+ if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
1422
+ console.warn(
1423
+ 'getClientsInfoByService is not supported by this version of ROS 2'
1424
+ );
1425
+ return null;
1426
+ }
1427
+ return rclnodejs.getClientsInfoByService(
1428
+ this.handle,
1429
+ this._getValidatedServiceName(service, noDemangle),
1430
+ noDemangle
1431
+ );
1432
+ }
1433
+
1434
+ /**
1435
+ * Return a list of servers on a given service.
1436
+ *
1437
+ * The returned parameter is a list of ServiceEndpointInfo objects, where each will contain
1438
+ * the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
1439
+ *
1440
+ * When the `no_mangle` parameter is `true`, the provided `service` should be a valid
1441
+ * service name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
1442
+ * apps). When the `no_mangle` parameter is `false`, the provided `service` should
1443
+ * follow ROS service name conventions.
1444
+ *
1445
+ * `service` may be a relative, private, or fully qualified service name.
1446
+ * A relative or private service will be expanded using this node's namespace and name.
1447
+ * The queried `service` is not remapped.
1448
+ *
1449
+ * @param {string} service - The service on which to find the servers.
1450
+ * @param {boolean} [noDemangle=false] - If `true`, `service` needs to be a valid middleware service
1451
+ * name, otherwise it should be a valid ROS service name. Defaults to `false`.
1452
+ * @returns {Array} - list of servers
1453
+ */
1454
+ getServersInfoByService(service, noDemangle = false) {
1455
+ if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
1456
+ console.warn(
1457
+ 'getServersInfoByService is not supported by this version of ROS 2'
1458
+ );
1459
+ return null;
1460
+ }
1461
+ return rclnodejs.getServersInfoByService(
1462
+ this.handle,
1463
+ this._getValidatedServiceName(service, noDemangle),
1464
+ noDemangle
1465
+ );
1466
+ }
1467
+
1352
1468
  /**
1353
1469
  * Get the list of nodes discovered by the provided node.
1354
1470
  * @return {Array<string>} - An array of the names.
@@ -2142,6 +2258,22 @@ class Node extends rclnodejs.ShadowNode {
2142
2258
  validateFullTopicName(fqTopicName);
2143
2259
  return rclnodejs.remapTopicName(this.handle, fqTopicName);
2144
2260
  }
2261
+
2262
+ _getValidatedServiceName(serviceName, noDemangle) {
2263
+ if (typeof serviceName !== 'string') {
2264
+ throw new TypeValidationError('serviceName', serviceName, 'string', {
2265
+ nodeName: this.name(),
2266
+ });
2267
+ }
2268
+
2269
+ if (noDemangle) {
2270
+ return serviceName;
2271
+ }
2272
+
2273
+ const resolvedServiceName = this.resolveServiceName(serviceName);
2274
+ rclnodejs.validateTopicName(resolvedServiceName);
2275
+ return resolvedServiceName;
2276
+ }
2145
2277
  }
2146
2278
 
2147
2279
  /**