• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RobotWebTools / rclnodejs / 15724382650

18 Jun 2025 05:06AM UTC coverage: 84.731% (+0.02%) from 84.711%
15724382650

push

github

minggangw
Fix flakiness of test-message-type.js (#1171)

This PR addresses the flakiness of test-message-type.js by ensuring that subscriptions are properly destroyed after each test execution.

- Added node.destroySubscription(subscription) to each test callback following publisher.kill('SIGINT') to ensure a clean test environment.  
- Improves test reliability by explicitly cleaning up subscriptions in all message type tests.

Fix: #1170

768 of 995 branches covered (77.19%)

Branch coverage included in aggregate %.

1890 of 2142 relevant lines covered (88.24%)

3221.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.01
/lib/node.js
1
// Copyright (c) 2017 Intel Corporation. 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 rclnodejs = require('bindings')('rclnodejs');
208✔
18

19
const ActionInterfaces = require('./action/interfaces.js');
208✔
20
const Client = require('./client.js');
208✔
21
const Clock = require('./clock.js');
208✔
22
const Context = require('./context.js');
208✔
23
const debug = require('debug')('rclnodejs:node');
208✔
24
const DistroUtils = require('./distro.js');
208✔
25
const GuardCondition = require('./guard_condition.js');
208✔
26
const loader = require('./interface_loader.js');
208✔
27
const Logging = require('./logging.js');
208✔
28
const NodeOptions = require('./node_options.js');
208✔
29
const {
30
  ParameterType,
31
  Parameter,
32
  ParameterDescriptor,
33
} = require('./parameter.js');
208✔
34
const ParameterService = require('./parameter_service.js');
208✔
35
const Publisher = require('./publisher.js');
208✔
36
const QoS = require('./qos.js');
208✔
37
const Rates = require('./rate.js');
208✔
38
const Service = require('./service.js');
208✔
39
const Subscription = require('./subscription.js');
208✔
40
const TimeSource = require('./time_source.js');
208✔
41
const Timer = require('./timer.js');
208✔
42
const TypeDescriptionService = require('./type_description_service.js');
208✔
43
const Entity = require('./entity.js');
208✔
44
const { SubscriptionEventCallbacks } = require('../lib/event_handler.js');
208✔
45
const { PublisherEventCallbacks } = require('../lib/event_handler.js');
208✔
46

47
// Parameter event publisher constants
48
const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent';
208✔
49
const PARAMETER_EVENT_TOPIC = 'parameter_events';
208✔
50

51
/**
52
 * @class - Class representing a Node in ROS
53
 */
54

55
class Node extends rclnodejs.ShadowNode {
56
  /**
57
   * Create a ROS2Node.
58
   * model using the {@link https://github.com/ros2/rcl/tree/master/rcl_lifecycle|ros2 client library (rcl) lifecyle api}.
59
   * @param {string} nodeName - The name used to register in ROS.
60
   * @param {string} [namespace=''] - The namespace used in ROS.
61
   * @param {Context} [context=Context.defaultContext()] - The context to create the node in.
62
   * @param {NodeOptions} [options=NodeOptions.defaultOptions] - The options to configure the new node behavior.
63
   * @throws {Error} If the given context is not registered.
64
   */
65
  constructor(
66
    nodeName,
67
    namespace = '',
234✔
68
    context = Context.defaultContext(),
514✔
69
    options = NodeOptions.defaultOptions,
530✔
70
    args = [],
682✔
71
    useGlobalArguments = true
682✔
72
  ) {
73
    super();
4,902✔
74

75
    if (typeof nodeName !== 'string' || typeof namespace !== 'string') {
4,902✔
76
      throw new TypeError('Invalid argument.');
176✔
77
    }
78

79
    this._init(nodeName, namespace, options, context, args, useGlobalArguments);
4,726✔
80
    debug(
4,654✔
81
      'Finish initializing node, name = %s and namespace = %s.',
82
      nodeName,
83
      namespace
84
    );
85
  }
86

87
  _init(name, namespace, options, context, args, useGlobalArguments) {
88
    this.handle = rclnodejs.createNode(
4,726✔
89
      name,
90
      namespace,
91
      context.handle,
92
      args,
93
      useGlobalArguments
94
    );
95
    Object.defineProperty(this, 'handle', {
4,654✔
96
      configurable: false,
97
      writable: false,
98
    }); // make read-only
99

100
    this._context = context;
4,654✔
101
    this.context.onNodeCreated(this);
4,654✔
102

103
    this._publishers = [];
4,654✔
104
    this._subscriptions = [];
4,654✔
105
    this._clients = [];
4,654✔
106
    this._services = [];
4,654✔
107
    this._timers = [];
4,654✔
108
    this._guards = [];
4,654✔
109
    this._events = [];
4,654✔
110
    this._actionClients = [];
4,654✔
111
    this._actionServers = [];
4,654✔
112
    this._rateTimerServer = null;
4,654✔
113
    this._parameterDescriptors = new Map();
4,654✔
114
    this._parameters = new Map();
4,654✔
115
    this._parameterService = null;
4,654✔
116
    this._typeDescriptionService = null;
4,654✔
117
    this._parameterEventPublisher = null;
4,654✔
118
    this._setParametersCallbacks = [];
4,654✔
119
    this._logger = new Logging(rclnodejs.getNodeLoggerName(this.handle));
4,654✔
120
    this._spinning = false;
4,654✔
121

122
    this._parameterEventPublisher = this.createPublisher(
4,654✔
123
      PARAMETER_EVENT_MSG_TYPE,
124
      PARAMETER_EVENT_TOPIC
125
    );
126

127
    // initialize _parameterOverrides from parameters defined on the commandline
128
    this._parameterOverrides = this._getNativeParameterOverrides();
4,654✔
129

130
    // override cli parameterOverrides with those specified in options
131
    if (options.parameterOverrides.length > 0) {
4,654✔
132
      for (const parameter of options.parameterOverrides) {
64✔
133
        if ((!parameter) instanceof Parameter) {
94!
134
          throw new TypeError(
×
135
            'Parameter-override must be an instance of Parameter.'
136
          );
137
        }
138
        this._parameterOverrides.set(parameter.name, parameter);
94✔
139
      }
140
    }
141

142
    // initialize _parameters from parameterOverrides
143
    if (options.automaticallyDeclareParametersFromOverrides) {
4,654✔
144
      for (const parameter of this._parameterOverrides.values()) {
40✔
145
        parameter.validate();
70✔
146
        const descriptor = ParameterDescriptor.fromParameter(parameter);
70✔
147
        this._parameters.set(parameter.name, parameter);
70✔
148
        this._parameterDescriptors.set(parameter.name, descriptor);
70✔
149
      }
150
    }
151

152
    // Clock that has support for ROS time.
153
    // Note: parameter overrides and parameter event publisher need to be ready at this point
154
    // to be able to declare 'use_sim_time' if it was not declared yet.
155
    this._clock = new Clock.ROSClock();
4,654✔
156
    this._timeSource = new TimeSource(this);
4,654✔
157
    this._timeSource.attachClock(this._clock);
4,654✔
158

159
    if (options.startParameterServices) {
4,654✔
160
      this._parameterService = new ParameterService(this);
4,606✔
161
      this._parameterService.start();
4,606✔
162
    }
163

164
    if (
4,654✔
165
      DistroUtils.getDistroId() >= DistroUtils.getDistroId('jazzy') &&
8,170✔
166
      options.startTypeDescriptionService
167
    ) {
168
      this._typeDescriptionService = new TypeDescriptionService(this);
3,516✔
169
      this._typeDescriptionService.start();
3,516✔
170
    }
171
  }
172

173
  execute(handles) {
174
    let timersReady = this._timers.filter((timer) =>
35,954✔
175
      handles.includes(timer.handle)
30,610✔
176
    );
177
    let guardsReady = this._guards.filter((guard) =>
35,954✔
178
      handles.includes(guard.handle)
24✔
179
    );
180
    let subscriptionsReady = this._subscriptions.filter((subscription) =>
35,954✔
181
      handles.includes(subscription.handle)
3,632✔
182
    );
183
    let clientsReady = this._clients.filter((client) =>
35,954✔
184
      handles.includes(client.handle)
752✔
185
    );
186
    let servicesReady = this._services.filter((service) =>
35,954✔
187
      handles.includes(service.handle)
101,062✔
188
    );
189
    let actionClientsReady = this._actionClients.filter((actionClient) =>
35,954✔
190
      handles.includes(actionClient.handle)
1,254✔
191
    );
192
    let actionServersReady = this._actionServers.filter((actionServer) =>
35,954✔
193
      handles.includes(actionServer.handle)
1,254✔
194
    );
195
    let eventsReady = this._events.filter((event) =>
35,954✔
196
      handles.includes(event.handle)
24✔
197
    );
198

199
    timersReady.forEach((timer) => {
35,954✔
200
      if (timer.isReady()) {
30,527✔
201
        rclnodejs.callTimer(timer.handle);
30,221✔
202
        timer.callback();
30,221✔
203
      }
204
    });
205

206
    eventsReady.forEach((event) => {
35,954✔
207
      event.takeData();
24✔
208
    });
209

210
    for (const subscription of subscriptionsReady) {
35,954✔
211
      if (subscription.isDestroyed()) continue;
3,348✔
212
      if (subscription.isRaw) {
3,268✔
213
        let rawMessage = rclnodejs.rclTakeRaw(subscription.handle);
8✔
214
        if (rawMessage) {
8!
215
          subscription.processResponse(rawMessage);
8✔
216
        }
217
        continue;
8✔
218
      }
219

220
      this._runWithMessageType(
3,260✔
221
        subscription.typeClass,
222
        (message, deserialize) => {
223
          let success = rclnodejs.rclTake(subscription.handle, message);
3,260✔
224
          if (success) {
3,260✔
225
            subscription.processResponse(deserialize());
2,818✔
226
          }
227
        }
228
      );
229
    }
230

231
    for (const guard of guardsReady) {
35,954✔
232
      if (guard.isDestroyed()) continue;
24!
233

234
      guard.callback();
24✔
235
    }
236

237
    for (const client of clientsReady) {
35,954✔
238
      if (client.isDestroyed()) continue;
382!
239
      this._runWithMessageType(
382✔
240
        client.typeClass.Response,
241
        (message, deserialize) => {
242
          let sequenceNumber = rclnodejs.rclTakeResponse(
382✔
243
            client.handle,
244
            message
245
          );
246
          if (sequenceNumber !== undefined) {
382✔
247
            client.processResponse(sequenceNumber, deserialize());
232✔
248
          }
249
        }
250
      );
251
    }
252

253
    for (const service of servicesReady) {
35,954✔
254
      if (service.isDestroyed()) continue;
490!
255
      this._runWithMessageType(
490✔
256
        service.typeClass.Request,
257
        (message, deserialize) => {
258
          let header = rclnodejs.rclTakeRequest(
490✔
259
            service.handle,
260
            this.handle,
261
            message
262
          );
263
          if (header) {
490✔
264
            service.processRequest(header, deserialize());
254✔
265
          }
266
        }
267
      );
268
    }
269

270
    for (const actionClient of actionClientsReady) {
35,954✔
271
      if (actionClient.isDestroyed()) continue;
594!
272

273
      const properties = actionClient.handle.properties;
594✔
274

275
      if (properties.isGoalResponseReady) {
594✔
276
        this._runWithMessageType(
283✔
277
          actionClient.typeClass.impl.SendGoalService.Response,
278
          (message, deserialize) => {
279
            let sequence = rclnodejs.actionTakeGoalResponse(
283✔
280
              actionClient.handle,
281
              message
282
            );
283
            if (sequence != undefined) {
283✔
284
              actionClient.processGoalResponse(sequence, deserialize());
240✔
285
            }
286
          }
287
        );
288
      }
289

290
      if (properties.isCancelResponseReady) {
594✔
291
        this._runWithMessageType(
53✔
292
          actionClient.typeClass.impl.CancelGoal.Response,
293
          (message, deserialize) => {
294
            let sequence = rclnodejs.actionTakeCancelResponse(
53✔
295
              actionClient.handle,
296
              message
297
            );
298
            if (sequence != undefined) {
53✔
299
              actionClient.processCancelResponse(sequence, deserialize());
32✔
300
            }
301
          }
302
        );
303
      }
304

305
      if (properties.isResultResponseReady) {
594✔
306
        this._runWithMessageType(
124✔
307
          actionClient.typeClass.impl.GetResultService.Response,
308
          (message, deserialize) => {
309
            let sequence = rclnodejs.actionTakeResultResponse(
124✔
310
              actionClient.handle,
311
              message
312
            );
313
            if (sequence != undefined) {
124✔
314
              actionClient.processResultResponse(sequence, deserialize());
120✔
315
            }
316
          }
317
        );
318
      }
319

320
      if (properties.isFeedbackReady) {
594✔
321
        this._runWithMessageType(
60✔
322
          actionClient.typeClass.impl.FeedbackMessage,
323
          (message, deserialize) => {
324
            let success = rclnodejs.actionTakeFeedback(
60✔
325
              actionClient.handle,
326
              message
327
            );
328
            if (success) {
60✔
329
              actionClient.processFeedbackMessage(deserialize());
40✔
330
            }
331
          }
332
        );
333
      }
334

335
      if (properties.isStatusReady) {
594✔
336
        this._runWithMessageType(
313✔
337
          actionClient.typeClass.impl.GoalStatusArray,
338
          (message, deserialize) => {
339
            let success = rclnodejs.actionTakeStatus(
313✔
340
              actionClient.handle,
341
              message
342
            );
343
            if (success) {
313✔
344
              actionClient.processStatusMessage(deserialize());
268✔
345
            }
346
          }
347
        );
348
      }
349
    }
350

351
    for (const actionServer of actionServersReady) {
35,954✔
352
      if (actionServer.isDestroyed()) continue;
736!
353

354
      const properties = actionServer.handle.properties;
736✔
355

356
      if (properties.isGoalRequestReady) {
736✔
357
        this._runWithMessageType(
267✔
358
          actionServer.typeClass.impl.SendGoalService.Request,
359
          (message, deserialize) => {
360
            const result = rclnodejs.actionTakeGoalRequest(
267✔
361
              actionServer.handle,
362
              message
363
            );
364
            if (result) {
267✔
365
              actionServer.processGoalRequest(result, deserialize());
240✔
366
            }
367
          }
368
        );
369
      }
370

371
      if (properties.isCancelRequestReady) {
736✔
372
        this._runWithMessageType(
56✔
373
          actionServer.typeClass.impl.CancelGoal.Request,
374
          (message, deserialize) => {
375
            const result = rclnodejs.actionTakeCancelRequest(
56✔
376
              actionServer.handle,
377
              message
378
            );
379
            if (result) {
56✔
380
              actionServer.processCancelRequest(result, deserialize());
32✔
381
            }
382
          }
383
        );
384
      }
385

386
      if (properties.isResultRequestReady) {
736✔
387
        this._runWithMessageType(
178✔
388
          actionServer.typeClass.impl.GetResultService.Request,
389
          (message, deserialize) => {
390
            const result = rclnodejs.actionTakeResultRequest(
178✔
391
              actionServer.handle,
392
              message
393
            );
394
            if (result) {
178✔
395
              actionServer.processResultRequest(result, deserialize());
120✔
396
            }
397
          }
398
        );
399
      }
400

401
      if (properties.isGoalExpired) {
736✔
402
        let GoalInfoArray = ActionInterfaces.GoalInfo.ArrayType;
32✔
403
        let message = new GoalInfoArray(actionServer._goalHandles.size);
32✔
404
        let count = rclnodejs.actionExpireGoals(
32✔
405
          actionServer.handle,
406
          actionServer._goalHandles.size,
407
          message._refArray.buffer
408
        );
409
        if (count > 0) {
32✔
410
          actionServer.processGoalExpired(message, count);
24✔
411
        }
412
        GoalInfoArray.freeArray(message);
32✔
413
      }
414
    }
415

416
    // At this point it is safe to clear the cache of any
417
    // destroyed entity references
418
    Entity._gcHandles();
35,954✔
419
  }
420

421
  /**
422
   * Determine if this node is spinning.
423
   * @returns {boolean} - true when spinning; otherwise returns false.
424
   */
425
  get spinning() {
426
    return this._spinning;
32,344✔
427
  }
428

429
  /**
430
   * Trigger the event loop to continuously check for and route.
431
   * incoming events.
432
   * @param {Node} node - The node to be spun up.
433
   * @param {number} [timeout=10] - Timeout to wait in milliseconds. Block forever if negative. Don't wait if 0.
434
   * @throws {Error} If the node is already spinning.
435
   * @return {undefined}
436
   */
437
  spin(timeout = 10) {
106✔
438
    if (this.spinning) {
3,426!
439
      throw new Error('The node is already spinning.');
×
440
    }
441
    this.start(this.context.handle, timeout);
3,426✔
442
    this._spinning = true;
3,426✔
443
  }
444

445
  /**
446
   * Use spin().
447
   * @deprecated, since 0.18.0
448
   */
449
  startSpinning(timeout) {
450
    this.spin(timeout);
×
451
  }
452

453
  /**
454
   * Terminate spinning - no further events will be received.
455
   * @returns {undefined}
456
   */
457
  stop() {
458
    super.stop();
3,426✔
459
    this._spinning = false;
3,426✔
460
  }
461

462
  /**
463
   * Terminate spinning - no further events will be received.
464
   * @returns {undefined}
465
   * @deprecated since 0.18.0, Use stop().
466
   */
467
  stopSpinning() {
468
    super.stop();
×
469
    this._spinning = false;
×
470
  }
471

472
  /**
473
   * Spin the node and trigger the event loop to check for one incoming event. Thereafter the node
474
   * will not received additional events until running additional calls to spin() or spinOnce().
475
   * @param {Node} node - The node to be spun.
476
   * @param {number} [timeout=10] - Timeout to wait in milliseconds. Block forever if negative. Don't wait if 0.
477
   * @throws {Error} If the node is already spinning.
478
   * @return {undefined}
479
   */
480
  spinOnce(timeout = 10) {
16✔
481
    if (this.spinning) {
24,072✔
482
      throw new Error('The node is already spinning.');
16✔
483
    }
484
    super.spinOnce(this.context.handle, timeout);
24,056✔
485
  }
486

487
  _removeEntityFromArray(entity, array) {
488
    let index = array.indexOf(entity);
1,056✔
489
    if (index > -1) {
1,056✔
490
      array.splice(index, 1);
1,040✔
491
    }
492
  }
493

494
  _destroyEntity(entity, array, syncHandles = true) {
1,066✔
495
    if (entity['isDestroyed'] && entity.isDestroyed()) return;
1,120✔
496

497
    this._removeEntityFromArray(entity, array);
1,056✔
498
    if (syncHandles) {
1,056✔
499
      this.syncHandles();
1,018✔
500
    }
501

502
    if (entity['_destroy']) {
1,056✔
503
      entity._destroy();
1,008✔
504
    } else {
505
      // guards and timers
506
      entity.handle.release();
48✔
507
    }
508
  }
509

510
  _validateOptions(options) {
511
    if (
43,266✔
512
      options !== undefined &&
43,650✔
513
      (options === null || typeof options !== 'object')
514
    ) {
515
      throw new TypeError('Invalid argument of options');
160✔
516
    }
517

518
    if (options === undefined) {
43,106✔
519
      return Node.getDefaultOptions();
42,994✔
520
    }
521

522
    if (options.enableTypedArray === undefined) {
112✔
523
      options = Object.assign(options, { enableTypedArray: true });
16✔
524
    }
525

526
    if (options.qos === undefined) {
112✔
527
      options = Object.assign(options, { qos: QoS.profileDefault });
32✔
528
    }
529

530
    if (options.isRaw === undefined) {
112✔
531
      options = Object.assign(options, { isRaw: false });
32✔
532
    }
533

534
    return options;
112✔
535
  }
536

537
  /**
538
   * Create a Timer.
539
   * @param {bigint} period - The number representing period in nanoseconds.
540
   * @param {function} callback - The callback to be called when timeout.
541
   * @param {Clock} [clock] - The clock which the timer gets time from.
542
   * @return {Timer} - An instance of Timer.
543
   */
544
  createTimer(period, callback, clock = null) {
462✔
545
    if (arguments.length === 3 && !(arguments[2] instanceof Clock)) {
462!
546
      clock = null;
×
547
    } else if (arguments.length === 4) {
462!
548
      clock = arguments[3];
×
549
    }
550

551
    if (typeof period !== 'bigint' || typeof callback !== 'function') {
462✔
552
      throw new TypeError('Invalid argument');
16✔
553
    }
554

555
    const timerClock = clock || this._clock;
446✔
556
    let timerHandle = rclnodejs.createTimer(
446✔
557
      timerClock.handle,
558
      this.context.handle,
559
      period
560
    );
561
    let timer = new Timer(timerHandle, period, callback);
446✔
562
    debug('Finish creating timer, period = %d.', period);
446✔
563
    this._timers.push(timer);
446✔
564
    this.syncHandles();
446✔
565

566
    return timer;
446✔
567
  }
568

569
  /**
570
   * Create a Rate.
571
   *
572
   * @param {number} hz - The frequency of the rate timer; default is 1 hz.
573
   * @returns {Promise<Rate>} - Promise resolving to new instance of Rate.
574
   */
575
  async createRate(hz = 1) {
32✔
576
    if (typeof hz !== 'number') {
72!
577
      throw new TypeError('Invalid argument');
×
578
    }
579

580
    const MAX_RATE_HZ_IN_MILLISECOND = 1000.0;
72✔
581
    if (hz <= 0.0 || hz > MAX_RATE_HZ_IN_MILLISECOND) {
72✔
582
      throw new RangeError(
16✔
583
        `Hz must be between 0.0 and ${MAX_RATE_HZ_IN_MILLISECOND}`
584
      );
585
    }
586

587
    // lazy initialize rateTimerServer
588
    if (!this._rateTimerServer) {
56✔
589
      this._rateTimerServer = new Rates.RateTimerServer(this);
40✔
590
      await this._rateTimerServer.init();
40✔
591
    }
592

593
    const period = Math.round(1000 / hz);
56✔
594
    const timer = this._rateTimerServer.createTimer(BigInt(period) * 1000000n);
56✔
595
    const rate = new Rates.Rate(hz, timer);
56✔
596

597
    return rate;
56✔
598
  }
599

600
  /**
601
   * Create a Publisher.
602
   * @param {function|string|object} typeClass - The ROS message class,
603
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
604
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
605
   * @param {string} topic - The name of the topic.
606
   * @param {object} options - The options argument used to parameterize the publisher.
607
   * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
608
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the publisher, default: QoS.profileDefault.
609
   * @param {PublisherEventCallbacks} eventCallbacks - The event callbacks for the publisher.
610
   * @return {Publisher} - An instance of Publisher.
611
   */
612
  createPublisher(typeClass, topic, options, eventCallbacks) {
613
    return this._createPublisher(
7,526✔
614
      typeClass,
615
      topic,
616
      options,
617
      Publisher,
618
      eventCallbacks
619
    );
620
  }
621

622
  _createPublisher(typeClass, topic, options, publisherClass, eventCallbacks) {
623
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
7,550✔
624
      typeClass = loader.loadInterface(typeClass);
7,358✔
625
    }
626
    options = this._validateOptions(options);
7,494✔
627

628
    if (
7,494✔
629
      typeof typeClass !== 'function' ||
22,270✔
630
      typeof topic !== 'string' ||
631
      (eventCallbacks && !(eventCallbacks instanceof PublisherEventCallbacks))
632
    ) {
633
      throw new TypeError('Invalid argument');
160✔
634
    }
635

636
    let publisher = publisherClass.createPublisher(
7,334✔
637
      this,
638
      typeClass,
639
      topic,
640
      options,
641
      eventCallbacks
642
    );
643
    debug('Finish creating publisher, topic = %s.', topic);
7,254✔
644
    this._publishers.push(publisher);
7,254✔
645
    return publisher;
7,254✔
646
  }
647

648
  /**
649
   * This callback is called when a message is published
650
   * @callback SubscriptionCallback
651
   * @param {Object} message - The message published
652
   * @see [Node.createSubscription]{@link Node#createSubscription}
653
   * @see [Node.createPublisher]{@link Node#createPublisher}
654
   * @see {@link Publisher}
655
   * @see {@link Subscription}
656
   */
657

658
  /**
659
   * Create a Subscription with optional content-filtering.
660
   * @param {function|string|object} typeClass - The ROS message class,
661
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
662
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
663
   * @param {string} topic - The name of the topic.
664
   * @param {object} options - The options argument used to parameterize the subscription.
665
   * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
666
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the subscription, default: QoS.profileDefault.
667
   * @param {boolean} options.isRaw - The topic is serialized when true, default: false.
668
   * @param {object} [options.contentFilter=undefined] - The content-filter, default: undefined.
669
   *  Confirm that your RMW supports content-filtered topics before use. 
670
   * @param {string} options.contentFilter.expression - Specifies the criteria to select the data samples of
671
   *  interest. It is similar to the WHERE part of an SQL clause.
672
   * @param {string[]} [options.contentFilter.parameters=undefined] - Array of strings that give values to
673
   *  the ‘parameters’ (i.e., "%n" tokens) in the filter_expression. The number of supplied parameters must
674
   *  fit with the requested values in the filter_expression (i.e., the number of %n tokens). default: undefined.
675
   * @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.
676
   * @param {SubscriptionEventCallbacks} eventCallbacks - The event callbacks for the subscription.
677
   * @return {Subscription} - An instance of Subscription.
678
   * @throws {ERROR} - May throw an RMW error if content-filter is malformed. 
679
   * @see {@link SubscriptionCallback}
680
   * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|Content-filter details at DDS 1.4 specification, Annex B}
681
   */
682
  createSubscription(typeClass, topic, options, callback, eventCallbacks) {
683
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
3,016✔
684
      typeClass = loader.loadInterface(typeClass);
2,944✔
685
    }
686

687
    if (typeof options === 'function') {
2,960✔
688
      callback = options;
2,682✔
689
      options = undefined;
2,682✔
690
    }
691
    options = this._validateOptions(options);
2,960✔
692

693
    if (
2,880✔
694
      typeof typeClass !== 'function' ||
11,352✔
695
      typeof topic !== 'string' ||
696
      typeof callback !== 'function' ||
697
      (eventCallbacks &&
698
        !(eventCallbacks instanceof SubscriptionEventCallbacks))
699
    ) {
700
      throw new TypeError('Invalid argument');
80✔
701
    }
702

703
    let subscription = Subscription.createSubscription(
2,800✔
704
      this,
705
      typeClass,
706
      topic,
707
      options,
708
      callback,
709
      eventCallbacks
710
    );
711
    debug('Finish creating subscription, topic = %s.', topic);
2,712✔
712
    this._subscriptions.push(subscription);
2,712✔
713
    this.syncHandles();
2,712✔
714

715
    return subscription;
2,712✔
716
  }
