UNPKG

12.1 kBJavaScriptView Raw
1
2/**
3 * socket.io
4 * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
5 * MIT Licensed
6 */
7
8(function (exports, io, global) {
9
10 /**
11 * Expose constructor.
12 */
13
14 exports.Socket = Socket;
15
16 /**
17 * Create a new `Socket.IO client` which can establish a persistent
18 * connection with a Socket.IO enabled server.
19 *
20 * @api public
21 */
22
23 function Socket (options) {
24 this.options = {
25 port: 80
26 , secure: false
27 , document: 'document' in global ? document : false
28 , resource: 'socket.io'
29 , transports: io.transports
30 , 'connect timeout': 10000
31 , 'try multiple transports': true
32 , 'reconnect': true
33 , 'reconnection delay': 500
34 , 'reconnection limit': Infinity
35 , 'reopen delay': 3000
36 , 'max reconnection attempts': 10
37 , 'sync disconnect on unload': true
38 , 'auto connect': true
39 };
40
41 io.util.merge(this.options, options);
42
43 this.connected = false;
44 this.open = false;
45 this.connecting = false;
46 this.reconnecting = false;
47 this.namespaces = {};
48 this.buffer = [];
49 this.doBuffer = false;
50
51 if (this.options['sync disconnect on unload'] &&
52 (!this.isXDomain() || io.util.ua.hasCORS)) {
53 var self = this;
54
55 io.util.on(global, 'beforeunload', function () {
56 self.disconnectSync();
57 }, false);
58 }
59
60 if (this.options['auto connect']) {
61 this.connect();
62 }
63};
64
65 /**
66 * Apply EventEmitter mixin.
67 */
68
69 io.util.mixin(Socket, io.EventEmitter);
70
71 /**
72 * Returns a namespace listener/emitter for this socket
73 *
74 * @api public
75 */
76
77 Socket.prototype.of = function (name) {
78 if (!this.namespaces[name]) {
79 this.namespaces[name] = new io.SocketNamespace(this, name);
80
81 if (name !== '') {
82 this.namespaces[name].packet({ type: 'connect' });
83 }
84 }
85
86 return this.namespaces[name];
87 };
88
89 /**
90 * Emits the given event to the Socket and all namespaces
91 *
92 * @api private
93 */
94
95 Socket.prototype.publish = function () {
96 this.emit.apply(this, arguments);
97
98 var nsp;
99
100 for (var i in this.namespaces) {
101 if (this.namespaces.hasOwnProperty(i)) {
102 nsp = this.of(i);
103 nsp.$emit.apply(nsp, arguments);
104 }
105 }
106 };
107
108 /**
109 * Performs the handshake
110 *
111 * @api private
112 */
113
114 function empty () { };
115
116 Socket.prototype.handshake = function (fn) {
117 var self = this
118 , options = this.options;
119
120 function complete (data) {
121 if (data instanceof Error) {
122 self.onError(data.message);
123 } else {
124 fn.apply(null, data.split(':'));
125 }
126 };
127
128 var url = [
129 'http' + (options.secure ? 's' : '') + ':/'
130 , options.host + ':' + options.port
131 , this.options.resource
132 , io.protocol
133 , io.util.query(this.options.query, 't=' + +new Date)
134 ].join('/');
135
136 if (this.isXDomain()) {
137 var insertAt = document.getElementsByTagName('script')[0]
138 , script = document.createElement('SCRIPT');
139
140 script.src = url + '&jsonp=' + io.j.length;
141 insertAt.parentNode.insertBefore(script, insertAt);
142
143 io.j.push(function (data) {
144 complete(data);
145 script.parentNode.removeChild(script);
146 });
147 } else {
148 var xhr = io.util.request();
149
150 xhr.open('GET', url, true);
151 xhr.onreadystatechange = function () {
152 if (xhr.readyState == 4) {
153 xhr.onreadystatechange = empty;
154
155 if (xhr.status == 200) {
156 complete(xhr.responseText);
157 } else {
158 !self.reconnecting && self.onError(xhr.responseText);
159 }
160 }
161 };
162 xhr.send(null);
163 }
164 };
165
166 /**
167 * Find an available transport based on the options supplied in the constructor.
168 *
169 * @api private
170 */
171
172 Socket.prototype.getTransport = function (override) {
173 var transports = override || this.transports, match;
174
175 for (var i = 0, transport; transport = transports[i]; i++) {
176 if (io.Transport[transport]
177 && io.Transport[transport].check(this)
178 && (!this.isXDomain() || io.Transport[transport].xdomainCheck())) {
179 return new io.Transport[transport](this, this.sessionid);
180 }
181 }
182
183 return null;
184 };
185
186 /**
187 * Connects to the server.
188 *
189 * @param {Function} [fn] Callback.
190 * @returns {io.Socket}
191 * @api public
192 */
193
194 Socket.prototype.connect = function (fn) {
195 if (this.connecting) {
196 return this;
197 }
198
199 var self = this;
200
201 this.handshake(function (sid, heartbeat, close, transports) {
202 self.sessionid = sid;
203 self.closeTimeout = close * 1000;
204 self.heartbeatTimeout = heartbeat * 1000;
205 self.transports = io.util.intersect(
206 transports.split(',')
207 , self.options.transports
208 );
209
210 function connect (transports){
211 self.transport = self.getTransport(transports);
212 if (!self.transport) return self.publish('connect_failed');
213
214 // once the transport is ready
215 self.transport.ready(self, function () {
216 self.connecting = true;
217 self.publish('connecting', self.transport.name);
218 self.transport.open();
219
220 if (self.options['connect timeout']) {
221 self.connectTimeoutTimer = setTimeout(function () {
222 if (!self.connected) {
223 self.connecting = false;
224
225 if (self.options['try multiple transports']) {
226 if (!self.remainingTransports) {
227 self.remainingTransports = self.transports.slice(0);
228 }
229
230 var remaining = self.remainingTransports;
231
232 while (remaining.length > 0 && remaining.splice(0,1)[0] !=
233 self.transport.name) {}
234
235 if (remaining.length){
236 connect(remaining);
237 } else {
238 self.publish('connect_failed');
239 }
240 }
241 }
242 }, self.options['connect timeout']);
243 }
244 });
245 }
246
247 connect();
248
249 self.once('connect', function (){
250 clearTimeout(self.connectTimeoutTimer);
251
252 fn && typeof fn == 'function' && fn();
253 });
254 });
255
256 return this;
257 };
258
259 /**
260 * Sends a message.
261 *
262 * @param {Object} data packet.
263 * @returns {io.Socket}
264 * @api public
265 */
266
267 Socket.prototype.packet = function (data) {
268 if (this.connected && !this.doBuffer) {
269 this.transport.packet(data);
270 } else {
271 this.buffer.push(data);
272 }
273
274 return this;
275 };
276
277 /**
278 * Sets buffer state
279 *
280 * @api private
281 */
282
283 Socket.prototype.setBuffer = function (v) {
284 this.doBuffer = v;
285
286 if (!v && this.connected && this.buffer.length) {
287 this.transport.payload(this.buffer);
288 this.buffer = [];
289 }
290 };
291
292 /**
293 * Disconnect the established connect.
294 *
295 * @returns {io.Socket}
296 * @api public
297 */
298
299 Socket.prototype.disconnect = function () {
300 if (this.connected) {
301 if (this.open) {
302 this.of('').packet({ type: 'disconnect' });
303 }
304
305 // handle disconnection immediately
306 this.onDisconnect('booted');
307 }
308
309 return this;
310 };
311
312 /**
313 * Disconnects the socket with a sync XHR.
314 *
315 * @api private
316 */
317
318 Socket.prototype.disconnectSync = function () {
319 // ensure disconnection
320 var xhr = io.util.request()
321 , uri = this.resource + '/' + io.protocol + '/' + this.sessionid;
322
323 xhr.open('GET', uri, true);
324
325 // handle disconnection immediately
326 this.onDisconnect('booted');
327 };
328
329 /**
330 * Check if we need to use cross domain enabled transports. Cross domain would
331 * be a different port or different domain name.
332 *
333 * @returns {Boolean}
334 * @api private
335 */
336
337 Socket.prototype.isXDomain = function () {
338 // if node
339 return false;
340 // end node
341
342 var locPort = window.location.port || 80;
343 return this.options.host !== document.domain || this.options.port != locPort;
344 };
345
346 /**
347 * Called upon handshake.
348 *
349 * @api private
350 */
351
352 Socket.prototype.onConnect = function () {
353 if (!this.connected) {
354 this.connected = true;
355 this.connecting = false;
356 if (!this.doBuffer) {
357 // make sure to flush the buffer
358 this.setBuffer(false);
359 }
360 this.emit('connect');
361 }
362 };
363
364 /**
365 * Called when the transport opens
366 *
367 * @api private
368 */
369
370 Socket.prototype.onOpen = function () {
371 this.open = true;
372 };
373
374 /**
375 * Called when the transport closes.
376 *
377 * @api private
378 */
379
380 Socket.prototype.onClose = function () {
381 this.open = false;
382 };
383
384 /**
385 * Called when the transport first opens a connection
386 *
387 * @param text
388 */
389
390 Socket.prototype.onPacket = function (packet) {
391 this.of(packet.endpoint).onPacket(packet);
392 };
393
394 /**
395 * Handles an error.
396 *
397 * @api private
398 */
399
400 Socket.prototype.onError = function (err) {
401 if (err && err.advice) {
402 if (err.advice === 'reconnect' && this.connected) {
403 this.disconnect();
404 this.reconnect();
405 }
406 }
407
408 this.publish('error', err && err.reason ? err.reason : err);
409 };
410
411 /**
412 * Called when the transport disconnects.
413 *
414 * @api private
415 */
416
417 Socket.prototype.onDisconnect = function (reason) {
418 var wasConnected = this.connected;
419
420 this.connected = false;
421 this.connecting = false;
422 this.open = false;
423
424 if (wasConnected) {
425 this.transport.close();
426 this.transport.clearTimeouts();
427 this.publish('disconnect', reason);
428
429 if ('booted' != reason && this.options.reconnect && !this.reconnecting) {
430 this.reconnect();
431 }
432 }
433 };
434
435 /**
436 * Called upon reconnection.
437 *
438 * @api private
439 */
440
441 Socket.prototype.reconnect = function () {
442 this.reconnecting = true;
443 this.reconnectionAttempts = 0;
444 this.reconnectionDelay = this.options['reconnection delay'];
445
446 var self = this
447 , maxAttempts = this.options['max reconnection attempts']
448 , tryMultiple = this.options['try multiple transports']
449 , limit = this.options['reconnection limit'];
450
451 function reset () {
452 if (self.connected) {
453 for (var i in self.namespaces) {
454 if (self.namespaces.hasOwnProperty(i) && '' !== i) {
455 self.namespaces[i].packet({ type: 'connect' });
456 }
457 }
458 self.publish('reconnect', self.transport.name, self.reconnectionAttempts);
459 }
460
461 self.removeListener('connect_failed', maybeReconnect);
462 self.removeListener('connect', maybeReconnect);
463
464 self.reconnecting = false;
465
466 delete self.reconnectionAttempts;
467 delete self.reconnectionDelay;
468 delete self.reconnectionTimer;
469 delete self.redoTransports;
470
471 self.options['try multiple transports'] = tryMultiple;
472 };
473
474 function maybeReconnect () {
475 if (!self.reconnecting) {
476 return;
477 }
478
479 if (self.connected) {
480 return reset();
481 };
482
483 if (self.connecting && self.reconnecting) {
484 return self.reconnectionTimer = setTimeout(maybeReconnect, 1000);
485 }
486
487 if (self.reconnectionAttempts++ >= maxAttempts) {
488 if (!self.redoTransports) {
489 self.on('connect_failed', maybeReconnect);
490 self.options['try multiple transports'] = true;
491 self.transport = self.getTransport();
492 self.redoTransports = true;
493 self.connect();
494 } else {
495 self.publish('reconnect_failed');
496 reset();
497 }
498 } else {
499 if (self.reconnectionDelay < limit) {
500 self.reconnectionDelay *= 2; // exponential back off
501 }
502
503 self.connect();
504 self.publish('reconnecting', self.reconnectionDelay, self.reconnectionAttempts);
505 self.reconnectionTimer = setTimeout(maybeReconnect, self.reconnectionDelay);
506 }
507 };
508
509 this.options['try multiple transports'] = false;
510 this.reconnectionTimer = setTimeout(maybeReconnect, this.reconnectionDelay);
511
512 this.on('connect', maybeReconnect);
513 };
514
515})(
516 'undefined' != typeof io ? io : module.exports
517 , 'undefined' != typeof io ? io : module.parent.exports
518 , this
519);