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

RobotWebTools / rclnodejs / 23184582340

17 Mar 2026 08:10AM UTC coverage: 86.04% (+0.1%) from 85.941%
23184582340

Pull #1442

github

web-flow
Merge f9e6f8fd3 into e71e42739
Pull Request #1442: Add spinUntilFutureComplete for Promise-based spin lifecycle

1416 of 1782 branches covered (79.46%)

Branch coverage included in aggregate %.

26 of 26 new or added lines in 1 file covered. (100.0%)

51 existing lines in 1 file now uncovered.

2892 of 3225 relevant lines covered (89.67%)

461.07 hits per line

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

87.89
/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('./native_loader.js');
26✔
18

19
const ActionInterfaces = require('./action/interfaces.js');
26✔
20
const Client = require('./client.js');
26✔
21
const Clock = require('./clock.js');
26✔
22
const Context = require('./context.js');
26✔
23
const debug = require('debug')('rclnodejs:node');
26✔
24
const DistroUtils = require('./distro.js');
26✔
25
const GuardCondition = require('./guard_condition.js');
26✔
26
const loader = require('./interface_loader.js');
26✔
27
const Logging = require('./logging.js');
26✔
28
const NodeOptions = require('./node_options.js');
26✔
29
const {
30
  ParameterType,
31
  Parameter,
32
  ParameterDescriptor,
33
} = require('./parameter.js');
26✔
34
const { isValidSerializationMode } = require('./message_serialization.js');
26✔
35
const {
36
  TypeValidationError,
37
  RangeValidationError,
38
  ValidationError,
39
} = require('./errors.js');
26✔
40
const ParameterService = require('./parameter_service.js');
26✔
41
const ParameterClient = require('./parameter_client.js');
26✔
42
const ParameterWatcher = require('./parameter_watcher.js');
26✔
43
const Publisher = require('./publisher.js');
26✔
44
const QoS = require('./qos.js');
26✔
45
const Rates = require('./rate.js');
26✔
46
const Service = require('./service.js');
26✔
47
const Subscription = require('./subscription.js');
26✔
48
const ObservableSubscription = require('./observable_subscription.js');
26✔
49
const TimeSource = require('./time_source.js');
26✔
50
const Timer = require('./timer.js');
26✔
51
const TypeDescriptionService = require('./type_description_service.js');
26✔
52
const Entity = require('./entity.js');
26✔
53
const { SubscriptionEventCallbacks } = require('../lib/event_handler.js');
26✔
54
const { PublisherEventCallbacks } = require('../lib/event_handler.js');
26✔
55
const { validateFullTopicName } = require('./validator.js');
26✔
56

57
// Parameter event publisher constants
58
const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent';
26✔
59
const PARAMETER_EVENT_TOPIC = 'parameter_events';
26✔
60

61
/**
62
 * @class - Class representing a Node in ROS
63
 */
64