717

718
  /**
719
   * Create a Client.
720
   * @param {function|string|object} typeClass - The ROS message class,
721
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
722
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
723
   * @param {string} serviceName - The service name to request.
724
   * @param {object} options - The options argument used to parameterize the client.
725
   * @param {boolean} options.enableTypedArray - The response will use TypedArray if necessary, default: true.
726
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the client, default: QoS.profileDefault.
727
   * @return {Client} - An instance of Client.
728
   */
729
  createClient(typeClass, serviceName, options) {
730
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
652✔
731
      typeClass = loader.loadInterface(typeClass);
572✔
732
    }
733
    options = this._validateOptions(options);
596✔
734

735
    if (typeof typeClass !== 'function' || typeof serviceName !== 'string') {
596✔
736
      throw new TypeError('Invalid argument');
160✔
737
    }
738

739
    let client = Client.createClient(
436✔
740
      this.handle,
741
      serviceName,
742
      typeClass,
743
      options
744
    );
745
    debug('Finish creating client, service = %s.', serviceName);
356✔
746
    this._clients.push(client);
356✔
747
    this.syncHandles();
356✔
748

749
    return client;
356✔
750
  }
751

752
  /**
753
   * This callback is called when a request is sent to service
754
   * @callback RequestCallback
755
   * @param {Object} request - The request sent to the service
756
   * @param {Response} response - The response to client.
757
        Use [response.send()]{@link Response#send} to send response object to client
758
   * @return {undefined}
759
   * @see [Node.createService]{@link Node#createService}
760
   * @see [Client.sendRequest]{@link Client#sendRequest}
761
   * @see {@link Client}
762
   * @see {@link Service}
763
   * @see {@link Response#send}
764
   */
