Source: lib/node.js

// Copyright (c) 2017 Intel Corporation. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

const ActionInterfaces = require('./action/interfaces.js');
const Client = require('./client.js');
const Clock = require('./clock.js');
const Context = require('./context.js');
const debug = require('debug')('rclnodejs:node');
const GuardCondition = require('./guard_condition.js');
const loader = require('./interface_loader.js');
const Logging = require('./logging.js');
const NodeOptions = require('./node_options.js');
const {
  ParameterType,
  Parameter,
  ParameterDescriptor,
} = require('./parameter.js');
const ParameterService = require('./parameter_service.js');
const Publisher = require('./publisher.js');
const QoS = require('./qos.js');
const Rates = require('./rate.js');
const rclnodejs = require('bindings')('rclnodejs');
const Service = require('./service.js');
const Subscription = require('./subscription.js');
const TimeSource = require('./time_source.js');
const Timer = require('./timer.js');

// Parameter event publisher constants
const PARAMETER_EVENT_MSG_TYPE = 'rcl_interfaces/msg/ParameterEvent';
const PARAMETER_EVENT_TOPIC = 'parameter_events';

/**
 * @class - Class representing a Node in ROS
 * @hideconstructor
 */

class Node {
  init(name, namespace, context, options) {
    this._publishers = [];
    this._subscriptions = [];
    this._clients = [];
    this._services = [];
    this._timers = [];
    this._guards = [];
    this._actionClients = [];
    this._actionServers = [];
    this._rateTimerServer = null;
    this._parameterDescriptors = new Map();
    this._parameters = new Map();
    this._parameterService = null;
    this._parameterEventPublisher = null;
    this._setParametersCallbacks = [];
    this._logger = new Logging(rclnodejs.getNodeLoggerName(this.handle));
    this.spinning = false;

    this._parameterEventPublisher = this.createPublisher(
      PARAMETER_EVENT_MSG_TYPE,
      PARAMETER_EVENT_TOPIC
    );

    // initialize _parameterOverrides from parameters defined on the commandline
    this._parameterOverrides = this._getNativeParameterOverrides(context);

    // override cli parameterOverrides with those specified in options
    if (options.parameterOverrides.length > 0) {
      for (const parameter of options.parameterOverrides) {
        if (!parameter instanceof Parameter) {
          throw new TypeError(
            'Parameter-override must be an instance of Parameter.'
          );
        }
        this._parameterOverrides.set(parameter.name, parameter);
      }
    }

    // initialize _parameters from parameterOverrides
    if (options.automaticallyDeclareParametersFromOverrides) {
      for (const parameter of this._parameterOverrides.values()) {
        parameter.validate();
        const descriptor = ParameterDescriptor.fromParameter(parameter);
        this._parameters.set(parameter.name, parameter);
        this._parameterDescriptors.set(parameter.name, descriptor);
      }
    }

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

    if (options.startParameterServices) {
      this._parameterService = new ParameterService(this);
      this._parameterService.start();
    }
  }