65
class Node extends rclnodejs.ShadowNode {
66
  /**
67
   * Create a ROS2Node.
68
   * model using the {@link https://github.com/ros2/rcl/tree/master/rcl_lifecycle|ros2 client library (rcl) lifecyle api}.
69
   * @param {string} nodeName - The name used to register in ROS.
70
   * @param {string} [namespace=''] - The namespace used in ROS.
71
   * @param {Context} [context=Context.defaultContext()] - The context to create the node in.
72
   * @param {NodeOptions} [options=NodeOptions.defaultOptions] - The options to configure the new node behavior.
73
   * @throws {Error} If the given context is not registered.
74
   */
75
  constructor(
76
    nodeName,
77
    namespace = '',
44✔
78
    context = Context.defaultContext(),
81✔
79
    options = NodeOptions.defaultOptions,
81✔
80
    args = [],
102✔
81
    useGlobalArguments = true
102✔
82
  ) {
83
    super();
850✔
84

85
    if (typeof nodeName !== 'string') {
850✔
86
      throw new TypeValidationError('nodeName', nodeName, 'string');
12✔
87
    }
88
    if (typeof namespace !== 'string') {
838✔
89
      throw new TypeValidationError('namespace', namespace, 'string');
10✔
90
    }
91

92
    this._init(nodeName, namespace, options, context, args, useGlobalArguments);
828✔
93
    debug(
817✔
94
      'Finish initializing node, name = %s and namespace = %s.',
95
      nodeName,
96
      namespace
97
    );
98
  }
99

100
  static _normalizeOptions(options) {
101
    if (options instanceof NodeOptions) {
828✔
102
      return options;
826✔
103
    }
104
    const defaults = NodeOptions.defaultOptions;
2✔
105
    return {
2✔
106
      startParameterServices:
107
        options.startParameterServices ?? defaults.startParameterServices,
4✔
108
      parameterOverrides:
109
        options.parameterOverrides ?? defaults.parameterOverrides,
4✔
110
      automaticallyDeclareParametersFromOverrides:
111
        options.automaticallyDeclareParametersFromOverrides ??
4✔
112
        defaults.automaticallyDeclareParametersFromOverrides,
113
      startTypeDescriptionService:
114
        options.startTypeDescriptionService ??
4✔
115
        defaults.startTypeDescriptionService,
116
      enableRosout: options.enableRosout ?? defaults.enableRosout,
3✔
117
      rosoutQos: options.rosoutQos ?? defaults.rosoutQos,
4✔
118
    };
119
  }
120

121
  _init(name, namespace, options, context, args, useGlobalArguments) {
122
    options = Node._normalizeOptions(options);
828✔
123

124
    this.handle = rclnodejs.createNode(
828✔
125
      name,
126
      namespace,
127
      context.handle,
128
      args,
129
      useGlobalArguments,
130
      options.rosoutQos
131
    );
132
    Object.defineProperty(this, 'handle', {
818✔
133
      configurable: false,
134
      writable: false,
135
    }); // make read-only
136

137
    this._context = context;
818✔
138
    this.context.onNodeCreated(this);
818✔
139

140
    this._publishers = [];
818✔
141
    this._subscriptions = [];
818✔
142
    this._clients = [];
818✔
143
    this._services = [];
818✔
144
    this._timers = [];
818✔
145
    this._guards = [];
818✔
146
    this._events = [];
818✔
147
    this._actionClients = [];
818✔
148
    this._actionServers = [];
818✔
149
    this._parameterClients = [];
818✔
150
    this._parameterWatchers = [];
818✔
151
    this._rateTimerServer = null;
818✔
152
    this._parameterDescriptors = new Map();
818✔
153
    this._parameters = new Map();
818✔
154
    this._parameterService = null;
818✔
155
    this._typeDescriptionService = null;
818✔
156
    this._parameterEventPublisher = null;
818✔
157
    this._setParametersCallbacks = [];
818✔
158
    this._logger = new Logging(rclnodejs.getNodeLoggerName(this.handle));
818✔
159
    this._spinning = false;
818✔
160
    this._enableRosout = options.enableRosout;
818✔
161

162
    if (this._enableRosout) {
818✔
163
      rclnodejs.initRosoutPublisherForNode(this.handle);
816✔
164
    }
165

166
    this._parameterEventPublisher = this.createPublisher(
818✔
167
      PARAMETER_EVENT_MSG_TYPE,
168
      PARAMETER_EVENT_TOPIC
169
    );
170

171
    // initialize _parameterOverrides from parameters defined on the commandline
172
    this._parameterOverrides = this._getNativeParameterOverrides();
818✔
173

174
    // override cli parameterOverrides with those specified in options
175
    if (options.parameterOverrides.length > 0) {
818✔
176
      for (const parameter of options.parameterOverrides) {
11✔
177
        if (!(parameter instanceof Parameter)) {
16✔
178
          throw new TypeValidationError(
1✔
179
            'parameterOverride',
180
            parameter,
181
            'Parameter instance',
182
            {
183
              nodeName: name,
184
            }
185
          );
186
        }
187
        this._parameterOverrides.set(parameter.name, parameter);
15✔
188
      }
189
    }
190

191
    // initialize _parameters from parameterOverrides
192
    if (options.automaticallyDeclareParametersFromOverrides) {
817✔
193
      for (const parameter of this._parameterOverrides.values()) {
5✔
194
        parameter.validate();
10✔
195
        const descriptor = ParameterDescriptor.fromParameter(parameter);
10✔
196
        this._parameters.set(parameter.name, parameter);
10✔
197
        this._parameterDescriptors.set(parameter.name, descriptor);
10✔
198
      }
199
    }
200

201
    // Clock that has support for ROS time.
202
    // Note: parameter overrides and parameter event publisher need to be ready at this point
203
    // to be able to declare 'use_sim_time' if it was not declared yet.
204
    this._clock = new Clock.ROSClock();
817✔
205
    this._timeSource = new TimeSource(this);
817✔
206
    this._timeSource.attachClock(this._clock);
817✔
207

208
    if (options.startParameterServices) {
817✔
209
      this._parameterService = new ParameterService(this);
811✔
210
      this._parameterService.start();
811✔
211
    }
212

213
    if (
817!
214
      DistroUtils.getDistroId() >= DistroUtils.getDistroId('jazzy') &&
1,634✔
215
      options.startTypeDescriptionService
216
    ) {
217
      this._typeDescriptionService = new TypeDescriptionService(this);
817✔
218
      this._typeDescriptionService.start();
817✔
219
    }
220
  }
221

222
  execute(handles) {
223
    let timersReady = this._timers.filter((timer) =>
6,256✔
224
      handles.includes(timer.handle)
5,290✔
225
    );
226
    let guardsReady = this._guards.filter((guard) =>
6,256✔
227
      handles.includes(guard.handle)
3✔
228
    );
229
    let subscriptionsReady = this._subscriptions.filter((subscription) =>
6,256✔
230
      handles.includes(subscription.handle)
499✔
231
    );
232
    let clientsReady = this._clients.filter((client) =>
6,256✔
233
      handles.includes(client.handle)
312✔
234
    );
235
    let servicesReady = this._services.filter((service) =>
6,256✔
236
      handles.includes(service.handle)
25,896✔
237
    );
238
    let actionClientsReady = this._actionClients.filter((actionClient) =>
6,256✔
239
      handles.includes(actionClient.handle)
161✔
240
    );
241
    let actionServersReady = this._actionServers.filter((actionServer) =>
6,256✔
242
      handles.includes(actionServer.handle)
156✔
243
    );
244
    let eventsReady = this._events.filter((event) =>
6,256✔
245
      handles.includes(event.handle)
4✔
246
    );
247

248
    timersReady.forEach((timer) => {
6,256✔
249
      if (timer.isReady()) {
5,275✔
250
        rclnodejs.callTimer(timer.handle);
5,210✔
251
        timer.callback();
5,210✔
252
      }
253
    });
254

255
    eventsReady.forEach((event) => {
6,256✔
256
      event.takeData();
4✔
257
    });
258

259
    for (const subscription of subscriptionsReady) {
6,256✔
260
      if (subscription.isDestroyed()) continue;
460✔
261
      if (subscription.isRaw) {
456✔
262
        let rawMessage = rclnodejs.rclTakeRaw(subscription.handle);
1✔
263
        if (rawMessage) {
1!
264
          subscription.processResponse(rawMessage);
1✔
265
        }
266
        continue;
1✔
267
      }
268

269
      this._runWithMessageType(
455✔
270
        subscription.typeClass,
271
        (message, deserialize) => {
272
          let success = rclnodejs.rclTake(subscription.handle, message);
455✔
273
          if (success) {
455✔
274
            subscription.processResponse(deserialize());
377✔
275
          }
276
        }
277
      );
278
    }
279

280
    for (const guard of guardsReady) {
6,256✔
281
      if (guard.isDestroyed()) continue;
3!
282

283
      guard.callback();
3✔
284
    }
285

286
    for (const client of clientsReady) {
6,256✔
287
      if (client.isDestroyed()) continue;
162!
288
      this._runWithMessageType(
162✔
289
        client.typeClass.Response,
290
        (message, deserialize) => {
291
          let sequenceNumber = rclnodejs.rclTakeResponse(
162✔
292
            client.handle,
293
            message
294
          );
295
          if (sequenceNumber !== undefined) {
162✔
296
            client.processResponse(sequenceNumber, deserialize());
86✔
297
          }
298
        }
299
      );
300
    }
301

302
    for (const service of servicesReady) {
6,256✔
303
      if (service.isDestroyed()) continue;
208!
304
      this._runWithMessageType(
208✔
305
        service.typeClass.Request,
306
        (message, deserialize) => {
307
          let header = rclnodejs.rclTakeRequest(
208✔
308
            service.handle,
309
            this.handle,
310
            message
311
          );
312
          if (header) {
208✔
313
            service.processRequest(header, deserialize());
91✔
314
          }
315
        }
316
      );
317
    }
318

319
    for (const actionClient of actionClientsReady) {
6,256✔
320
      if (actionClient.isDestroyed()) continue;
78!
321

322
      const properties = actionClient.handle.properties;
78✔
323

324
      if (properties.isGoalResponseReady) {
78✔
325
        this._runWithMessageType(
40✔
326
          actionClient.typeClass.impl.SendGoalService.Response,
327
          (message, deserialize) => {
328
            let sequence = rclnodejs.actionTakeGoalResponse(
40✔
329
              actionClient.handle,
330
              message
331
            );
332
            if (sequence != undefined) {
40✔
333
              actionClient.processGoalResponse(sequence, deserialize());
30✔
334
            }
335
          }
336
        );
337
      }
338

339
      if (properties.isCancelResponseReady) {
78✔
340
        this._runWithMessageType(
6✔
341
          actionClient.typeClass.impl.CancelGoal.Response,
342
          (message, deserialize) => {
343
            let sequence = rclnodejs.actionTakeCancelResponse(
6✔
344
              actionClient.handle,
345
              message
346
            );
347
            if (sequence != undefined) {
6✔
348
              actionClient.processCancelResponse(sequence, deserialize());
4✔
349
            }
350
          }
351
        );
352
      }
353

354
      if (properties.isResultResponseReady) {
78✔
355
        this._runWithMessageType(
15✔
356
          actionClient.typeClass.impl.GetResultService.Response,
357
          (message, deserialize) => {
358
            let sequence = rclnodejs.actionTakeResultResponse(
15✔
359
              actionClient.handle,
360
              message
361
            );
362
            if (sequence != undefined) {
15!
363
              actionClient.processResultResponse(sequence, deserialize());
15✔
364
            }
365
          }
366
        );
367
      }
368

369
      if (properties.isFeedbackReady) {
78✔
370
        this._runWithMessageType(
9✔
371
          actionClient.typeClass.impl.FeedbackMessage,
372
          (message, deserialize) => {
373
            let success = rclnodejs.actionTakeFeedback(
9✔
374
              actionClient.handle,
375
              message
376
            );
377
            if (success) {
9✔
378
              actionClient.processFeedbackMessage(deserialize());
5✔
379
            }
380
          }
381
        );
382
      }
383

384
      if (properties.isStatusReady) {
78✔
385
        this._runWithMessageType(
42✔
386
          actionClient.typeClass.impl.GoalStatusArray,
387
          (message, deserialize) => {
388
            let success = rclnodejs.actionTakeStatus(
42✔
389
              actionClient.handle,
390
              message
391
            );
392
            if (success) {
42✔
393
              actionClient.processStatusMessage(deserialize());
33✔
394
            }
395
          }
396
        );
397
      }
398
    }
399

400
    for (const actionServer of actionServersReady) {
6,256✔
401
      if (actionServer.isDestroyed()) continue;
92!
402

403
      const properties = actionServer.handle.properties;
92✔
404

405
      if (properties.isGoalRequestReady) {
92✔
406
        this._runWithMessageType(
34✔
407
          actionServer.typeClass.impl.SendGoalService.Request,
408
          (message, deserialize) => {
409
            const result = rclnodejs.actionTakeGoalRequest(
34✔
410
              actionServer.handle,
411
              message
412
            );
413
            if (result) {
34✔
414
              actionServer.processGoalRequest(result, deserialize());
30✔
415
            }
416
          }
417
        );
418
      }
419

420
      if (properties.isCancelRequestReady) {
92✔
421
        this._runWithMessageType(
5✔
422
          actionServer.typeClass.impl.CancelGoal.Request,
423
          (message, deserialize) => {
424
            const result = rclnodejs.actionTakeCancelRequest(
5✔
425
              actionServer.handle,
426
              message
427
            );
428
            if (result) {
5✔
429
              actionServer.processCancelRequest(result, deserialize());
4✔
430
            }
431
          }
432
        );
433
      }
434

435
      if (properties.isResultRequestReady) {
92✔
436
        this._runWithMessageType(
23✔
437
          actionServer.typeClass.impl.GetResultService.Request,
438
          (message, deserialize) => {
439
            const result = rclnodejs.actionTakeResultRequest(
23✔
440
              actionServer.handle,
441
              message
442
            );
443
            if (result) {
23✔
444
              actionServer.processResultRequest(result, deserialize());
15✔
445
            }
446
          }
447
        );
448
      }
449

450
      if (properties.isGoalExpired) {
92✔
451
        let numGoals = actionServer._goalHandles.size;
4✔
452
        if (numGoals > 0) {
4✔
453
          let GoalInfoArray = ActionInterfaces.GoalInfo.ArrayType;
3✔
454
          let message = new GoalInfoArray(numGoals);
3✔
455
          let count = rclnodejs.actionExpireGoals(
3✔
456
            actionServer.handle,
457
            numGoals,
458
            message._refArray.buffer
459
          );
460
          if (count > 0) {
3!
461
            actionServer.processGoalExpired(message, count);
3✔
462
          }
463
          GoalInfoArray.freeArray(message);
3✔
464
        }
465
      }
466
    }
467

468
    // At this point it is safe to clear the cache of any
469
    // destroyed entity references
470
    Entity._gcHandles();
6,256✔
471
  }
472

473
  /**
474
   * Determine if this node is spinning.
475
   * @returns {boolean} - true when spinning; otherwise returns false.
476
   */
477
  get spinning() {
478
    return this._spinning;
4,504✔
479
  }
480

481
  /**
482
   * Trigger the event loop to continuously check for and route.
483
   * incoming events.
484
   * @param {Node} node - The node to be spun up.
485
   * @param {number} [timeout=10] - Timeout to wait in milliseconds. Block forever if negative. Don't wait if 0.
486
   * @throws {Error} If the node is already spinning.
487
   * @return {undefined}
488
   */
489
  spin(timeout = 10) {
26✔
490
    if (this.spinning) {
630!
UNCOV
491
      throw new Error('The node is already spinning.');
×
492
    }
493
    this.start(this.context.handle, timeout);
630✔
494
    this._spinning = true;
630✔
495
  }
496

497
  /**
498
   * Use spin().
499
   * @deprecated, since 0.18.0
500
   */
501
  startSpinning(timeout) {
UNCOV
502
    this.spin(timeout);
×
503
  }
504

505
  /**
506
   * Terminate spinning - no further events will be received.
507
   * @returns {undefined}
508
   */
509
  stop() {
510
    super.stop();
630✔
511
    this._spinning = false;
630✔
512
  }
513

514
  /**
515
   * Terminate spinning - no further events will be received.
516
   * @returns {undefined}
517
   * @deprecated since 0.18.0, Use stop().
518
   */
519
  stopSpinning() {
UNCOV
520
    super.stop();
×
521
    this._spinning = false;
×
522
  }
523

524
  /**
525
   * Spin the node and trigger the event loop to check for one incoming event. Thereafter the node
526
   * will not received additional events until running additional calls to spin() or spinOnce().
527
   * @param {Node} node - The node to be spun.
528
   * @param {number} [timeout=10] - Timeout to wait in milliseconds. Block forever if negative. Don't wait if 0.
529
   * @throws {Error} If the node is already spinning.
530
   * @return {undefined}
531
   */
532
  spinOnce(timeout = 10) {
2✔
533
    if (this.spinning) {
3,009✔
534
      throw new Error('The node is already spinning.');
2✔
535
    }
536
    super.spinOnce(this.context.handle, timeout);
3,007✔
537
  }
538

539
  /**
540
   * Spin the node until a Promise resolves, rejects, or a timeout expires.
541
   *
542
   * This is the rclnodejs equivalent of rclpy's `spin_until_future_complete`.
543
   * It starts spinning (if not already spinning), waits for the promise to
544
   * settle, and then stops spinning (if it started it).
545
   *
546
   * @param {Promise} promise - The Promise to wait for.
547
   * @param {number} [timeoutMs] - Optional timeout in milliseconds.
548
   *   If provided and the promise does not settle within the timeout,
549
   *   a TimeoutError is thrown. If omitted, waits indefinitely.
550
   * @returns {Promise<*>} - Resolves with the value of the input promise.
551
   * @throws {Error} If the promise rejects or the timeout expires.
552
   *
553
   * @example
554
   * // Wait for a service response with a 5-second timeout
555
   * const response = await node.spinUntilFutureComplete(
556
   *   client.sendRequest(request),
557
   *   5000
558
   * );
559
   *
560
   * @example
561
   * // Wait indefinitely
562
   * const response = await node.spinUntilFutureComplete(
563
   *   client.sendRequest(request)
564
   * );
565
   */
566
  async spinUntilFutureComplete(promise, timeoutMs) {
567
    if (!(promise && typeof promise.then === 'function')) {
9✔
568
      throw new TypeValidationError('promise', promise, 'Promise (thenable)', {
2✔
569
        nodeName: this.name(),
570
      });
571
    }
572

573
    const wasSpinning = this.spinning;
7✔
574
    if (!wasSpinning) {
7✔
575
      this.spin();
6✔
576
    }
577

578
    try {
7✔
579
      if (timeoutMs != null && timeoutMs >= 0) {
7✔
580
        let timer;
581
        const timeoutPromise = new Promise((_, reject) => {
2✔
582
          timer = setTimeout(() => {
2✔
583
            reject(
1✔
584
              new Error(
585
                `spinUntilFutureComplete timed out after ${timeoutMs}ms`
586
              )
587
            );
588
          }, timeoutMs);
589
        });
590

591
        try {
2✔
592
          return await Promise.race([promise, timeoutPromise]);
2✔
593
        } finally {
594
          clearTimeout(timer);
2✔
595
        }
596
      }
597

598
      return await promise;
5✔
599
    } finally {
600
      if (!wasSpinning) {
7✔
601
        this.stop();
6✔
602
      }
603
    }
604
  }
605

606
  _removeEntityFromArray(entity, array) {
607
    let index = array.indexOf(entity);
337✔
608
    if (index > -1) {
337✔
609
      array.splice(index, 1);
335✔
610
    }
611
  }
612

613
  _destroyEntity(entity, array, syncHandles = true) {
283✔
614
    if (entity['isDestroyed'] && entity.isDestroyed()) return;
290✔
615

616
    this._removeEntityFromArray(entity, array);
282✔
617
    if (syncHandles) {
282✔
618
      this.syncHandles();
277✔
619
    }
620

621
    if (entity['_destroy']) {
282✔
622
      entity._destroy();
276✔
623
    } else {
624
      // guards and timers
625
      entity.handle.release();
6✔
626
    }
627
  }
628

629
  _validateOptions(options) {
630
    if (
7,612✔
631
      options !== undefined &&
7,686✔
632
      (options === null || typeof options !== 'object')
633
    ) {
634
      throw new TypeValidationError('options', options, 'object', {
20✔
635
        nodeName: this.name(),
636
      });
637
    }
638

639
    if (options === undefined) {
7,592✔
640
      return Node.getDefaultOptions();
7,565✔
641
    }
642

643
    if (options.enableTypedArray === undefined) {
27✔
644
      options = Object.assign(options, { enableTypedArray: true });
13✔
645
    }
646

647
    if (options.qos === undefined) {
27✔
648
      options = Object.assign(options, { qos: QoS.profileDefault });
13✔
649
    }
650

651
    if (options.isRaw === undefined) {
27✔
652
      options = Object.assign(options, { isRaw: false });
16✔
653
    }
654

655
    if (options.serializationMode === undefined) {
27✔
656
      options = Object.assign(options, { serializationMode: 'default' });
10✔
657
    } else if (!isValidSerializationMode(options.serializationMode)) {
17✔
658
      throw new ValidationError(
1✔
659
        `Invalid serializationMode: ${options.serializationMode}. Valid modes are: 'default', 'plain', 'json'`,
660
        {
661
          code: 'INVALID_SERIALIZATION_MODE',
662
          argumentName: 'serializationMode',
663
          providedValue: options.serializationMode,
664
          expectedType: "'default' | 'plain' | 'json'",
665
          nodeName: this.name(),
666
        }
667
      );
668
    }
669

670
    return options;
26✔
671
  }
672

673
  /**
674
   * Create a Timer.
675
   * @param {bigint} period - The number representing period in nanoseconds.
676
   * @param {function} callback - The callback to be called when timeout.
677
   * @param {Clock} [clock] - The clock which the timer gets time from.
678
   * @return {Timer} - An instance of Timer.
679
   */
680
  createTimer(period, callback, clock = null) {
67✔
681
    if (arguments.length === 3 && !(arguments[2] instanceof Clock)) {
67!
UNCOV
682
      clock = null;
×
683
    } else if (arguments.length === 4) {
67!
UNCOV
684
      clock = arguments[3];
×
685
    }
686

687
    if (typeof period !== 'bigint') {
67✔
688
      throw new TypeValidationError('period', period, 'bigint', {
1✔
689
        nodeName: this.name(),
690
      });
691
    }
692
    if (typeof callback !== 'function') {
66✔
693
      throw new TypeValidationError('callback', callback, 'function', {
1✔
694
        nodeName: this.name(),
695
      });
696
    }
697

698
    const timerClock = clock || this._clock;
65✔
699
    let timerHandle = rclnodejs.createTimer(
65✔
700
      timerClock.handle,
701
      this.context.handle,
702
      period
703
    );
704
    let timer = new Timer(timerHandle, period, callback);
65✔
705
    debug('Finish creating timer, period = %d.', period);
65✔
706
    this._timers.push(timer);
65✔
707
    this.syncHandles();
65✔
708

709
    return timer;
65✔
710
  }
711

712
  /**
713
   * Create a Rate.
714
   *
715
   * @param {number} hz - The frequency of the rate timer; default is 1 hz.
716
   * @returns {Promise<Rate>} - Promise resolving to new instance of Rate.
717
   */
718
  async createRate(hz = 1) {
4✔
719
    if (typeof hz !== 'number') {
9!
UNCOV
720
      throw new TypeValidationError('hz', hz, 'number', {
×
721
        nodeName: this.name(),
722
      });
723
    }
724

725
    const MAX_RATE_HZ_IN_MILLISECOND = 1000.0;
9✔
726
    if (hz <= 0.0 || hz > MAX_RATE_HZ_IN_MILLISECOND) {
9✔
727
      throw new RangeValidationError(
2✔
728
        'hz',
729
        hz,
730
        `0.0 < hz <= ${MAX_RATE_HZ_IN_MILLISECOND}`,
731
        {
732
          nodeName: this.name(),
733
        }
734
      );
735
    }
736

737
    // lazy initialize rateTimerServer
738
    if (!this._rateTimerServer) {
7✔
739
      this._rateTimerServer = new Rates.RateTimerServer(this);
5✔
740
      await this._rateTimerServer.init();
5✔
741
    }
742

743
    const period = Math.round(1000 / hz);
7✔
744
    const timer = this._rateTimerServer.createTimer(BigInt(period) * 1000000n);
7✔
745
    const rate = new Rates.Rate(hz, timer);
7✔
746

747
    return rate;
7✔
748
  }
749

750
  /**
751
   * Create a Publisher.
752
   * @param {function|string|object} typeClass - The ROS message class,
753
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
754
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
755
   * @param {string} topic - The name of the topic.
756
   * @param {object} options - The options argument used to parameterize the publisher.
757
   * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
758
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the publisher, default: QoS.profileDefault.
759
   * @param {PublisherEventCallbacks} eventCallbacks - The event callbacks for the publisher.
760
   * @return {Publisher} - An instance of Publisher.
761
   */
762
  createPublisher(typeClass, topic, options, eventCallbacks) {
763
    return this._createPublisher(
1,181✔
764
      typeClass,
765
      topic,
766
      options,
767
      Publisher,
768
      eventCallbacks
769
    );
770
  }
771

772
  _createPublisher(typeClass, topic, options, publisherClass, eventCallbacks) {
773
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
1,184✔
774
      typeClass = loader.loadInterface(typeClass);
1,160✔
775
    }
776
    options = this._validateOptions(options);
1,177✔
777

778
    if (typeof typeClass !== 'function') {
1,177✔
779
      throw new TypeValidationError('typeClass', typeClass, 'function', {
8✔
780
        nodeName: this.name(),
781
        entityType: 'publisher',
782
      });
783
    }
784
    if (typeof topic !== 'string') {
1,169✔
785
      throw new TypeValidationError('topic', topic, 'string', {
12✔
786
        nodeName: this.name(),
787
        entityType: 'publisher',
788
      });
789
    }
790
    if (
1,157!
791
      eventCallbacks &&
1,159✔
792
      !(eventCallbacks instanceof PublisherEventCallbacks)
793
    ) {
UNCOV
794
      throw new TypeValidationError(
×
795
        'eventCallbacks',
796
        eventCallbacks,
797
        'PublisherEventCallbacks',
798
        {
799
          nodeName: this.name(),
800
          entityType: 'publisher',
801
          entityName: topic,
802
        }
803
      );
804
    }
805

806
    let publisher = publisherClass.createPublisher(
1,157✔
807
      this,
808
      typeClass,
809
      topic,
810
      options,
811
      eventCallbacks
812
    );
813
    debug('Finish creating publisher, topic = %s.', topic);
1,147✔
814
    this._publishers.push(publisher);
1,147✔
815
    return publisher;
1,147✔
816
  }