765

766
  /**
767
   * Create a Service.
768
   * @param {function|string|object} typeClass - The ROS message class,
769
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
770
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
771
   * @param {string} serviceName - The service name to offer.
772
   * @param {object} options - The options argument used to parameterize the service.
773
   * @param {boolean} options.enableTypedArray - The request will use TypedArray if necessary, default: true.
774
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the service, default: QoS.profileDefault.
775
   * @param {RequestCallback} callback - The callback to be called when receiving request.
776
   * @return {Service} - An instance of Service.
777
   * @see {@link RequestCallback}
778
   */
779
  createService(typeClass, serviceName, options, callback) {
780
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
28,180✔
781
      typeClass = loader.loadInterface(typeClass);
28,100✔
782
    }
783

784
    if (typeof options === 'function') {
28,124✔
785
      callback = options;
27,948✔
786
      options = undefined;
27,948✔
787
    }
788
    options = this._validateOptions(options);
28,124✔
789

790
    if (
28,044✔
791
      typeof typeClass !== 'function' ||
84,020✔
792
      typeof serviceName !== 'string' ||
793
      typeof callback !== 'function'
794
    ) {
795
      throw new TypeError('Invalid argument');
80✔
796
    }
797

798
    let service = Service.createService(
27,964✔
799
      this.handle,
800
      serviceName,
801
      typeClass,
802
      options,
803
      callback
804
    );
