UNPKG

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