817

818
  /**
819
   * This callback is called when a message is published
820
   * @callback SubscriptionCallback
821
   * @param {Object} message - The message published
822
   * @see [Node.createSubscription]{@link Node#createSubscription}
823
   * @see [Node.createPublisher]{@link Node#createPublisher}
824
   * @see {@link Publisher}
825
   * @see {@link Subscription}
826
   */
827

828
  /**
829
   * Create a Subscription with optional content-filtering.
830
   * @param {function|string|object} typeClass - The ROS message class,
831
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
832
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
833
   * @param {string} topic - The name of the topic.
834
   * @param {object} options - The options argument used to parameterize the subscription.
835
   * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
836
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the subscription, default: QoS.profileDefault.
837
   * @param {boolean} options.isRaw - The topic is serialized when true, default: false.
838
   * @param {string} [options.serializationMode='default'] - Controls message serialization format:
839
   *  'default': Use native rclnodejs behavior (respects enableTypedArray setting),
840
   *  'plain': Convert TypedArrays to regular arrays,
841
   *  'json': Fully JSON-safe (handles TypedArrays, BigInt, etc.).
842
   * @param {object} [options.contentFilter=undefined] - The content-filter, default: undefined.
843
   *  Confirm that your RMW supports content-filtered topics before use. 
844
   * @param {string} options.contentFilter.expression - Specifies the criteria to select the data samples of
845
   *  interest. It is similar to the WHERE part of an SQL clause.
846
   * @param {string[]} [options.contentFilter.parameters=undefined] - Array of strings that give values to
847
   *  the ‘parameters’ (i.e., "%n" tokens) in the filter_expression. The number of supplied parameters must
848
   *  fit with the requested values in the filter_expression (i.e., the number of %n tokens). default: undefined.
849
   * @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.
850
   * @param {SubscriptionEventCallbacks} eventCallbacks - The event callbacks for the subscription.
851
   * @return {Subscription} - An instance of Subscription.
852
   * @throws {ERROR} - May throw an RMW error if content-filter is malformed. 
853
   * @see {@link SubscriptionCallback}
854
   * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|Content-filter details at DDS 1.4 specification, Annex B}
855
   */
856
  createSubscription(typeClass, topic, options, callback, eventCallbacks) {
857
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
428✔
858
      typeClass = loader.loadInterface(typeClass);
419✔
859
    }
860

861
    if (typeof options === 'function') {
421✔
862
      callback = options;
373✔
863
      options = undefined;
373✔
864
    }
865
    options = this._validateOptions(options);
421✔
866

867
    if (typeof typeClass !== 'function') {
411✔
868
      throw new TypeValidationError('typeClass', typeClass, 'function', {
4✔
869
        nodeName: this.name(),
870
        entityType: 'subscription',
871
      });
872
    }
873
    if (typeof topic !== 'string') {
407✔
874
      throw new TypeValidationError('topic', topic, 'string', {
6✔
875
        nodeName: this.name(),
876
        entityType: 'subscription',
877
      });
878
    }
879
    if (typeof callback !== 'function') {
401!
UNCOV
880
      throw new TypeValidationError('callback', callback, 'function', {
×
881
        nodeName: this.name(),
882
        entityType: 'subscription',
883
        entityName: topic,
884
      });
885
    }
886
    if (
401!
887
      eventCallbacks &&
405✔
888
      !(eventCallbacks instanceof SubscriptionEventCallbacks)
889
    ) {
UNCOV
890
      throw new TypeValidationError(
×
891
        'eventCallbacks',
892
        eventCallbacks,
893
        'SubscriptionEventCallbacks',
894
        {
895
          nodeName: this.name(),
896
          entityType: 'subscription',
897
          entityName: topic,
898
        }
899
      );
900
    }
901

902
    let subscription = Subscription.createSubscription(
401✔
903
      this,
904
      typeClass,
905
      topic,
906
      options,
907
      callback,
908
      eventCallbacks
909
    );
910
    debug('Finish creating subscription, topic = %s.', topic);
390✔
911
    this._subscriptions.push(subscription);
390✔
912
    this.syncHandles();
390✔
913

914
    return subscription;
390✔
915
  }
916

917
  /**
918
   * Create a Subscription that returns an RxJS Observable.
919
   * This allows using reactive programming patterns with ROS 2 messages.
920
   *
921
   * @param {function|string|object} typeClass - The ROS message class,
922
   *      OR a string representing the message class, e.g. 'std_msgs/msg/String',
923
   *      OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
924
   * @param {string} topic - The name of the topic.
925
   * @param {object} [options] - The options argument used to parameterize the subscription.
926
   * @param {boolean} [options.enableTypedArray=true] - The topic will use TypedArray if necessary.
927
   * @param {QoS} [options.qos=QoS.profileDefault] - ROS Middleware "quality of service" settings.
928
   * @param {boolean} [options.isRaw=false] - The topic is serialized when true.
929
   * @param {string} [options.serializationMode='default'] - Controls message serialization format.
930
   * @param {object} [options.contentFilter] - The content-filter (if supported by RMW).
931
   * @param {SubscriptionEventCallbacks} [eventCallbacks] - The event callbacks for the subscription.
932
   * @return {ObservableSubscription} - An ObservableSubscription with an RxJS Observable.
933
   */