805
    debug('Finish creating service, service = %s.', serviceName);
27,884✔
806
    this._services.push(service);
27,884✔
807
    this.syncHandles();
27,884✔
808

809
    return service;
27,884✔
810
  }
811

812
  /**
813
   * Create a guard condition.
814
   * @param {Function} callback - The callback to be called when the guard condition is triggered.
815
   * @return {GuardCondition} - An instance of GuardCondition.
816
   */
817
  createGuardCondition(callback) {
818
    if (typeof callback !== 'function') {
24!
819
      throw new TypeError('Invalid argument');
×
820
    }
821

822
    let guard = GuardCondition.createGuardCondition(callback, this.context);
24✔
823
    debug('Finish creating guard condition');
24✔
824
    this._guards.push(guard);
24✔
825
    this.syncHandles();
24✔
826

827
    return guard;
24✔
828
  }
829

830
  /**
831
   * Destroy all resource allocated by this node, including
832
   * <code>Timer</code>s/<code>Publisher</code>s/<code>Subscription</code>s
833
   * /<code>Client</code>s/<code>Service</code>s
834
   * @return {undefined}
835
   */
836
  destroy() {
837
    if (this.spinning) {
4,830✔
838
      this.stop();
3,406✔
839
    }
840

841
    // Action servers/clients require manual destruction due to circular reference with goal handles.
842
    this._actionClients.forEach((actionClient) => actionClient.destroy());
4,830✔
843
    this._actionServers.forEach((actionServer) => actionServer.destroy());
4,830✔
844

845
    this.context.onNodeDestroyed(this);
4,830✔
846

847
    this.handle.release();
4,830✔
848
    this._clock = null;
4,830✔
849
    this._timers = [];
4,830✔
850
    this._publishers = [];
4,830✔
851
    this._subscriptions = [];
4,830✔
852
    this._clients = [];
4,830✔
853
    this._services = [];
4,830✔
854
    this._guards = [];
4,830✔
855
    this._actionClients = [];
4,830✔
856
    this._actionServers = [];
4,830✔
857

858
    if (this._rateTimerServer) {
4,830✔
859
      this._rateTimerServer.shutdown();
40✔
860
      this._rateTimerServer = null;
40✔
861
    }
862
  }
863

864
  /**
865
   * Destroy a Publisher.
866
   * @param {Publisher} publisher - The Publisher to be destroyed.
867
   * @return {undefined}
868
   */
869
  destroyPublisher(publisher) {
870
    if (!(publisher instanceof Publisher)) {
70✔
871
      throw new TypeError('Invalid argument');
16✔
872
    }
873
    if (publisher.events) {
54!
874
      publisher.events.forEach((event) => {
×
875
        this._destroyEntity(event, this._events);
×
876
      });
877
      publisher.events = [];
×
878
    }
879
    this._destroyEntity(publisher, this._publishers, false);
54✔
880
  }
881

882
  /**
883
   * Destroy a Subscription.
884
   * @param {Subscription} subscription - The Subscription to be destroyed.
885
   * @return {undefined}
886
   */
887
  destroySubscription(subscription) {
888
    if (!(subscription instanceof Subscription)) {
308✔
889
      throw new TypeError('Invalid argument');
16✔
890
    }
891
    if (subscription.events) {
292✔
892
      subscription.events.forEach((event) => {
6✔
893
        this._destroyEntity(event, this._events);
6✔
894
      });
895
      subscription.events = [];
6✔
896
    }
897

898
    this._destroyEntity(subscription, this._subscriptions);
292✔
899
  }
900

901
  /**
902
   * Destroy a Client.
903
   * @param {Client} client - The Client to be destroyed.
904
   * @return {undefined}
905
   */
906
  destroyClient(client) {
907
    if (!(client instanceof Client)) {
64✔
908
      throw new TypeError('Invalid argument');
16✔
909
    }
910
    this._destroyEntity(client, this._clients);
48✔
911
  }
912

913
  /**
914
   * Destroy a Service.
915
   * @param {Service} service - The Service to be destroyed.
916
   * @return {undefined}
917
   */
918
  destroyService(service) {
919
    if (!(service instanceof Service)) {
64✔
920
      throw new TypeError('Invalid argument');
16✔
921
    }
922
    this._destroyEntity(service, this._services);
48✔
923
  }
924

925
  /**
926
   * Destroy a Timer.
927
   * @param {Timer} timer - The Timer to be destroyed.
928
   * @return {undefined}
929
   */
930
  destroyTimer(timer) {
931
    if (!(timer instanceof Timer)) {
64✔
932
      throw new TypeError('Invalid argument');
16✔
933
    }
934
    this._destroyEntity(timer, this._timers);
48✔
935
  }
936

937
  /**
938
   * Destroy a guard condition.
939
   * @param {GuardCondition} guard - The guard condition to be destroyed.
940
   * @return {undefined}
941
   */
