Source: pstream.js

const C = require('./constants');
const EventEmitter = require('events').EventEmitter;
const util = require('util');
const log = require('./log');

const WSTransport = require('./wstransport').default;

const PSTREAM_VERSION = '1.4';

/**
 * Constructor for PStream objects.
 *
 * @exports PStream as Twilio.PStream
 * @memberOf Twilio
 * @borrows EventEmitter#addListener as #addListener
 * @borrows EventEmitter#removeListener as #removeListener
 * @borrows EventEmitter#emit as #emit
 * @borrows EventEmitter#hasListener as #hasListener
 * @constructor
 * @param {string} token The Twilio capabilities JWT
 * @param {string} uri The PStream endpoint URI
 * @param {object} [options]
 * @config {boolean} [options.debug=false] Enable debugging
 */
function PStream(token, uri, options) {
  if (!(this instanceof PStream)) {
    return new PStream(token, uri, options);
  }
  const defaults = {
    logPrefix: '[PStream]',
    TransportFactory: WSTransport,
    debug: false
  };
  options = options || {};
  for (const prop in defaults) {
    if (prop in options) continue;
    options[prop] = defaults[prop];
  }
  this.options = options;
  this.token = token || '';
  this.status = 'disconnected';
  this.uri = uri;
  this.gateway = null;
  this.region = null;
  this._messageQueue = [];

  this._handleTransportClose = this._handleTransportClose.bind(this);
  this._handleTransportError = this._handleTransportError.bind(this);
  this._handleTransportMessage = this._handleTransportMessage.bind(this);
  this._handleTransportOpen = this._handleTransportOpen.bind(this);

  log.mixinLog(this, this.options.logPrefix);
  this.log.enabled = this.options.debug;

  // NOTE(mroberts): EventEmitter requires that we catch all errors.
  this.on('error', () => { });

  /*
   *events used by device
   *'invite',
   *'ready',
   *'error',
   *'offline',
   *
   *'cancel',
   *'presence',
   *'roster',
   *'answer',
   *'candidate',
   *'hangup'
   */

  const self = this;

  this.addListener('ready', () => {
    self.status = 'ready';
  });

  this.addListener('offline', () => {
    self.status = 'offline';
  });

  this.addListener('close', () => {
    self.log('Received "close" from server. Destroying PStream...');
    self._destroy();
  });

  this.transport = new this.options.TransportFactory(this.uri, {
    logLevel: this.options.debug ? 'debug' : 'off'
  });

  this.transport.on('close', this._handleTransportClose);
  this.transport.on('error', this._handleTransportError);
  this.transport.on('message', this._handleTransportMessage);
  this.transport.on('open', this._handleTransportOpen);
  this.transport.open();

  return this;
}

util.inherits(PStream, EventEmitter);

PStream.prototype._handleTransportClose = function() {
  if (this.status !== 'disconnected') {
    if (this.status !== 'offline') {
      this.emit('offline', this);
    }
    this.status = 'disconnected';
  }
};

PStream.prototype._handleTransportError = function(err) {
  this.emit('error', err);
};

PStream.prototype._handleTransportMessage = function(msg) {
  if (!msg || !msg.data || typeof msg.data !== 'string') {
    return;
  }

  const { type, payload = {} } = JSON.parse(msg.data);
  this.gateway = payload.gateway || this.gateway;
  this.region = payload.region || this.region;
  this.emit(type, payload);
};

PStream.prototype._handleTransportOpen = function() {
  this.status = 'connected';
  this.setToken(this.token);

  const messages = this._messageQueue.splice(0, this._messageQueue.length);
  messages.forEach(message => this._publish(...message));
};

/**
 * @return {string}
 */
PStream.toString = () => '[Twilio.PStream class]';
PStream.prototype.toString = () => '[Twilio.PStream instance]';

PStream.prototype.setToken = function(token) {
  this.log('Setting token and publishing listen');
  this.token = token;
  const payload = {
    token,
    browserinfo: getBrowserInfo()
  };
  this._publish('listen', payload);
};

PStream.prototype.register = function(mediaCapabilities) {
  const regPayload = {
    media: mediaCapabilities
  };
  this._publish('register', regPayload, true);
};

PStream.prototype._destroy = function() {
  this.transport.removeListener('close', this._handleTransportClose);
  this.transport.removeListener('error', this._handleTransportError);
  this.transport.removeListener('message', this._handleTransportMessage);
  this.transport.removeListener('open', this._handleTransportOpen);
  this.transport.close();

  this.emit('offline', this);
};

PStream.prototype.destroy = function() {
  this.log('PStream.destroy() called...');
  this._destroy();
  return this;
};

PStream.prototype.publish = function(type, payload) {
  return this._publish(type, payload, true);
};

PStream.prototype._publish = function(type, payload, shouldRetry) {
  const msg = JSON.stringify({
    type,
    version: PSTREAM_VERSION,
    payload
  });

  if (!this.transport.send(msg) && shouldRetry) {
    this._messageQueue.push([type, payload, true]);
  }
};

function getBrowserInfo() {
  const nav = typeof navigator !== 'undefined' ? navigator : {};

  const info = {
    p: 'browser',
    v: C.RELEASE_VERSION,
    browser: {
      userAgent: nav.userAgent || 'unknown',
      platform: nav.platform || 'unknown'
    },
    plugin: 'rtc'
  };

  return info;
}

exports.PStream = PStream;