934
  createObservableSubscription(typeClass, topic, options, eventCallbacks) {
935
    let observableSubscription = null;
7✔
936

937
    const subscription = this.createSubscription(
7✔
938
      typeClass,
939
      topic,
940
      options,
941
      (message) => {
942
        if (observableSubscription) {
8!
943
          observableSubscription._emit(message);
8✔
944
        }
945
      },
946
      eventCallbacks
947
    );
948

949
    observableSubscription = new ObservableSubscription(subscription);
7✔
950
    return observableSubscription;
7✔
951
  }
952

953
  /**
954
   * Create a Client.
955
   * @param {function|string|object} typeClass - The ROS message class,
956
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
957
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
958
   * @param {string} serviceName - The service name to request.
959
   * @param {object} options - The options argument used to parameterize the client.
960
   * @param {boolean} options.enableTypedArray - The response will use TypedArray if necessary, default: true.
961
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the client, default: QoS.profileDefault.
962
   * @return {Client} - An instance of Client.
963
   */
964
  createClient(typeClass, serviceName, options) {
965
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
196✔
966
      typeClass = loader.loadInterface(typeClass);
186✔
967
    }
968
    options = this._validateOptions(options);
189✔
969

970
    if (typeof typeClass !== 'function') {
189✔
971
      throw new TypeValidationError('typeClass', typeClass, 'function', {
8✔
972
        nodeName: this.name(),
973
        entityType: 'client',
974
      });
975
    }
976
    if (typeof serviceName !== 'string') {
181✔
977
      throw new TypeValidationError('serviceName', serviceName, 'string', {
12✔
978
        nodeName: this.name(),
979
        entityType: 'client',
980
      });
981
    }
982

983
    let client = Client.createClient(
169✔
984
      this.handle,
985
      serviceName,
986
      typeClass,
987
      options
988
    );
989
    debug('Finish creating client, service = %s.', serviceName);
159✔
990
    this._clients.push(client);
159✔
991
    this.syncHandles();
159✔
992

993
    return client;
159✔
994
  }
995

996
  /**
997
   * This callback is called when a request is sent to service
998
   * @callback RequestCallback
999
   * @param {Object} request - The request sent to the service
1000
   * @param {Response} response - The response to client.
1001
        Use [response.send()]{@link Response#send} to send response object to client
1002
   * @return {undefined}
1003
   * @see [Node.createService]{@link Node#createService}
1004
   * @see [Client.sendRequest]{@link Client#sendRequest}
1005
   * @see {@link Client}
1006
   * @see {@link Service}
1007
   * @see {@link Response#send}
1008
   */
1009

1010
  /**
1011
   * Create a Service.
1012
   * @param {function|string|object} typeClass - The ROS message class,
1013
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
1014
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
1015
   * @param {string} serviceName - The service name to offer.
1016
   * @param {object} options - The options argument used to parameterize the service.
1017
   * @param {boolean} options.enableTypedArray - The request will use TypedArray if necessary, default: true.
1018
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the service, default: QoS.profileDefault.
1019
   * @param {RequestCallback} callback - The callback to be called when receiving request.
1020
   * @return {Service} - An instance of Service.
1021
   * @see {@link RequestCallback}
1022
   */
1023
  createService(typeClass, serviceName, options, callback) {
1024
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
4,939✔
1025
      typeClass = loader.loadInterface(typeClass);
4,929✔
1026
    }
1027

1028
    if (typeof options === 'function') {
4,932✔
1029
      callback = options;
4,909✔
1030
      options = undefined;
4,909✔
1031
    }
1032
    options = this._validateOptions(options);
4,932✔
1033

1034
    if (typeof typeClass !== 'function') {
4,922✔
1035
      throw new TypeValidationError('typeClass', typeClass, 'function', {
4✔
1036
        nodeName: this.name(),
1037
        entityType: 'service',
1038
      });
1039
    }
1040
    if (typeof serviceName !== 'string') {
4,918✔
1041
      throw new TypeValidationError('serviceName', serviceName, 'string', {
6✔
1042
        nodeName: this.name(),
1043
        entityType: 'service',
1044
      });
1045
    }
1046
    if (typeof callback !== 'function') {
4,912!
UNCOV
1047
      throw new TypeValidationError('callback', callback, 'function', {
×
1048
        nodeName: this.name(),
1049
        entityType: 'service',
1050
        entityName: serviceName,
1051
      });
1052
    }
1053

1054
    let service = Service.createService(
4,912✔
1055
      this.handle,
1056
      serviceName,
1057
      typeClass,
1058
      options,
1059
      callback
1060
    );
1061
    debug('Finish creating service, service = %s.', serviceName);
4,902✔
1062
    this._services.push(service);
4,902✔
1063
    this.syncHandles();
4,902✔
1064

1065
    return service;
4,902✔
1066
  }
1067

1068
  /**
1069
   * Create a ParameterClient for accessing parameters on a remote node.
1070
   * @param {string} remoteNodeName - The name of the remote node whose parameters to access.
1071
   * @param {object} [options] - Options for parameter client.
1072
   * @param {number} [options.timeout=5000] - Default timeout in milliseconds for service calls.
1073
   * @return {ParameterClient} - An instance of ParameterClient.
1074
   */
1075
  createParameterClient(remoteNodeName, options = {}) {
50✔
1076
    if (typeof remoteNodeName !== 'string' || remoteNodeName.trim() === '') {
103!
UNCOV
1077
      throw new TypeError('Remote node name must be a non-empty string');
×
1078
    }
1079

1080
    const parameterClient = new ParameterClient(this, remoteNodeName, options);
103✔
1081
    debug(
103✔
1082
      'Finish creating parameter client for remote node = %s.',
1083
      remoteNodeName
1084
    );
1085
    this._parameterClients.push(parameterClient);
103✔
1086

1087
    return parameterClient;
103✔
1088
  }
1089

1090
  /**
1091
   * Create a ParameterWatcher for watching parameter changes on a remote node.
1092
   * @param {string} remoteNodeName - The name of the remote node whose parameters to watch.
1093
   * @param {string[]} parameterNames - Array of parameter names to watch.
1094
   * @param {object} [options] - Options for parameter watcher.
1095
   * @param {number} [options.timeout=5000] - Default timeout in milliseconds for service calls.
1096
   * @return {ParameterWatcher} - An instance of ParameterWatcher.
1097
   */
1098
  createParameterWatcher(remoteNodeName, parameterNames, options = {}) {
56✔
1099
    const watcher = new ParameterWatcher(
57✔
1100
      this,
1101
      remoteNodeName,
1102
      parameterNames,
1103
      options
1104
    );
1105
    debug(
53✔
1106
      'Finish creating parameter watcher for remote node = %s.',
1107
      remoteNodeName
1108
    );
1109
    this._parameterWatchers.push(watcher);
53✔
1110

1111
    return watcher;
53✔
1112
  }
1113

1114
  /**
1115
   * Create a guard condition.
1116
   * @param {Function} callback - The callback to be called when the guard condition is triggered.
1117
   * @return {GuardCondition} - An instance of GuardCondition.
1118
   */
1119
  createGuardCondition(callback) {
1120
    if (typeof callback !== 'function') {
3!
UNCOV
1121
      throw new TypeValidationError('callback', callback, 'function', {
×
1122
        nodeName: this.name(),
1123
        entityType: 'guard_condition',
1124
      });
1125
    }
1126

1127
    let guard = GuardCondition.createGuardCondition(callback, this.context);
3✔
1128
    debug('Finish creating guard condition');
3✔
1129
    this._guards.push(guard);
3✔
1130
    this.syncHandles();
3✔
1131

1132
    return guard;
3✔
1133
  }
1134

1135
  /**
1136
   * Destroy all resource allocated by this node, including
1137
   * <code>Timer</code>s/<code>Publisher</code>s/<code>Subscription</code>s
1138
   * /<code>Client</code>s/<code>Service</code>s
1139
   * @return {undefined}
1140
   */
1141
  destroy() {
1142
    if (this.spinning) {
841✔
1143
      this.stop();
538✔
1144
    }
1145

1146
    // Action servers/clients require manual destruction due to circular reference with goal handles.
1147
    this._actionClients.forEach((actionClient) => actionClient.destroy());
841✔
1148
    this._actionServers.forEach((actionServer) => actionServer.destroy());
841✔
1149

1150
    this._parameterClients.forEach((paramClient) => paramClient.destroy());
841✔
1151
    this._parameterWatchers.forEach((watcher) => watcher.destroy());
841✔
1152

1153
    this.context.onNodeDestroyed(this);
841✔
1154

1155
    if (this._enableRosout) {
841✔
1156
      rclnodejs.finiRosoutPublisherForNode(this.handle);
816✔
1157
      this._enableRosout = false;
816✔
1158
    }
1159

1160
    this.handle.release();
841✔
1161
    this._clock = null;
841✔
1162
    this._timers = [];
841✔
1163
    this._publishers = [];
841✔
1164
    this._subscriptions = [];
841✔
1165
    this._clients = [];
841✔
1166
    this._services = [];
841✔
1167
    this._guards = [];
841✔
1168
    this._actionClients = [];
841✔
1169
    this._actionServers = [];
841✔
1170
    this._parameterClients = [];
841✔
1171
    this._parameterWatchers = [];
841✔
1172

1173
    if (this._rateTimerServer) {
841✔
1174
      this._rateTimerServer.shutdown();
5✔
1175
      this._rateTimerServer = null;
5✔
1176
    }
1177
  }
1178

1179
  /**
1180
   * Destroy a Publisher.
1181
   * @param {Publisher} publisher - The Publisher to be destroyed.
1182
   * @return {undefined}
1183
   */
1184
  destroyPublisher(publisher) {
1185
    if (!(publisher instanceof Publisher)) {
9✔
1186
      throw new TypeValidationError(
2✔
1187
        'publisher',
1188
        publisher,
1189
        'Publisher instance',
1190
        {
1191
          nodeName: this.name(),
1192
        }
1193
      );
1194
    }
1195
    if (publisher.events) {
7!
UNCOV
1196
      publisher.events.forEach((event) => {
×
UNCOV
1197
        this._destroyEntity(event, this._events);
×
1198
      });
UNCOV
1199
      publisher.events = [];
×
1200
    }
1201
    this._destroyEntity(publisher, this._publishers, false);
7✔
1202
  }
1203

1204
  /**
1205
   * Destroy a Subscription.
1206
   * @param {Subscription} subscription - The Subscription to be destroyed.
1207
   * @return {undefined}
1208
   */
1209
  destroySubscription(subscription) {
1210
    if (!(subscription instanceof Subscription)) {
78✔
1211
      throw new TypeValidationError(
2✔
1212
        'subscription',
1213
        subscription,
1214
        'Subscription instance',
1215
        {
1216
          nodeName: this.name(),
1217
        }
1218
      );
1219
    }
1220
    if (subscription.events) {
76✔
1221
      subscription.events.forEach((event) => {
1✔
1222
        this._destroyEntity(event, this._events);
1✔
1223
      });
1224
      subscription.events = [];
1✔
1225
    }
1226

1227
    this._destroyEntity(subscription, this._subscriptions);
76✔
1228
  }