942
  destroyGuardCondition(guard) {
943
    if (!(guard instanceof GuardCondition)) {
24!
944
      throw new TypeError('Invalid argument');
×
945
    }
946
    this._destroyEntity(guard, this._guards);
24✔
947
  }
948

949
  /**
950
   * Get the name of the node.
951
   * @return {string}
952
   */
953
  name() {
954
    return rclnodejs.getNodeName(this.handle);
21,436✔
955
  }
956

957
  /**
958
   * Get the namespace of the node.
959
   * @return {string}
960
   */
961
  namespace() {
962
    return rclnodejs.getNamespace(this.handle);
17,144✔
963
  }
964

965
  /**
966
   * Get the context in which this node was created.
967
   * @return {Context}
968
   */
969
  get context() {
970
    return this._context;
42,090✔
971
  }
972

973
  /**
974
   * Get the nodes logger.
975
   * @returns {Logger} - The logger for the node.
976
   */
977
  getLogger() {
978
    return this._logger;
1,128✔
979
  }
980

981
  /**
982
   * Get the clock used by the node.
983
   * @returns {Clock} - The nodes clock.
984
   */
985
  getClock() {
986
    return this._clock;
656✔
987
  }
988

989
  /**
990
   * Get the current time using the node's clock.
991
   * @returns {Time} - The current time.
992
   */
993
  now() {
994
    return this.getClock().now();
16✔
995
  }
996

997
  /**
998
   * Get the list of published topics discovered by the provided node for the remote node name.
999
   * @param {string} nodeName - The name of the node.
1000
   * @param {string} namespace - The name of the namespace.
1001
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
1002
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1003
   */
1004
  getPublisherNamesAndTypesByNode(nodeName, namespace, noDemangle = false) {
×
1005
    return rclnodejs.getPublisherNamesAndTypesByNode(
×
1006
      this.handle,
1007
      nodeName,
1008
      namespace,
1009
      noDemangle
1010
    );
1011
  }
1012

1013
  /**
1014
   * Get the list of published topics discovered by the provided node for the remote node name.
1015
   * @param {string} nodeName - The name of the node.
1016
   * @param {string} namespace - The name of the namespace.
1017
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
1018
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1019
   */
1020
  getSubscriptionNamesAndTypesByNode(nodeName, namespace, noDemangle = false) {
×
1021
    return rclnodejs.getSubscriptionNamesAndTypesByNode(
×
1022
      this.handle,
1023
      nodeName,
1024
      namespace,
1025
      noDemangle
1026
    );
1027
  }
1028

1029
  /**
1030
   * Get service names and types for which a remote node has servers.
1031
   * @param {string} nodeName - The name of the node.
1032
   * @param {string} namespace - The name of the namespace.
1033
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1034
   */
1035
  getServiceNamesAndTypesByNode(nodeName, namespace) {
1036
    return rclnodejs.getServiceNamesAndTypesByNode(
×
1037
      this.handle,
1038
      nodeName,
1039
      namespace
1040
    );
1041
  }
1042

1043
  /**
1044
   * Get service names and types for which a remote node has clients.
1045
   * @param {string} nodeName - The name of the node.
1046
   * @param {string} namespace - The name of the namespace.
1047
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1048
   */
1049
  getClientNamesAndTypesByNode(nodeName, namespace) {
1050
    return rclnodejs.getClientNamesAndTypesByNode(
×
1051
      this.handle,
1052
      nodeName,
1053
      namespace
1054
    );
1055
  }
1056

1057
  /**
1058
   * Get the list of topics discovered by the provided node.
1059
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
1060
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1061
   */
1062
  getTopicNamesAndTypes(noDemangle = false) {
×
1063
    return rclnodejs.getTopicNamesAndTypes(this.handle, noDemangle);
×
1064
  }
1065

1066
  /**
1067
   * Get the list of services discovered by the provided node.
1068
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1069
   */
1070
  getServiceNamesAndTypes() {
1071
    return rclnodejs.getServiceNamesAndTypes(this.handle);
16✔
1072
  }
1073

1074
  /**
1075
   * Get a list of publishers on a given topic.
1076
   * @param {string} topic - the topic name to get the publishers for.
1077
   * @param {boolean} noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name,
1078
   *       otherwise it should be a valid ROS topic name.
1079
   * @returns {Array} - list of publishers
1080
   */
1081
  getPublishersInfoByTopic(topic, noDemangle) {
1082
    return rclnodejs.getPublishersInfoByTopic(this.handle, topic, noDemangle);
14✔
1083
  }
1084

1085
  /**
1086
   * Get a list of subscriptions on a given topic.
1087
   * @param {string} topic - the topic name to get the subscriptions for.
1088
   * @param {boolean} noDemangle - if `true`, `topic_name` needs to be a valid middleware topic name,
1089
   *       otherwise it should be a valid ROS topic name.
1090
   * @returns {Array} - list of subscriptions
1091
   */
1092
  getSubscriptionsInfoByTopic(topic, noDemangle) {
1093
    return rclnodejs.getSubscriptionsInfoByTopic(
8✔
1094
      this.handle,
1095
      topic,
1096
      noDemangle
1097
    );
1098
  }
1099

1100
  /**
1101
   * Get the list of nodes discovered by the provided node.
1102
   * @return {Array<string>} - An array of the names.
1103
   */
1104
  getNodeNames() {
1105
    return this.getNodeNamesAndNamespaces().map((item) => item.name);
320✔
1106
  }
1107

1108
  /**
1109
   * Get the list of nodes and their namespaces discovered by the provided node.
1110
   * @return {Array<{name: string, namespace: string}>} An array of the names and namespaces.
1111
   */
1112
  getNodeNamesAndNamespaces() {
1113
    return rclnodejs.getNodeNames(this.handle, /*getEnclaves=*/ false);
136✔
1114
  }
1115

1116
  /**
1117
   * Get the list of nodes and their namespaces with enclaves discovered by the provided node.
1118
   * @return {Array<{name: string, namespace: string, enclave: string}>} An array of the names, namespaces and enclaves.
1119
   */
1120
  getNodeNamesAndNamespacesWithEnclaves() {
1121
    return rclnodejs.getNodeNames(this.handle, /*getEnclaves=*/ true);
8✔
1122
  }
1123

1124
  /**
1125
   * Return the number of publishers on a given topic.
1126
   * @param {string} topic - The name of the topic.
1127
   * @returns {number} - Number of publishers on the given topic.
1128
   */
1129
  countPublishers(topic) {
1130
    let expandedTopic = rclnodejs.expandTopicName(
48✔
1131
      topic,
1132
      this.name(),
1133
      this.namespace()
1134
    );
1135
    rclnodejs.validateTopicName(expandedTopic);
48✔
1136

1137
    return rclnodejs.countPublishers(this.handle, expandedTopic);
48✔
1138
  }
1139

1140
  /**
1141
   * Return the number of subscribers on a given topic.
1142
   * @param {string} topic - The name of the topic.
1143
   * @returns {number} - Number of subscribers on the given topic.
1144
   */
1145
  countSubscribers(topic) {
1146
    let expandedTopic = rclnodejs.expandTopicName(
48✔
1147
      topic,
1148
      this.name(),
1149
      this.namespace()
1150
    );
1151
    rclnodejs.validateTopicName(expandedTopic);
48✔
1152

1153
    return rclnodejs.countSubscribers(this.handle, expandedTopic);
48✔
1154
  }
1155

1156
  /**
1157
   * Get the number of clients on a given service name.
1158
   * @param {string} serviceName - the service name
1159
   * @returns {Number}
1160
   */
1161
  countClients(serviceName) {
1162
    if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
12!
1163
      console.warn('countClients is not supported by this version of ROS 2');
×
1164
      return null;
×
1165
    }
1166
    return rclnodejs.countClients(this.handle, serviceName);
12✔
1167
  }
1168

1169
  /**
1170
   * Get the number of services on a given service name.
1171
   * @param {string} serviceName - the service name
1172
   * @returns {Number}
1173
   */
1174
  countServices(serviceName) {
1175
    if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
6!
1176
      console.warn('countServices is not supported by this version of ROS 2');
×
1177
      return null;
×
1178
    }
1179
    return rclnodejs.countServices(this.handle, serviceName);
6✔
1180
  }
1181

1182
  /**
1183
   * Get the list of parameter-overrides found on the commandline and
1184
   * in the NodeOptions.parameter_overrides property.
1185
   *
1186
   * @return {Array<Parameter>} - An array of Parameters.
1187
   */
1188
  getParameterOverrides() {
1189
    return Array.from(this._parameterOverrides.values());
64✔
1190
  }
1191

