mirror of
https://github.com/socketio/socket.io.git
synced 2026-04-30 03:00:39 -04:00
* 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
561 lines
12 KiB
JavaScript
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);
|
|
};
|