1229

1230
  /**
1231
   * Destroy a Client.
1232
   * @param {Client} client - The Client to be destroyed.
1233
   * @return {undefined}
1234
   */
1235
  destroyClient(client) {
1236
    if (!(client instanceof Client)) {
117✔
1237
      throw new TypeValidationError('client', client, 'Client instance', {
2✔
1238
        nodeName: this.name(),
1239
      });
1240
    }
1241
    this._destroyEntity(client, this._clients);
115✔
1242
  }
1243

1244
  /**
1245
   * Destroy a Service.
1246
   * @param {Service} service - The Service to be destroyed.
1247
   * @return {undefined}
1248
   */
1249
  destroyService(service) {
1250
    if (!(service instanceof Service)) {
8✔
1251
      throw new TypeValidationError('service', service, 'Service instance', {
2✔
1252
        nodeName: this.name(),
1253
      });
1254
    }
1255
    this._destroyEntity(service, this._services);
6✔
1256
  }
1257

1258
  /**
1259
   * Destroy a ParameterClient.
1260
   * @param {ParameterClient} parameterClient - The ParameterClient to be destroyed.
1261
   * @return {undefined}
1262
   */
1263
  destroyParameterClient(parameterClient) {
1264
    if (!(parameterClient instanceof ParameterClient)) {
54!
UNCOV
1265
      throw new TypeError('Invalid argument');
×
1266
    }
1267
    this._removeEntityFromArray(parameterClient, this._parameterClients);
54✔
1268
    parameterClient.destroy();
54✔
1269
  }
1270

1271
  /**
1272
   * Destroy a ParameterWatcher.
1273
   * @param {ParameterWatcher} watcher - The ParameterWatcher to be destroyed.
1274
   * @return {undefined}
1275
   */
1276
  destroyParameterWatcher(watcher) {
1277
    if (!(watcher instanceof ParameterWatcher)) {
1!
UNCOV
1278
      throw new TypeError('Invalid argument');
×
1279
    }
1280
    this._removeEntityFromArray(watcher, this._parameterWatchers);
1✔
1281
    watcher.destroy();
1✔
1282
  }
1283

1284
  /**
1285
   * Destroy a Timer.
1286
   * @param {Timer} timer - The Timer to be destroyed.
1287
   * @return {undefined}
1288
   */
1289
  destroyTimer(timer) {
1290
    if (!(timer instanceof Timer)) {
8✔
1291
      throw new TypeValidationError('timer', timer, 'Timer instance', {
2✔
1292
        nodeName: this.name(),
1293
      });
1294
    }
1295
    this._destroyEntity(timer, this._timers);
6✔
1296
  }
1297

1298
  /**
1299
   * Destroy a guard condition.
1300
   * @param {GuardCondition} guard - The guard condition to be destroyed.
1301
   * @return {undefined}
1302
   */
1303
  destroyGuardCondition(guard) {
1304
    if (!(guard instanceof GuardCondition)) {
3!
UNCOV
1305
      throw new TypeValidationError('guard', guard, 'GuardCondition instance', {
×
1306
        nodeName: this.name(),
1307
      });
1308
    }
1309
    this._destroyEntity(guard, this._guards);
3✔
1310
  }
1311

1312
  /**
1313
   * Get the name of the node.
1314
   * @return {string}
1315
   */
1316
  name() {
1317
    return rclnodejs.getNodeName(this.handle);
4,819✔
1318
  }
1319

1320
  /**
1321
   * Get the namespace of the node.
1322
   * @return {string}
1323
   */
1324
  namespace() {
1325
    return rclnodejs.getNamespace(this.handle);
4,518✔
1326
  }
1327

1328
  /**
1329
   * Get the context in which this node was created.
1330
   * @return {Context}
1331
   */
1332
  get context() {
1333
    return this._context;
6,182✔
1334
  }
1335

1336
  /**
1337
   * Get the nodes logger.
1338
   * @returns {Logger} - The logger for the node.
1339
   */
1340
  getLogger() {
1341
    return this._logger;
146✔
1342
  }
1343

1344
  /**
1345
   * Get the clock used by the node.
1346
   * @returns {Clock} - The nodes clock.
1347
   */
1348
  getClock() {
1349
    return this._clock;
86✔
1350
  }
1351

1352
  /**
1353
   * Get the current time using the node's clock.
1354
   * @returns {Timer} - The current time.
1355
   */
1356
  now() {
1357
    return this.getClock().now();
2✔
1358
  }
1359

1360
  /**
1361
   * Get the list of published topics discovered by the provided node for the remote node name.
1362
   * @param {string} nodeName - The name of the node.
1363
   * @param {string} namespace - The name of the namespace.
1364
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
1365
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1366
   */
1367
  getPublisherNamesAndTypesByNode(nodeName, namespace, noDemangle = false) {
2✔
1368
    return rclnodejs.getPublisherNamesAndTypesByNode(
2✔
1369
      this.handle,
1370
      nodeName,
1371
      namespace,
1372
      noDemangle
1373
    );
1374
  }
1375

1376
  /**
1377
   * Get the list of published topics discovered by the provided node for the remote node name.
1378
   * @param {string} nodeName - The name of the node.
1379
   * @param {string} namespace - The name of the namespace.
1380
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
1381
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1382
   */
1383
  getSubscriptionNamesAndTypesByNode(nodeName, namespace, noDemangle = false) {
×
UNCOV
1384
    return rclnodejs.getSubscriptionNamesAndTypesByNode(
×
1385
      this.handle,
1386
      nodeName,
1387
      namespace,
1388
      noDemangle
1389
    );
1390
  }
1391

1392
  /**
1393
   * Get service names and types for which a remote node has servers.
1394
   * @param {string} nodeName - The name of the node.
1395
   * @param {string} namespace - The name of the namespace.
1396
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1397
   */
1398
  getServiceNamesAndTypesByNode(nodeName, namespace) {
UNCOV
1399
    return rclnodejs.getServiceNamesAndTypesByNode(
×
1400
      this.handle,
1401
      nodeName,
1402
      namespace
1403
    );
1404
  }
1405

1406
  /**
1407
   * Get service names and types for which a remote node has clients.
1408
   * @param {string} nodeName - The name of the node.
1409
   * @param {string} namespace - The name of the namespace.
1410
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1411
   */
1412
  getClientNamesAndTypesByNode(nodeName, namespace) {
UNCOV
1413
    return rclnodejs.getClientNamesAndTypesByNode(
×
1414
      this.handle,
1415
      nodeName,
1416
      namespace
1417
    );
1418
  }
1419

1420
  /**
1421
   * Get the list of topics discovered by the provided node.
1422
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
1423
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1424
   */
1425
  getTopicNamesAndTypes(noDemangle = false) {
×
UNCOV
1426
    return rclnodejs.getTopicNamesAndTypes(this.handle, noDemangle);
×
1427
  }
1428

1429
  /**
1430
   * Get the list of services discovered by the provided node.
1431
   * @return {Array<{name: string, types: Array<string>}>} - An array of the names and types.
1432
   */
1433
  getServiceNamesAndTypes() {
1434
    return rclnodejs.getServiceNamesAndTypes(this.handle);
2✔
1435
  }
1436

1437
  /**
1438
   * Return a list of publishers on a given topic.
1439
   *
1440
   * The returned parameter is a list of TopicEndpointInfo objects, where each will contain
1441
   * the node name, node namespace, topic type, topic endpoint's GID, and its QoS profile.
1442
   *
1443
   * When the `no_mangle` parameter is `true`, the provided `topic` should be a valid
1444
   * topic name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
1445
   * apps).  When the `no_mangle` parameter is `false`, the provided `topic` should
1446
   * follow ROS topic name conventions.
1447
   *
1448
   * `topic` may be a relative, private, or fully qualified topic name.
1449
   *  A relative or private topic will be expanded using this node's namespace and name.
1450
   *  The queried `topic` is not remapped.
1451
   *
1452
   * @param {string} topic - The topic on which to find the publishers.
1453
   * @param {boolean} [noDemangle=false] - If `true`, `topic` needs to be a valid middleware topic
1454
   *                               name, otherwise it should be a valid ROS topic name. Defaults to `false`.
1455
   * @returns {Array} - list of publishers
1456
   */
1457
  getPublishersInfoByTopic(topic, noDemangle = false) {
1✔
1458
    return rclnodejs.getPublishersInfoByTopic(
4✔
1459
      this.handle,
1460
      this._getValidatedTopic(topic, noDemangle),
1461
      noDemangle
1462
    );
1463
  }
1464

1465
  /**
1466
   * Return a list of subscriptions on a given topic.
1467
   *
1468
   * The returned parameter is a list of TopicEndpointInfo objects, where each will contain
1469
   * the node name, node namespace, topic type, topic endpoint's GID, and its QoS profile.
1470
   *
1471
   * When the `no_mangle` parameter is `true`, the provided `topic` should be a valid
1472
   * topic name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
1473
   * apps).  When the `no_mangle` parameter is `false`, the provided `topic` should
1474
   * follow ROS topic name conventions.
1475
   *
1476
   * `topic` may be a relative, private, or fully qualified topic name.
1477
   *  A relative or private topic will be expanded using this node's namespace and name.
1478
   *  The queried `topic` is not remapped.
1479
   *
1480
   * @param {string} topic - The topic on which to find the subscriptions.
1481
   * @param {boolean} [noDemangle=false] -  If `true`, `topic` needs to be a valid middleware topic
1482
                                    name, otherwise it should be a valid ROS topic name. Defaults to `false`.
1483
   * @returns {Array} - list of subscriptions
1484
   */
1485
  getSubscriptionsInfoByTopic(topic, noDemangle = false) {
×
1486
    return rclnodejs.getSubscriptionsInfoByTopic(
2✔
1487
      this.handle,
1488
      this._getValidatedTopic(topic, noDemangle),
1489
      noDemangle
1490
    );
1491
  }
1492

1493
  /**
1494
   * Return a list of clients on a given service.
1495
   *
1496
   * The returned parameter is a list of ServiceEndpointInfo objects, where each will contain
1497
   * the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
1498
   *
1499
   * When the `no_mangle` parameter is `true`, the provided `service` should be a valid
1500
   * service name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
1501
   * apps).  When the `no_mangle` parameter is `false`, the provided `service` should
1502
   * follow ROS service name conventions.
1503
   *
1504
   * `service` may be a relative, private, or fully qualified service name.
1505
   *  A relative or private service will be expanded using this node's namespace and name.
1506
   *  The queried `service` is not remapped.
1507
   *
1508
   * @param {string} service - The service on which to find the clients.
1509
   * @param {boolean} [noDemangle=false] - If `true`, `service` needs to be a valid middleware service
1510
   *                               name, otherwise it should be a valid ROS service name. Defaults to `false`.
1511
   * @returns {Array} - list of clients
1512
   */
1513
  getClientsInfoByService(service, noDemangle = false) {
×
1514
    if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
2!
UNCOV
1515
      console.warn(
×
1516
        'getClientsInfoByService is not supported by this version of ROS 2'
1517
      );
UNCOV
1518
      return null;
×
1519
    }
1520
    return rclnodejs.getClientsInfoByService(
2✔
1521
      this.handle,
1522
      this._getValidatedServiceName(service, noDemangle),
1523
      noDemangle
1524
    );
1525
  }
1526