1192
  /**
1193
   * Declare a parameter.
1194
   *
1195
   * Internally, register a parameter and it's descriptor.
1196
   * If a parameter-override exists, it's value will replace that of the parameter
1197
   * unless ignoreOverride is true.
1198
   * If the descriptor is undefined, then a ParameterDescriptor will be inferred
1199
   * from the parameter's state.
1200
   *
1201
   * If a parameter by the same name has already been declared then an Error is thrown.
1202
   * A parameter must be undeclared before attempting to redeclare it.
1203
   *
1204
   * @param {Parameter} parameter - Parameter to declare.
1205
   * @param {ParameterDescriptor} [descriptor] - Optional descriptor for parameter.
1206
   * @param {boolean} [ignoreOveride] - When true disregard any parameter-override that may be present.
1207
   * @return {Parameter} - The newly declared parameter.
1208
   */
1209
  declareParameter(parameter, descriptor, ignoreOveride = false) {
8,412✔
1210
    const parameters = this.declareParameters(
8,420✔
1211
      [parameter],
1212
      descriptor ? [descriptor] : [],
8,420✔
1213
      ignoreOveride
1214
    );
1215
    return parameters.length == 1 ? parameters[0] : null;
8,420!
1216
  }
1217

1218
  /**
1219
   * Declare a list of parameters.
1220
   *
1221
   * Internally register parameters with their corresponding descriptor one by one
1222
   * in the order they are provided. This is an atomic operation. If an error
1223
   * occurs the process halts and no further parameters are declared.
1224
   * Parameters that have already been processed are undeclared.
1225
   *
1226
   * While descriptors is an optional parameter, when provided there must be
1227
   * a descriptor for each parameter; otherwise an Error is thrown.
1228
   * If descriptors is not provided then a descriptor will be inferred
1229
   * from each parameter's state.
1230
   *
1231
   * When a parameter-override is available, the parameter's value
1232
   * will be replaced with that of the parameter-override unless ignoreOverrides
1233
   * is true.
1234
   *
1235
   * If a parameter by the same name has already been declared then an Error is thrown.
1236
   * A parameter must be undeclared before attempting to redeclare it.
1237
   *
1238
   * Prior to declaring the parameters each SetParameterEventCallback registered
1239
   * using setOnParameterEventCallback() is called in succession with the parameters
1240
   * list. Any SetParameterEventCallback that retuns does not return a successful
1241
   * result will cause the entire operation to terminate with no changes to the
1242
   * parameters. When all SetParameterEventCallbacks return successful then the
1243
   * list of parameters is updated.
1244
   *
1245
   * @param {Parameter[]} parameters - The parameters to declare.
1246
   * @param {ParameterDescriptor[]} [descriptors] - Optional descriptors,
1247
   *    a 1-1 correspondence with parameters.
1248
   * @param {boolean} ignoreOverrides - When true, parameter-overrides are
1249
   *    not considered, i.e.,ignored.
1250
   * @return {Parameter[]} - The declared parameters.
1251
   */
1252
  declareParameters(parameters, descriptors = [], ignoreOverrides = false) {
×
1253
    if (!Array.isArray(parameters)) {
8,420!
1254
      throw new TypeError('Invalid parameter: expected array of Parameter');
×
1255
    }
1256
    if (!Array.isArray(descriptors)) {
8,420!
1257
      throw new TypeError(
×
1258
        'Invalid parameters: expected array of ParameterDescriptor'
1259
      );
1260
    }
1261
    if (descriptors.length > 0 && parameters.length !== descriptors.length) {
8,420!
1262
      throw new TypeError(
×
1263
        'Each parameter must have a cooresponding ParameterDescriptor'
1264
      );
1265
    }
1266

1267
    const declaredDescriptors = [];
8,420✔
1268
    const declaredParameters = [];
8,420✔
1269
    const declaredParameterCollisions = [];
8,420✔
1270
    for (let i = 0; i < parameters.length; i++) {
8,420✔
1271
      let parameter =
1272
        !ignoreOverrides && this._parameterOverrides.has(parameters[i].name)
8,420✔
1273
          ? this._parameterOverrides.get(parameters[i].name)
1274
          : parameters[i];
1275

1276
      // stop processing parameters that have already been declared
1277
      if (this._parameters.has(parameter.name)) {
8,420!
1278
        declaredParameterCollisions.push(parameter);
×
1279
        continue;
×
1280
      }
1281

1282
      // create descriptor for parameter if not provided
1283
      let descriptor =
1284
        descriptors.length > 0
8,420✔
1285
          ? descriptors[i]
1286
          : ParameterDescriptor.fromParameter(parameter);
1287

1288
      descriptor.validate();
8,420✔
1289

1290
      declaredDescriptors.push(descriptor);
8,420✔
1291
      declaredParameters.push(parameter);
8,420✔
1292
    }
1293

1294
    if (declaredParameterCollisions.length > 0) {
8,420!
1295
      const errorMsg =
1296
        declaredParameterCollisions.length == 1
×
1297
          ? `Parameter(${declaredParameterCollisions[0]}) already declared.`
1298
          : `Multiple parameters already declared, e.g., Parameter(${declaredParameterCollisions[0]}).`;
1299
      throw new Error(errorMsg);
×
1300
    }
1301

1302
    // register descriptor
1303
    for (const descriptor of declaredDescriptors) {
8,420✔
1304
      this._parameterDescriptors.set(descriptor.name, descriptor);
8,420✔
1305
    }
1306

1307
    const result = this._setParametersAtomically(declaredParameters, true);
8,420✔
1308
    if (!result.successful) {
8,420!
1309
      // unregister descriptors
1310
      for (const descriptor of declaredDescriptors) {
×
1311
        this._parameterDescriptors.delete(descriptor.name);
×
1312
      }
1313

1314
      throw new Error(result.reason);
×
1315
    }
1316

1317
    return this.getParameters(declaredParameters.map((param) => param.name));
8,420✔
1318
  }
1319

1320
  /**
1321
   * Undeclare a parameter.
1322
   *
1323
   * Readonly parameters can not be undeclared or updated.
1324
   * @param {string} name - Name of parameter to undeclare.
1325
   * @return {undefined} -
1326
   */
1327
  undeclareParameter(name) {
1328
    if (!this.hasParameter(name)) return;
8!
1329

1330
    const descriptor = this.getParameterDescriptor(name);
8✔
1331
    if (descriptor.readOnly) {
8!
1332
      throw new Error(
×
1333
        `${name} parameter is read-only and can not be undeclared`
1334
      );
1335
    }
1336

1337
    this._parameters.delete(name);
8✔
1338
    this._parameterDescriptors.delete(name);
8✔
1339
  }
1340

1341
  /**
1342
   * Determine if a parameter has been declared.
1343
   * @param {string} name - name of parameter
1344
   * @returns {boolean} - Return true if parameter is declared; false otherwise.
1345
   */
1346
  hasParameter(name) {
1347
    return this._parameters.has(name);
24,950✔
1348
  }
1349

1350
  /**
1351
   * Get a declared parameter by name.
1352
   *
1353
   * If unable to locate a declared parameter then a
1354
   * parameter with type == PARAMETER_NOT_SET is returned.
1355
   *
1356
   * @param {string} name - The name of the parameter.
1357
   * @return {Parameter} - The parameter.
1358
   */
1359
  getParameter(name) {
1360
    return this.getParameters([name])[0];
8,266✔
1361
  }
1362

1363
  /**
1364
   * Get a list of parameters.
1365
   *
1366
   * Find and return the declared parameters.
1367
   * If no names are provided return all declared parameters.
1368
   *
1369
   * If unable to locate a declared parameter then a
1370
   * parameter with type == PARAMETER_NOT_SET is returned in
1371
   * it's place.
1372
   *
1373
   * @param {string[]} [names] - The names of the declared parameters
1374
   *    to find or null indicating to return all declared parameters.
1375
   * @return {Parameter[]} - The parameters.
1376
   */
1377
  getParameters(names = []) {
70✔
1378
    let params = [];
16,770✔
1379

1380
    if (names.length == 0) {
16,770✔
1381
      // get all parameters
1382
      params = [...this._parameters.values()];
70✔
1383
      return params;
70✔
1384
    }
1385

1386
    for (const name of names) {
16,700✔
1387
      const param = this.hasParameter(name)
16,708!
1388
        ? this._parameters.get(name)
1389
        : new Parameter(name, ParameterType.PARAMETER_NOT_SET);
1390

1391
      params.push(param);
16,708✔
1392
    }
1393

1394
    return params;
16,700✔
1395
  }
1396

1397
  /**
1398
   * Get the types of given parameters.
1399
   *
1400
   * Return the types of given parameters.
1401
   *
1402
   * @param {string[]} [names] - The names of the declared parameters.
1403
   * @return {Uint8Array} - The types.
1404
   */
