Source: pinger.js

/**
 * Contains the implementation of pinger
 * @module pinger
 * @license Apache-2.0
 */
const NanoTimer = require('nanotimer');

const Measure = require('./measure');
const Request = require('./request');
const Response = require('./response');

/**
 * Implements the logic of the first negotiation phase.
 */
class Pinger {
  /**
   * Constructor
   * @param {Number} sessionId - The id of the session
   * @param {ClientNetwork|ServerNetwork} network - Network object
   * @param {Number} periodPings - Period in ms
   * @param {Number} numberPings - Number of pings
   * @param {Boolean} proactive - Should this pinger be the first
   */
  constructor(sessionId, network, periodPings, numberPings, proactive) {
    /**
     * Session id of the session. It is included in every request
     * @member {Number}
     */
    this.sessionId = sessionId;
    /**
     * Network object which emits events with the messages. Can be used both
     * client and server network objects.
     * @member {ClientNetwork|ServerNetwork}
     */
    this.network = network;
    /**
     * Time between outbound pings requests. In ms.
     * @member {Number}
     */
    this.periodPings = periodPings;
    /**
     * Number of PING requests to be generated.
     * @member {Number}
     */
    this.numberPings = numberPings;
    /**
     * Array containing the kickout time for Ping requests
     * @member {Array}
     */
    this.sendingTime = [];
    /**
     * Array containing the arriving time of the Responses for the requests.
     * @member {Array}
     */
    this.recieveTime = [];
    /**
     * Array containing the arriving time of the recieved Requests.
     * @member {Array}
     */
    this.reqTime = [];
    /**
     * The current local created measures.
     * @member {Measure}
     */
    this.localMeas = new Measure();
    /**
     * The reported measures from the other endpoint.
     * @member {Measure}
     */
    this.reporMeas = new Measure();
    /**
     * The sequence number which is included in every Request.
     * @member {Number}
     */
    this.sequence = 0;
    /**
     * Tells whether this pinger should start to send requests after start
     * or to wait to recieve the first ping and then start its own Pings.
     * @member {Boolean}
     */
    this.proactive = proactive;
    /**
     * This property describes whether this pinger is currently running
     * or it is not active. It means that the listeners are bound to
     * the network.
     * @member {Boolean}
     */
    this.isActive = false;

    /**
     * Timer
     * @member {NanoTimer}
     */
    this.timer = new NanoTimer();
  }

  /**
   * Generates a new pinger with Client settings.
   * @param {Number} sessionId - The id of the session
   * @param {ClientNetwork|ServerNetwork} network - Network object
   * @param {Number} periodPings - Period in ms
   * @param {Number} numberPings - Number of pings
   * @return {Pinger}
   */
  static genClient(sessionId, network, periodPings, numberPings) {
    return new Pinger(sessionId, network, periodPings, numberPings, true);
  }

  /**
   * Generates a new pinger with Server settings.
   * @param {Number} sessionId - The id of the session
   * @param {ClientNetwork|ServerNetwork} network - Network object
   * @param {Number} periodPings - Period in ms
   * @param {Number} numberPings - Number of pings
   * @return {Pinger}
   */
  static genServer(sessionId, network, periodPings, numberPings) {
    return new Pinger(sessionId, network, periodPings, numberPings, false);
  }

  /**
   * Star the measurement stage. It will resolve the promise with the obtained
   * measures and the last reported metrics from the other end. May reject if
   * someone cancels the measure.
   * @return {Promise<Object>} The measures obtained.
   */
  measure() {
    return new Promise((resolve, reject) => {
      this.resFunc = (res, time, record) => {
        if (typeof record == 'undefined' || record.id === this.sessionId) {
          const seq = parseInt(res.headers['Sequence-Number']);
          if (!isNaN(seq)) {
            this.recieveTime.push({seq: seq, time: time});
            this.localMeas.extractLatency(this.sendingTime, this.recieveTime);
          }
        }
      };

      this.reqFunc = (req, time, record) => {
        if (typeof record == 'undefined' || record.id === this.sessionId) {
          const seq = req.headers['Sequence-Number'];
          const headers = {
            'Sequence-Number': seq,
            'Session-Id': req.headers['Session-Id'],
          };
          this.network.sendUDP(Response.genRes(200, headers, undefined)
              , this.sessionId);
          this.reqTime.push({seq: parseInt(seq), time: time});
          this.localMeas.extractJitter(this.reqTime);
          this.reporMeas.fromHeader(req.headers.Measurements);
          if (this.timer.hasTimeout()) {
            this.timer.clearTimeout();
            this.timer.setTimeout(this.endFunc, '', '300m');
          }
        }
      };

      this.endFunc = () => {
        this.sequence = 0;
        this.isActive = false;
        this.network.removeListener('UDP-Res', this.resFunc);
        this.network.removeListener('UDP-Req', this.reqFunc);
        this.localMeas.extractLatency(this.sendingTime, this.recieveTime);
        this.localMeas.extractJitter(this.reqTime);
        setImmediate(resolve, {
          local: this.localMeas,
          remote: this.reporMeas,
        });
      };

      this.intFunc = () => {
        const headers = {
          'Sequence-Number': this.sequence,
          'Session-Id': this.sessionId,
          'Measurements': this.localMeas.toHeader(),
        };
        this.network.sendUDP(Request.genReq(
            'PING',
            'q4s://www.example.com',
            headers), this.sessionId);
        this.sendingTime.push({seq: this.sequence, time: new Date()});
        this.sequence++;
        if (this.numberPings && this.sequence === this.numberPings) {
          this.timer.clearInterval();
          this.timer.setTimeout(this.endFunc, '', '300m');
        }
      };

      const startIterrupt = (req, time, record) => {
        if (record.id === this.sessionId) {
          this.timer.setInterval(this.intFunc, '', this.periodPings + 'm');
          this.network.off('UDP-Req', startIterrupt);
        }
      };

      this.network.on('UDP-Res', this.resFunc);
      this.network.on('UDP-Req', this.reqFunc);
      this.isActive = true;

      if (this.proactive) {
        this.timer.setInterval(this.intFunc, '', this.periodPings + 'm');
      } else {
        this.network.on('UDP-Req', startIterrupt);
      }
    });
  }
  /**
   * Stop the pinger. If there is a measure ongoing there will not be any
   * action, if a measure is going it will stop abruptly. it resets the
   * pinger for another measure.
   */
  stop() {
    this.timer.clearInterval();
    this.timer.clearTimeout();
    if (this.isActive) {
      this.sequence = 0;
      this.isActive = false;
      this.network.removeListener('UDPResponse', this.resFunc);
      this.network.removeListener('UDPRequest', this.reqFunc);
      setImmediate(this.rejectCallback);
    }
  }
}

module.exports = Pinger;