1527
  /**
1528
   * Return a list of servers on a given service.
1529
   *
1530
   * The returned parameter is a list of ServiceEndpointInfo objects, where each will contain
1531
   * the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
1532
   *
1533
   * When the `no_mangle` parameter is `true`, the provided `service` should be a valid
1534
   * service name for the middleware (useful when combining ROS with native middleware (e.g. DDS)
1535
   * apps).  When the `no_mangle` parameter is `false`, the provided `service` should
1536
   * follow ROS service name conventions.
1537
   *
1538
   * `service` may be a relative, private, or fully qualified service name.
1539
   *  A relative or private service will be expanded using this node's namespace and name.
1540
   *  The queried `service` is not remapped.
1541
   *
1542
   * @param {string} service - The service on which to find the servers.
1543
   * @param {boolean} [noDemangle=false] - If `true`, `service` needs to be a valid middleware service
1544
   *                               name, otherwise it should be a valid ROS service name. Defaults to `false`.
1545
   * @returns {Array} - list of servers
1546
   */
1547
  getServersInfoByService(service, noDemangle = false) {
×
1548
    if (DistroUtils.getDistroId() < DistroUtils.DistroId.ROLLING) {
2!
UNCOV
1549
      console.warn(
×
1550
        'getServersInfoByService is not supported by this version of ROS 2'
1551
      );
UNCOV
1552
      return null;
×
1553
    }
1554
    return rclnodejs.getServersInfoByService(
2✔
1555
      this.handle,
1556
      this._getValidatedServiceName(service, noDemangle),
1557
      noDemangle
1558
    );
1559
  }
1560

1561
  /**
1562
   * Get the list of nodes discovered by the provided node.
1563
   * @return {Array<string>} - An array of the names.
1564
   */
1565
  getNodeNames() {
1566
    return this.getNodeNamesAndNamespaces().map((item) => item.name);
41✔
1567
  }
1568

1569
  /**
1570
   * Get the list of nodes and their namespaces discovered by the provided node.
1571
   * @return {Array<{name: string, namespace: string}>} An array of the names and namespaces.
1572
   */
1573
  getNodeNamesAndNamespaces() {
1574
    return rclnodejs.getNodeNames(this.handle, /*getEnclaves=*/ false);
17✔
1575
  }
1576

1577
  /**
1578
   * Get the list of nodes and their namespaces with enclaves discovered by the provided node.
1579
   * @return {Array<{name: string, namespace: string, enclave: string}>} An array of the names, namespaces and enclaves.
1580
   */
1581
  getNodeNamesAndNamespacesWithEnclaves() {
1582
    return rclnodejs.getNodeNames(this.handle, /*getEnclaves=*/ true);
1✔
1583
  }
1584

1585
  /**
1586
   * Return the number of publishers on a given topic.
1587
   * @param {string} topic - The name of the topic.
1588
   * @returns {number} - Number of publishers on the given topic.
1589
   */
1590
  countPublishers(topic) {
1591
    let expandedTopic = rclnodejs.expandTopicName(
6✔
1592
      topic,
1593
      this.name(),
1594
      this.namespace()
1595
    );
1596
    rclnodejs.validateTopicName(expandedTopic);
6✔
1597

1598
    return rclnodejs.countPublishers(this.handle, expandedTopic);
6✔
1599
  }
1600

1601
  /**
1602
   * Return the number of subscribers on a given topic.
1603
   * @param {string} topic - The name of the topic.
1604
   * @returns {number} - Number of subscribers on the given topic.
1605
   */
1606
  countSubscribers(topic) {
1607
    let expandedTopic = rclnodejs.expandTopicName(
6✔
1608
      topic,
1609
      this.name(),
1610
      this.namespace()
1611
    );
1612
    rclnodejs.validateTopicName(expandedTopic);
6✔
1613

1614
    return rclnodejs.countSubscribers(this.handle, expandedTopic);
6✔
1615
  }
1616

1617
  /**
1618
   * Get the number of clients on a given service name.
1619
   * @param {string} serviceName - the service name
1620
   * @returns {Number}
1621
   */
1622
  countClients(serviceName) {
1623
    if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
2!
UNCOV
1624
      console.warn('countClients is not supported by this version of ROS 2');
×
UNCOV
1625
      return null;
×
1626
    }
1627
    return rclnodejs.countClients(this.handle, serviceName);
2✔
1628
  }
1629

1630
  /**
1631
   * Get the number of services on a given service name.
1632
   * @param {string} serviceName - the service name
1633
   * @returns {Number}
1634
   */
1635
  countServices(serviceName) {
1636
    if (DistroUtils.getDistroId() <= DistroUtils.getDistroId('humble')) {
1!
UNCOV
1637
      console.warn('countServices is not supported by this version of ROS 2');
×
UNCOV
1638
      return null;
×
1639
    }
1640
    return rclnodejs.countServices(this.handle, serviceName);
1✔
1641
  }
1642

1643
  /**
1644
   * Get the list of parameter-overrides found on the commandline and
1645
   * in the NodeOptions.parameter_overrides property.
1646
   *
1647
   * @return {Array<Parameter>} - An array of Parameters.
1648
   */
1649
  getParameterOverrides() {
1650
    return Array.from(this._parameterOverrides.values());
8✔
1651
  }
1652

1653
  /**
1654
   * Declare a parameter.
1655
   *
1656
   * Internally, register a parameter and it's descriptor.
1657
   * If a parameter-override exists, it's value will replace that of the parameter
1658
   * unless ignoreOverride is true.
1659
   * If the descriptor is undefined, then a ParameterDescriptor will be inferred
1660
   * from the parameter's state.
1661
   *
1662
   * If a parameter by the same name has already been declared then an Error is thrown.
1663
   * A parameter must be undeclared before attempting to redeclare it.
1664
   *
1665
   * @param {Parameter} parameter - Parameter to declare.
1666
   * @param {ParameterDescriptor} [descriptor] - Optional descriptor for parameter.
1667
   * @param {boolean} [ignoreOverride] - When true disregard any parameter-override that may be present.
1668
   * @return {Parameter} - The newly declared parameter.
1669
   */
1670
  declareParameter(parameter, descriptor, ignoreOverride = false) {
2,216✔
1671
    const parameters = this.declareParameters(
2,217✔
1672
      [parameter],
1673
      descriptor ? [descriptor] : [],
2,217✔
1674
      ignoreOverride
1675
    );
1676
    return parameters.length == 1 ? parameters[0] : null;
2,217!
1677
  }
1678

1679
  /**
1680
   * Declare a list of parameters.
1681
   *
1682
   * Internally register parameters with their corresponding descriptor one by one
1683
   * in the order they are provided. This is an atomic operation. If an error
1684
   * occurs the process halts and no further parameters are declared.
1685
   * Parameters that have already been processed are undeclared.
1686
   *
1687
   * While descriptors is an optional parameter, when provided there must be
1688
   * a descriptor for each parameter; otherwise an Error is thrown.
1689
   * If descriptors is not provided then a descriptor will be inferred
1690
   * from each parameter's state.
1691
   *
1692
   * When a parameter-override is available, the parameter's value
1693
   * will be replaced with that of the parameter-override unless ignoreOverrides
1694
   * is true.
1695
   *
1696
   * If a parameter by the same name has already been declared then an Error is thrown.
1697
   * A parameter must be undeclared before attempting to redeclare it.
1698
   *
1699
   * Prior to declaring the parameters each SetParameterEventCallback registered
1700
   * using setOnParameterEventCallback() is called in succession with the parameters
1701
   * list. Any SetParameterEventCallback that retuns does not return a successful
1702
   * result will cause the entire operation to terminate with no changes to the
1703
   * parameters. When all SetParameterEventCallbacks return successful then the
1704
   * list of parameters is updated.
1705
   *
1706
   * @param {Parameter[]} parameters - The parameters to declare.
1707
   * @param {ParameterDescriptor[]} [descriptors] - Optional descriptors,
1708
   *    a 1-1 correspondence with parameters.
1709
   * @param {boolean} ignoreOverrides - When true, parameter-overrides are
1710
   *    not considered, i.e.,ignored.
1711
   * @return {Parameter[]} - The declared parameters.
1712
   */
1713
  declareParameters(parameters, descriptors = [], ignoreOverrides = false) {
×
1714
    if (!Array.isArray(parameters)) {
2,217!
UNCOV
1715
      throw new TypeValidationError('parameters', parameters, 'Array', {
×
1716
        nodeName: this.name(),
1717
      });
1718
    }
1719
    if (!Array.isArray(descriptors)) {
2,217!
UNCOV
1720
      throw new TypeValidationError('descriptors', descriptors, 'Array', {
×
1721
        nodeName: this.name(),
1722
      });
1723
    }
1724
    if (descriptors.length > 0 && parameters.length !== descriptors.length) {
2,217!
UNCOV
1725
      throw new ValidationError(
×
1726
        'Each parameter must have a corresponding ParameterDescriptor',
1727
        {
1728
          code: 'PARAMETER_DESCRIPTOR_MISMATCH',
1729
          argumentName: 'descriptors',
1730
          providedValue: descriptors.length,
1731
          expectedType: `Array with length ${parameters.length}`,
1732
          nodeName: this.name(),
1733
        }
1734
      );
1735
    }
1736

1737
    const declaredDescriptors = [];
2,217✔
1738
    const declaredParameters = [];
2,217✔
1739
    const declaredParameterCollisions = [];
2,217✔
1740
    for (let i = 0; i < parameters.length; i++) {
2,217✔
1741
      let parameter =
1742
        !ignoreOverrides && this._parameterOverrides.has(parameters[i].name)
2,217✔
1743
          ? this._parameterOverrides.get(parameters[i].name)
1744
          : parameters[i];
1745

1746
      // stop processing parameters that have already been declared
1747
      if (this._parameters.has(parameter.name)) {
2,217!
UNCOV
1748
        declaredParameterCollisions.push(parameter);
×
UNCOV
1749
        continue;
×
1750
      }
1751

1752
      // create descriptor for parameter if not provided
1753
      let descriptor =
1754
        descriptors.length > 0
2,217✔
1755
          ? descriptors[i]
1756
          : ParameterDescriptor.fromParameter(parameter);
1757

1758
      descriptor.validate();
2,217✔
1759

1760
      declaredDescriptors.push(descriptor);
2,217✔
1761
      declaredParameters.push(parameter);
2,217✔
1762
    }
1763

1764
    if (declaredParameterCollisions.length > 0) {
2,217!
1765
      const errorMsg =
1766
        declaredParameterCollisions.length == 1
×
1767
          ? `Parameter(${declaredParameterCollisions[0]}) already declared.`
1768
          : `Multiple parameters already declared, e.g., Parameter(${declaredParameterCollisions[0]}).`;
UNCOV
1769
      throw new Error(errorMsg);
×
1770
    }
1771

1772
    // register descriptor
1773
    for (const descriptor of declaredDescriptors) {
2,217✔
1774
      this._parameterDescriptors.set(descriptor.name, descriptor);
2,217✔
1775
    }
1776

1777
    const result = this._setParametersAtomically(declaredParameters, true);
2,217✔
1778
    if (!result.successful) {
2,217!
1779
      // unregister descriptors
UNCOV
1780
      for (const descriptor of declaredDescriptors) {
×
UNCOV
1781
        this._parameterDescriptors.delete(descriptor.name);
×
1782
      }
1783

1784
      throw new Error(result.reason);
×
1785
    }
1786

1787
    return this.getParameters(declaredParameters.map((param) => param.name));
2,217✔
1788
  }
1789

1790
  /**
1791
   * Undeclare a parameter.
1792
   *
1793
   * Readonly parameters can not be undeclared or updated.
1794
   * @param {string} name - Name of parameter to undeclare.
1795
   * @return {undefined} -
1796
   */