1405
  getParameterTypes(names = []) {
×
1406
    let types = [];
8✔
1407

1408
    for (const name of names) {
8✔
1409
      const descriptor = this._parameterDescriptors.get(name);
24✔
1410
      if (descriptor) {
24!
1411
        types.push(descriptor.type);
24✔
1412
      }
1413
    }
1414
    return types;
8✔
1415
  }
1416

1417
  /**
1418
   * Get the names of all declared parameters.
1419
   *
1420
   * @return {Array<string>} - The declared parameter names or empty array if
1421
   *    no parameters have been declared.
1422
   */
1423
  getParameterNames() {
1424
    return this.getParameters().map((param) => param.name);
150✔
1425
  }
1426

1427
  /**
1428
   * Determine if a parameter descriptor exists.
1429
   *
1430
   * @param {string} name - The name of a descriptor to for.
1431
   * @return {boolean} - True if a descriptor has been declared; otherwise false.
1432
   */
1433
  hasParameterDescriptor(name) {
1434
    return !!this.getParameterDescriptor(name);
8,468✔
1435
  }
1436

1437
  /**
1438
   * Get a declared parameter descriptor by name.
1439
   *
1440
   * If unable to locate a declared parameter descriptor then a
1441
   * descriptor with type == PARAMETER_NOT_SET is returned.
1442
   *
1443
   * @param {string} name - The name of the parameter descriptor to find.
1444
   * @return {ParameterDescriptor} - The parameter descriptor.
1445
   */
1446
  getParameterDescriptor(name) {
1447
    return this.getParameterDescriptors([name])[0];
16,944✔
1448
  }
1449

1450
  /**
1451
   * Find a list of declared ParameterDescriptors.
1452
   *
1453
   * If no names are provided return all declared descriptors.
1454
   *
1455
   * If unable to locate a declared descriptor then a
1456
   * descriptor with type == PARAMETER_NOT_SET is returned in
1457
   * it's place.
1458
   *
1459
   * @param {string[]} [names] - The names of the declared parameter
1460
   *    descriptors to find or null indicating to return all declared descriptors.
1461
   * @return {ParameterDescriptor[]} - The parameter descriptors.
1462
   */
1463
  getParameterDescriptors(names = []) {
×
1464
    let descriptors = [];
16,952✔
1465

1466
    if (names.length == 0) {
16,952!
1467
      // get all parameters
1468
      descriptors = [...this._parameterDescriptors.values()];
×
1469
      return descriptors;
×
1470
    }
1471

1472
    for (const name of names) {
16,952✔
1473
      let descriptor = this._parameterDescriptors.get(name);
16,960✔
1474
      if (!descriptor) {
16,960!
1475
        descriptor = new ParameterDescriptor(
×
1476
          name,
1477
          ParameterType.PARAMETER_NOT_SET
1478
        );
1479
      }
1480
      descriptors.push(descriptor);
16,960✔
1481
    }
1482

1483
    return descriptors;
16,952✔
1484
  }
1485

1486
  /**
1487
   * Replace a declared parameter.
1488
   *
1489
   * The parameter being replaced must be a declared parameter who's descriptor
1490
   * is not readOnly; otherwise an Error is thrown.
1491
   *
1492
   * @param {Parameter} parameter - The new parameter.
1493
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult} - The result of the operation.
1494
   */
1495
  setParameter(parameter) {
1496
    const results = this.setParameters([parameter]);
32✔
1497
    return results[0];
32✔
1498
  }
1499

1500
  /**
1501
   * Replace a list of declared parameters.
1502
   *
1503
   * Declared parameters are replaced in the order they are provided and
1504
   * a ParameterEvent is published for each individual parameter change.
1505
   *
1506
   * Prior to setting the parameters each SetParameterEventCallback registered
1507
   * using setOnParameterEventCallback() is called in succession with the parameters
1508
   * list. Any SetParameterEventCallback that retuns does not return a successful
1509
   * result will cause the entire operation to terminate with no changes to the
1510
   * parameters. When all SetParameterEventCallbacks return successful then the
1511
   * list of parameters is updated.
1512
   *
1513
   * If an error occurs, the process is stopped and returned. Parameters
1514
   * set before an error remain unchanged.
1515
   *
1516
   * @param {Parameter[]} parameters - The parameters to set.
1517
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult[]} - A list of SetParameterResult, one for each parameter that was set.
1518
   */
1519
  setParameters(parameters = []) {
×
1520
    return parameters.map((parameter) =>
40✔
1521
      this.setParametersAtomically([parameter])
40✔
1522
    );
1523
  }
1524

1525
  /**
1526
   * Repalce a list of declared parameters atomically.
1527
   *
1528
   * Declared parameters are replaced in the order they are provided.
1529
   * A single ParameterEvent is published collectively for all changed
1530
   * parameters.
1531
   *
1532
   * Prior to setting the parameters each SetParameterEventCallback registered
1533
   * using setOnParameterEventCallback() is called in succession with the parameters
1534
   * list. Any SetParameterEventCallback that retuns does not return a successful
1535
   * result will cause the entire operation to terminate with no changes to the
1536
   * parameters. When all SetParameterEventCallbacks return successful then the
1537
   * list of parameters is updated.d
1538
   *
1539
   * If an error occurs, the process stops immediately. All parameters updated to
1540
   * the point of the error are reverted to their previous state.
1541
   *
1542
   * @param {Parameter[]} parameters - The parameters to set.
1543
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult} - describes the result of setting 1 or more parameters.
1544
   */
1545
  setParametersAtomically(parameters = []) {
×
1546
    return this._setParametersAtomically(parameters);
48✔
1547
  }
1548

1549
  /**
1550
   * Internal method for updating parameters atomically.
1551
   *
1552
   * Prior to setting the parameters each SetParameterEventCallback registered
1553
   * using setOnParameterEventCallback() is called in succession with the parameters
1554
   * list. Any SetParameterEventCallback that retuns does not return a successful
1555
   * result will cause the entire operation to terminate with no changes to the
1556
   * parameters. When all SetParameterEventCallbacks return successful then the
1557
   * list of parameters is updated.
1558
   *
1559
   * @param {Paramerter[]} parameters - The parameters to update.
1560
   * @param {boolean} declareParameterMode - When true parameters are being declared;
1561
   *    otherwise they are being changed.
1562
   * @return {SetParameterResult} - A single collective result.
1563
   */
1564
  _setParametersAtomically(parameters = [], declareParameterMode = false) {
48!
1565
    let result = this._validateParameters(parameters, declareParameterMode);
8,468✔
1566
    if (!result.successful) {
8,468!
1567
      return result;
×
1568
    }
1569

1570
    // give all SetParametersCallbacks a chance to veto this change
1571
    for (const callback of this._setParametersCallbacks) {
8,468✔
1572
      result = callback(parameters);
3,862✔
1573
      if (!result.successful) {
3,862✔
1574
        // a callback has vetoed a parameter change
1575
        return result;
8✔
1576
      }
1577
    }
1578

1579
    // collectively track updates to parameters for use
1580
    // when publishing a ParameterEvent
1581
    const newParameters = [];
8,460✔
1582
    const changedParameters = [];
8,460✔
1583
    const deletedParameters = [];
8,460✔
1584

1585
    for (const parameter of parameters) {
8,460✔
1586
      if (parameter.type == ParameterType.PARAMETER_NOT_SET) {
8,460✔
1587
        this.undeclareParameter(parameter.name);
8✔
1588
        deletedParameters.push(parameter);
8✔
1589
      } else {
1590
        this._parameters.set(parameter.name, parameter);
8,452✔
1591
        if (declareParameterMode) {
8,452✔
1592
          newParameters.push(parameter);
8,420✔
1593
        } else {
1594
          changedParameters.push(parameter);
32✔
1595
        }
1596
      }
1597
    }
1598

1599
    // create ParameterEvent
1600
    const parameterEvent = new (loader.loadInterface(
8,460✔
1601
      PARAMETER_EVENT_MSG_TYPE
1602
    ))();
1603

1604
    const { seconds, nanoseconds } = this._clock.now().secondsAndNanoseconds;
8,460✔
1605
    parameterEvent.stamp = {
8,460✔
1606
      sec: Number(seconds),
1607
      nanosec: Number(nanoseconds),
1608
    };
1609

1610
    parameterEvent.node =
8,460✔
1611
      this.namespace() === '/'
8,460✔
1612
        ? this.namespace() + this.name()
1613
        : this.namespace() + '/' + this.name();
1614

1615
    if (newParameters.length > 0) {
8,460✔
1616
      parameterEvent['new_parameters'] = newParameters.map((parameter) =>
8,420✔
1617
        parameter.toParameterMessage()
8,420✔
1618
      );
1619
    }
1620
    if (changedParameters.length > 0) {
8,460✔
1621
      parameterEvent['changed_parameters'] = changedParameters.map(
32✔
1622
        (parameter) => parameter.toParameterMessage()
32✔
1623
      );
1624
    }
1625
    if (deletedParameters.length > 0) {
8,460✔
1626
      parameterEvent['deleted_parameters'] = deletedParameters.map(
8✔
1627
        (parameter) => parameter.toParameterMessage()
8✔
1628
      );
1629
    }
1630

1631
    // Publish ParameterEvent.
1632
    this._parameterEventPublisher.publish(parameterEvent);
8,460✔
1633

1634
    return {
8,460✔
1635
      successful: true,
1636
      reason: '',
1637
    };
1638
  }
