579 lines
14 KiB
JavaScript
579 lines
14 KiB
JavaScript
/**
|
|
* socket.io
|
|
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
|
* MIT Licensed
|
|
*/
|
|
|
|
(function (exports, io, global) {
|
|
|
|
/**
|
|
* Expose constructor.
|
|
*/
|
|
|
|
exports.Socket = Socket;
|
|
|
|
/**
|
|
* Create a new `Socket.IO client` which can establish a persistent
|
|
* connection with a Socket.IO enabled server.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
function Socket (options) {
|
|
this.options = {
|
|
port: 80
|
|
, secure: false
|
|
, document: 'document' in global ? document : false
|
|
, resource: 'socket.io'
|
|
, transports: io.transports
|
|
, 'connect timeout': 10000
|
|
, 'try multiple transports': true
|
|
, 'reconnect': true
|
|
, 'reconnection delay': 500
|
|
, 'reconnection limit': Infinity
|
|
, 'reopen delay': 3000
|
|
, 'max reconnection attempts': 10
|
|
, 'sync disconnect on unload': false
|
|
, 'auto connect': true
|
|
, 'flash policy port': 10843
|
|
, 'manualFlush': false
|
|
};
|
|
|
|
io.util.merge(this.options, options);
|
|
|
|
this.connected = false;
|
|
this.open = false;
|
|
this.connecting = false;
|
|
this.reconnecting = false;
|
|
this.namespaces = {};
|
|
this.buffer = [];
|
|
this.doBuffer = false;
|
|
|
|
if (this.options['sync disconnect on unload'] &&
|
|
(!this.isXDomain() || io.util.ua.hasCORS)) {
|
|
var self = this;
|
|
io.util.on(global, 'beforeunload', function () {
|
|
self.disconnectSync();
|
|
}, false);
|
|
}
|
|
|
|
if (this.options['auto connect']) {
|
|
this.connect();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Apply EventEmitter mixin.
|
|
*/
|
|
|
|
io.util.mixin(Socket, io.EventEmitter);
|
|
|
|
/**
|
|
* Returns a namespace listener/emitter for this socket
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.of = function (name) {
|
|
if (!this.namespaces[name]) {
|
|
this.namespaces[name] = new io.SocketNamespace(this, name);
|
|
|
|
if (name !== '') {
|
|
this.namespaces[name].packet({ type: 'connect' });
|
|
}
|
|
}
|
|
|
|
return this.namespaces[name];
|
|
};
|
|
|
|
/**
|
|
* Emits the given event to the Socket and all namespaces
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.publish = function () {
|
|
this.emit.apply(this, arguments);
|
|
|
|
var nsp;
|
|
|
|
for (var i in this.namespaces) {
|
|
if (this.namespaces.hasOwnProperty(i)) {
|
|
nsp = this.of(i);
|
|
nsp.$emit.apply(nsp, arguments);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Performs the handshake
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
function empty () { };
|
|
|
|
Socket.prototype.handshake = function (fn) {
|
|
var self = this
|
|
, options = this.options;
|
|
|
|
function complete (data) {
|
|
if (data instanceof Error) {
|
|
self.connecting = false;
|
|
self.onError(data.message);
|
|
} else {
|
|
fn.apply(null, data.split(':'));
|
|
}
|
|
};
|
|
|
|
var url = [
|
|
'http' + (options.secure ? 's' : '') + ':/'
|
|
, options.host + ':' + options.port
|
|
, options.resource
|
|
, io.protocol
|
|
, io.util.query(this.options.query, 't=' + +new Date)
|
|
].join('/');
|
|
|
|
if (this.isXDomain() && !io.util.ua.hasCORS) {
|
|
var insertAt = document.getElementsByTagName('script')[0]
|
|
, script = document.createElement('script');
|
|
|
|
script.src = url + '&jsonp=' + io.j.length;
|
|
insertAt.parentNode.insertBefore(script, insertAt);
|
|
|
|
io.j.push(function (data) {
|
|
complete(data);
|
|
script.parentNode.removeChild(script);
|
|
});
|
|
} else {
|
|
var xhr = io.util.request();
|
|
|
|
xhr.open('GET', url, true);
|
|
if (this.isXDomain()) {
|
|
xhr.withCredentials = true;
|
|
}
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState == 4) {
|
|
xhr.onreadystatechange = empty;
|
|
|
|
if (xhr.status == 200) {
|
|
complete(xhr.responseText);
|
|
} else if (xhr.status == 403) {
|
|
self.onError(xhr.responseText);
|
|
} else {
|
|
self.connecting = false;
|
|
!self.reconnecting && self.onError(xhr.responseText);
|
|
}
|
|
}
|
|
};
|
|
xhr.send(null);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Find an available transport based on the options supplied in the constructor.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.getTransport = function (override) {
|
|
var transports = override || this.transports, match;
|
|
|
|
for (var i = 0, transport; transport = transports[i]; i++) {
|
|
if (io.Transport[transport]
|
|
&& io.Transport[transport].check(this)
|
|
&& (!this.isXDomain() || io.Transport[transport].xdomainCheck(this))) {
|
|
return new io.Transport[transport](this, this.sessionid);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Connects to the server.
|
|
*
|
|
* @param {Function} [fn] Callback.
|
|
* @returns {io.Socket}
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.connect = function (fn) {
|
|
if (this.connecting) {
|
|
return this;
|
|
}
|
|
|
|
var self = this;
|
|
self.connecting = true;
|
|
|
|
this.handshake(function (sid, heartbeat, close, transports) {
|
|
self.sessionid = sid;
|
|
self.closeTimeout = close * 1000;
|
|
self.heartbeatTimeout = heartbeat * 1000;
|
|
if(!self.transports)
|
|
self.transports = self.origTransports = (transports ? io.util.intersect(
|
|
transports.split(',')
|
|
, self.options.transports
|
|
) : self.options.transports);
|
|
|
|
self.setHeartbeatTimeout();
|
|
|
|
function connect (transports){
|
|
if (self.transport) self.transport.clearTimeouts();
|
|
|
|
self.transport = self.getTransport(transports);
|
|
if (!self.transport) return self.publish('connect_failed');
|
|
|
|
// once the transport is ready
|
|
self.transport.ready(self, function () {
|
|
self.connecting = true;
|
|
self.publish('connecting', self.transport.name);
|
|
self.transport.open();
|
|
|
|
if (self.options['connect timeout']) {
|
|
self.connectTimeoutTimer = setTimeout(function () {
|
|
if (!self.connected) {
|
|
self.connecting = false;
|
|
|
|
if (self.options['try multiple transports']) {
|
|
var remaining = self.transports;
|
|
|
|
while (remaining.length > 0 && remaining.splice(0,1)[0] !=
|
|
self.transport.name) {}
|
|
|
|
if (remaining.length){
|
|
connect(remaining);
|
|
} else {
|
|
self.publish('connect_failed');
|
|
}
|
|
}
|
|
}
|
|
}, self.options['connect timeout']);
|
|
}
|
|
});
|
|
}
|
|
|
|
connect(self.transports);
|
|
|
|
self.once('connect', function (){
|
|
clearTimeout(self.connectTimeoutTimer);
|
|
|
|
fn && typeof fn == 'function' && fn();
|
|
});
|
|
});
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Clears and sets a new heartbeat timeout using the value given by the
|
|
* server during the handshake.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.setHeartbeatTimeout = function () {
|
|
clearTimeout(this.heartbeatTimeoutTimer);
|
|
if(this.transport && !this.transport.heartbeats()) return;
|
|
|
|
var self = this;
|
|
this.heartbeatTimeoutTimer = setTimeout(function () {
|
|
self.transport.onClose();
|
|
}, this.heartbeatTimeout);
|
|
};
|
|
|
|
/**
|
|
* Sends a message.
|
|
*
|
|
* @param {Object} data packet.
|
|
* @returns {io.Socket}
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.packet = function (data) {
|
|
if (this.connected && !this.doBuffer) {
|
|
this.transport.packet(data);
|
|
} else {
|
|
this.buffer.push(data);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sets buffer state
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.setBuffer = function (v) {
|
|
this.doBuffer = v;
|
|
|
|
if (!v && this.connected && this.buffer.length) {
|
|
if (!this.options['manualFlush']) {
|
|
this.flushBuffer();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Flushes the buffer data over the wire.
|
|
* To be invoked manually when 'manualFlush' is set to true.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.flushBuffer = function() {
|
|
this.transport.payload(this.buffer);
|
|
this.buffer = [];
|
|
};
|
|
|
|
|
|
/**
|
|
* Disconnect the established connect.
|
|
*
|
|
* @returns {io.Socket}
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.disconnect = function () {
|
|
if (this.connected || this.connecting) {
|
|
if (this.open) {
|
|
this.of('').packet({ type: 'disconnect' });
|
|
}
|
|
|
|
// handle disconnection immediately
|
|
this.onDisconnect('booted');
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Disconnects the socket with a sync XHR.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.disconnectSync = function () {
|
|
// ensure disconnection
|
|
var xhr = io.util.request();
|
|
var uri = [
|
|
'http' + (this.options.secure ? 's' : '') + ':/'
|
|
, this.options.host + ':' + this.options.port
|
|
, this.options.resource
|
|
, io.protocol
|
|
, ''
|
|
, this.sessionid
|
|
].join('/') + '/?disconnect=1';
|
|
|
|
xhr.open('GET', uri, false);
|
|
xhr.send(null);
|
|
|
|
// handle disconnection immediately
|
|
this.onDisconnect('booted');
|
|
};
|
|
|
|
/**
|
|
* Check if we need to use cross domain enabled transports. Cross domain would
|
|
* be a different port or different domain name.
|
|
*
|
|
* @returns {Boolean}
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.isXDomain = function () {
|
|
// if node
|
|
return false;
|
|
// end node
|
|
|
|
var port = global.location.port ||
|
|
('https:' == global.location.protocol ? 443 : 80);
|
|
|
|
return this.options.host !== global.location.hostname
|
|
|| this.options.port != port;
|
|
};
|
|
|
|
/**
|
|
* Called upon handshake.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onConnect = function () {
|
|
if (!this.connected) {
|
|
this.connected = true;
|
|
this.connecting = false;
|
|
if (!this.doBuffer) {
|
|
// make sure to flush the buffer
|
|
this.setBuffer(false);
|
|
}
|
|
this.emit('connect');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called when the transport opens
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onOpen = function () {
|
|
this.open = true;
|
|
};
|
|
|
|
/**
|
|
* Called when the transport closes.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onClose = function () {
|
|
this.open = false;
|
|
clearTimeout(this.heartbeatTimeoutTimer);
|
|
};
|
|
|
|
/**
|
|
* Called when the transport first opens a connection
|
|
*
|
|
* @param text
|
|
*/
|
|
|
|
Socket.prototype.onPacket = function (packet) {
|
|
this.of(packet.endpoint).onPacket(packet);
|
|
};
|
|
|
|
/**
|
|
* Handles an error.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onError = function (err) {
|
|
if (err && err.advice) {
|
|
if (err.advice === 'reconnect' && (this.connected || this.connecting)) {
|
|
this.disconnect();
|
|
if (this.options.reconnect) {
|
|
this.reconnect();
|
|
}
|
|
}
|
|
}
|
|
|
|
this.publish('error', err && err.reason ? err.reason : err);
|
|
};
|
|
|
|
/**
|
|
* Called when the transport disconnects.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onDisconnect = function (reason) {
|
|
var wasConnected = this.connected
|
|
, wasConnecting = this.connecting;
|
|
|
|
this.connected = false;
|
|
this.connecting = false;
|
|
this.open = false;
|
|
|
|
if (wasConnected || wasConnecting) {
|
|
this.transport.close();
|
|
this.transport.clearTimeouts();
|
|
if (wasConnected) {
|
|
this.publish('disconnect', reason);
|
|
|
|
if ('booted' != reason && this.options.reconnect && !this.reconnecting) {
|
|
this.reconnect();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called upon reconnection.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.reconnect = function () {
|
|
this.reconnecting = true;
|
|
this.reconnectionAttempts = 0;
|
|
this.reconnectionDelay = this.options['reconnection delay'];
|
|
|
|
var self = this
|
|
, maxAttempts = this.options['max reconnection attempts']
|
|
, tryMultiple = this.options['try multiple transports']
|
|
, limit = this.options['reconnection limit'];
|
|
|
|
function reset () {
|
|
if (self.connected) {
|
|
for (var i in self.namespaces) {
|
|
if (self.namespaces.hasOwnProperty(i) && '' !== i) {
|
|
self.namespaces[i].packet({ type: 'connect' });
|
|
}
|
|
}
|
|
self.publish('reconnect', self.transport.name, self.reconnectionAttempts);
|
|
}
|
|
|
|
clearTimeout(self.reconnectionTimer);
|
|
|
|
self.removeListener('connect_failed', maybeReconnect);
|
|
self.removeListener('connect', maybeReconnect);
|
|
|
|
self.reconnecting = false;
|
|
|
|
delete self.reconnectionAttempts;
|
|
delete self.reconnectionDelay;
|
|
delete self.reconnectionTimer;
|
|
delete self.redoTransports;
|
|
|
|
self.options['try multiple transports'] = tryMultiple;
|
|
};
|
|
|
|
function maybeReconnect () {
|
|
if (!self.reconnecting) {
|
|
return;
|
|
}
|
|
|
|
if (self.connected) {
|
|
return reset();
|
|
};
|
|
|
|
if (self.connecting && self.reconnecting) {
|
|
return self.reconnectionTimer = setTimeout(maybeReconnect, 1000);
|
|
}
|
|
|
|
if (self.reconnectionAttempts++ >= maxAttempts) {
|
|
if (!self.redoTransports) {
|
|
self.on('connect_failed', maybeReconnect);
|
|
self.options['try multiple transports'] = true;
|
|
self.transports = self.origTransports;
|
|
self.transport = self.getTransport();
|
|
self.redoTransports = true;
|
|
self.connect();
|
|
} else {
|
|
self.publish('reconnect_failed');
|
|
reset();
|
|
}
|
|
} else {
|
|
if (self.reconnectionDelay < limit) {
|
|
self.reconnectionDelay *= 2; // exponential back off
|
|
}
|
|
|
|
self.connect();
|
|
self.publish('reconnecting', self.reconnectionDelay, self.reconnectionAttempts);
|
|
self.reconnectionTimer = setTimeout(maybeReconnect, self.reconnectionDelay);
|
|
}
|
|
};
|
|
|
|
this.options['try multiple transports'] = false;
|
|
this.reconnectionTimer = setTimeout(maybeReconnect, this.reconnectionDelay);
|
|
|
|
this.on('connect', maybeReconnect);
|
|
};
|
|
|
|
})(
|
|
'undefined' != typeof io ? io : module.exports
|
|
, 'undefined' != typeof io ? io : module.parent.exports
|
|
, this
|
|
);
|