3 * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
7 (function (exports, io, global) {
13 exports.Socket = Socket;
16 * Create a new `Socket.IO client` which can establish a persistent
17 * connection with a Socket.IO enabled server.
22 function Socket (options) {
26 , document: 'document' in global ? document : false
27 , resource: 'socket.io'
28 , transports: io.transports
29 , 'connect timeout': 10000
30 , 'try multiple transports': true
32 , 'reconnection delay': 500
33 , 'reconnection limit': Infinity
34 , 'reopen delay': 3000
35 , 'max reconnection attempts': 10
36 , 'sync disconnect on unload': false
37 , 'auto connect': true
38 , 'flash policy port': 10843
39 , 'manualFlush': false
42 io.util.merge(this.options, options);
44 this.connected = false;
46 this.connecting = false;
47 this.reconnecting = false;
50 this.doBuffer = false;
52 if (this.options['sync disconnect on unload'] &&
53 (!this.isXDomain() || io.util.ua.hasCORS)) {
55 io.util.on(global, 'beforeunload', function () {
56 self.disconnectSync();
60 if (this.options['auto connect']) {
66 * Apply EventEmitter mixin.
69 io.util.mixin(Socket, io.EventEmitter);
72 * Returns a namespace listener/emitter for this socket
77 Socket.prototype.of = function (name) {
78 if (!this.namespaces[name]) {
79 this.namespaces[name] = new io.SocketNamespace(this, name);
82 this.namespaces[name].packet({ type: 'connect' });
86 return this.namespaces[name];
90 * Emits the given event to the Socket and all namespaces
95 Socket.prototype.publish = function () {
96 this.emit.apply(this, arguments);
100 for (var i in this.namespaces) {
101 if (this.namespaces.hasOwnProperty(i)) {
103 nsp.$emit.apply(nsp, arguments);
109 * Performs the handshake
114 function empty () { };
116 Socket.prototype.handshake = function (fn) {
118 , options = this.options;
120 function complete (data) {
121 if (data instanceof Error) {
122 self.connecting = false;
123 self.onError(data.message);
125 fn.apply(null, data.split(':'));
130 'http' + (options.secure ? 's' : '') + ':/'
131 , options.host + ':' + options.port
134 , io.util.query(this.options.query, 't=' + +new Date)
137 if (this.isXDomain() && !io.util.ua.hasCORS) {
138 var insertAt = document.getElementsByTagName('script')[0]
139 , script = document.createElement('script');
141 script.src = url + '&jsonp=' + io.j.length;
142 insertAt.parentNode.insertBefore(script, insertAt);
144 io.j.push(function (data) {
146 script.parentNode.removeChild(script);
149 var xhr = io.util.request();
151 xhr.open('GET', url, true);
152 if (this.isXDomain()) {
153 xhr.withCredentials = true;
155 xhr.onreadystatechange = function () {
156 if (xhr.readyState == 4) {
157 xhr.onreadystatechange = empty;
159 if (xhr.status == 200) {
160 complete(xhr.responseText);
161 } else if (xhr.status == 403) {
162 self.onError(xhr.responseText);
164 self.connecting = false;
165 !self.reconnecting && self.onError(xhr.responseText);
174 * Find an available transport based on the options supplied in the constructor.
179 Socket.prototype.getTransport = function (override) {
180 var transports = override || this.transports, match;
182 for (var i = 0, transport; transport = transports[i]; i++) {
183 if (io.Transport[transport]
184 && io.Transport[transport].check(this)
185 && (!this.isXDomain() || io.Transport[transport].xdomainCheck(this))) {
186 return new io.Transport[transport](this, this.sessionid);
194 * Connects to the server.
196 * @param {Function} [fn] Callback.
197 * @returns {io.Socket}
201 Socket.prototype.connect = function (fn) {
202 if (this.connecting) {
207 self.connecting = true;
209 this.handshake(function (sid, heartbeat, close, transports) {
210 self.sessionid = sid;
211 self.closeTimeout = close * 1000;
212 self.heartbeatTimeout = heartbeat * 1000;
214 self.transports = self.origTransports = (transports ? io.util.intersect(
215 transports.split(',')
216 , self.options.transports
217 ) : self.options.transports);
219 self.setHeartbeatTimeout();
221 function connect (transports){
222 if (self.transport) self.transport.clearTimeouts();
224 self.transport = self.getTransport(transports);
225 if (!self.transport) return self.publish('connect_failed');
227 // once the transport is ready
228 self.transport.ready(self, function () {
229 self.connecting = true;
230 self.publish('connecting', self.transport.name);
231 self.transport.open();
233 if (self.options['connect timeout']) {
234 self.connectTimeoutTimer = setTimeout(function () {
235 if (!self.connected) {
236 self.connecting = false;
238 if (self.options['try multiple transports']) {
239 var remaining = self.transports;
241 while (remaining.length > 0 && remaining.splice(0,1)[0] !=
242 self.transport.name) {}
244 if (remaining.length){
247 self.publish('connect_failed');
251 }, self.options['connect timeout']);
256 connect(self.transports);
258 self.once('connect', function (){
259 clearTimeout(self.connectTimeoutTimer);
261 fn && typeof fn == 'function' && fn();
269 * Clears and sets a new heartbeat timeout using the value given by the
270 * server during the handshake.
275 Socket.prototype.setHeartbeatTimeout = function () {
276 clearTimeout(this.heartbeatTimeoutTimer);
277 if(this.transport && !this.transport.heartbeats()) return;
280 this.heartbeatTimeoutTimer = setTimeout(function () {
281 self.transport.onClose();
282 }, this.heartbeatTimeout);
288 * @param {Object} data packet.
289 * @returns {io.Socket}
293 Socket.prototype.packet = function (data) {
294 if (this.connected && !this.doBuffer) {
295 this.transport.packet(data);
297 this.buffer.push(data);
309 Socket.prototype.setBuffer = function (v) {
312 if (!v && this.connected && this.buffer.length) {
313 if (!this.options['manualFlush']) {
320 * Flushes the buffer data over the wire.
321 * To be invoked manually when 'manualFlush' is set to true.
326 Socket.prototype.flushBuffer = function() {
327 this.transport.payload(this.buffer);
333 * Disconnect the established connect.
335 * @returns {io.Socket}
339 Socket.prototype.disconnect = function () {
340 if (this.connected || this.connecting) {
342 this.of('').packet({ type: 'disconnect' });
345 // handle disconnection immediately
346 this.onDisconnect('booted');
353 * Disconnects the socket with a sync XHR.
358 Socket.prototype.disconnectSync = function () {
359 // ensure disconnection
360 var xhr = io.util.request();
362 'http' + (this.options.secure ? 's' : '') + ':/'
363 , this.options.host + ':' + this.options.port
364 , this.options.resource
368 ].join('/') + '/?disconnect=1';
370 xhr.open('GET', uri, false);
373 // handle disconnection immediately
374 this.onDisconnect('booted');
378 * Check if we need to use cross domain enabled transports. Cross domain would
379 * be a different port or different domain name.
385 Socket.prototype.isXDomain = function () {
390 var port = global.location.port ||
391 ('https:' == global.location.protocol ? 443 : 80);
393 return this.options.host !== global.location.hostname
394 || this.options.port != port;
398 * Called upon handshake.
403 Socket.prototype.onConnect = function () {
404 if (!this.connected) {
405 this.connected = true;
406 this.connecting = false;
407 if (!this.doBuffer) {
408 // make sure to flush the buffer
409 this.setBuffer(false);
411 this.emit('connect');
416 * Called when the transport opens
421 Socket.prototype.onOpen = function () {
426 * Called when the transport closes.
431 Socket.prototype.onClose = function () {
433 clearTimeout(this.heartbeatTimeoutTimer);
437 * Called when the transport first opens a connection
442 Socket.prototype.onPacket = function (packet) {
443 this.of(packet.endpoint).onPacket(packet);
452 Socket.prototype.onError = function (err) {
453 if (err && err.advice) {
454 if (err.advice === 'reconnect' && (this.connected || this.connecting)) {
456 if (this.options.reconnect) {
462 this.publish('error', err && err.reason ? err.reason : err);
466 * Called when the transport disconnects.
471 Socket.prototype.onDisconnect = function (reason) {
472 var wasConnected = this.connected
473 , wasConnecting = this.connecting;
475 this.connected = false;
476 this.connecting = false;
479 if (wasConnected || wasConnecting) {
480 this.transport.close();
481 this.transport.clearTimeouts();
483 this.publish('disconnect', reason);
485 if ('booted' != reason && this.options.reconnect && !this.reconnecting) {
493 * Called upon reconnection.
498 Socket.prototype.reconnect = function () {
499 this.reconnecting = true;
500 this.reconnectionAttempts = 0;
501 this.reconnectionDelay = this.options['reconnection delay'];
504 , maxAttempts = this.options['max reconnection attempts']
505 , tryMultiple = this.options['try multiple transports']
506 , limit = this.options['reconnection limit'];
509 if (self.connected) {
510 for (var i in self.namespaces) {
511 if (self.namespaces.hasOwnProperty(i) && '' !== i) {
512 self.namespaces[i].packet({ type: 'connect' });
515 self.publish('reconnect', self.transport.name, self.reconnectionAttempts);
518 clearTimeout(self.reconnectionTimer);
520 self.removeListener('connect_failed', maybeReconnect);
521 self.removeListener('connect', maybeReconnect);
523 self.reconnecting = false;
525 delete self.reconnectionAttempts;
526 delete self.reconnectionDelay;
527 delete self.reconnectionTimer;
528 delete self.redoTransports;
530 self.options['try multiple transports'] = tryMultiple;
533 function maybeReconnect () {
534 if (!self.reconnecting) {
538 if (self.connected) {
542 if (self.connecting && self.reconnecting) {
543 return self.reconnectionTimer = setTimeout(maybeReconnect, 1000);
546 if (self.reconnectionAttempts++ >= maxAttempts) {
547 if (!self.redoTransports) {
548 self.on('connect_failed', maybeReconnect);
549 self.options['try multiple transports'] = true;
550 self.transports = self.origTransports;
551 self.transport = self.getTransport();
552 self.redoTransports = true;
555 self.publish('reconnect_failed');
559 if (self.reconnectionDelay < limit) {
560 self.reconnectionDelay *= 2; // exponential back off
564 self.publish('reconnecting', self.reconnectionDelay, self.reconnectionAttempts);
565 self.reconnectionTimer = setTimeout(maybeReconnect, self.reconnectionDelay);
569 this.options['try multiple transports'] = false;
570 this.reconnectionTimer = setTimeout(maybeReconnect, this.reconnectionDelay);
572 this.on('connect', maybeReconnect);
576 'undefined' != typeof io ? io : module.exports
577 , 'undefined' != typeof io ? io : module.parent.exports