  execute(handles) {
    let timersReady = this._timers.filter(
      timer => handles.indexOf(timer.handle) !== -1
    );
    let guardsReady = this._guards.filter(
      guard => handles.indexOf(guard.handle) !== -1
    );
    let subscriptionsReady = this._subscriptions.filter(
      subscription => handles.indexOf(subscription.handle) !== -1
    );
    let clientsReady = this._clients.filter(
      client => handles.indexOf(client.handle) !== -1
    );
    let servicesReady = this._services.filter(
      service => handles.indexOf(service.handle) !== -1
    );
    let actionClientsReady = this._actionClients.filter(
      actionClient => handles.indexOf(actionClient.handle) !== -1
    );
    let actionServersReady = this._actionServers.filter(
      actionServer => handles.indexOf(actionServer.handle) !== -1
    );

    timersReady.forEach(timer => {
      if (timer.isReady()) {
        rclnodejs.callTimer(timer.handle);
        timer.callback();
      }
    });

    subscriptionsReady.forEach(subscription => {
      this._runWithMessageType(
        subscription.typeClass,
        (message, deserialize) => {
          let success = rclnodejs.rclTake(subscription.handle, message);
          if (success) {
            subscription.processResponse(deserialize());
          }
        }
      );
    });

    guardsReady.forEach(guard => {
      guard.callback();
    });

    clientsReady.forEach(client => {
      this._runWithMessageType(
        client.typeClass.Response,
        (message, deserialize) => {
          let success = rclnodejs.rclTakeResponse(
            client.handle,
            client.sequenceNumber,
            message
          );
          if (success) {
            client.processResponse(deserialize());
          }
        }
      );
    });

    servicesReady.forEach(service => {
      this._runWithMessageType(
        service.typeClass.Request,
        (message, deserialize) => {
          let header = rclnodejs.rclTakeRequest(
            service.handle,
            this.handle,
            message
          );
          if (header) {
            service.processRequest(header, deserialize());
          }
        }
      );
    });

    actionClientsReady.forEach(actionClient => {
      const properties = actionClient.handle.properties;

      if (properties.isGoalResponseReady) {
        this._runWithMessageType(
          actionClient.typeClass.impl.SendGoalService.Response,
          (message, deserialize) => {
            let sequence = rclnodejs.actionTakeGoalResponse(
              actionClient.handle,
              message
            );
            if (sequence != null) {
              actionClient.processGoalResponse(sequence, deserialize());
            }
          }
        );
      }

      if (properties.isCancelResponseReady) {
        this._runWithMessageType(
          actionClient.typeClass.impl.CancelGoal.Response,
          (message, deserialize) => {
            let sequence = rclnodejs.actionTakeCancelResponse(
              actionClient.handle,
              message
            );
            if (sequence != null) {
              actionClient.processCancelResponse(sequence, deserialize());
            }
          }
        );
      }

      if (properties.isResultResponseReady) {
        this._runWithMessageType(
          actionClient.typeClass.impl.GetResultService.Response,
          (message, deserialize) => {
            let sequence = rclnodejs.actionTakeResultResponse(
              actionClient.handle,
              message
            );
            if (sequence != null) {
              actionClient.processResultResponse(sequence, deserialize());
            }
          }
        );
      }

      if (properties.isFeedbackReady) {
        this._runWithMessageType(
          actionClient.typeClass.impl.FeedbackMessage,
          (message, deserialize) => {
            let success = rclnodejs.actionTakeFeedback(
              actionClient.handle,
              message
            );
            if (success) {
              actionClient.processFeedbackMessage(deserialize());
            }
          }
        );
      }

      if (properties.isStatusReady) {
        this._runWithMessageType(
          actionClient.typeClass.impl.GoalStatusArray,
          (message, deserialize) => {
            let success = rclnodejs.actionTakeStatus(
              actionClient.handle,
              message
            );
            if (success) {
              actionClient.processStatusMessage(deserialize());
            }
          }
        );
      }
    });

    actionServersReady.forEach(actionServer => {
      const properties = actionServer.handle.properties;

      if (properties.isGoalRequestReady) {
        this._runWithMessageType(
          actionServer.typeClass.impl.SendGoalService.Request,
          (message, deserialize) => {
            const result = rclnodejs.actionTakeGoalRequest(
              actionServer.handle,
              message
            );
            if (result) {
              actionServer.processGoalRequest(result, deserialize());
            }
          }
        );
      }

      if (properties.isCancelRequestReady) {
        this._runWithMessageType(
          actionServer.typeClass.impl.CancelGoal.Request,
          (message, deserialize) => {
            const result = rclnodejs.actionTakeCancelRequest(
              actionServer.handle,
              message
            );
            if (result) {
              actionServer.processCancelRequest(result, deserialize());
            }
          }
        );
      }

      if (properties.isResultRequestReady) {
        this._runWithMessageType(
          actionServer.typeClass.impl.GetResultService.Request,
          (message, deserialize) => {
            const result = rclnodejs.actionTakeResultRequest(
              actionServer.handle,
              message
            );
            if (result) {
              actionServer.processResultRequest(result, deserialize());
            }
          }
        );
      }

      if (properties.isGoalExpired) {
        let GoalInfoArray = ActionInterfaces.GoalInfo.ArrayType;
        let message = new GoalInfoArray(actionServer._goalHandles.size);
        let count = rclnodejs.actionExpireGoals(
          actionServer.handle,
          actionServer._goalHandles.size,
          message._refArray.buffer
        );
        if (count > 0) {
          actionServer.processGoalExpired(message, count);
        }
        GoalInfoArray.freeArray(message);
      }
    });
  }

  startSpinning(timeout) {
    this.start(this.context.handle(), timeout);
    this.spinning = true;
  }

  stopSpinning() {
    this.stop();
    this.spinning = false;
  }

  _removeEntityFromArray(entity, array) {
    let index = array.indexOf(entity);
    if (index > -1) {
      array.splice(index, 1);
    }
  }

  _destroyEntity(entity, array, syncHandles = true) {
    this._removeEntityFromArray(entity, array);
    if (syncHandles) {
      this.syncHandles();
    }
    entity.handle.release();
  }

  _validateOptions(options) {
    if (
      options !== undefined &&
      (options === null || typeof options !== 'object')
    ) {
      throw new TypeError('Invalid argument of options');
    }

    if (options === undefined) {
      options = { enableTypedArray: true, qos: QoS.profileDefault };
      return options;
    }

    if (options.enableTypedArray === undefined) {
      options = Object.assign(options, { enableTypedArray: true });
    }

    if (options.qos === undefined) {
      options = Object.assign(options, { qos: QoS.profileDefault });
    }
    return options;
  }

  /**
   * Create a Timer.
   * @param {number} period - The number representing period in millisecond.
   * @param {function} callback - The callback to be called when timeout.
   * @param {Clock} [clock] - The clock which the timer gets time from.
   * @return {Timer} - An instance of Timer.
   */
  createTimer(
    period,
    callback,
    clock = null
  ) {
    if (arguments.length === 3 && !(arguments[2] instanceof Clock)) {
      clock = null;
    } else if (arguments.length === 4) {
      clock = arguments[3];
    }

    if (typeof period !== 'number' || typeof callback !== 'function') {
      throw new TypeError('Invalid argument');
    }

    // The period unit is millisecond in JavaScript side. When being passed to the
    // C++ side, the value will be converted to nanosecond, which goes into a uint64_t
    // with maxmium value of 2^64-1. So the maxmium is UINT64_MAX in ns, that's 0x10c6f7a0b5ed in ms.
    const MAX_TIMER_PERIOD_IN_MILLISECOND = 0x10c6f7a0b5ed;
    if (period > 0x10c6f7a0b5ed || period < 0) {
      throw new RangeError(
        'Parameter must be between ' +
          0 +
          ' and ' +
          MAX_TIMER_PERIOD_IN_MILLISECOND
      );
    }

    const timerClock = clock || this._clock;

    let timerHandle = rclnodejs.createTimer(
      timerClock.handle,
      this.context.handle(),
      period
    );
    let timer = new Timer(timerHandle, period, callback);
    debug('Finish creating timer, period = %d.', period);
    this._timers.push(timer);
    this.syncHandles();

    return timer;
  }