1797
  undeclareParameter(name) {
1798
    if (!this.hasParameter(name)) return;
1!
1799

1800
    const descriptor = this.getParameterDescriptor(name);
1✔
1801
    if (descriptor.readOnly) {
1!
1802
      throw new Error(
×
1803
        `${name} parameter is read-only and can not be undeclared`
1804
      );
1805
    }
1806

1807
    this._parameters.delete(name);
1✔
1808
    this._parameterDescriptors.delete(name);
1✔
1809
  }
1810

1811
  /**
1812
   * Determine if a parameter has been declared.
1813
   * @param {string} name - name of parameter
1814
   * @returns {boolean} - Return true if parameter is declared; false otherwise.
1815
   */
1816
  hasParameter(name) {
1817
    return this._parameters.has(name);
5,559✔
1818
  }
1819

1820
  /**
1821
   * Get a declared parameter by name.
1822
   *
1823
   * If unable to locate a declared parameter then a
1824
   * parameter with type == PARAMETER_NOT_SET is returned.
1825
   *
1826
   * @param {string} name - The name of the parameter.
1827
   * @return {Parameter} - The parameter.
1828
   */
1829
  getParameter(name) {
1830
    return this.getParameters([name])[0];
1,656✔
1831
  }
1832

1833
  /**
1834
   * Get a list of parameters.
1835
   *
1836
   * Find and return the declared parameters.
1837
   * If no names are provided return all declared parameters.
1838
   *
1839
   * If unable to locate a declared parameter then a
1840
   * parameter with type == PARAMETER_NOT_SET is returned in
1841
   * it's place.
1842
   *
1843
   * @param {string[]} [names] - The names of the declared parameters
1844
   *    to find or null indicating to return all declared parameters.
1845
   * @return {Parameter[]} - The parameters.
1846
   */
1847
  getParameters(names = []) {
12✔
1848
    let params = [];
3,911✔
1849

1850
    if (names.length == 0) {
3,911✔
1851
      // get all parameters
1852
      params = [...this._parameters.values()];
12✔
1853
      return params;
12✔
1854
    }
1855

1856
    for (const name of names) {
3,899✔
1857
      const param = this.hasParameter(name)
3,906✔
1858
        ? this._parameters.get(name)
1859
        : new Parameter(name, ParameterType.PARAMETER_NOT_SET);
1860

1861
      params.push(param);
3,906✔
1862
    }
1863

1864
    return params;
3,899✔
1865
  }
1866

1867
  /**
1868
   * Get the types of given parameters.
1869
   *
1870
   * Return the types of given parameters.
1871
   *
1872
   * @param {string[]} [names] - The names of the declared parameters.
1873
   * @return {Uint8Array} - The types.
1874
   */
1875
  getParameterTypes(names = []) {
×
1876
    let types = [];
3✔
1877

1878
    for (const name of names) {
3✔
1879
      const descriptor = this._parameterDescriptors.get(name);
7✔
1880
      if (descriptor) {
7!
1881
        types.push(descriptor.type);
7✔
1882
      }
1883
    }
1884
    return types;
3✔
1885
  }
1886

1887
  /**
1888
   * Get the names of all declared parameters.
1889
   *
1890
   * @return {Array<string>} - The declared parameter names or empty array if
1891
   *    no parameters have been declared.
1892
   */
1893
  getParameterNames() {
1894
    return this.getParameters().map((param) => param.name);
53✔
1895
  }
1896

1897
  /**
1898
   * Determine if a parameter descriptor exists.
1899
   *
1900
   * @param {string} name - The name of a descriptor to for.
1901
   * @return {boolean} - true if a descriptor has been declared; otherwise false.
1902
   */
1903
  hasParameterDescriptor(name) {
1904
    return !!this.getParameterDescriptor(name);
2,242✔
1905
  }
1906

1907
  /**
1908
   * Get a declared parameter descriptor by name.
1909
   *
1910
   * If unable to locate a declared parameter descriptor then a
1911
   * descriptor with type == PARAMETER_NOT_SET is returned.
1912
   *
1913
   * @param {string} name - The name of the parameter descriptor to find.
1914
   * @return {ParameterDescriptor} - The parameter descriptor.
1915
   */
1916
  getParameterDescriptor(name) {
1917
    return this.getParameterDescriptors([name])[0];
4,485✔
1918
  }
1919

1920
  /**
1921
   * Find a list of declared ParameterDescriptors.
1922
   *
1923
   * If no names are provided return all declared descriptors.
1924
   *
1925
   * If unable to locate a declared descriptor then a
1926
   * descriptor with type == PARAMETER_NOT_SET is returned in
1927
   * it's place.
1928
   *
1929
   * @param {string[]} [names] - The names of the declared parameter
1930
   *    descriptors to find or null indicating to return all declared descriptors.
1931
   * @return {ParameterDescriptor[]} - The parameter descriptors.
1932
   */
1933
  getParameterDescriptors(names = []) {
×
1934
    let descriptors = [];
4,488✔
1935

1936
    if (names.length == 0) {
4,488!
1937
      // get all parameters
UNCOV
1938
      descriptors = [...this._parameterDescriptors.values()];
×
UNCOV
1939
      return descriptors;
×
1940
    }
1941

1942
    for (const name of names) {
4,488✔
1943
      let descriptor = this._parameterDescriptors.get(name);
4,490✔
1944
      if (!descriptor) {
4,490!
UNCOV
1945
        descriptor = new ParameterDescriptor(
×
1946
          name,
1947
          ParameterType.PARAMETER_NOT_SET
1948
        );
1949
      }
1950
      descriptors.push(descriptor);
4,490✔
1951
    }
1952

1953
    return descriptors;
4,488✔
1954
  }
1955

1956
  /**
1957
   * Replace a declared parameter.
1958
   *
1959
   * The parameter being replaced must be a declared parameter who's descriptor
1960
   * is not readOnly; otherwise an Error is thrown.
1961
   *
1962
   * @param {Parameter} parameter - The new parameter.
1963
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult} - The result of the operation.
1964
   */
1965
  setParameter(parameter) {
1966
    const results = this.setParameters([parameter]);
10✔
1967
    return results[0];
10✔
1968
  }
1969

1970
  /**
1971
   * Replace a list of declared parameters.
1972
   *
1973
   * Declared parameters are replaced in the order they are provided and
1974
   * a ParameterEvent is published for each individual parameter change.
1975
   *
1976
   * Prior to setting the parameters each SetParameterEventCallback registered
1977
   * using setOnParameterEventCallback() is called in succession with the parameters
1978
   * list. Any SetParameterEventCallback that retuns does not return a successful
1979
   * result will cause the entire operation to terminate with no changes to the
1980
   * parameters. When all SetParameterEventCallbacks return successful then the
1981
   * list of parameters is updated.
1982
   *
1983
   * If an error occurs, the process is stopped and returned. Parameters
1984
   * set before an error remain unchanged.
1985
   *
1986
   * @param {Parameter[]} parameters - The parameters to set.
1987
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult[]} - A list of SetParameterResult, one for each parameter that was set.
1988
   */
1989
  setParameters(parameters = []) {
×
1990
    return parameters.map((parameter) =>
21✔
1991
      this.setParametersAtomically([parameter])
24✔
1992
    );
1993
  }
1994

1995
  /**
1996
   * Repalce a list of declared parameters atomically.
1997
   *
1998
   * Declared parameters are replaced in the order they are provided.
1999
   * A single ParameterEvent is published collectively for all changed
2000
   * parameters.
2001
   *
2002
   * Prior to setting the parameters each SetParameterEventCallback registered
2003
   * using setOnParameterEventCallback() is called in succession with the parameters
2004
   * list. Any SetParameterEventCallback that retuns does not return a successful
2005
   * result will cause the entire operation to terminate with no changes to the
2006
   * parameters. When all SetParameterEventCallbacks return successful then the
2007
   * list of parameters is updated.d
2008
   *
2009
   * If an error occurs, the process stops immediately. All parameters updated to
2010
   * the point of the error are reverted to their previous state.
2011
   *
2012
   * @param {Parameter[]} parameters - The parameters to set.
2013
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult} - describes the result of setting 1 or more parameters.
2014
   */
2015
  setParametersAtomically(parameters = []) {
×
2016
    return this._setParametersAtomically(parameters);
25✔
2017
  }
2018

2019
  /**
2020
   * Internal method for updating parameters atomically.
2021
   *
2022
   * Prior to setting the parameters each SetParameterEventCallback registered
2023
   * using setOnParameterEventCallback() is called in succession with the parameters
2024
   * list. Any SetParameterEventCallback that retuns does not return a successful
2025
   * result will cause the entire operation to terminate with no changes to the
2026
   * parameters. When all SetParameterEventCallbacks return successful then the
2027
   * list of parameters is updated.
2028
   *
2029
   * @param {Paramerter[]} parameters - The parameters to update.
2030
   * @param {boolean} declareParameterMode - When true parameters are being declared;
2031
   *    otherwise they are being changed.
2032
   * @return {SetParameterResult} - A single collective result.
2033
   */
2034
  _setParametersAtomically(parameters = [], declareParameterMode = false) {
25!
2035
    let result = this._validateParameters(parameters, declareParameterMode);
2,242✔
2036
    if (!result.successful) {
2,242!
UNCOV
2037
      return result;
×
2038
    }
2039

2040
    // give all SetParametersCallbacks a chance to veto this change
2041
    for (const callback of this._setParametersCallbacks) {
2,242✔
2042
      result = callback(parameters);
1,431✔
2043
      if (!result.successful) {
1,431✔
2044
        // a callback has vetoed a parameter change
2045
        return result;
1✔
2046
      }
2047
    }
2048

2049
    // collectively track updates to parameters for use
2050
    // when publishing a ParameterEvent
2051
    const newParameters = [];
2,241✔
2052
    const changedParameters = [];
2,241✔
2053
    const deletedParameters = [];
2,241✔
2054

2055
    for (const parameter of parameters) {
2,241✔
2056
      if (parameter.type == ParameterType.PARAMETER_NOT_SET) {
2,241✔
2057
        this.undeclareParameter(parameter.name);
1✔
2058
        deletedParameters.push(parameter);
1✔
2059
      } else {
2060
        this._parameters.set(parameter.name, parameter);
2,240✔
2061
        if (declareParameterMode) {
2,240✔
2062
          newParameters.push(parameter);
2,217✔
2063
        } else {
2064
          changedParameters.push(parameter);
23✔
2065
        }
2066
      }
2067
    }
2068

2069
    // create ParameterEvent
2070
    const parameterEvent = new (loader.loadInterface(
2,241✔
2071
      PARAMETER_EVENT_MSG_TYPE
2072
    ))();
2073

2074
    const { seconds, nanoseconds } = this._clock.now().secondsAndNanoseconds;
2,241✔
2075
    parameterEvent.stamp = {
2,241✔
2076
      sec: Number(seconds),
2077
      nanosec: Number(nanoseconds),
2078
    };
2079

2080
    parameterEvent.node =
2,241✔
2081
      this.namespace() === '/'
2,241✔
2082
        ? this.namespace() + this.name()
2083
        : this.namespace() + '/' + this.name();
2084

2085
    if (newParameters.length > 0) {
2,241✔
2086
      parameterEvent['new_parameters'] = newParameters.map((parameter) =>
2,217✔
2087
        parameter.toParameterMessage()
2,217✔
2088
      );
2089
    }
2090
    if (changedParameters.length > 0) {
2,241✔
2091
      parameterEvent['changed_parameters'] = changedParameters.map(
23✔
2092
        (parameter) => parameter.toParameterMessage()
23✔
2093
      );
2094
    }
2095
    if (deletedParameters.length > 0) {
2,241✔
2096
      parameterEvent['deleted_parameters'] = deletedParameters.map(
1✔
2097
        (parameter) => parameter.toParameterMessage()
1✔
2098
      );
2099
    }
2100

2101
    // Publish ParameterEvent.
2102
    this._parameterEventPublisher.publish(parameterEvent);
2,241✔
2103

2104
    return {
2,241✔
2105
      successful: true,
2106
      reason: '',
2107
    };
2108
  }
