/** * socket.io * Copyright(c) 2011 LearnBoost * 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 );