Source: lib/rate.js

//
// 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 rclnodejs = require('../index.js');
const Context = require('./context.js');
const NodeOptions = require('./node_options.js');

const NOP_FN = () => {};

/**
 * A timer that runs at a regular frequency (hz).
 *
 * A client calls Rate#sleep() to block until the end of the current cycle.
 * This makes Rate useful for looping at a regular frequency (hz). Rate#sleep()
 * avoids blocking the JS event-loop by returning a Promise that the caller
 * should block on, e.g. use 'await rate.sleep()'.
 *
 * Note that Rate.sleep() does not prevent rclnodejs from invoking callbacks
 * such as a subscription or client if the entity's node is spinning. Thus
 * if your intent is to use rate to synchronize when callbacks are invoked
 * then use a spinOnce() just after rate.sleep() as in the example below.
 *
 * Rate runs within it's own private rcl context. This enables it to be
 * available immediately after construction. That is, unlike Timer, Rate
 * does not require a spin or spinOnce to be active.
 *
 * @example
 * async function run() {
 *   await rclnodejs.init();
 *   const node = rclnodejs.createNode('mynode');
 *   const rate = await node.createRate(1); // 1 hz
 *   while (true) {
 *     doSomeStuff();
 *     await rate.sleep();
 *     rclnodejs.spinOnce(node);
 *   }
 * }
 */
class Rate {
  /**
   * Create a new instance.
   * @hideconstructor
   * @param {number} hz - The frequency (hz) between (0.0,1000] hz,
   * @param {Timer} timer - The internal timer used by this instance.
   *   default = 1 hz
   */
  constructor(hz, timer) {
    this._hz = hz;
    this._timer = timer;
  }

  /**
   * Get the frequency in hertz (hz) of this timer.
   *
   * @returns {number} - hertz
   */
  get frequency() {
    return this._hz;
  }

  /**
   * Returns a Promise that when waited on, will block the sender
   * until the end of the current timer cycle.
   *
   * If the Rate has been cancelled, calling this method will
   * result in an error.
   *
   * @example
   * (async () => {
   *   await rate.sleep();
   * })();
   *
   * @returns {Promise} - Waiting on the promise will delay the sender
   * (not the Node event-loop) until the end of the current timer cycle.
   */
  async sleep() {
    if (this.isCanceled()) {
      throw new Error('Rate has been cancelled.');
    }

    return new Promise((resolve) => {
      this._timer.callback = () => {
        this._timer.callback = NOP_FN;
        resolve();
      };
    });
  }

  /**
   * Permanently stops the timing behavior.
   *
   * @returns {undefined}
   */
  cancel() {
    this._timer.cancel();
  }

  /**
   * Determine if this rate has been cancelled.
   *
   * @returns {boolean} - True when cancel() has been called; False otherwise.
   */
  isCanceled() {
    return this._timer.isCanceled();
  }
}

/**
 * Internal class that creates Timer instances in a common private rcl context
 * for use with Rate. The private rcl context ensures that Rate timers do not
 * deadlock waiting for spinOnce/spin on the main rcl context.
 */
class RateTimerServer {
  /**
   * Create a new instance.
   *
   * @constructor
   * @param {Node} parentNode - The parent node for which this server
   *  supplies timers to.
   */
  constructor(parentNode) {
    this._parentNode = parentNode;
    this._context = new Context();
  }

  /**
   * Setup the server's rcl context and node in preparation for creating
   * rate timer instances.
   *
   * @returns {undefined}
   */
  async init() {
    await rclnodejs.init(this._context);

    // create hidden node
    const nodeName = `_${this._parentNode.name()}_rate_timer_server`;
    const nodeNamespace = this._parentNode.namespace();
    const options = new NodeOptions();
    options.startParameterServices = false;
    options.parameterOverrides = this._parentNode.getParameters();
    options.automaticallyDeclareParametersFromOverrides = true;

    this._node = rclnodejs.createNode(
      nodeName,
      nodeNamespace,
      this._context,
      options
    );

    // spin node
    rclnodejs.spin(this._node, 10);
  }

  /**
   * Create a new timer instance with callback set to NOP.
   *
   * @param {number} period - The period in milliseconds
   * @returns {Timer} - The new timer instance.
   */
  createTimer(period) {
    const timer = this._node.createTimer(period, () => {});
    return timer;
  }

  /**
   * Permanently cancel all timers produced by this server and discontinue
   * the ability to create new Timers.
   *
   * The private rcl context is shutdown in the process and may not be
   * restarted.
   *
   * @returns {undefined}
   */
  shutdown() {
    rclnodejs.shutdown(this._context);
  }
}
// module.exports = {Rate, RateTimerServer};
module.exports = { Rate, RateTimerServer };