File: source/socket-channel.js

/**
 * The list of channel connection error.
 * - The errors that would occur are:
 * @attribute SOCKET_ERROR
 * @type JSON
 * @param {Integer} CONNECTION_FAILED The connection failed. Up to user's
 *   defined reconnection attempts to decide on a reconnection.
 * @param {String} RECONNECTION_FAILED The reconnection failed. Up to user's
 *   defined reconnection attempts to decide on another reconnection.
 * @param {String} CONNECTION_ABORTED No reconnection specified.
 *   Connection is aborted.
 * @param {String} RECONNECTION_ABORTED All reconnection attempts have failed.
 *   Reconnection is aborted.
 * @param {String} RECONNECTION_ATTEMPT A reconnection attempt has been fired.
 * @readOnly
 * @component Socket
 * @for Skylink
 * @since 0.5.6
 */
Skylink.prototype.SOCKET_ERROR = {
  CONNECTION_FAILED: 0,
  RECONNECTION_FAILED: -1,
  CONNECTION_ABORTED: -2,
  RECONNECTION_ABORTED: -3,
  RECONNECTION_ATTEMPT: -4
};

/**
 * The queue of messages to be sent to signaling server.
 * @attribute _socketMessageQueue
 * @type Array
 * @private
 * @required
 * @component Socket
 * @for Skylink
 * @since 0.5.8
 */
Skylink.prototype._socketMessageQueue = [];

/**
 * The timeout used to send socket message queue.
 * @attribute _socketMessageTimeout
 * @type Function
 * @private
 * @required
 * @component Socket
 * @for Skylink
 * @since 0.5.8
 */
Skylink.prototype._socketMessageTimeout = null;

/**
 * The list of channel connection fallback states.
 * - The fallback states that would occur are:
 * @attribute SOCKET_FALLBACK
 * @type JSON
 * @param {String} NON_FALLBACK Non-fallback state,
 * @param {String} FALLBACK_PORT Fallback to non-ssl port for channel re-try.
 * @param {String} FALLBACK_PORT_SSL Fallback to ssl port for channel re-try.
 * @param {String} LONG_POLLING Fallback to non-ssl long-polling.
 * @param {String} LONG_POLLING_SSL Fallback to ssl port for long-polling.
 * @readOnly
 * @component Socket
 * @for Skylink
 * @since 0.5.6
 */
Skylink.prototype.SOCKET_FALLBACK = {
  NON_FALLBACK: 'nonfallback',
  FALLBACK_PORT: 'fallbackPortNonSSL',
  FALLBACK_SSL_PORT: 'fallbackPortSSL',
  LONG_POLLING: 'fallbackLongPollingNonSSL',
  LONG_POLLING_SSL: 'fallbackLongPollingSSL'
};

/**
 * The current socket opened state.
 * @attribute _channelOpen
 * @type Boolean
 * @private
 * @required
 * @component Socket
 * @for Skylink
 * @since 0.5.2
 */
Skylink.prototype._channelOpen = false;

/**
 * The signaling server to connect to.
 * @attribute _signalingServer
 * @type String
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.2
 */
Skylink.prototype._signalingServer = null;

/**
 * The signaling server protocol to use.
 * <ul>
 * <li><code>https:</code>
 * <ul><li>Default port is <code>443</code>.</li>
 *     <li>Fallback port is <code>3443</code>.</li>
 * </ul></li>
 * <li><code>http:</code>
 * <ul><li>Default port is <code>80</code>.</li>
 *     <li>Fallback port is <code>3000</code>.</li>
 * </ul></li>
 * </ul>
 * @attribute _signalingServerProtocol
 * @type String
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.4
 */
Skylink.prototype._signalingServerProtocol = window.location.protocol;

/**
 * The signaling server port to connect to.
 * @attribute _signalingServerPort
 * @type Integer
 * @default https: = 443, http = 80
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.4
 */
Skylink.prototype._signalingServerPort =
  (window.location.protocol === 'https:') ? 443 : 80;

/**
 * The actual socket object that handles the connection.
 * @attribute _socket
 * @type Object
 * @required
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.1.0
 */
