|
|
/** * Module dependencies. */
var eio = require('engine.io-client'); var Socket = require('./socket'); var Emitter = require('component-emitter'); var parser = require('socket.io-parser'); var on = require('./on'); var bind = require('component-bind'); var debug = require('debug')('socket.io-client:manager'); var indexOf = require('indexof'); var Backoff = require('backo2');
/** * IE6+ hasOwnProperty */
var has = Object.prototype.hasOwnProperty;
/** * Module exports */
module.exports = Manager;
/** * `Manager` constructor. * * @param {String} engine instance or engine uri/opts * @param {Object} options * @api public */
function Manager (uri, opts) { if (!(this instanceof Manager)) return new Manager(uri, opts); if (uri && ('object' === typeof uri)) { opts = uri; uri = undefined; } opts = opts || {};
opts.path = opts.path || '/socket.io'; this.nsps = {}; this.subs = []; this.opts = opts; this.reconnection(opts.reconnection !== false); this.reconnectionAttempts(opts.reconnectionAttempts || Infinity); this.reconnectionDelay(opts.reconnectionDelay || 1000); this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000); this.randomizationFactor(opts.randomizationFactor || 0.5); this.backoff = new Backoff({ min: this.reconnectionDelay(), max: this.reconnectionDelayMax(), jitter: this.randomizationFactor() }); this.timeout(null == opts.timeout ? 20000 : opts.timeout); this.readyState = 'closed'; this.uri = uri; this.connecting = []; this.lastPing = null; this.encoding = false; this.packetBuffer = []; this.encoder = new parser.Encoder(); this.decoder = new parser.Decoder(); this.autoConnect = opts.autoConnect !== false; if (this.autoConnect) this.open(); }
/** * Propagate given event to sockets and emit on `this` * * @api private */
Manager.prototype.emitAll = function () { this.emit.apply(this, arguments); for (var nsp in this.nsps) { if (has.call(this.nsps, nsp)) { this.nsps[nsp].emit.apply(this.nsps[nsp], arguments); } } };
/** * Update `socket.id` of all sockets * * @api private */
Manager.prototype.updateSocketIds = function () { for (var nsp in this.nsps) { if (has.call(this.nsps, nsp)) { this.nsps[nsp].id = this.engine.id; } } };
/** * Mix in `Emitter`. */
Emitter(Manager.prototype);
/** * Sets the `reconnection` config. * * @param {Boolean} true/false if it should automatically reconnect * @return {Manager} self or value * @api public */
Manager.prototype.reconnection = function (v) { if (!arguments.length) return this._reconnection; this._reconnection = !!v; return this; };
/** * Sets the reconnection attempts config. * * @param {Number} max reconnection attempts before giving up * @return {Manager} self or value * @api public */
Manager.prototype.reconnectionAttempts = function (v) { if (!arguments.length) return this._reconnectionAttempts; this._reconnectionAttempts = v; return this; };
/** * Sets the delay between reconnections. * * @param {Number} delay * @return {Manager} self or value * @api public */
Manager.prototype.reconnectionDelay = function (v) { if (!arguments.length) return this._reconnectionDelay; this._reconnectionDelay = v; this.backoff && this.backoff.setMin(v); return this; };
Manager.prototype.randomizationFactor = function (v) { if (!arguments.length) return this._randomizationFactor; this._randomizationFactor = v; this.backoff && this.backoff.setJitter(v); return this; };
/** * Sets the maximum delay between reconnections. * * @param {Number} delay * @return {Manager} self or value * @api public */
Manager.prototype.reconnectionDelayMax = function (v) { if (!arguments.length) return this._reconnectionDelayMax; this._reconnectionDelayMax = v; this.backoff && this.backoff.setMax(v); return this; };
/** * Sets the connection timeout. `false` to disable * * @return {Manager} self or value * @api public */
Manager.prototype.timeout = function (v) { if (!arguments.length) return this._timeout; this._timeout = v; return this; };
/** * Starts trying to reconnect if reconnection is enabled and we have not * started reconnecting yet * * @api private */
Manager.prototype.maybeReconnectOnOpen = function () { // Only try to reconnect if it's the first time we're connecting
if (!this.reconnecting && this._reconnection && this.backoff.attempts === 0) { // keeps reconnection from firing twice for the same reconnection loop
this.reconnect(); } };
/** * Sets the current transport `socket`. * * @param {Function} optional, callback * @return {Manager} self * @api public */
Manager.prototype.open = Manager.prototype.connect = function (fn, opts) { debug('readyState %s', this.readyState); if (~this.readyState.indexOf('open')) return this;
debug('opening %s', this.uri); this.engine = eio(this.uri, this.opts); var socket = this.engine; var self = this; this.readyState = 'opening'; this.skipReconnect = false;
// emit `open`
var openSub = on(socket, 'open', function () { self.onopen(); fn && fn(); });
// emit `connect_error`
var errorSub = on(socket, 'error', function (data) { debug('connect_error'); self.cleanup(); self.readyState = 'closed'; self.emitAll('connect_error', data); if (fn) { var err = new Error('Connection error'); err.data = data; fn(err); } else { // Only do this if there is no fn to handle the error
self.maybeReconnectOnOpen(); } });
// emit `connect_timeout`
if (false !== this._timeout) { var timeout = this._timeout; debug('connect attempt will timeout after %d', timeout);
// set timer
var timer = setTimeout(function () { debug('connect attempt timed out after %d', timeout); openSub.destroy(); socket.close(); socket.emit('error', 'timeout'); self.emitAll('connect_timeout', timeout); }, timeout);
this.subs.push({ destroy: function () { clearTimeout(timer); } }); }
this.subs.push(openSub); this.subs.push(errorSub);
return this; };
/** * Called upon transport open. * * @api private */
Manager.prototype.onopen = function () { debug('open');
// clear old subs
this.cleanup();
// mark as open
this.readyState = 'open'; this.emit('open');
// add new subs
var socket = this.engine; this.subs.push(on(socket, 'data', bind(this, 'ondata'))); this.subs.push(on(socket, 'ping', bind(this, 'onping'))); this.subs.push(on(socket, 'pong', bind(this, 'onpong'))); this.subs.push(on(socket, 'error', bind(this, 'onerror'))); this.subs.push(on(socket, 'close', bind(this, 'onclose'))); this.subs.push(on(this.decoder, 'decoded', bind(this, 'ondecoded'))); };
/** * Called upon a ping. * * @api private */
Manager.prototype.onping = function () { this.lastPing = new Date(); this.emitAll('ping'); };
/** * Called upon a packet. * * @api private */
Manager.prototype.onpong = function () { this.emitAll('pong', new Date() - this.lastPing); };
/** * Called with data. * * @api private */
Manager.prototype.ondata = function (data) { this.decoder.add(data); };
/** * Called when parser fully decodes a packet. * * @api private */
Manager.prototype.ondecoded = function (packet) { this.emit('packet', packet); };
/** * Called upon socket error. * * @api private */
Manager.prototype.onerror = function (err) { debug('error', err); this.emitAll('error', err); };
/** * Creates a new socket for the given `nsp`. * * @return {Socket} * @api public */
Manager.prototype.socket = function (nsp, opts) { var socket = this.nsps[nsp]; if (!socket) { socket = new Socket(this, nsp, opts); this.nsps[nsp] = socket; var self = this; socket.on('connecting', onConnecting); socket.on('connect', function () { socket.id = self.engine.id; });
if (this.autoConnect) { // manually call here since connecting evnet is fired before listening
onConnecting(); } }
function onConnecting () { if (!~indexOf(self.connecting, socket)) { self.connecting.push(socket); } }
return socket; };
/** * Called upon a socket close. * * @param {Socket} socket */
Manager.prototype.destroy = function (socket) { var index = indexOf(this.connecting, socket); if (~index) this.connecting.splice(index, 1); if (this.connecting.length) return;
this.close(); };
/** * Writes a packet. * * @param {Object} packet * @api private */
Manager.prototype.packet = function (packet) { debug('writing packet %j', packet); var self = this; if (packet.query && packet.type === 0) packet.nsp += '?' + packet.query;
if (!self.encoding) { // encode, then write to engine with result
self.encoding = true; this.encoder.encode(packet, function (encodedPackets) { for (var i = 0; i < encodedPackets.length; i++) { self.engine.write(encodedPackets[i], packet.options); } self.encoding = false; self.processPacketQueue(); }); } else { // add packet to the queue
self.packetBuffer.push(packet); } };
/** * If packet buffer is non-empty, begins encoding the * next packet in line. * * @api private */
Manager.prototype.processPacketQueue = function () { if (this.packetBuffer.length > 0 && !this.encoding) { var pack = this.packetBuffer.shift(); this.packet(pack); } };
/** * Clean up transport subscriptions and packet buffer. * * @api private */
Manager.prototype.cleanup = function () { debug('cleanup');
var subsLength = this.subs.length; for (var i = 0; i < subsLength; i++) { var sub = this.subs.shift(); sub.destroy(); }
this.packetBuffer = []; this.encoding = false; this.lastPing = null;
this.decoder.destroy(); };
/** * Close the current socket. * * @api private */
Manager.prototype.close = Manager.prototype.disconnect = function () { debug('disconnect'); this.skipReconnect = true; this.reconnecting = false; if ('opening' === this.readyState) { // `onclose` will not fire because
// an open event never happened
this.cleanup(); } this.backoff.reset(); this.readyState = 'closed'; if (this.engine) this.engine.close(); };
/** * Called upon engine close. * * @api private */
Manager.prototype.onclose = function (reason) { debug('onclose');
this.cleanup(); this.backoff.reset(); this.readyState = 'closed'; this.emit('close', reason);
if (this._reconnection && !this.skipReconnect) { this.reconnect(); } };
/** * Attempt a reconnection. * * @api private */
Manager.prototype.reconnect = function () { if (this.reconnecting || this.skipReconnect) return this;
var self = this;
if (this.backoff.attempts >= this._reconnectionAttempts) { debug('reconnect failed'); this.backoff.reset(); this.emitAll('reconnect_failed'); this.reconnecting = false; } else { var delay = this.backoff.duration(); debug('will wait %dms before reconnect attempt', delay);
this.reconnecting = true; var timer = setTimeout(function () { if (self.skipReconnect) return;
debug('attempting reconnect'); self.emitAll('reconnect_attempt', self.backoff.attempts); self.emitAll('reconnecting', self.backoff.attempts);
// check again for the case socket closed in above events
if (self.skipReconnect) return;
self.open(function (err) { if (err) { debug('reconnect attempt error'); self.reconnecting = false; self.reconnect(); self.emitAll('reconnect_error', err.data); } else { debug('reconnect success'); self.onreconnect(); } }); }, delay);
this.subs.push({ destroy: function () { clearTimeout(timer); } }); } };
/** * Called upon successful reconnect. * * @api private */
Manager.prototype.onreconnect = function () { var attempt = this.backoff.attempts; this.reconnecting = false; this.backoff.reset(); this.updateSocketIds(); this.emitAll('reconnect', attempt); };
|