2109

2110
  /**
2111
   * This callback is called when declaring a parameter or setting a parameter.
2112
   * The callback is provided a list of parameters and returns a SetParameterResult
2113
   * to indicate approval or veto of the operation.
2114
   *
2115
   * @callback SetParametersCallback
2116
   * @param {Parameter[]} parameters - The message published
2117
   * @returns {rcl_interfaces.msg.SetParameterResult} -
2118
   *
2119
   * @see [Node.addOnSetParametersCallback]{@link Node#addOnSetParametersCallback}
2120
   * @see [Node.removeOnSetParametersCallback]{@link Node#removeOnSetParametersCallback}
2121
   */
2122

2123
  /**
2124
   * Add a callback to the front of the list of callbacks invoked for parameter declaration
2125
   * and setting. No checks are made for duplicate callbacks.
2126
   *
2127
   * @param {SetParametersCallback} callback - The callback to add.
2128
   * @returns {undefined}
2129
   */
2130
  addOnSetParametersCallback(callback) {
2131
    this._setParametersCallbacks.unshift(callback);
835✔
2132
  }
2133

2134
  /**
2135
   * Remove a callback from the list of SetParametersCallbacks.
2136
   * If the callback is not found the process is a nop.
2137
   *
2138
   * @param {SetParametersCallback} callback - The callback to be removed
2139
   * @returns {undefined}
2140
   */
2141
  removeOnSetParametersCallback(callback) {
2142
    const idx = this._setParametersCallbacks.indexOf(callback);
2✔
2143
    if (idx > -1) {
2!
2144
      this._setParametersCallbacks.splice(idx, 1);
2✔
2145
    }
2146
  }
2147

2148
  /**
2149
   * Get the fully qualified name of the node.
2150
   *
2151
   * @returns {string} - String containing the fully qualified name of the node.
2152
   */
2153
  getFullyQualifiedName() {
2154
    return rclnodejs.getFullyQualifiedName(this.handle);
1✔
2155
  }
2156

2157
  /**
2158
   * Get the RMW implementation identifier
2159
   * @returns {string} - The RMW implementation identifier.
2160
   */
2161
  getRMWImplementationIdentifier() {
2162
    return rclnodejs.getRMWImplementationIdentifier();
1✔
2163
  }
2164

2165
  /**
2166
   * Return a topic name expanded and remapped.
2167
   * @param {string} topicName - Topic name to be expanded and remapped.
2168
   * @param {boolean} [onlyExpand=false] - If `true`, remapping rules won't be applied.
2169
   * @returns {string} - A fully qualified topic name.
2170
   */
2171
  resolveTopicName(topicName, onlyExpand = false) {
2✔
2172
    if (typeof topicName !== 'string') {
3!
UNCOV
2173
      throw new TypeValidationError('topicName', topicName, 'string', {
×
2174
        nodeName: this.name(),
2175
      });
2176
    }
2177
    return rclnodejs.resolveName(
3✔
2178
      this.handle,
2179
      topicName,
2180
      onlyExpand,
2181
      /*isService=*/ false
2182
    );
2183
  }
2184

2185
  /**
2186
   * Return a service name expanded and remapped.
2187
   * @param {string} service - Service name to be expanded and remapped.
2188
   * @param {boolean} [onlyExpand=false] - If `true`, remapping rules won't be applied.
2189
   * @returns {string} - A fully qualified service name.
2190
   */
2191
  resolveServiceName(service, onlyExpand = false) {
6✔
2192
    if (typeof service !== 'string') {
7!
UNCOV
2193
      throw new TypeValidationError('service', service, 'string', {
×
2194
        nodeName: this.name(),
2195
      });
2196
    }
2197
    return rclnodejs.resolveName(
7✔
2198
      this.handle,
2199
      service,
2200
      onlyExpand,
2201
      /*isService=*/ true
2202
    );
2203
  }
2204

2205
  // returns on 1st error or result {successful, reason}
2206
  _validateParameters(parameters = [], declareParameterMode = false) {
×
2207
    for (const parameter of parameters) {
2,242✔
2208
      // detect invalid parameter
2209
      try {
2,242✔
2210
        parameter.validate();
2,242✔
2211
      } catch {
UNCOV
2212
        return {
×
2213
          successful: false,
2214
          reason: `Invalid ${parameter.name}`,
2215
        };
2216
      }
2217

2218
      // detect undeclared parameter
2219
      if (!this.hasParameterDescriptor(parameter.name)) {
2,242!
UNCOV
2220
        return {
×
2221
          successful: false,
2222
          reason: `Parameter ${parameter.name} has not been declared`,
2223
        };
2224
      }
2225

2226
      // detect readonly parameter that can not be updated
2227
      const descriptor = this.getParameterDescriptor(parameter.name);
2,242✔
2228
      if (!declareParameterMode && descriptor.readOnly) {
2,242!
UNCOV
2229
        return {
×
2230
          successful: false,
2231
          reason: `Parameter ${parameter.name} is readonly`,
2232
        };
2233
      }
2234

2235
      // validate parameter against descriptor if not an undeclare action
2236
      if (parameter.type != ParameterType.PARAMETER_NOT_SET) {
2,242✔
2237
        try {
2,241✔
2238
          descriptor.validateParameter(parameter);
2,241✔
2239
        } catch {
UNCOV
2240
          return {
×
2241
            successful: false,
2242
            reason: `Parameter ${parameter.name} does not  readonly`,
2243
          };
2244
        }
2245
      }
2246
    }
2247

2248
    return {
2,242✔
2249
      successful: true,
2250
      reason: null,
2251
    };
2252
  }
2253

2254
  // Get a Map(nodeName->Parameter[]) of CLI parameter args that
2255
  // apply to 'this' node, .e.g., -p mynode:foo:=bar -p hello:=world
2256
  _getNativeParameterOverrides() {
2257
    const overrides = new Map();
818✔
2258

2259
    // Get native parameters from rcl context->global_arguments.
2260
    // rclnodejs returns an array of objects, 1 for each node e.g., -p my_node:foo:=bar,
2261
    // and a node named '/**' for global parameter rules,
2262
    // i.e., does not include a node identifier, e.g., -p color:=red
2263
    // {
2264
    //   name: string // node name
2265
    //   parameters[] = {
2266
    //     name: string
2267
    //     type: uint
2268
    //     value: object
2269
    // }
2270
    const cliParamOverrideData = rclnodejs.getParameterOverrides(
818✔
2271
      this.context.handle
2272
    );
2273

2274
    // convert native CLI parameterOverrides to Map<nodeName,Array<ParameterOverride>>
2275
    const cliParamOverrides = new Map();
818✔
2276
    if (cliParamOverrideData) {
818✔
2277
      for (let nodeParamData of cliParamOverrideData) {
8✔
2278
        const nodeName = nodeParamData.name;
12✔
2279
        const nodeParamOverrides = [];
12✔
2280
        for (let paramData of nodeParamData.parameters) {
12✔
2281
          const paramOverride = new Parameter(
17✔
2282
            paramData.name,
2283
            paramData.type,
2284
            paramData.value
2285
          );
2286
          nodeParamOverrides.push(paramOverride);
17✔
2287
        }
2288
        cliParamOverrides.set(nodeName, nodeParamOverrides);
12✔
2289
      }
2290
    }
2291

2292
    // collect global CLI global parameters, name == /**
2293
    let paramOverrides = cliParamOverrides.get('/**'); // array of ParameterOverrides
818✔
2294
    if (paramOverrides) {
818✔
2295
      for (const parameter of paramOverrides) {
5✔
2296
        overrides.set(parameter.name, parameter);
6✔
2297
      }
2298
    }
2299

2300
    // merge CLI node parameterOverrides with global parameterOverrides, replace existing
2301
    paramOverrides = cliParamOverrides.get(this.name()); // array of ParameterOverrides
818✔
2302
    if (paramOverrides) {
818✔
2303
      for (const parameter of paramOverrides) {
5✔
2304
        overrides.set(parameter.name, parameter);
7✔
2305
      }
2306
    }
2307

2308
    return overrides;
818✔
2309
  }
2310

2311
  /**
2312
   * Invokes the callback with a raw message of the given type. After the callback completes
2313
   * the message will be destroyed.
2314
   * @param {function} Type - Message type to create.
2315
   * @param {function} callback - Callback to invoke. First parameter will be the raw message,
2316
   * and the second is a function to retrieve the deserialized message.
2317
   * @returns {undefined}
2318
   */
2319
  _runWithMessageType(Type, callback) {
2320
    let message = new Type();
999✔
2321

2322
    callback(message.toRawROS(), () => {
999✔
2323
      let result = new Type();
690✔
2324
      result.deserialize(message.refObject);
690✔
2325

2326
      return result;
690✔
2327
    });
2328

2329
    Type.destroyRawROS(message);
999✔
2330
  }
2331

2332
  _addActionClient(actionClient) {
2333
    this._actionClients.push(actionClient);
41✔
2334
    this.syncHandles();
41✔
2335
  }
2336

2337
  _addActionServer(actionServer) {
2338
    this._actionServers.push(actionServer);
41✔
2339
    this.syncHandles();
41✔
2340
  }
2341

2342
  _getValidatedTopic(topicName, noDemangle) {
2343
    if (noDemangle) {
6!
UNCOV
2344
      return topicName;
×
2345
    }
2346
    const fqTopicName = rclnodejs.expandTopicName(
6✔
2347
      topicName,
2348
      this.name(),
2349
      this.namespace()
2350
    );
2351
    validateFullTopicName(fqTopicName);
6✔
2352
    return rclnodejs.remapTopicName(this.handle, fqTopicName);
6✔
2353
  }
2354

2355
  _getValidatedServiceName(serviceName, noDemangle) {
2356
    if (typeof serviceName !== 'string') {
4!
UNCOV
2357
      throw new TypeValidationError('serviceName', serviceName, 'string', {
×
2358
        nodeName: this.name(),
2359
      });
2360
    }
2361

2362
    if (noDemangle) {
4!
UNCOV
2363
      return serviceName;
×
2364
    }
2365

2366
    const resolvedServiceName = this.resolveServiceName(serviceName);
4✔
2367
    rclnodejs.validateTopicName(resolvedServiceName);
4✔
2368
    return resolvedServiceName;
4✔
2369
  }
2370
}
2371

2372
/**
2373
 * Create an Options instance initialized with default values.
2374
 * @returns {Options} - The new initialized instance.
2375
 * @static
2376
 * @example
2377
 * {
2378
 *   enableTypedArray: true,
2379
 *   isRaw: false,
2380
 *   qos: QoS.profileDefault,
2381
 *   contentFilter: undefined,
2382
 *   serializationMode: 'default',
2383
 * }
2384
 */
2385
Node.getDefaultOptions = function () {
26✔
2386
  return {
7,576✔
2387
    enableTypedArray: true,
2388
    isRaw: false,
2389
    qos: QoS.profileDefault,
2390
    contentFilter: undefined,
2391
    serializationMode: 'default',
2392
  };
2393
};
2394

2395
module.exports = Node;
26✔
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

© 2026 Coveralls, Inc