Skylink.prototype._socket = null;

/**
 * The socket connection timeout
 * <ul>
 * <li><code>0</code> Uses the default timeout from socket.io
 *     <code>20000</code>ms.</li>
 * <li><code>>0</code> Uses the user set timeout</li>
 * </ul>
 * @attribute _socketTimeout
 * @type Integer
 * @default 0
 * @required
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.4
 */
Skylink.prototype._socketTimeout = 0;

/**
 * The socket connection to use XDomainRequest.
 * @attribute _socketUseXDR
 * @type Boolean
 * @default false
 * @required
 * @component Socket
 * @private
 * @for Skylink
 * @since 0.5.4
 */
Skylink.prototype._socketUseXDR = false;

/**
 * Sends a message to the signaling server.
 * - Not to be confused with method
 *   {{#crossLink "Skylink/sendMessage:method"}}sendMessage(){{/crossLink}}
 *   that broadcasts messages. This is for sending socket messages.
 * @method _sendChannelMessage
 * @param {JSON} message
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.8
 */
Skylink.prototype._sendChannelMessage = function(message) {
  var self = this;
  var interval = 1000;
  var throughput = 16;

  if (!self._channelOpen) {
    return;
  }

  var messageString = JSON.stringify(message);

  var sendLater = function(){
    if (self._socketMessageQueue.length > 0){

      if (self._socketMessageQueue.length<throughput){

        log.debug([(message.target ? message.target : 'server'), null, null,
          'Sending delayed message' + ((!message.target) ? 's' : '') + ' ->'], {
            type: self._SIG_MESSAGE_TYPE.GROUP,
            lists: self._socketMessageQueue.slice(0,self._socketMessageQueue.length),
            mid: self._user.sid,
            rid: self._room.id
          });

        self._socket.send({
          type: self._SIG_MESSAGE_TYPE.GROUP,
          lists: self._socketMessageQueue.splice(0,self._socketMessageQueue.length),
          mid: self._user.sid,
          rid: self._room.id
        });

        clearTimeout(self._socketMessageTimeout);
        self._socketMessageTimeout = null;

      }
      else{

        log.debug([(message.target ? message.target : 'server'), null, null,
          'Sending delayed message' + ((!message.target) ? 's' : '') + ' ->'], {
            type: self._SIG_MESSAGE_TYPE.GROUP,
            lists: self._socketMessageQueue.slice(0,throughput),
            mid: self._user.sid,
            rid: self._room.id
          });

        self._socket.send({
          type: self._SIG_MESSAGE_TYPE.GROUP,
          lists: self._socketMessageQueue.splice(0,throughput),
          mid: self._user.sid,
          rid: self._room.id
        });

        clearTimeout(self._socketMessageTimeout);
        self._socketMessageTimeout = null;
        self._socketMessageTimeout = setTimeout(sendLater,interval);

      }
      self._timestamp.now = Date.now() || function() { return +new Date(); };
    }
  };

  //Delay when messages are sent too rapidly
  if ((Date.now() || function() { return +new Date(); }) - self._timestamp.now < interval && 
    (message.type === self._SIG_MESSAGE_TYPE.PUBLIC_MESSAGE ||
    message.type === self._SIG_MESSAGE_TYPE.UPDATE_USER)) {

      log.warn([(message.target ? message.target : 'server'), null, null,
      'Messages fired too rapidly. Delaying.'], {
        interval: 1000,
        throughput: 16,
        message: message
      });

      self._socketMessageQueue.push(messageString);

      if (!self._socketMessageTimeout){
        self._socketMessageTimeout = setTimeout(sendLater,
          interval - ((Date.now() || function() { return +new Date(); })-self._timestamp.now));
      }
      return;
  }

  log.debug([(message.target ? message.target : 'server'), null, null,
    'Sending to peer' + ((!message.target) ? 's' : '') + ' ->'], message);

  //Normal case when messages are sent not so rapidly
  self._socket.send(messageString);
  self._timestamp.now = Date.now() || function() { return +new Date(); };

};

