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 this.encoder = new parser.Encoder();
66 this.decoder = new parser.Decoder();
67 this.autoConnect = opts.autoConnect !== false;
68 if (this.autoConnect) this.open();
72 * Propagate given event to sockets and emit on `this`
77 Manager.prototype.emitAll = function () {
78 this.emit.apply(this, arguments);
79 for (var nsp in this.nsps) {
80 if (has.call(this.nsps, nsp)) {
81 this.nsps[nsp].emit.apply(this.nsps[nsp], arguments);
87 * Update `socket.id` of all sockets
92 Manager.prototype.updateSocketIds = function () {
93 for (var nsp in this.nsps) {
94 if (has.call(this.nsps, nsp)) {
95 this.nsps[nsp].id = this.engine.id;
104 Emitter(Manager.prototype);
107 * Sets the `reconnection` config.
109 * @param {Boolean} true/false if it should automatically reconnect
110 * @return {Manager} self or value
114 Manager.prototype.reconnection = function (v) {
115 if (!arguments.length) return this._reconnection;
116 this._reconnection = !!v;
121 * Sets the reconnection attempts config.
123 * @param {Number} max reconnection attempts before giving up
124 * @return {Manager} self or value
128 Manager.prototype.reconnectionAttempts = function (v) {
129 if (!arguments.length) return this._reconnectionAttempts;
130 this._reconnectionAttempts = v;
135 * Sets the delay between reconnections.
137 * @param {Number} delay
138 * @return {Manager} self or value
142 Manager.prototype.reconnectionDelay = function (v) {
143 if (!arguments.length) return this._reconnectionDelay;
144 this._reconnectionDelay = v;
145 this.backoff && this.backoff.setMin(v);
149 Manager.prototype.randomizationFactor = function (v) {
150 if (!arguments.length) return this._randomizationFactor;
151 this._randomizationFactor = v;
152 this.backoff && this.backoff.setJitter(v);
157 * Sets the maximum delay between reconnections.
159 * @param {Number} delay
160 * @return {Manager} self or value
164 Manager.prototype.reconnectionDelayMax = function (v) {
165 if (!arguments.length) return this._reconnectionDelayMax;
166 this._reconnectionDelayMax = v;
167 this.backoff && this.backoff.setMax(v);
172 * Sets the connection timeout. `false` to disable
174 * @return {Manager} self or value
178 Manager.prototype.timeout = function (v) {
179 if (!arguments.length) return this._timeout;
185 * Starts trying to reconnect if reconnection is enabled and we have not
186 * started reconnecting yet
191 Manager.prototype.maybeReconnectOnOpen = function () {
192 // Only try to reconnect if it's the first time we're connecting
193 if (!this.reconnecting && this._reconnection && this.backoff.attempts === 0) {
194 // keeps reconnection from firing twice for the same reconnection loop
200 * Sets the current transport `socket`.
202 * @param {Function} optional, callback
203 * @return {Manager} self
207 Manager.prototype.open =
208 Manager.prototype.connect = function (fn, opts) {
209 debug('readyState %s', this.readyState);
210 if (~this.readyState.indexOf('open')) return this;
212 debug('opening %s', this.uri);
213 this.engine = eio(this.uri, this.opts);
214 var socket = this.engine;
216 this.readyState = 'opening';
217 this.skipReconnect = false;
220 var openSub = on(socket, 'open', function () {
225 // emit `connect_error`
226 var errorSub = on(socket, 'error', function (data) {
227 debug('connect_error');
229 self.readyState = 'closed';
230 self.emitAll('connect_error', data);
232 var err = new Error('Connection error');
236 // Only do this if there is no fn to handle the error
237 self.maybeReconnectOnOpen();
241 // emit `connect_timeout`
242 if (false !== this._timeout) {
243 var timeout = this._timeout;
244 debug('connect attempt will timeout after %d', timeout);
247 var timer = setTimeout(function () {
248 debug('connect attempt timed out after %d', timeout);
251 socket.emit('error', 'timeout');
252 self.emitAll('connect_timeout', timeout);
256 destroy: function () {
262 this.subs.push(openSub);
263 this.subs.push(errorSub);
269 * Called upon transport open.
274 Manager.prototype.onopen = function () {
281 this.readyState = 'open';
285 var socket = this.engine;
286 this.subs.push(on(socket, 'data', bind(this, 'ondata')));
287 this.subs.push(on(socket, 'ping', bind(this, 'onping')));
288 this.subs.push(on(socket, 'pong', bind(this, 'onpong')));
289 this.subs.push(on(socket, 'error', bind(this, 'onerror')));
290 this.subs.push(on(socket, 'close', bind(this, 'onclose')));
291 this.subs.push(on(this.decoder, 'decoded', bind(this, 'ondecoded')));
295 * Called upon a ping.
300 Manager.prototype.onping = function () {
301 this.lastPing = new Date();
302 this.emitAll('ping');
306 * Called upon a packet.
311 Manager.prototype.onpong = function () {
312 this.emitAll('pong', new Date() - this.lastPing);
321 Manager.prototype.ondata = function (data) {
322 this.decoder.add(data);
326 * Called when parser fully decodes a packet.
331 Manager.prototype.ondecoded = function (packet) {
332 this.emit('packet', packet);
336 * Called upon socket error.
341 Manager.prototype.onerror = function (err) {
343 this.emitAll('error', err);
347 * Creates a new socket for the given `nsp`.
353 Manager.prototype.socket = function (nsp, opts) {
354 var socket = this.nsps[nsp];
356 socket = new Socket(this, nsp, opts);
357 this.nsps[nsp] = socket;
359 socket.on('connecting', onConnecting);
360 socket.on('connect', function () {
361 socket.id = self.engine.id;
364 if (this.autoConnect) {
365 // manually call here since connecting evnet is fired before listening
370 function onConnecting () {
371 if (!~indexOf(self.connecting, socket)) {
372 self.connecting.push(socket);
380 * Called upon a socket close.
382 * @param {Socket} socket
385 Manager.prototype.destroy = function (socket) {
386 var index = indexOf(this.connecting, socket);
387 if (~index) this.connecting.splice(index, 1);
388 if (this.connecting.length) return;
396 * @param {Object} packet
400 Manager.prototype.packet = function (packet) {
401 debug('writing packet %j', packet);
403 if (packet.query && packet.type === 0) packet.nsp += '?' + packet.query;
405 if (!self.encoding) {
406 // encode, then write to engine with result
407 self.encoding = true;
408 this.encoder.encode(packet, function (encodedPackets) {
409 for (var i = 0; i < encodedPackets.length; i++) {
410 self.engine.write(encodedPackets[i], packet.options);
412 self.encoding = false;
413 self.processPacketQueue();
415 } else { // add packet to the queue
416 self.packetBuffer.push(packet);
421 * If packet buffer is non-empty, begins encoding the
422 * next packet in line.
427 Manager.prototype.processPacketQueue = function () {
428 if (this.packetBuffer.length > 0 && !this.encoding) {
429 var pack = this.packetBuffer.shift();
435 * Clean up transport subscriptions and packet buffer.
440 Manager.prototype.cleanup = function () {
443 var subsLength = this.subs.length;
444 for (var i = 0; i < subsLength; i++) {
445 var sub = this.subs.shift();
449 this.packetBuffer = [];
450 this.encoding = false;
451 this.lastPing = null;
453 this.decoder.destroy();
457 * Close the current socket.
462 Manager.prototype.close =
463 Manager.prototype.disconnect = function () {
465 this.skipReconnect = true;
466 this.reconnecting = false;
467 if ('opening' === this.readyState) {
468 // `onclose` will not fire because
469 // an open event never happened
472 this.backoff.reset();
473 this.readyState = 'closed';
474 if (this.engine) this.engine.close();
478 * Called upon engine close.
483 Manager.prototype.onclose = function (reason) {
487 this.backoff.reset();
488 this.readyState = 'closed';
489 this.emit('close', reason);
491 if (this._reconnection && !this.skipReconnect) {
497 * Attempt a reconnection.
502 Manager.prototype.reconnect = function () {
503 if (this.reconnecting || this.skipReconnect) return this;
507 if (this.backoff.attempts >= this._reconnectionAttempts) {
508 debug('reconnect failed');
509 this.backoff.reset();
510 this.emitAll('reconnect_failed');
511 this.reconnecting = false;
513 var delay = this.backoff.duration();
514 debug('will wait %dms before reconnect attempt', delay);
516 this.reconnecting = true;
517 var timer = setTimeout(function () {
518 if (self.skipReconnect) return;
520 debug('attempting reconnect');
521 self.emitAll('reconnect_attempt', self.backoff.attempts);
522 self.emitAll('reconnecting', self.backoff.attempts);
524 // check again for the case socket closed in above events
525 if (self.skipReconnect) return;
527 self.open(function (err) {
529 debug('reconnect attempt error');
530 self.reconnecting = false;
532 self.emitAll('reconnect_error', err.data);
534 debug('reconnect success');
541 destroy: function () {
549 * Called upon successful reconnect.
554 Manager.prototype.onreconnect = function () {
555 var attempt = this.backoff.attempts;
556 this.reconnecting = false;
557 this.backoff.reset();
558 this.updateSocketIds();
559 this.emitAll('reconnect', attempt);