/**
* These are the list of socket connection error states that Skylink would trigger.
* - These error states references the [socket.io-client events](http://socket.io/docs/client-api/).
* @attribute SOCKET_ERROR
* @type JSON
* @param {Number} CONNECTION_FAILED <small>Value <code>0</code></small>
* The error state when Skylink have failed to establish a socket connection with
* platform signaling in the first attempt.
* @param {String} RECONNECTION_FAILED <small>Value <code>-1</code></small>
* The error state when Skylink have failed to
* reestablish a socket connection with platform signaling after the first attempt
* <code>CONNECTION_FAILED</code>.
* @param {String} CONNECTION_ABORTED <small>Value <code>-2</code></small>
* The error state when attempt to reestablish socket connection
* with platform signaling has been aborted after the failed first attempt
* <code>CONNECTION_FAILED</code>.
* @param {String} RECONNECTION_ABORTED <small>Value <code>-3</code></small>
* The error state when attempt to reestablish socket connection
* with platform signaling has been aborted after several failed reattempts
* <code>RECONNECTION_FAILED</code>.
* @param {String} RECONNECTION_ATTEMPT <small>Value <code>-4</code></small>
* The error state when Skylink is attempting to reestablish
* a socket connection with platform signaling after a failed attempt
* <code>CONNECTION_FAILED</code> or <code>RECONNECTION_FAILED</code>.
* @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
};
/**
* Stores the queued socket messages to sent to the platform signaling to
* prevent messages from being dropped due to messages being sent in
* less than a second interval.
* @attribute _socketMessageQueue
* @type Array
* @private
* @required
* @component Socket
* @for Skylink
* @since 0.5.8
*/
Skylink.prototype._socketMessageQueue = [];
/**
* Limits the socket messages being sent in less than a second interval
* using the <code>setTimeout</code> object to prevent messages being sent
* in less than a second interval.
* The messaegs are stored in
* {{#crossLink "Skylink/_socketMessageQueue:attribute"}}_socketMessageQueue{{/crossLink}}.
* @attribute _socketMessageTimeout
* @type Object
* @private
* @required
* @component Socket
* @for Skylink
* @since 0.5.8
*/
Skylink.prototype._socketMessageTimeout = null;
/**
* Stores the list of fallback ports that Skylink can attempt
* to establish a socket connection with platform signaling.
* @attribute _socketPorts
* @type JSON
* @param {Array} http:// The array of <code>HTTP</code> protocol fallback ports.
* By default, the ports are <code>[80, 3000]</code>.
* @param {Array} https:// The The array of <code>HTTP</code> protocol fallback ports.
* By default, the ports are <code>[443, 3443]</code>.
* @private
* @required
* @component Socket
* @for Skylink
* @since 0.5.8
*/
Skylink.prototype._socketPorts = {
'http:': [80, 3000],
'https:': [443, 3443]
};
/**
* These are the list of fallback attempt types that Skylink would attempt with.
* @attribute SOCKET_FALLBACK
* @type JSON
* @param {String} NON_FALLBACK <small>Value <code>"nonfallback"</code> | Protocol <code>"http:"</code>,
* <code>"https:"</code> | Transports <code>"WebSocket"</code>, <code>"Polling"</code></small>
* The current socket connection attempt
* is using the first selected socket connection port for
* the current selected transport <code>"Polling"</code> or <code>"WebSocket"</code>.
* @param {String} FALLBACK_PORT <small>Value <code>"fallbackPortNonSSL"</code> | Protocol <code>"http:"</code>
* | Transports <code>"WebSocket"</code></small>
* The current socket connection reattempt
* is using the next selected socket connection port for
* <code>HTTP</code> protocol connection with the current selected transport
* <code>"Polling"</code> or <code>"WebSocket"</code>.
* @param {String} FALLBACK_PORT_SSL <small>Value <code>"fallbackPortSSL"</code> | Protocol <code>"https:"</code>
* | Transports <code>"WebSocket"</code></small>
* The current socket connection reattempt
* is using the next selected socket connection port for
* <code>HTTPS</code> protocol connection with the current selected transport
* <code>"Polling"</code> or <code>"WebSocket"</code>.
* @param {String} LONG_POLLING <small>Value <code>"fallbackLongPollingNonSSL"</code> | Protocol <code>"http:"</code>
* | Transports <code>"Polling"</code></small>
* The current socket connection reattempt
* is using the next selected socket connection port for
* <code>HTTP</code> protocol connection with <code>"Polling"</code> after
* many attempts of <code>"WebSocket"</code> has failed.
* This occurs only for socket connection that is originally using
* <code>"WebSocket"</code> transports.
* @param {String} LONG_POLLING_SSL <small>Value <code>"fallbackLongPollingSSL"</code> | Protocol <code>"https:"</code>
* | Transports <code>"Polling"</code></small>
* The current socket connection reattempt
* is using the next selected socket connection port for
* <code>HTTPS</code> protocol connection with <code>"Polling"</code> after
* many attempts of <code>"WebSocket"</code> has failed.
* This occurs only for socket connection that is originally using
* <code>"WebSocket"</code> transports.
* @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 flag that indicates if the current socket connection with
* platform signaling is opened.
* @attribute _channelOpen
* @type Boolean
* @private
* @required
* @component Socket
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._channelOpen = false;
/**
* Stores the platform signaling endpoint URI to open socket connection with.
* @attribute _signalingServer
* @type String
* @private
* @component Socket
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._signalingServer = null;
/**
* Stores the current platform signaling protocol to open socket connection with.
* @attribute _signalingServerProtocol
* @type String
* @private
* @component Socket
* @for Skylink
* @since 0.5.4
*/
Skylink.prototype._signalingServerProtocol = window.location.protocol;
/**
* Stores the current platform signaling port to open socket connection with.
* @attribute _signalingServerPort
* @type Number
* @private
* @component Socket
* @for Skylink
* @since 0.5.4
*/
Skylink.prototype._signalingServerPort = null;
/**
* Stores the [socket.io-client <code>io</code> object](http://socket.io/docs/client-api/) that
* handles the middleware socket connection with platform signaling.
* @attribute _socket
* @type Object
* @required
* @private
* @component Socket
* @for Skylink
* @since 0.1.0
*/
Skylink.prototype._socket = null;
/**
* Stores the timeout (in ms) set to await in seconds for response from platform signaling
* before throwing a connection timeout exception when Skylink is attemtping
* to establish a connection with platform signaling.
* If the value is <code>0</code>, it will use the default timeout from
* socket.io-client that is in <code>20000</code>.
* @attribute _socketTimeout
* @type Number
* @default 0
* @required
* @private
* @component Socket
* @for Skylink
* @since 0.5.4
*/
Skylink.prototype._socketTimeout = 0;
/**
* The flag that indicates if the current socket connection for
* transports types with <code>"Polling"</code> uses
* [XDomainRequest](https://msdn.microsoft.com/en-us/library/cc288060(v=vs.85).aspx)
* instead of [XMLHttpRequest](http://www.w3schools.com/Xml/dom_httprequest.asp)
* due to the IE 8 / 9 <code>XMLHttpRequest</code> not supporting CORS access.
* @attribute _socketUseXDR
* @type Boolean
* @default false
* @required
* @component Socket
* @private
* @for Skylink
* @since 0.5.4
*/
Skylink.prototype._socketUseXDR = false;
/**
* Sends socket message over the platform signaling socket connection.
* @method _sendChannelMessage
* @param {JSON} message The socket message object.
* @param {String} message.type Required. Protocol type of the socket message object.
* @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
});
// fix for self._socket undefined errors in firefox
if (self._socket) {
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
});
} else {
log.error([(message.target ? message.target : 'server'), null, null,
'Dropping delayed message' + ((!message.target) ? 's' : '') +
' as socket object is no longer defined ->'], {
type: self._SIG_MESSAGE_TYPE.GROUP,
lists: self._socketMessageQueue.slice(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
});
// fix for self._socket undefined errors in firefox
if (self._socket) {
self._socket.send({
type: self._SIG_MESSAGE_TYPE.GROUP,
lists: self._socketMessageQueue.splice(0,throughput),
mid: self._user.sid,
rid: self._room.id
});
} else {
log.error([(message.target ? message.target : 'server'), null, null,
'Dropping delayed message' + ((!message.target) ? 's' : '') +
' as socket object is no longer defined ->'], {
type: self._SIG_MESSAGE_TYPE.GROUP,
lists: self._socketMessageQueue.slice(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 &&
self._groupMessageList.indexOf(message.type) > -1) {
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(); };
};
/**
* Starts a socket.io connection with the platform signaling.
* @method _createSocket
* @param {String} type The transport type of socket.io connection to use.
* <ul>
* <li><code>"WebSocket"</code>: Uses the WebSocket connection.<br>
* <code>options.transports = ["websocket"]</code></li>
* <li><code>"Polling"</code>: Uses the Polling connection.<br>
* <code>options.transports = ["xhr-polling", "jsonp-polling", "polling"]</code></li>
* </ul>
* @private
* @component Socket
* @for Skylink
* @since 0.5.10
*/
Skylink.prototype._createSocket = function (type) {
var self = this;
var options = {
forceNew: true,
//'sync disconnect on unload' : true,
reconnection: false
};
var ports = self._socketPorts[self._signalingServerProtocol];
var connectionType = null;
// just beginning
if (self._signalingServerPort === null) {
self._signalingServerPort = ports[0];
connectionType = self.SOCKET_FALLBACK.NON_FALLBACK;
// reached the end of the last port for the protocol type
} else if ( ports.indexOf(self._signalingServerPort) === ports.length - 1 ) {
// re-refresh to long-polling port
if (type === 'WebSocket') {
console.log(type, self._signalingServerPort);
type = 'Polling';
self._signalingServerPort = ports[0];
} else if (type === 'Polling') {
options.reconnection = true;
options.reconnectionAttempts = 4;
options.reconectionDelayMax = 1000;
}
// move to the next port
} else {
self._signalingServerPort = ports[ ports.indexOf(self._signalingServerPort) + 1 ];
}
var url = self._signalingServerProtocol + '//' + self._signalingServer + ':' + self._signalingServerPort;
//'http://ec2-52-8-93-170.us-west-1.compute.amazonaws.com:6001';
if (type === 'WebSocket') {
options.transports = ['websocket'];
} else if (type === 'Polling') {
options.transports = ['xhr-polling', 'jsonp-polling', 'polling'];
}
// if socket instance already exists, exit
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,
options: options
});
self._socket = io.connect(url, options);
if (connectionType === null) {
connectionType = self._signalingServerProtocol === 'http:' ?
(type === 'Polling' ? self.SOCKET_FALLBACK.LONG_POLLING :
self.SOCKET_FALLBACK.FALLBACK_PORT) :
(type === 'Polling' ? self.SOCKET_FALLBACK.LONG_POLLING_SSL :
self.SOCKET_FALLBACK.FALLBACK_SSL_PORT);
}
self._socket.on('connect_error', function (error) {
self._channelOpen = false;
self._trigger('socketError', self.SOCKET_ERROR.CONNECTION_FAILED,
error, connectionType);
self._trigger('channelRetry', connectionType, 1);
if (options.reconnection === false) {
self._createSocket(type);
}
});
self._socket.on('reconnect_attempt', function (attempt) {
self._channelOpen = false;
self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_ATTEMPT,
attempt, connectionType);
self._trigger('channelRetry', connectionType, attempt);
});
self._socket.on('reconnect_error', function (error) {
self._channelOpen = false;
self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_FAILED,
error, connectionType);
});
self._socket.on('reconnect_failed', function (error) {
self._channelOpen = false;
self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_ABORTED,
error, connectionType);
});
self._socket.on('connect', function () {
if (!self._channelOpen) {
self._channelOpen = true;
self._trigger('channelOpen');
log.log([null, 'Socket', null, 'Channel opened']);
}
});
self._socket.on('reconnect', function () {
if (!self._channelOpen) {
self._channelOpen = true;
self._trigger('channelOpen');
log.log([null, 'Socket', null, 'Channel opened']);
}
});
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);
});
};
/**
* Connects to the socket connection endpoint URI to platform signaling that is constructed with
* {{#crossLink "Skylink/_signalingServerProtocol:attribute"}}_signalingServerProtocol{{/crossLink}},
* {{#crossLink "Skylink/_signalingServer:attribute"}}_signalingServer{{/crossLink}} and
* {{#crossLink "Skylink/_signalingServerPort:attribute"}}_signalingServerPort{{/crossLink}}.
* <small>Example format: <code>protocol//serverUrl:port</code></small>.<br>
* Once URI is formed, it will start a new socket.io connection with
* {{#crossLink "Skylink/_createSocket:method"}}_createSocket(){{/crossLink}}.
* @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) {
log.error([null, 'Socket', null, 'Unable to instantiate a new channel connection ' +
'as there is already an ongoing channel connection']);
return;
}
if (self._readyState !== self.READY_STATE_CHANGE.COMPLETED) {
log.error([null, 'Socket', null, 'Unable to instantiate a new channel connection ' +
'as readyState is not ready']);
return;
}
// set if forceSSL
if (self._forceSSL) {
self._signalingServerProtocol = 'https:';
} else {
self._signalingServerProtocol = window.location.protocol;
}
var socketType = 'WebSocket';
// For IE < 9 that doesn't support WebSocket
if (!window.WebSocket) {
socketType = 'Polling';
}
// Begin with a websocket connection
self._createSocket(socketType);
};
/**
* Disconnects the current socket connection with the platform signaling.
* @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');
};