/**
 * Create the socket object to refresh connection.
 * @method _createSocket
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.6
 */
Skylink.prototype._createSocket = function (url, options) {
  var self = this;

  options = options || {};

  if (self._socket) {
    self._socket.removeAllListeners('connect_error');
    self._socket.removeAllListeners('reconnect_attempt');
    self._socket.removeAllListeners('reconnect_error');
    self._socket.removeAllListeners('reconnect_failed');
    self._socket.removeAllListeners('connect');
    self._socket.removeAllListeners('reconnect');
    self._socket.removeAllListeners('error');
    self._socket.removeAllListeners('disconnect');
    self._socket.removeAllListeners('message');
    self._socket.disconnect();
    self._socket = null;
  }
  self._channelOpen = false;

  log.log('Opening channel with signaling server url:', {
    url: url,
    useXDR: self._socketUseXDR,
    socketOptions: options.socketOptions
  });

  self._socket = io.connect(url, options.socketOptions);

  if (typeof options.connectErrorFn === 'function') {
    self._socket.on('connect_error', options.connectErrorFn);
  }

  if (typeof options.reconnectAttemptFn === 'function') {
    self._socket.on('reconnect_attempt', options.reconnectAttemptFn);
  }

  if (typeof options.reconnectErrorFn === 'function') {
    self._socket.on('reconnect_error', options.reconnectErrorFn);
  }

  if (typeof options.reconnectFailedFn === 'function') {
    self._socket.on('reconnect_failed', options.reconnectFailedFn);
  }

  var connectFn = function () {
    if (!self._channelOpen) {
      self._channelOpen = true;
      self._trigger('channelOpen');
      log.log([null, 'Socket', null, 'Channel opened']);
    }
  };

  self._socket.on('connect', connectFn);

  self._socket.on('reconnect', connectFn);

  self._socket.on('error', function(error) {
    self._channelOpen = false;
    self._trigger('channelError', error);
    log.error([null, 'Socket', null, 'Exception occurred:'], error);
  });

  self._socket.on('disconnect', function() {
    self._channelOpen = false;
    self._trigger('channelClose');
    log.log([null, 'Socket', null, 'Channel closed']);
  });

  self._socket.on('message', function(message) {
    log.log([null, 'Socket', null, 'Received message']);
    self._processSigMessage(message);
  });
};

/**
 * Create the default socket object connection.
 * @method _createDefaultSocket
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.6
 */
Skylink.prototype._createDefaultSocket = function () {
  var self = this;

  // create the sig url
  var ip_signaling = self._signalingServerProtocol + '//' +
    self._signalingServer + ':' + self._signalingServerPort;

  var socketOptions = {
    forceNew: true,
    //'sync disconnect on unload' : true,
    reconnection: false,
    transports: ['websocket']
  };

  if (self._socketTimeout !== 0) {
    socketOptions.timeout = self._socketTimeout;
  }

  var connectErrorFn = function (error) {
    self._channelOpen = false;
    self._trigger('socketError', self.SOCKET_ERROR.CONNECTION_FAILED,
      error, self.SOCKET_FALLBACK.NON_FALLBACK);

    self._createFallbackSocket();
  };

  self._createSocket(ip_signaling, {
    socketOptions: socketOptions,
    connectErrorFn: connectErrorFn
  });
};

/**
 * Create the fallback socket object reconnection.
 * @method _createFallbackSocket
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.6
 */
Skylink.prototype._createFallbackSocket = function () {
  var self = this;

  var fallback = (self._signalingServerProtocol ===
    'https:') ? self.SOCKET_FALLBACK.FALLBACK_SSL_PORT :
    self.SOCKET_FALLBACK.FALLBACK_PORT;

  self._signalingServerPort = (self._signalingServerProtocol ===
    'https:') ? 3443 : 3000;

  // create the sig url
  var ip_signaling = self._signalingServerProtocol + '//' +
    self._signalingServer + ':' + self._signalingServerPort;

  var socketOptions = {
    forceNew: true,
    //'sync disconnect on unload' : true,
    reconnection: false,
    transports: ['websocket']
  };

  if (self._socketTimeout !== 0) {
    socketOptions.timeout = self._socketTimeout;
  }

  var connectErrorFn = function (error) {
    self._channelOpen = false;
    self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_ABORTED,
      error, fallback);

    self._createLongpollingSocket();
  };

  self._createSocket(ip_signaling, {
    socketOptions: socketOptions,
    connectErrorFn: connectErrorFn
  });

  self._trigger('channelRetry', fallback, 0);
  self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_ATTEMPT,
    1, fallback);
};