  /**
   * Create a Rate.
   *
   * @param {number} hz - The frequency of the rate timer; default is 1 hz.
   * @returns {Promise<Rate>} - Promise resolving to new instance of Rate.
   */
  async createRate(hz = 1) {
    if (typeof hz !== 'number') {
      throw new TypeError('Invalid argument');
    }

    const MAX_RATE_HZ_IN_MILLISECOND = 1000.0;
    if (hz <= 0.0 || hz > MAX_RATE_HZ_IN_MILLISECOND) {
      throw new RangeError(
        `Hz must be between 0.0 and ${MAX_RATE_HZ_IN_MILLISECOND}`
      );
    }

    // lazy initialize rateTimerServer
    if (!this._rateTimerServer) {
      this._rateTimerServer = new Rates.RateTimerServer(this);
      await this._rateTimerServer.init();
    }

    const period = Math.round(1000 / hz);
    const timer = this._rateTimerServer.createTimer(period);
    const rate = new Rates.Rate(hz, timer);

    return rate;
  }

  /**
   * Create a Publisher.
   * @param {function|string|object} typeClass - The ROS message class,
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
   * @param {string} topic - The name of the topic.
   * @param {object} options - The options argument used to parameterize the publisher.
   * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the publisher, default: QoS.profileDefault.
   * @return {Publisher} - An instance of Publisher.
   */
  createPublisher(typeClass, topic, options) {
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
      typeClass = loader.loadInterface(typeClass);
    }
    options = this._validateOptions(options);

    if (typeof typeClass !== 'function' || typeof topic !== 'string') {
      throw new TypeError('Invalid argument');
    }

