Files
socket.io/lib/manager.js
Zheng Weihan 4578f7ed35 Merge branch 'orgmaster' into queryStringFix
* orgmaster: (44 commits)
  bump zuul
  Remove jspm browser config
  Run lint before test instead of before build
  ESlint manual fix
  Eslint autofix
  Add env to eslintrc
  Exclude generated files from linting
  updated babel-eslint dep to 4.1.7 (4.1.6 broken)
  refactor gulpfile
  Add eslint to gulpfile and create gulp task default
  fixed regex
  Rename gulp task webpack to build
  Remove commented make recipe
  removed babel react preset
  Makefile use gulp from node_modules instead of global
  Remove browserify stuff, add zuul-builder-webpack devDep
  Migrate zuul config from yml to js
  Refactor webpack config out to support/webpack.config.js
  removed ide-specific entries from gitignore
  updated makefile to use gulp commands
  ...

Conflicts:
	lib/manager.js
	lib/socket.js
2016-04-12 19:17:09 +08:00

561 lines
12 KiB
JavaScript

/**
* 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);
};