/**
 * Create the long-polling fallback socket object reconnection.
 * @method _createLongpollingSocket
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.6
 */
Skylink.prototype._createLongpollingSocket = function () {
  var self = this;

  var fallback = (self._signalingServerProtocol ===
    'https:') ? self.SOCKET_FALLBACK.LONG_POLLING_SSL :
    self.SOCKET_FALLBACK.LONG_POLLING;

  self._signalingServerPort = (self._signalingServerProtocol ===
    'https:') ? 443 : 80;

  // create the sig url
  var ip_signaling = self._signalingServerProtocol + '//' +
    self._signalingServer + ':' + self._signalingServerPort;

  var socketOptions = {
    forceNew: true,
    //'sync disconnect on unload' : true,
    reconnection: true,
    transports: ['xhr-polling', 'jsonp-polling', 'polling']
  };

  if (self._socketTimeout !== 0) {
    //socketOptions.reconnectionDelay = self._socketTimeout;
    socketOptions.timeout = self._socketTimeout;
  }

  var reconnectAttemptFn = function (attempt) {
    self._channelOpen = false;
    self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_ATTEMPT,
      attempt, fallback);
    self._trigger('channelRetry', fallback, attempt);
  };

  var reconnectErrorFn = function (error) {
    self._channelOpen = false;
    self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_FAILED,
      error, fallback);
  };

  var reconnectFailedFn = function (error) {
    self._channelOpen = false;
    self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_ABORTED,
      error, fallback);
  };

  self._createSocket(ip_signaling, {
    socketOptions: socketOptions,
    reconnectAttemptFn: reconnectAttemptFn,
    reconnectErrorFn: reconnectErrorFn,
    reconnectFailedFn: reconnectFailedFn
  });

  self._trigger('channelRetry', fallback, 0);
  self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_ATTEMPT,
    1, fallback);
};

/**
 * Initiate a socket signaling connection.
 * @method _openChannel
 * @trigger channelMessage, channelOpen, channelError, channelClose
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.5
 */
Skylink.prototype._openChannel = function() {
  var self = this;
  if (self._channelOpen ||
    self._readyState !== self.READY_STATE_CHANGE.COMPLETED) {
    log.error([null, 'Socket', null, 'Unable to instantiate a new channel connection ' +
      'as readyState is not ready or there is already an ongoing channel connection']);
    return;
  }

  // set if forceSSL
  if (self._forceSSL) {
    self._signalingServerProtocol = 'https:';
    self._signalingServerPort = 443;
  } else {
    self._signalingServerProtocol = window.location.protocol;
    self._signalingServerPort = (window.location.protocol === 'https:') ? 443 : 80;
  }

  self._createDefaultSocket();
};

/**
 * Closes the socket signaling connection.
 * @method _closeChannel
 * @private
 * @component Socket
 * @for Skylink
 * @since 0.5.5
 */
Skylink.prototype._closeChannel = function() {
  if (!this._channelOpen) {
    return;
  }
  if (this._socket) {
    this._socket.removeAllListeners('connect_error');
    this._socket.removeAllListeners('reconnect_attempt');
    this._socket.removeAllListeners('reconnect_error');
    this._socket.removeAllListeners('reconnect_failed');
    this._socket.removeAllListeners('connect');
    this._socket.removeAllListeners('reconnect');
    this._socket.removeAllListeners('error');
    this._socket.removeAllListeners('disconnect');
    this._socket.removeAllListeners('message');
    this._socket.disconnect();
    this._socket = null;
  }
  this._channelOpen = false;
  this._trigger('channelClose');
};