    let publisher = Publisher.createPublisher(
      this.handle,
      typeClass,
      topic,
      options
    );
    debug('Finish creating publisher, topic = %s.', topic);
    this._publishers.push(publisher);
    return publisher;
  }

  /**
   * This callback is called when a message is published
   * @callback SubscriptionCallback
   * @param {Object} message - The message published
   * @see [Node.createSubscription]{@link Node#createSubscription}
   * @see [Node.createPublisher]{@link Node#createPublisher}
   * @see {@link Publisher}
   * @see {@link Subscription}
   */

  /**
   * Create a Subscription.
   * @param {function|string|object} typeClass - The ROS message class,
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
   * @param {string} topic - The name of the topic.
   * @param {object} options - The options argument used to parameterize the subscription.
   * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the subscription, default: QoS.profileDefault.
   * @param {SubscriptionCallback} callback - The callback to be call when receiving the topic subscribed.
   * @return {Subscription} - An instance of Subscription.
   * @see {@link SubscriptionCallback}
   */
  createSubscription(typeClass, topic, options, callback) {
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
      typeClass = loader.loadInterface(typeClass);
    }

    if (typeof options === 'function') {
      callback = options;
      options = undefined;
    }
    options = this._validateOptions(options);

    if (
      typeof typeClass !== 'function' ||
      typeof topic !== 'string' ||
      typeof callback !== 'function'
    ) {
      throw new TypeError('Invalid argument');
    }

    let subscription = Subscription.createSubscription(
      this.handle,
      typeClass,
      topic,
      options,
      callback
    );
    debug('Finish creating subscription, topic = %s.', topic);
    this._subscriptions.push(subscription);
    this.syncHandles();

    return subscription;
  }

  /**
   * Create a Client.
   * @param {function|string|object} typeClass - The ROS message class,
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
   * @param {string} serviceName - The service name to request.
   * @param {object} options - The options argument used to parameterize the client.
   * @param {boolean} options.enableTypedArray - The response will use TypedArray if necessary, default: true.
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the client, default: QoS.profileDefault.
   * @return {Client} - An instance of Client.
   */
  createClient(typeClass, serviceName, options) {
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
      typeClass = loader.loadInterface(typeClass);
    }
    options = this._validateOptions(options);

    if (typeof typeClass !== 'function' || typeof serviceName !== 'string') {
      throw new TypeError('Invalid argument');
    }

    let client = Client.createClient(
      this.handle,
      serviceName,
      typeClass,
      options
    );
    debug('Finish creating client, service = %s.', serviceName);
    this._clients.push(client);
    this.syncHandles();

    return client;
  }

  /**
   * This callback is called when a request is sent to service
   * @callback RequestCallback
   * @param {Object} request - The request sent to the service
   * @param {Response} response - The response to client.
        Use [response.send()]{@link Response#send} to send response object to client
   * @return {undefined}
   * @see [Node.createService]{@link Node#createService}
   * @see [Client.sendRequest]{@link Client#sendRequest}
   * @see {@link Client}
   * @see {@link Service}
   * @see {@link Response#send}
   */

  /**
   * Create a Service.
   * @param {function|string|object} typeClass - The ROS message class,
        OR a string representing the message class, e.g. 'std_msgs/msg/String',
        OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
   * @param {string} serviceName - The service name to offer.
   * @param {object} options - The options argument used to parameterize the service.
   * @param {boolean} options.enableTypedArray - The request will use TypedArray if necessary, default: true.
   * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the service, default: QoS.profileDefault.
   * @param {RequestCallback} callback - The callback to be called when receiving request.
   * @return {Service} - An instance of Service.
   * @see {@link RequestCallback}
   */
  createService(typeClass, serviceName, options, callback) {
    if (typeof typeClass === 'string' || typeof typeClass === 'object') {
      typeClass = loader.loadInterface(typeClass);
    }

    if (typeof options === 'function') {
      callback = options;
      options = undefined;
    }
    options = this._validateOptions(options);

    if (
      typeof typeClass !== 'function' ||
      typeof serviceName !== 'string' ||
      typeof callback !== 'function'
    ) {
      throw new TypeError('Invalid argument');
    }

    let service = Service.createService(
      this.handle,
      serviceName,
      typeClass,
      options,
      callback
    );
    debug('Finish creating service, service = %s.', serviceName);
    this._services.push(service);
    this.syncHandles();

    return service;
  }

  /**
   * Create a guard condition.
   * @param {Function} callback - The callback to be called when the guard condition is triggered.
   * @return {GuardCondition} - An instance of GuardCondition.
   */
  createGuardCondition(callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('Invalid argument');
    }

    let guard = GuardCondition.createGuardCondition(callback, this.context);
    debug('Finish creating guard condition');
    this._guards.push(guard);
    this.syncHandles();

    return guard;
  }

  /**
   * Destroy all resource allocated by this node, including
   * <code>Timer</code>s/<code>Publisher</code>s/<code>Subscription</code>s
   * /<code>Client</code>s/<code>Service</code>s
   * @return {undefined}
   */
  destroy() {
    if (this.spinning) {
      this.stopSpinning();
    }

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

    this.handle.release();
    this._clock = null;
    this._timers = [];
    this._publishers = [];
    this._subscriptions = [];
    this._clients = [];
    this._services = [];
    this._guards = [];
    this._actionClients = [];
    this._actionServers = [];

    if (this._rateTimerServer) {
      this._rateTimerServer.shutdown();
      this._rateTimerServer = null;
    }
  }

  /**
   * Destroy a Publisher.
   * @param {Publisher} publisher - The Publisher to be destroyed.
   * @return {undefined}
   */
  destroyPublisher(publisher) {
    if (!(publisher instanceof Publisher)) {
      throw new TypeError('Invalid argument');
    }
    this._destroyEntity(publisher, this._publishers, false);
  }

  /**
   * Destroy a Subscription.
   * @param {Subscription} subscription - The Subscription to be destroyed.
   * @return {undefined}
   */
  destroySubscription(subscription) {
    if (!(subscription instanceof Subscription)) {
      throw new TypeError('Invalid argument');
    }
    this._destroyEntity(subscription, this._subscriptions);
  }

  /**
   * Destroy a Client.
   * @param {Client} client - The Client to be destroyed.
   * @return {undefined}
   */
  destroyClient(client) {
    if (!(client instanceof Client)) {
      throw new TypeError('Invalid argument');
    }
    this._destroyEntity(client, this._clients);
  }

  /**
   * Destroy a Service.
   * @param {Service} service - The Service to be destroyed.
   * @return {undefined}
   */
  destroyService(service) {
    if (!(service instanceof Service)) {
      throw new TypeError('Invalid argument');
    }
    this._destroyEntity(service, this._services);
  }

  /**
   * Destroy a Timer.
   * @param {Timer} timer - The Timer to be destroyed.
   * @return {undefined}
   */
  destroyTimer(timer) {
    if (!(timer instanceof Timer)) {
      throw new TypeError('Invalid argument');
    }
    this._destroyEntity(timer, this._timers);
  }

  /**
   * Destroy a guard condition.
   * @param {GuardCondition} guard - The guard condition to be destroyed.
   * @return {undefined}
   */
  destroyGuardCondition(guard) {
    if (!(guard instanceof GuardCondition)) {
      throw new TypeError('Invalid argument');
    }
    this._destroyEntity(guard, this._guards);
  }

  /* Get the name of the node.
   * @return {string}
   */
  name() {
    return rclnodejs.getNodeName(this.handle);
  }

  /* Get the namespace of the node.
   * @return {string}
   */
  namespace() {
    return rclnodejs.getNamespace(this.handle);
  }

  /**
   * Get the nodes logger.
   * @returns {Logger} - The logger for the node.
   */
  getLogger() {
    return this._logger;
  }

  /**
   * Get the clock used by the node.
   * @returns {Clock} - The nodes clock.
   */
  getClock() {
    return this._clock;
  }

  /**
   * Get the current time using the node's clock.
   * @returns {Time} - The current time.
   */
  now() {
    return this.getClock().now();
  }

  /**
   * Get the list of published topics discovered by the provided node for the remote node name.
   * @param {string} nodeName - The name of the node.
   * @param {string} namespace - The name of the namespace.
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
   * @return {array} - An array of the names and types.
   */
  getPublisherNamesAndTypesByNode(nodeName, namespace, noDemangle = false) {
    return rclnodejs.getPublisherNamesAndTypesByNode(
      this.handle,
      nodeName,
      namespace,
      noDemangle
    );
  }

  /**
   * Get the list of published topics discovered by the provided node for the remote node name.
   * @param {string} nodeName - The name of the node.
   * @param {string} namespace - The name of the namespace.
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
   * @return {array} - An array of the names and types.
   */
  getSubscriptionNamesAndTypesByNode(nodeName, namespace, noDemangle = false) {
    return rclnodejs.getSubscriptionNamesAndTypesByNode(
      this.handle,
      nodeName,
      namespace,
      noDemangle
    );
  }

  /**
   * Get the list of service topics discovered by the provided node for the remote node name.
   * @param {string} nodeName - The name of the node.
   * @param {string} namespace - The name of the namespace.
   * @return {array} - An array of the names and types.
   */
  getServiceNamesAndTypesByNode(nodeName, namespace) {
    return rclnodejs.getServiceNamesAndTypesByNode(
      this.handle,
      nodeName,
      namespace
    );
  }

  /**
   * Get the list of topics discovered by the provided node.
   * @param {boolean} noDemangle - If true topic names and types returned will not be demangled, default: false.
   * @return {array} - An array of the names and types.
   */
  getTopicNamesAndTypes(noDemangle = false) {
    return rclnodejs.getTopicNamesAndTypes(this.handle, noDemangle);
  }

  /**
   * Get the list of services discovered by the provided node.
   * @return {array} - An array of the names and types.
   */
  getServiceNamesAndTypes() {
    return rclnodejs.getServiceNamesAndTypes(this.handle);
  }

  /**
   * Get the list of nodes discovered by the provided node.
   * @return {array} - An array of the names.
   */
  getNodeNames() {
    return this.getNodeNamesAndNamespaces().map(item => item.name);
  }

  /**
   * Get the list of nodes and their namespaces discovered by the provided node.
   * @return {array} - An array of the names and namespaces.
   */
  getNodeNamesAndNamespaces() {
    return rclnodejs.getNodeNames(this.handle);
  }

  /**
   * Return the number of publishers on a given topic.
   * @param {string} topic - The name of the topic.
   * @returns {number} - Number of publishers on the given topic.
   */
  countPublishers(topic) {
    let expandedTopic = rclnodejs.expandTopicName(
      topic,
      this.name(),
      this.namespace()
    );
    rclnodejs.validateTopicName(expandedTopic);

    return rclnodejs.countPublishers(this.handle, expandedTopic);
  }

  /**
   * Return the number of subscribers on a given topic.
   * @param {string} topic - The name of the topic.
   * @returns {number} - Number of subscribers on the given topic.
   */
  countSubscribers(topic) {
    let expandedTopic = rclnodejs.expandTopicName(
      topic,
      this.name(),
      this.namespace()
    );
    rclnodejs.validateTopicName(expandedTopic);

    return rclnodejs.countSubscribers(this.handle, expandedTopic);
  }

  /**
   * Get the list of parameter-overrides found on the commandline and
   * in the NodeOptions.parameter_overrides property.
   *
   * @return {array} - An array of Parameters
   */
  getParameterOverrides() {
    return Array.from(this._parameterOverrides.values());
  }

  /**
   * Declare a parameter.
   *
   * Internally, register a parameter and it's descriptor.
   * If a parameter-override exists, it's value will replace that of the parameter
   * unless ignoreOverride is true.
   * If the descriptor is undefined, then a ParameterDescriptor will be inferred
   * from the parameter's state.
   *
   * If a parameter by the same name has already been declared then an Error is thrown.
   * A parameter must be undeclared before attempting to redeclare it.
   *
   * @param {Parameter} parameter - Parameter to declare.
   * @param {ParameterDescriptor} [descriptor] - Optional descriptor for parameter.
   * @param {boolean} [ignoreOveride] - When true disregard any parameter-override that may be present.
   * @return {Parameter} - The newly declared parameter.
   */
  declareParameter(parameter, descriptor, ignoreOveride = false) {
    const parameters = this.declareParameters(
      [parameter],
      descriptor ? [descriptor] : [],
      ignoreOveride
    );
    return parameters.length == 1 ? parameters[0] : null;
  }

  /**
   * Declare a list of parameters.
   *
   * Internally register parameters with their corresponding descriptor one by one
   * in the order they are provided. This is an atomic operation. If an error
   * occurs the process halts and no further parameters are declared.
   * Parameters that have already been processed are undeclared.
   *
   * While descriptors is an optional parameter, when provided there must be
   * a descriptor for each parameter; otherwise an Error is thrown.
   * If descriptors is not provided then a descriptor will be inferred
   * from each parameter's state.
   *
   * When a parameter-override is available, the parameter's value
   * will be replaced with that of the parameter-override unless ignoreOverrides
   * is true.
   *
   * If a parameter by the same name has already been declared then an Error is thrown.
   * A parameter must be undeclared before attempting to redeclare it.
   *
   * Prior to declaring the parameters each SetParameterEventCallback registered
   * using setOnParameterEventCallback() is called in succession with the parameters
   * list. Any SetParameterEventCallback that retuns does not return a successful
   * result will cause the entire operation to terminate with no changes to the
   * parameters. When all SetParameterEventCallbacks return successful then the
   * list of parameters is updated.
   *
   * @param {Parameter[]} parameters - The parameters to declare.
   * @param {ParameterDescriptor[]} [descriptors] - Optional descriptors,
   *    a 1-1 correspondence with parameters.
   * @param {boolean} ignoreOverrides - When true, parameter-overrides are
   *    not considered, i.e.,ignored.
   * @return {Parameter[]} - The declared parameters.
   */
  declareParameters(parameters, descriptors = [], ignoreOverrides = false) {
    if (!Array.isArray(parameters)) {
      throw new TypeError('Invalid parameter: expected array of Parameter');
    }
    if (!Array.isArray(descriptors)) {
      throw new TypeError(
        'Invalid parameters: expected array of ParameterDescriptor'
      );
    }
    if (descriptors.length > 0 && parameters.length !== descriptors.length) {
      throw new TypeError(
        'Each parameter must have a cooresponding ParameterDescriptor'
      );
    }

    const declaredDescriptors = [];
    const declaredParameters = [];
    const declaredParameterCollisions = [];
    for (let i = 0; i < parameters.length; i++) {
      let parameter =
        !ignoreOverrides && this._parameterOverrides.has(parameters[i].name)
          ? this._parameterOverrides.get(parameters[i].name)
          : parameters[i];

      // stop processing parameters that have already been declared
      if (this._parameters.has(parameter.name)) {
        declaredParameterCollisions.push(parameter);
        continue;
      }

      // create descriptor for parameter if not provided
      let descriptor =
        descriptors.length > 0
          ? descriptors[i]
          : ParameterDescriptor.fromParameter(parameter);

      descriptor.validate();

      declaredDescriptors.push(descriptor);
      declaredParameters.push(parameter);
    }

    if (declaredParameterCollisions.length > 0) {
      const errorMsg =
        declaredParameterCollisions.length == 1
          ? `Parameter(${declaredParameterCollisions[0]}) already declared.`
          : `Multiple parameters already declared, e.g., Parameter(${declaredParameterCollisions[0]}).`;
      throw new Error(errorMsg);
    }

    // register descriptor
    for (const descriptor of declaredDescriptors) {
      this._parameterDescriptors.set(descriptor.name, descriptor);
    }

    const result = this._setParametersAtomically(declaredParameters, true);
    if (!result.successful) {
      // unregister descriptors
      for (const descriptor of declaredDescriptors) {
        this._parameterDescriptors.delete(descriptor.name);
      }

      throw new Error(result.reason);
    }

    return this.getParameters(declaredParameters.map(param => param.name));
  }

  /**
   * Undeclare a parameter.
   *
   * Readonly parameters can not be undeclared or updated.
   * @param {string} name - Name of parameter to undeclare.
   * @return {undefined} -
   */
  undeclareParameter(name) {
    if (!this.hasParameter(name)) return;

    const descriptor = this.getParameterDescriptor(name);
    if (descriptor.readOnly) {
      throw new Error(
        `${name} parameter is read-only and can not be undeclared`
      );
    }

    this._parameters.delete(name);
    this._parameterDescriptors.delete(name);
  }

  /**
   * Determine if a parameter has been declared.
   * @param {string} name - name of parameter
   * @returns {boolean} - Return true if parameter is declared; false otherwise.
   */
  hasParameter(name) {
    return this._parameters.has(name);
  }

  /**
   * Get a declared parameter by name.
   *
   * If unable to locate a declared parameter then a
   * parameter with type == PARAMETER_NOT_SET is returned.
   *
   * @param {string} name - The name of the parameter.
   * @return {Parameter} - The parameter.
   */
  getParameter(name) {
    return this.getParameters([name])[0];
  }

  /**
   * Get a list of parameters.
   *
   * Find and return the declared parameters.
   * If no names are provided return all declared parameters.
   *
   * If unable to locate a declared parameter then a
   * parameter with type == PARAMETER_NOT_SET is returned in
   * it's place.
   *
   * @param {string[]} [names] - The names of the declared parameters
   *    to find or null indicating to return all declared parameters.
   * @return {Parameter[]} - The parameters.
   */
  getParameters(names = []) {
    let params = [];

    if (names.length == 0) {
      // get all parameters
      params = [...this._parameters.values()];
      return params;
    }

    for (const name of names) {
      const param = this.hasParameter(name)
        ? this._parameters.get(name)
        : new Parameter(name, ParameterType.PARAMETER_NOT_SET);

      params.push(param);
    }

    return params;
  }

  /**
   * Get the names of all declared parameters.
   *
   * @return {string[]} - The declared parameter names or empty array if
   *    no parameters have been declared.
   */
  getParameterNames() {
    return this.getParameters().map(param => param.name);
  }

  /**
   * Determine if a parameter descriptor exists.
   *
   * @param {string} name - The name of a descriptor to for.
   * @return {boolean} - True if a descriptor has been declared; otherwise false.
   */
  hasParameterDescriptor(name) {
    return !!this.getParameterDescriptor(name);
  }

  /**
   * Get a declared parameter descriptor by name.
   *
   * If unable to locate a declared parameter descriptor then a
   * descriptor with type == PARAMETER_NOT_SET is returned.
   *
   * @param {string} name - The name of the parameter descriptor to find.
   * @return {ParameterDescriptor} - The parameter descriptor.
   */
  getParameterDescriptor(name) {
    return this.getParameterDescriptors([name])[0];
  }

  /**
   * Find a list of declared ParameterDescriptors.
   *
   * If no names are provided return all declared descriptors.
   *
   * If unable to locate a declared descriptor then a
   * descriptor with type == PARAMETER_NOT_SET is returned in
   * it's place.
   *
   * @param {string[]} [names] - The names of the declared parameter
   *    descriptors to find or null indicating to return all declared descriptors.
   * @return {ParameterDescriptor[]} - The parameter descriptors.
   */
  getParameterDescriptors(names = []) {
    const descriptors = [];

    if (names.length == 0) {
      // get all parameters
      descriptors = [...this._parameterDescriptors.values()];
      return descriptors;
    }

    for (const name of names) {
      let descriptor = this._parameterDescriptors.get(name);
      if (!descriptor) {
        descriptor = new ParameterDescriptor(
          name,
          ParameterType.PARAMETER_NOT_SET
        );
      }
      descriptors.push(descriptor);
    }

    return descriptors;
  }

  /**
   * Replace a declared parameter.
   *
   * The parameter being replaced must be a declared parameter who's descriptor
   * is not readOnly; otherwise an Error is thrown.
   *
   * @param {Parameter} parameter - The new parameter.
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult} - The result of the operation.
   */
  setParameter(parameter) {
    const results = this.setParameters([parameter]);
    return results[0];
  }

  /**
   * Replace a list of declared parameters.
   *
   * Declared parameters are replaced in the order they are provided and
   * a ParameterEvent is published for each individual parameter change.
   *
   * Prior to setting the parameters each SetParameterEventCallback registered
   * using setOnParameterEventCallback() is called in succession with the parameters
   * list. Any SetParameterEventCallback that retuns does not return a successful
   * result will cause the entire operation to terminate with no changes to the
   * parameters. When all SetParameterEventCallbacks return successful then the
   * list of parameters is updated.
   *
   * If an error occurs, the process is stopped and returned. Parameters
   * set before an error remain unchanged.
   *
   * @param {Parameter[]} parameters - The parameters to set.
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult[]} - A list of SetParameterResult, one for each parameter that was set.
   */
  setParameters(parameters = []) {
    return parameters.map(parameter =>
      this.setParametersAtomically([parameter])
    );
  }

  /**
   * Repalce a list of declared parameters atomically.
   *
   * Declared parameters are replaced in the order they are provided.
   * A single ParameterEvent is published collectively for all changed
   * parameters.
   *
   * Prior to setting the parameters each SetParameterEventCallback registered
   * using setOnParameterEventCallback() is called in succession with the parameters
   * list. Any SetParameterEventCallback that retuns does not return a successful
   * result will cause the entire operation to terminate with no changes to the
   * parameters. When all SetParameterEventCallbacks return successful then the
   * list of parameters is updated.d
   *
   * If an error occurs, the process stops immediately. All parameters updated to
   * the point of the error are reverted to their previous state.
   *
   * @param {Parameter[]} parameters - The parameters to set.
   * @return {rclnodejs.rcl_interfaces.msg.SetParameterResult} - describes the result of setting 1 or more parameters.
   */
  setParametersAtomically(parameters = []) {
    return this._setParametersAtomically(parameters);
  }

  /**
   * Internal method for updating parameters atomically.
   *
   * Prior to setting the parameters each SetParameterEventCallback registered
   * using setOnParameterEventCallback() is called in succession with the parameters
   * list. Any SetParameterEventCallback that retuns does not return a successful
   * result will cause the entire operation to terminate with no changes to the
   * parameters. When all SetParameterEventCallbacks return successful then the
   * list of parameters is updated.
   *
   * @param {Paramerter[]} parameters - The parameters to update.
   * @param {boolean} declareParameterMode - When true parameters are being declared;
   *    otherwise they are being changed.
   * @return {SetParameterResult} - A single collective result.
   */
  _setParametersAtomically(parameters = [], declareParameterMode = false) {
    let result = this._validateParameters(parameters, declareParameterMode);
    if (!result.successful) {
      return result;
    }

    // give all SetParametersCallbacks a chance to veto this change
    for (const callback of this._setParametersCallbacks) {
      result = callback(parameters);
      if (!result.successful) {
        // a callback has vetoed a parameter change
        return result;
      }
    }

    // collectively track updates to parameters for use
    // when publishing a ParameterEvent
    const newParameters = [];
    const changedParameters = [];
    const deletedParameters = [];

    for (const parameter of parameters) {
      if (parameter.type == ParameterType.PARAMETER_NOT_SET) {
        this.undeclareParameter(parameter.name);
        deletedParameters.push(parameter);
      } else {
        this._parameters.set(parameter.name, parameter);
        if (declareParameterMode) {
          newParameters.push(parameter);
        } else {
          changedParameters.push(parameter);
        }
      }
    }

    // create ParameterEvent
    const parameterEvent = new (loader.loadInterface(
      PARAMETER_EVENT_MSG_TYPE
    ))();

    const secondsAndNanos = this._clock.now().secondsAndNanoseconds;
    parameterEvent.stamp = {
      sec: secondsAndNanos.seconds,
      nanosec: secondsAndNanos.nanoseconds,
    };

    parameterEvent.node =
      this.namespace() === '/'
        ? this.namespace() + this.name()
        : this.namespace() + '/' + this.name();

    if (newParameters.length > 0) {
      parameterEvent['new_parameters'] = newParameters.map(parameter =>
        parameter.toParameterMessage()
      );
    }
    if (changedParameters.length > 0) {
      parameterEvent['changed_parameters'] = changedParameters.map(parameter =>
        parameter.toParameterMessage()
      );
    }
    if (deletedParameters.length > 0) {
      parameterEvent['deleted_parameters'] = deletedParameters.map(parameter =>
        parameter.toParameterMessage()
      );
    }

    // publish ParameterEvent
    this._parameterEventPublisher.publish(parameterEvent);

    return {
      successful: true,
      reason: '',
    };
  }

  /**
   * This callback is called when declaring a parameter or setting a parameter.
   * The callback is provided a list of parameters and returns a SetParameterResult
   * to indicate approval or veto of the operation.
   *
   * @callback SetParametersCallback
   * @param {Parameter[]} parameters - The message published
   * @returns {rcl_interfaces.msg.SetParameterResult} -
   *
   * @see [Node.addOnSetParametersCallback]{@link Node#addOnSetParametersCallback}
   * @see [Node.removeOnSetParametersCallback]{@link Node#removeOnSetParametersCallback}
   */

  /**
   * Add a callback to the front of the list of callbacks invoked for parameter declaration
   * and setting. No checks are made for duplicate callbacks.
   *
   * @param {SetParametersCallback} callback - The callback to add.
   * @returns {undefined}
   */
  addOnSetParametersCallback(callback) {
    this._setParametersCallbacks.unshift(callback);
  }

  /**
   * Remove a callback from the list of SetParametersCallbacks.
   * If the callback is not found the process is a nop.
   *
   * @param {SetParametersCallback} callback - The callback to be removed
   * @returns {undefined}
   */
  removeOnSetParametersCallback(callback) {
    const idx = this._setParametersCallbacks.indexOf(callback);
    if (idx > -1) {
      this._setParametersCallbacks.splice(idx, 1);
    }
  }

  // returns on 1st error or result {successful, reason}
  _validateParameters(parameters = [], declareParameterMode = false) {
    for (const parameter of parameters) {
      // detect invalid parameter
      try {
        parameter.validate();
      } catch (e) {
        return {
          successful: false,
          reason: `Invalid ${parameter.name}`,
        };
      }

      // detect undeclared parameter
      if (!this.hasParameterDescriptor(parameter.name)) {
        return {
          successful: false,
          reason: `Parameter ${parameter.name} has not been declared`,
        };
      }

      // detect readonly parameter that can not be updated
      const descriptor = this.getParameterDescriptor(parameter.name);
      if (!declareParameterMode && descriptor.readOnly) {
        return {
          successful: false,
          reason: `Parameter ${parameter.name} is readonly`,
        };
      }

      // validate parameter against descriptor if not an undeclare action
      if (parameter.type != ParameterType.PARAMETER_NOT_SET) {
        try {
          descriptor.validateParameter(parameter);
        } catch (e) {
          return {
            successful: false,
            reason: `Parameter ${parameter.name} does not  readonly`,
          };
        }
      }
    }

    return {
      successful: true,
      reason: null,
    };
  }

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

    // Get native parameters from rcl context->global_arguments.
    // rclnodejs returns an array of objects, 1 for each node e.g., -p my_node:foo:=bar,
    // and a node named '/**' for global parameter rules,
    // i.e., does not include a node identifier, e.g., -p color:=red
    // {
    //   name: string // node name
    //   parameters[] = {
    //     name: string
    //     type: uint
    //     value: object
    // }
    const cliParamOverrideData = rclnodejs.getParameterOverrides(
      context.handle()
    );

    // convert native CLI parameterOverrides to Map<nodeName,Array<ParameterOverride>>
    const cliParamOverrides = new Map();
    if (cliParamOverrideData) {
      for (let nodeParamData of cliParamOverrideData) {
        const nodeName = nodeParamData.name;
        const nodeParamOverrides = [];
        for (let paramData of nodeParamData.parameters) {
          const paramOverride = new Parameter(
            paramData.name,
            paramData.type,
            paramData.value
          );
          nodeParamOverrides.push(paramOverride);
        }
        cliParamOverrides.set(nodeName, nodeParamOverrides);
      }
    }

    // collect global CLI global parameters, name == /**
    let paramOverrides = cliParamOverrides.get('/**'); // array of ParameterOverrides
    if (paramOverrides) {
      for (const parameter of paramOverrides) {
        overrides.set(parameter.name, parameter);
      }
    }

    // merge CLI node parameterOverrides with global parameterOverrides, replace existing
    paramOverrides = cliParamOverrides.get(this.name()); // array of ParameterOverrides
    if (paramOverrides) {
      for (const parameter of paramOverrides) {
        overrides.set(parameter.name, parameter);
      }
    }

    return overrides;
  }

  /**
   * Invokes the callback with a raw message of the given type. After the callback completes
   * the message will be destroyed.
   * @param {function} Type - Message type to create.
   * @param {function} callback - Callback to invoke. First parameter will be the raw message,
   * and the second is a function to retrieve the deserialized message.
   * @returns {undefined}
   */
  _runWithMessageType(Type, callback) {
    let message = new Type();

    callback(message.toRawROS(), () => {
      let result = new Type();
      result.deserialize(message.refObject);

      return result;
    });

    Type.destoryRawROS(message);
  }

  _addActionClient(actionClient) {
    this._actionClients.push(actionClient);
    this.syncHandles();
  }

  _addActionServer(actionServer) {
    this._actionServers.push(actionServer);
    this.syncHandles();
  }
}

module.exports = Node;