PageRenderTime 35ms CodeModel.GetById 6ms app.highlight 24ms RepoModel.GetById 1ms app.codeStats 0ms

/platforms/node/node_modules/socket.io/node_modules/socket.io-client/node_modules/engine.io-client/lib/socket.js

https://github.com/paramarco/visible
JavaScript | 627 lines | 359 code | 106 blank | 162 comment | 91 complexity | 06b2d9d50ce26bd69f96e6c24e8864a4 MD5 | raw file
  1/**
  2 * Module dependencies.
  3 */
  4
  5var transports = require('./transports');
  6var Emitter = require('component-emitter');
  7var debug = require('debug')('engine.io-client:socket');
  8var index = require('indexof');
  9var parser = require('engine.io-parser');
 10var parseuri = require('parseuri');
 11var parsejson = require('parsejson');
 12var parseqs = require('parseqs');
 13
 14/**
 15 * Module exports.
 16 */
 17
 18module.exports = Socket;
 19
 20/**
 21 * Noop function.
 22 *
 23 * @api private
 24 */
 25
 26function noop(){}
 27
 28/**
 29 * Socket constructor.
 30 *
 31 * @param {String|Object} uri or options
 32 * @param {Object} options
 33 * @api public
 34 */
 35
 36function Socket(uri, opts){
 37  if (!(this instanceof Socket)) return new Socket(uri, opts);
 38
 39  opts = opts || {};
 40
 41  if (uri && 'object' == typeof uri) {
 42    opts = uri;
 43    uri = null;
 44  }
 45
 46  if (uri) {
 47    uri = parseuri(uri);
 48    opts.host = uri.host;
 49    opts.secure = uri.protocol == 'https' || uri.protocol == 'wss';
 50    opts.port = uri.port;
 51    if (uri.query) opts.query = uri.query;
 52  }
 53
 54  this.secure = null != opts.secure ? opts.secure :
 55    (global.location && 'https:' == location.protocol);
 56
 57  if (opts.host) {
 58    var pieces = opts.host.split(':');
 59    opts.hostname = pieces.shift();
 60    if (pieces.length) opts.port = pieces.pop();
 61  }
 62
 63  this.agent = opts.agent || false;
 64  this.hostname = opts.hostname ||
 65    (global.location ? location.hostname : 'localhost');
 66  this.port = opts.port || (global.location && location.port ?
 67       location.port :
 68       (this.secure ? 443 : 80));
 69  this.query = opts.query || {};
 70  if ('string' == typeof this.query) this.query = parseqs.decode(this.query);
 71  this.upgrade = false !== opts.upgrade;
 72  this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/';
 73  this.forceJSONP = !!opts.forceJSONP;
 74  this.forceBase64 = !!opts.forceBase64;
 75  this.timestampParam = opts.timestampParam || 't';
 76  this.timestampRequests = opts.timestampRequests;
 77  this.transports = opts.transports || ['polling', 'websocket'];
 78  this.readyState = '';
 79  this.writeBuffer = [];
 80  this.callbackBuffer = [];
 81  this.policyPort = opts.policyPort || 843;
 82  this.rememberUpgrade = opts.rememberUpgrade || false;
 83  this.open();
 84  this.binaryType = null;
 85  this.onlyBinaryUpgrades = opts.onlyBinaryUpgrades;
 86}
 87
 88Socket.priorWebsocketSuccess = false;
 89
 90/**
 91 * Mix in `Emitter`.
 92 */
 93
 94Emitter(Socket.prototype);
 95
 96/**
 97 * Protocol version.
 98 *
 99 * @api public
100 */
101
102Socket.protocol = parser.protocol; // this is an int
103
104/**
105 * Expose deps for legacy compatibility
106 * and standalone browser access.
107 */
108
109Socket.Socket = Socket;
110Socket.Transport = require('./transport');
111Socket.transports = require('./transports');
112Socket.parser = require('engine.io-parser');
113
114/**
115 * Creates transport of the given type.
116 *
117 * @param {String} transport name
118 * @return {Transport}
119 * @api private
120 */
121
122Socket.prototype.createTransport = function (name) {
123  debug('creating transport "%s"', name);
124  var query = clone(this.query);
125
126  // append engine.io protocol identifier
127  query.EIO = parser.protocol;
128
129  // transport name
130  query.transport = name;
131
132  // session id if we already have one
133  if (this.id) query.sid = this.id;
134
135  var transport = new transports[name]({
136    agent: this.agent,
137    hostname: this.hostname,
138    port: this.port,
139    secure: this.secure,
140    path: this.path,
141    query: query,
142    forceJSONP: this.forceJSONP,
143    forceBase64: this.forceBase64,
144    timestampRequests: this.timestampRequests,
145    timestampParam: this.timestampParam,
146    policyPort: this.policyPort,
147    socket: this
148  });
149
150  return transport;
151};
152
153function clone (obj) {
154  var o = {};
155  for (var i in obj) {
156    if (obj.hasOwnProperty(i)) {
157      o[i] = obj[i];
158    }
159  }
160  return o;
161}
162
163/**
164 * Initializes transport to use and starts probe.
165 *
166 * @api private
167 */
168Socket.prototype.open = function () {
169  var transport;
170  if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') != -1) {
171    transport = 'websocket';
172  } else {
173    transport = this.transports[0];
174  }
175  this.readyState = 'opening';
176  var transport = this.createTransport(transport);
177  transport.open();
178  this.setTransport(transport);
179};
180
181/**
182 * Sets the current transport. Disables the existing one (if any).
183 *
184 * @api private
185 */
186
187Socket.prototype.setTransport = function(transport){
188  debug('setting transport %s', transport.name);
189  var self = this;
190
191  if (this.transport) {
192    debug('clearing existing transport %s', this.transport.name);
193    this.transport.removeAllListeners();
194  }
195
196  // set up transport
197  this.transport = transport;
198
199  // set up transport listeners
200  transport
201  .on('drain', function(){
202    self.onDrain();
203  })
204  .on('packet', function(packet){
205    self.onPacket(packet);
206  })
207  .on('error', function(e){
208    self.onError(e);
209  })
210  .on('close', function(){
211    self.onClose('transport close');
212  });
213};
214
215/**
216 * Probes a transport.
217 *
218 * @param {String} transport name
219 * @api private
220 */
221
222Socket.prototype.probe = function (name) {
223  debug('probing transport "%s"', name);
224  var transport = this.createTransport(name, { probe: 1 })
225    , failed = false
226    , self = this;
227
228  Socket.priorWebsocketSuccess = false;
229
230  function onTransportOpen(){
231    if (self.onlyBinaryUpgrades) {
232      var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary;
233      failed = failed || upgradeLosesBinary;
234    }
235    if (failed) return;
236
237    debug('probe transport "%s" opened', name);
238    transport.send([{ type: 'ping', data: 'probe' }]);
239    transport.once('packet', function (msg) {
240      if (failed) return;
241      if ('pong' == msg.type && 'probe' == msg.data) {
242        debug('probe transport "%s" pong', name);
243        self.upgrading = true;
244        self.emit('upgrading', transport);
245        Socket.priorWebsocketSuccess = 'websocket' == transport.name;
246
247        debug('pausing current transport "%s"', self.transport.name);
248        self.transport.pause(function () {
249          if (failed) return;
250          if ('closed' == self.readyState || 'closing' == self.readyState) {
251            return;
252          }
253          debug('changing transport and sending upgrade packet');
254
255          cleanup();
256
257          self.setTransport(transport);
258          transport.send([{ type: 'upgrade' }]);
259          self.emit('upgrade', transport);
260          transport = null;
261          self.upgrading = false;
262          self.flush();
263        });
264      } else {
265        debug('probe transport "%s" failed', name);
266        var err = new Error('probe error');
267        err.transport = transport.name;
268        self.emit('upgradeError', err);
269      }
270    });
271  }
272
273  function freezeTransport() {
274    if (failed) return;
275
276    // Any callback called by transport should be ignored since now
277    failed = true;
278
279    cleanup();
280
281    transport.close();
282    transport = null;
283  }
284
285  //Handle any error that happens while probing
286  function onerror(err) {
287    var error = new Error('probe error: ' + err);
288    error.transport = transport.name;
289
290    freezeTransport();
291
292    debug('probe transport "%s" failed because of error: %s', name, err);
293
294    self.emit('upgradeError', error);
295  }
296
297  function onTransportClose(){
298    onerror("transport closed");
299  }
300
301  //When the socket is closed while we're probing
302  function onclose(){
303    onerror("socket closed");
304  }
305
306  //When the socket is upgraded while we're probing
307  function onupgrade(to){
308    if (transport && to.name != transport.name) {
309      debug('"%s" works - aborting "%s"', to.name, transport.name);
310      freezeTransport();
311    }
312  }
313
314  //Remove all listeners on the transport and on self
315  function cleanup(){
316    transport.removeListener('open', onTransportOpen);
317    transport.removeListener('error', onerror);
318    transport.removeListener('close', onTransportClose);
319    self.removeListener('close', onclose);
320    self.removeListener('upgrading', onupgrade);
321  }
322
323  transport.once('open', onTransportOpen);
324  transport.once('error', onerror);
325  transport.once('close', onTransportClose);
326
327  this.once('close', onclose);
328  this.once('upgrading', onupgrade);
329
330  transport.open();
331
332};
333
334/**
335 * Called when connection is deemed open.
336 *
337 * @api public
338 */
339
340Socket.prototype.onOpen = function () {
341  debug('socket open');
342  this.readyState = 'open';
343  Socket.priorWebsocketSuccess = 'websocket' == this.transport.name;
344  this.emit('open');
345  this.flush();
346
347  // we check for `readyState` in case an `open`
348  // listener already closed the socket
349  if ('open' == this.readyState && this.upgrade && this.transport.pause) {
350    debug('starting upgrade probes');
351    for (var i = 0, l = this.upgrades.length; i < l; i++) {
352      this.probe(this.upgrades[i]);
353    }
354  }
355};
356
357/**
358 * Handles a packet.
359 *
360 * @api private
361 */
362
363Socket.prototype.onPacket = function (packet) {
364  if ('opening' == this.readyState || 'open' == this.readyState) {
365    debug('socket receive: type "%s", data "%s"', packet.type, packet.data);
366
367    this.emit('packet', packet);
368
369    // Socket is live - any packet counts
370    this.emit('heartbeat');
371
372    switch (packet.type) {
373      case 'open':
374        this.onHandshake(parsejson(packet.data));
375        break;
376
377      case 'pong':
378        this.setPing();
379        break;
380
381      case 'error':
382        var err = new Error('server error');
383        err.code = packet.data;
384        this.emit('error', err);
385        break;
386
387      case 'message':
388        this.emit('data', packet.data);
389        this.emit('message', packet.data);
390        break;
391    }
392  } else {
393    debug('packet received with socket readyState "%s"', this.readyState);
394  }
395};
396
397/**
398 * Called upon handshake completion.
399 *
400 * @param {Object} handshake obj
401 * @api private
402 */
403
404Socket.prototype.onHandshake = function (data) {
405  this.emit('handshake', data);
406  this.id = data.sid;
407  this.transport.query.sid = data.sid;
408  this.upgrades = this.filterUpgrades(data.upgrades);
409  this.pingInterval = data.pingInterval;
410  this.pingTimeout = data.pingTimeout;
411  this.onOpen();
412  // In case open handler closes socket
413  if  ('closed' == this.readyState) return;
414  this.setPing();
415
416  // Prolong liveness of socket on heartbeat
417  this.removeListener('heartbeat', this.onHeartbeat);
418  this.on('heartbeat', this.onHeartbeat);
419};
420
421/**
422 * Resets ping timeout.
423 *
424 * @api private
425 */
426
427Socket.prototype.onHeartbeat = function (timeout) {
428  clearTimeout(this.pingTimeoutTimer);
429  var self = this;
430  self.pingTimeoutTimer = setTimeout(function () {
431    if ('closed' == self.readyState) return;
432    self.onClose('ping timeout');
433  }, timeout || (self.pingInterval + self.pingTimeout));
434};
435
436/**
437 * Pings server every `this.pingInterval` and expects response
438 * within `this.pingTimeout` or closes connection.
439 *
440 * @api private
441 */
442
443Socket.prototype.setPing = function () {
444  var self = this;
445  clearTimeout(self.pingIntervalTimer);
446  self.pingIntervalTimer = setTimeout(function () {
447    debug('writing ping packet - expecting pong within %sms', self.pingTimeout);
448    self.ping();
449    self.onHeartbeat(self.pingTimeout);
450  }, self.pingInterval);
451};
452
453/**
454* Sends a ping packet.
455*
456* @api public
457*/
458
459Socket.prototype.ping = function () {
460  this.sendPacket('ping');
461};
462
463/**
464 * Called on `drain` event
465 *
466 * @api private
467 */
468
469Socket.prototype.onDrain = function() {
470  for (var i = 0; i < this.prevBufferLen; i++) {
471    if (this.callbackBuffer[i]) {
472      this.callbackBuffer[i]();
473    }
474  }
475
476  this.writeBuffer.splice(0, this.prevBufferLen);
477  this.callbackBuffer.splice(0, this.prevBufferLen);
478
479  // setting prevBufferLen = 0 is very important
480  // for example, when upgrading, upgrade packet is sent over,
481  // and a nonzero prevBufferLen could cause problems on `drain`
482  this.prevBufferLen = 0;
483
484  if (this.writeBuffer.length == 0) {
485    this.emit('drain');
486  } else {
487    this.flush();
488  }
489};
490
491/**
492 * Flush write buffers.
493 *
494 * @api private
495 */
496
497Socket.prototype.flush = function () {
498  if ('closed' != this.readyState && this.transport.writable &&
499    !this.upgrading && this.writeBuffer.length) {
500    debug('flushing %d packets in socket', this.writeBuffer.length);
501    this.transport.send(this.writeBuffer);
502    // keep track of current length of writeBuffer
503    // splice writeBuffer and callbackBuffer on `drain`
504    this.prevBufferLen = this.writeBuffer.length;
505    this.emit('flush');
506  }
507};
508
509/**
510 * Sends a message.
511 *
512 * @param {String} message.
513 * @param {Function} callback function.
514 * @return {Socket} for chaining.
515 * @api public
516 */
517
518Socket.prototype.write =
519Socket.prototype.send = function (msg, fn) {
520  this.sendPacket('message', msg, fn);
521  return this;
522};
523
524/**
525 * Sends a packet.
526 *
527 * @param {String} packet type.
528 * @param {String} data.
529 * @param {Function} callback function.
530 * @api private
531 */
532
533Socket.prototype.sendPacket = function (type, data, fn) {
534  var packet = { type: type, data: data };
535  this.emit('packetCreate', packet);
536  this.writeBuffer.push(packet);
537  this.callbackBuffer.push(fn);
538  this.flush();
539};
540
541/**
542 * Closes the connection.
543 *
544 * @api private
545 */
546
547Socket.prototype.close = function () {
548  if ('opening' == this.readyState || 'open' == this.readyState) {
549    this.onClose('forced close');
550    debug('socket closing - telling transport to close');
551    this.transport.close();
552  }
553
554  return this;
555};
556
557/**
558 * Called upon transport error
559 *
560 * @api private
561 */
562
563Socket.prototype.onError = function (err) {
564  debug('socket error %j', err);
565  Socket.priorWebsocketSuccess = false;
566  this.emit('error', err);
567  this.onClose('transport error', err);
568};
569
570/**
571 * Called upon transport close.
572 *
573 * @api private
574 */
575
576Socket.prototype.onClose = function (reason, desc) {
577  if ('opening' == this.readyState || 'open' == this.readyState) {
578    debug('socket close with reason: "%s"', reason);
579    var self = this;
580
581    // clear timers
582    clearTimeout(this.pingIntervalTimer);
583    clearTimeout(this.pingTimeoutTimer);
584
585    // clean buffers in next tick, so developers can still
586    // grab the buffers on `close` event
587    setTimeout(function() {
588      self.writeBuffer = [];
589      self.callbackBuffer = [];
590      self.prevBufferLen = 0;
591    }, 0);
592
593    // stop event from firing again for transport
594    this.transport.removeAllListeners('close');
595
596    // ensure transport won't stay open
597    this.transport.close();
598
599    // ignore further transport communication
600    this.transport.removeAllListeners();
601
602    // set ready state
603    this.readyState = 'closed';
604
605    // clear session id
606    this.id = null;
607
608    // emit close event
609    this.emit('close', reason, desc);
610  }
611};
612
613/**
614 * Filters upgrades, returning only those matching client transports.
615 *
616 * @param {Array} server upgrades
617 * @api private
618 *
619 */
620
621Socket.prototype.filterUpgrades = function (upgrades) {
622  var filteredUpgrades = [];
623  for (var i = 0, j = upgrades.length; i<j; i++) {
624    if (~index(this.transports, upgrades[i])) filteredUpgrades.push(upgrades[i]);
625  }
626  return filteredUpgrades;
627};