1639

1640
  /**
1641
   * This callback is called when declaring a parameter or setting a parameter.
1642
   * The callback is provided a list of parameters and returns a SetParameterResult
1643
   * to indicate approval or veto of the operation.
1644
   *
1645
   * @callback SetParametersCallback
1646
   * @param {Parameter[]} parameters - The message published
1647
   * @returns {rcl_interfaces.msg.SetParameterResult} -
1648
   *
1649
   * @see [Node.addOnSetParametersCallback]{@link Node#addOnSetParametersCallback}
1650
   * @see [Node.removeOnSetParametersCallback]{@link Node#removeOnSetParametersCallback}
1651
   */
1652

1653
  /**
1654
   * Add a callback to the front of the list of callbacks invoked for parameter declaration
1655
   * and setting. No checks are made for duplicate callbacks.
1656
   *
1657
   * @param {SetParametersCallback} callback - The callback to add.
1658
   * @returns {undefined}
1659
   */
1660
  addOnSetParametersCallback(callback) {
1661
    this._setParametersCallbacks.unshift(callback);
4,718✔
1662
  }
1663

1664
  /**
1665
   * Remove a callback from the list of SetParametersCallbacks.
1666
   * If the callback is not found the process is a nop.
1667
   *
1668
   * @param {SetParametersCallback} callback - The callback to be removed
1669
   * @returns {undefined}
1670
   */
1671
  removeOnSetParametersCallback(callback) {
1672
    const idx = this._setParametersCallbacks.indexOf(callback);
16✔
1673
    if (idx > -1) {
16!
1674
      this._setParametersCallbacks.splice(idx, 1);
16✔
1675
    }
1676
  }
1677

1678
  /**
1679
   * Get the fully qualified name of the node.
1680
   *
1681
   * @returns {string} - String containing the fully qualified name of the node.
1682
   */
1683
  getFullyQualifiedName() {
1684
    return rclnodejs.getFullyQualifiedName(this.handle);
8✔
1685
  }
1686

1687
  /**
1688
   * Get the RMW implementation identifier
1689
   * @returns {string} - The RMW implementation identifier.
1690
   */
1691
  getRMWImplementationIdentifier() {
1692
    return rclnodejs.getRMWImplementationIdentifier();
8✔
1693
  }
1694

1695
  // returns on 1st error or result {successful, reason}
1696
  _validateParameters(parameters = [], declareParameterMode = false) {
×
1697
    for (const parameter of parameters) {
8,468✔
1698
      // detect invalid parameter
1699
      try {
8,468✔
1700
        parameter.validate();
8,468✔
1701
      } catch {
1702
        return {
×
1703
          successful: false,
1704
          reason: `Invalid ${parameter.name}`,
1705
        };
1706
      }
1707

1708
      // detect undeclared parameter
1709
      if (!this.hasParameterDescriptor(parameter.name)) {
8,468!
1710
        return {
×
1711
          successful: false,
1712
          reason: `Parameter ${parameter.name} has not been declared`,
1713
        };
1714
      }
1715

1716
      // detect readonly parameter that can not be updated
1717
      const descriptor = this.getParameterDescriptor(parameter.name);
8,468✔
1718
      if (!declareParameterMode && descriptor.readOnly) {
8,468!
1719
        return {
×
1720
          successful: false,
1721
          reason: `Parameter ${parameter.name} is readonly`,
1722
        };
1723
      }
1724

1725
      // validate parameter against descriptor if not an undeclare action
1726
      if (parameter.type != ParameterType.PARAMETER_NOT_SET) {
8,468✔
1727
        try {
8,460✔
1728
          descriptor.validateParameter(parameter);
8,460✔
1729
        } catch {
1730
          return {
×
1731
            successful: false,
1732
            reason: `Parameter ${parameter.name} does not  readonly`,
1733
          };
1734
        }
1735
      }
1736
    }
1737

1738
    return {
8,468✔
1739
      successful: true,
1740
      reason: null,
1741
    };
1742
  }
1743

1744
  // Get a Map(nodeName->Parameter[]) of CLI parameter args that
1745
  // apply to 'this' node, .e.g., -p mynode:foo:=bar -p hello:=world
1746
  _getNativeParameterOverrides() {
1747
    const overrides = new Map();
4,654✔
1748

1749
    // Get native parameters from rcl context->global_arguments.
1750
    // rclnodejs returns an array of objects, 1 for each node e.g., -p my_node:foo:=bar,
1751
    // and a node named '/**' for global parameter rules,
1752
    // i.e., does not include a node identifier, e.g., -p color:=red
1753
    // {
1754
    //   name: string // node name
1755
    //   parameters[] = {
1756
    //     name: string
1757
    //     type: uint
1758
    //     value: object
1759
    // }
1760
    const cliParamOverrideData = rclnodejs.getParameterOverrides(
4,654✔
1761
      this.context.handle
1762
    );
1763

1764
    // convert native CLI parameterOverrides to Map<nodeName,Array<ParameterOverride>>
1765
    const cliParamOverrides = new Map();
4,654✔
1766
    if (cliParamOverrideData) {
4,654✔
1767
      for (let nodeParamData of cliParamOverrideData) {
64✔
1768
        const nodeName = nodeParamData.name;
96✔
1769
        const nodeParamOverrides = [];
96✔
1770
        for (let paramData of nodeParamData.parameters) {
96✔
1771
          const paramOverride = new Parameter(
136✔
1772
            paramData.name,
1773
            paramData.type,
1774
            paramData.value
1775
          );
1776
          nodeParamOverrides.push(paramOverride);
136✔
1777
        }
1778
        cliParamOverrides.set(nodeName, nodeParamOverrides);
96✔
1779
      }
1780
    }
1781

1782
    // collect global CLI global parameters, name == /**
1783
    let paramOverrides = cliParamOverrides.get('/**'); // array of ParameterOverrides
4,654✔
1784
    if (paramOverrides) {
4,654✔
1785
      for (const parameter of paramOverrides) {
40✔
1786
        overrides.set(parameter.name, parameter);
48✔
1787
      }
1788
    }
1789

1790
    // merge CLI node parameterOverrides with global parameterOverrides, replace existing
1791
    paramOverrides = cliParamOverrides.get(this.name()); // array of ParameterOverrides
4,654✔
1792
    if (paramOverrides) {
4,654✔
1793
      for (const parameter of paramOverrides) {
40✔
1794
        overrides.set(parameter.name, parameter);
56✔
1795
      }
1796
    }
1797

1798
    return overrides;
4,654✔
1799
  }
1800

1801
  /**
1802
   * Invokes the callback with a raw message of the given type. After the callback completes
1803
   * the message will be destroyed.
1804
   * @param {function} Type - Message type to create.
1805
   * @param {function} callback - Callback to invoke. First parameter will be the raw message,
1806
   * and the second is a function to retrieve the deserialized message.
1807
   * @returns {undefined}
1808
   */
1809
  _runWithMessageType(Type, callback) {
1810
    let message = new Type();
5,466✔
1811

1812
    callback(message.toRawROS(), () => {
5,466✔
1813
      let result = new Type();
4,396✔
1814
      result.deserialize(message.refObject);
4,396✔
1815

1816
      return result;
4,396✔
1817
    });
1818

1819
    Type.destoryRawROS(message);
5,466✔
1820
  }
1821

1822
  _addActionClient(actionClient) {
1823
    this._actionClients.push(actionClient);
324✔
1824
    this.syncHandles();
324✔
1825
  }
1826

1827
  _addActionServer(actionServer) {
1828
    this._actionServers.push(actionServer);
324✔
1829
    this.syncHandles();
324✔
1830
  }
1831
}
1832

1833
/**
1834
 * Create an Options instance initialized with default values.
1835
 * @returns {Options} - The new initialized instance.
1836
 * @static
1837
 * @example
1838
 * {
1839
 *   enableTypedArray: true,
1840
 *   isRaw: false,
1841
 *   qos: QoS.profileDefault,
1842
 *   contentFilter: undefined,
1843
 * }
1844
 */
1845
Node.getDefaultOptions = function () {
208✔
1846
  return {
43,074✔
1847
    enableTypedArray: true,
1848
    isRaw: false,
1849
    qos: QoS.profileDefault,
1850
    contentFilter: undefined,
1851
  };
1852
};
1853

1854
module.exports = Node;
208✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc