6 var eio = require('engine.io-client');
7 var Socket = require('./socket');
8 var Emitter = require('component-emitter');
9 var parser = require('socket.io-parser');
10 var on = require('./on');
11 var bind = require('component-bind');
12 var debug = require('debug')('socket.io-client:manager');
13 var indexOf = require('indexof');
14 var Backoff = require('backo2');
20 var has = Object.prototype.hasOwnProperty;
26 module.exports = Manager;
29 * `Manager` constructor.
31 * @param {String} engine instance or engine uri/opts
32 * @param {Object} options
36 function Manager (uri, opts) {
37 if (!(this instanceof Manager)) return new Manager(uri, opts);
38 if (uri && ('object' === typeof uri)) {
44 opts.path = opts.path || '/socket.io';
48 this.reconnection(opts.reconnection !== false);
49 this.reconnectionAttempts(opts.reconnectionAttempts || Infinity);
50 this.reconnectionDelay(opts.reconnectionDelay || 1000);
51 this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000);
52 this.randomizationFactor(opts.randomizationFactor || 0.5);
53 this.backoff = new Backoff({
54 min: this.reconnectionDelay(),
55 max: this.reconnectionDelayMax(),
56 jitter: this.randomizationFactor()
58 this.timeout(null == opts.timeout ? 20000 : opts.timeout);
59 this.readyState = 'closed';
63 this.encoding = false;
64 this.packetBuffer = [];
65 var _parser = opts.parser || parser;
66 this.encoder = new _parser.Encoder();
67 this.decoder = new _parser.Decoder();
68 this.autoConnect = opts.autoConnect !== false;
69 if (this.autoConnect) this.open();
73 * Propagate given event to sockets and emit on `this`
78 Manager.prototype.emitAll = function () {
79 this.emit.apply(this, arguments);
80 for (var nsp in this.nsps) {
81 if (has.call(this.nsps, nsp)) {
82 this.nsps[nsp].emit.apply(this.nsps[nsp], arguments);
88 * Update `socket.id` of all sockets
93 Manager.prototype.updateSocketIds = function () {
94 for (var nsp in this.nsps) {
95 if (has.call(this.nsps, nsp)) {
96 this.nsps[nsp].id = this.generateId(nsp);
102 * generate `socket.id` for the given `nsp`
104 * @param {String} nsp
109 Manager.prototype.generateId = function (nsp) {
110 return (nsp === '/' ? '' : (nsp + '#')) + this.engine.id;
117 Emitter(Manager.prototype);
120 * Sets the `reconnection` config.
122 * @param {Boolean} true/false if it should automatically reconnect
123 * @return {Manager} self or value
127 Manager.prototype.reconnection = function (v) {
128 if (!arguments.length) return this._reconnection;
129 this._reconnection = !!v;
134 * Sets the reconnection attempts config.
136 * @param {Number} max reconnection attempts before giving up
137 * @return {Manager} self or value
141 Manager.prototype.reconnectionAttempts = function (v) {
142 if (!arguments.length) return this._reconnectionAttempts;
143 this._reconnectionAttempts = v;
148 * Sets the delay between reconnections.
150 * @param {Number} delay
151 * @return {Manager} self or value
155 Manager.prototype.reconnectionDelay = function (v) {
156 if (!arguments.length) return this._reconnectionDelay;
157 this._reconnectionDelay = v;
158 this.backoff && this.backoff.setMin(v);
162 Manager.prototype.randomizationFactor = function (v) {
163 if (!arguments.length) return this._randomizationFactor;
164 this._randomizationFactor = v;
165 this.backoff && this.backoff.setJitter(v);
170 * Sets the maximum delay between reconnections.
172 * @param {Number} delay
173 * @return {Manager} self or value
177 Manager.prototype.reconnectionDelayMax = function (v) {
178 if (!arguments.length) return this._reconnectionDelayMax;
179 this._reconnectionDelayMax = v;
180 this.backoff && this.backoff.setMax(v);
185 * Sets the connection timeout. `false` to disable
187 * @return {Manager} self or value
191 Manager.prototype.timeout = function (v) {
192 if (!arguments.length) return this._timeout;
198 * Starts trying to reconnect if reconnection is enabled and we have not
199 * started reconnecting yet
204 Manager.prototype.maybeReconnectOnOpen = function () {
205 // Only try to reconnect if it's the first time we're connecting
206 if (!this.reconnecting && this._reconnection && this.backoff.attempts === 0) {
207 // keeps reconnection from firing twice for the same reconnection loop
213 * Sets the current transport `socket`.
215 * @param {Function} optional, callback
216 * @return {Manager} self
220 Manager.prototype.open =
221 Manager.prototype.connect = function (fn, opts) {
222 debug('readyState %s', this.readyState);
223 if (~this.readyState.indexOf('open')) return this;
225 debug('opening %s', this.uri);
226 this.engine = eio(this.uri, this.opts);
227 var socket = this.engine;
229 this.readyState = 'opening';
230 this.skipReconnect = false;
233 var openSub = on(socket, 'open', function () {
238 // emit `connect_error`
239 var errorSub = on(socket, 'error', function (data) {
240 debug('connect_error');
242 self.readyState = 'closed';
243 self.emitAll('connect_error', data);
245 var err = new Error('Connection error');
249 // Only do this if there is no fn to handle the error
250 self.maybeReconnectOnOpen();
254 // emit `connect_timeout`
255 if (false !== this._timeout) {
256 var timeout = this._timeout;
257 debug('connect attempt will timeout after %d', timeout);
260 openSub.destroy(); // prevents a race condition with the 'open' event
264 var timer = setTimeout(function () {
265 debug('connect attempt timed out after %d', timeout);
268 socket.emit('error', 'timeout');
269 self.emitAll('connect_timeout', timeout);
273 destroy: function () {
279 this.subs.push(openSub);
280 this.subs.push(errorSub);
286 * Called upon transport open.
291 Manager.prototype.onopen = function () {
298 this.readyState = 'open';
302 var socket = this.engine;
303 this.subs.push(on(socket, 'data', bind(this, 'ondata')));
304 this.subs.push(on(socket, 'ping', bind(this, 'onping')));
305 this.subs.push(on(socket, 'pong', bind(this, 'onpong')));
306 this.subs.push(on(socket, 'error', bind(this, 'onerror')));
307 this.subs.push(on(socket, 'close', bind(this, 'onclose')));
308 this.subs.push(on(this.decoder, 'decoded', bind(this, 'ondecoded')));
312 * Called upon a ping.
317 Manager.prototype.onping = function () {
318 this.lastPing = new Date();
319 this.emitAll('ping');
323 * Called upon a packet.
328 Manager.prototype.onpong = function () {
329 this.emitAll('pong', new Date() - this.lastPing);
338 Manager.prototype.ondata = function (data) {
339 this.decoder.add(data);
343 * Called when parser fully decodes a packet.
348 Manager.prototype.ondecoded = function (packet) {
349 this.emit('packet', packet);
353 * Called upon socket error.
358 Manager.prototype.onerror = function (err) {
360 this.emitAll('error', err);
364 * Creates a new socket for the given `nsp`.
370 Manager.prototype.socket = function (nsp, opts) {
371 var socket = this.nsps[nsp];
373 socket = new Socket(this, nsp, opts);
374 this.nsps[nsp] = socket;
376 socket.on('connecting', onConnecting);
377 socket.on('connect', function () {
378 socket.id = self.generateId(nsp);
381 if (this.autoConnect) {
382 // manually call here since connecting event is fired before listening
387 function onConnecting () {
388 if (!~indexOf(self.connecting, socket)) {
389 self.connecting.push(socket);
397 * Called upon a socket close.
399 * @param {Socket} socket
402 Manager.prototype.destroy = function (socket) {
403 var index = indexOf(this.connecting, socket);
404 if (~index) this.connecting.splice(index, 1);
405 if (this.connecting.length) return;
413 * @param {Object} packet
417 Manager.prototype.packet = function (packet) {
418 debug('writing packet %j', packet);
420 if (packet.query && packet.type === 0) packet.nsp += '?' + packet.query;
422 if (!self.encoding) {
423 // encode, then write to engine with result
424 self.encoding = true;
425 this.encoder.encode(packet, function (encodedPackets) {
426 for (var i = 0; i < encodedPackets.length; i++) {
427 self.engine.write(encodedPackets[i], packet.options);
429 self.encoding = false;
430 self.processPacketQueue();
432 } else { // add packet to the queue
433 self.packetBuffer.push(packet);
438 * If packet buffer is non-empty, begins encoding the
439 * next packet in line.
444 Manager.prototype.processPacketQueue = function () {
445 if (this.packetBuffer.length > 0 && !this.encoding) {
446 var pack = this.packetBuffer.shift();
452 * Clean up transport subscriptions and packet buffer.
457 Manager.prototype.cleanup = function () {
460 var subsLength = this.subs.length;
461 for (var i = 0; i < subsLength; i++) {
462 var sub = this.subs.shift();
466 this.packetBuffer = [];
467 this.encoding = false;
468 this.lastPing = null;
470 this.decoder.destroy();
474 * Close the current socket.
479 Manager.prototype.close =
480 Manager.prototype.disconnect = function () {
482 this.skipReconnect = true;
483 this.reconnecting = false;
484 if ('opening' === this.readyState) {
485 // `onclose` will not fire because
486 // an open event never happened
489 this.backoff.reset();
490 this.readyState = 'closed';
491 if (this.engine) this.engine.close();
495 * Called upon engine close.
500 Manager.prototype.onclose = function (reason) {
504 this.backoff.reset();
505 this.readyState = 'closed';
506 this.emit('close', reason);
508 if (this._reconnection && !this.skipReconnect) {
514 * Attempt a reconnection.
519 Manager.prototype.reconnect = function () {
520 if (this.reconnecting || this.skipReconnect) return this;
524 if (this.backoff.attempts >= this._reconnectionAttempts) {
525 debug('reconnect failed');
526 this.backoff.reset();
527 this.emitAll('reconnect_failed');
528 this.reconnecting = false;
530 var delay = this.backoff.duration();
531 debug('will wait %dms before reconnect attempt', delay);
533 this.reconnecting = true;
534 var timer = setTimeout(function () {
535 if (self.skipReconnect) return;
537 debug('attempting reconnect');
538 self.emitAll('reconnect_attempt', self.backoff.attempts);
539 self.emitAll('reconnecting', self.backoff.attempts);
541 // check again for the case socket closed in above events
542 if (self.skipReconnect) return;
544 self.open(function (err) {
546 debug('reconnect attempt error');
547 self.reconnecting = false;
549 self.emitAll('reconnect_error', err.data);
551 debug('reconnect success');
558 destroy: function () {
566 * Called upon successful reconnect.
571 Manager.prototype.onreconnect = function () {
572 var attempt = this.backoff.attempts;
573 this.reconnecting = false;
574 this.backoff.reset();
575 this.updateSocketIds();
576 this.emitAll('reconnect', attempt);