You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

473 lines
10 KiB

  1. /**
  2. * Module dependencies.
  3. */
  4. var url = require('./url');
  5. var eio = require('engine.io-client');
  6. var Socket = require('./socket');
  7. var Emitter = require('component-emitter');
  8. var parser = require('socket.io-parser');
  9. var on = require('./on');
  10. var bind = require('component-bind');
  11. var object = require('object-component');
  12. var debug = require('debug')('socket.io-client:manager');
  13. var indexOf = require('indexof');
  14. /**
  15. * Module exports
  16. */
  17. module.exports = Manager;
  18. /**
  19. * `Manager` constructor.
  20. *
  21. * @param {String} engine instance or engine uri/opts
  22. * @param {Object} options
  23. * @api public
  24. */
  25. function Manager(uri, opts){
  26. if (!(this instanceof Manager)) return new Manager(uri, opts);
  27. if (uri && ('object' == typeof uri)) {
  28. opts = uri;
  29. uri = undefined;
  30. }
  31. opts = opts || {};
  32. opts.path = opts.path || '/socket.io';
  33. this.nsps = {};
  34. this.subs = [];
  35. this.opts = opts;
  36. this.reconnection(opts.reconnection !== false);
  37. this.reconnectionAttempts(opts.reconnectionAttempts || Infinity);
  38. this.reconnectionDelay(opts.reconnectionDelay || 1000);
  39. this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000);
  40. this.timeout(null == opts.timeout ? 20000 : opts.timeout);
  41. this.readyState = 'closed';
  42. this.uri = uri;
  43. this.connected = [];
  44. this.attempts = 0;
  45. this.encoding = false;
  46. this.packetBuffer = [];
  47. this.encoder = new parser.Encoder();
  48. this.decoder = new parser.Decoder();
  49. this.autoConnect = opts.autoConnect !== false;
  50. if (this.autoConnect) this.open();
  51. }
  52. /**
  53. * Propagate given event to sockets and emit on `this`
  54. *
  55. * @api private
  56. */
  57. Manager.prototype.emitAll = function() {
  58. this.emit.apply(this, arguments);
  59. for (var nsp in this.nsps) {
  60. this.nsps[nsp].emit.apply(this.nsps[nsp], arguments);
  61. }
  62. };
  63. /**
  64. * Mix in `Emitter`.
  65. */
  66. Emitter(Manager.prototype);
  67. /**
  68. * Sets the `reconnection` config.
  69. *
  70. * @param {Boolean} true/false if it should automatically reconnect
  71. * @return {Manager} self or value
  72. * @api public
  73. */
  74. Manager.prototype.reconnection = function(v){
  75. if (!arguments.length) return this._reconnection;
  76. this._reconnection = !!v;
  77. return this;
  78. };
  79. /**
  80. * Sets the reconnection attempts config.
  81. *
  82. * @param {Number} max reconnection attempts before giving up
  83. * @return {Manager} self or value
  84. * @api public
  85. */
  86. Manager.prototype.reconnectionAttempts = function(v){
  87. if (!arguments.length) return this._reconnectionAttempts;
  88. this._reconnectionAttempts = v;
  89. return this;
  90. };
  91. /**
  92. * Sets the delay between reconnections.
  93. *
  94. * @param {Number} delay
  95. * @return {Manager} self or value
  96. * @api public
  97. */
  98. Manager.prototype.reconnectionDelay = function(v){
  99. if (!arguments.length) return this._reconnectionDelay;
  100. this._reconnectionDelay = v;
  101. return this;
  102. };
  103. /**
  104. * Sets the maximum delay between reconnections.
  105. *
  106. * @param {Number} delay
  107. * @return {Manager} self or value
  108. * @api public
  109. */
  110. Manager.prototype.reconnectionDelayMax = function(v){
  111. if (!arguments.length) return this._reconnectionDelayMax;
  112. this._reconnectionDelayMax = v;
  113. return this;
  114. };
  115. /**
  116. * Sets the connection timeout. `false` to disable
  117. *
  118. * @return {Manager} self or value
  119. * @api public
  120. */
  121. Manager.prototype.timeout = function(v){
  122. if (!arguments.length) return this._timeout;
  123. this._timeout = v;
  124. return this;
  125. };
  126. /**
  127. * Starts trying to reconnect if reconnection is enabled and we have not
  128. * started reconnecting yet
  129. *
  130. * @api private
  131. */
  132. Manager.prototype.maybeReconnectOnOpen = function() {
  133. // Only try to reconnect if it's the first time we're connecting
  134. if (!this.openReconnect && !this.reconnecting && this._reconnection && this.attempts === 0) {
  135. // keeps reconnection from firing twice for the same reconnection loop
  136. this.openReconnect = true;
  137. this.reconnect();
  138. }
  139. };
  140. /**
  141. * Sets the current transport `socket`.
  142. *
  143. * @param {Function} optional, callback
  144. * @return {Manager} self
  145. * @api public
  146. */
  147. Manager.prototype.open =
  148. Manager.prototype.connect = function(fn){
  149. debug('readyState %s', this.readyState);
  150. if (~this.readyState.indexOf('open')) return this;
  151. debug('opening %s', this.uri);
  152. this.engine = eio(this.uri, this.opts);
  153. var socket = this.engine;
  154. var self = this;
  155. this.readyState = 'opening';
  156. this.skipReconnect = false;
  157. // emit `open`
  158. var openSub = on(socket, 'open', function() {
  159. self.onopen();
  160. fn && fn();
  161. });
  162. // emit `connect_error`
  163. var errorSub = on(socket, 'error', function(data){
  164. debug('connect_error');
  165. self.cleanup();
  166. self.readyState = 'closed';
  167. self.emitAll('connect_error', data);
  168. if (fn) {
  169. var err = new Error('Connection error');
  170. err.data = data;
  171. fn(err);
  172. }
  173. self.maybeReconnectOnOpen();
  174. });
  175. // emit `connect_timeout`
  176. if (false !== this._timeout) {
  177. var timeout = this._timeout;
  178. debug('connect attempt will timeout after %d', timeout);
  179. // set timer
  180. var timer = setTimeout(function(){
  181. debug('connect attempt timed out after %d', timeout);
  182. openSub.destroy();
  183. socket.close();
  184. socket.emit('error', 'timeout');
  185. self.emitAll('connect_timeout', timeout);
  186. }, timeout);
  187. this.subs.push({
  188. destroy: function(){
  189. clearTimeout(timer);
  190. }
  191. });
  192. }
  193. this.subs.push(openSub);
  194. this.subs.push(errorSub);
  195. return this;
  196. };
  197. /**
  198. * Called upon transport open.
  199. *
  200. * @api private
  201. */
  202. Manager.prototype.onopen = function(){
  203. debug('open');
  204. // clear old subs
  205. this.cleanup();
  206. // mark as open
  207. this.readyState = 'open';
  208. this.emit('open');
  209. // add new subs
  210. var socket = this.engine;
  211. this.subs.push(on(socket, 'data', bind(this, 'ondata')));
  212. this.subs.push(on(this.decoder, 'decoded', bind(this, 'ondecoded')));
  213. this.subs.push(on(socket, 'error', bind(this, 'onerror')));
  214. this.subs.push(on(socket, 'close', bind(this, 'onclose')));
  215. };
  216. /**
  217. * Called with data.
  218. *
  219. * @api private
  220. */
  221. Manager.prototype.ondata = function(data){
  222. this.decoder.add(data);
  223. };
  224. /**
  225. * Called when parser fully decodes a packet.
  226. *
  227. * @api private
  228. */
  229. Manager.prototype.ondecoded = function(packet) {
  230. this.emit('packet', packet);
  231. };
  232. /**
  233. * Called upon socket error.
  234. *
  235. * @api private
  236. */
  237. Manager.prototype.onerror = function(err){
  238. debug('error', err);
  239. this.emitAll('error', err);
  240. };
  241. /**
  242. * Creates a new socket for the given `nsp`.
  243. *
  244. * @return {Socket}
  245. * @api public
  246. */
  247. Manager.prototype.socket = function(nsp){
  248. var socket = this.nsps[nsp];
  249. if (!socket) {
  250. socket = new Socket(this, nsp);
  251. this.nsps[nsp] = socket;
  252. var self = this;
  253. socket.on('connect', function(){
  254. if (!~indexOf(self.connected, socket)) {
  255. self.connected.push(socket);
  256. }
  257. });
  258. }
  259. return socket;
  260. };
  261. /**
  262. * Called upon a socket close.
  263. *
  264. * @param {Socket} socket
  265. */
  266. Manager.prototype.destroy = function(socket){
  267. var index = indexOf(this.connected, socket);
  268. if (~index) this.connected.splice(index, 1);
  269. if (this.connected.length) return;
  270. this.close();
  271. };
  272. /**
  273. * Writes a packet.
  274. *
  275. * @param {Object} packet
  276. * @api private
  277. */
  278. Manager.prototype.packet = function(packet){
  279. debug('writing packet %j', packet);
  280. var self = this;
  281. if (!self.encoding) {
  282. // encode, then write to engine with result
  283. self.encoding = true;
  284. this.encoder.encode(packet, function(encodedPackets) {
  285. for (var i = 0; i < encodedPackets.length; i++) {
  286. self.engine.write(encodedPackets[i]);
  287. }
  288. self.encoding = false;
  289. self.processPacketQueue();
  290. });
  291. } else { // add packet to the queue
  292. self.packetBuffer.push(packet);
  293. }
  294. };
  295. /**
  296. * If packet buffer is non-empty, begins encoding the
  297. * next packet in line.
  298. *
  299. * @api private
  300. */
  301. Manager.prototype.processPacketQueue = function() {
  302. if (this.packetBuffer.length > 0 && !this.encoding) {
  303. var pack = this.packetBuffer.shift();
  304. this.packet(pack);
  305. }
  306. };
  307. /**
  308. * Clean up transport subscriptions and packet buffer.
  309. *
  310. * @api private
  311. */
  312. Manager.prototype.cleanup = function(){
  313. var sub;
  314. while (sub = this.subs.shift()) sub.destroy();
  315. this.packetBuffer = [];
  316. this.encoding = false;
  317. this.decoder.destroy();
  318. };
  319. /**
  320. * Close the current socket.
  321. *
  322. * @api private
  323. */
  324. Manager.prototype.close =
  325. Manager.prototype.disconnect = function(){
  326. this.skipReconnect = true;
  327. this.readyState = 'closed';
  328. this.engine && this.engine.close();
  329. };
  330. /**
  331. * Called upon engine close.
  332. *
  333. * @api private
  334. */
  335. Manager.prototype.onclose = function(reason){
  336. debug('close');
  337. this.cleanup();
  338. this.readyState = 'closed';
  339. this.emit('close', reason);
  340. if (this._reconnection && !this.skipReconnect) {
  341. this.reconnect();
  342. }
  343. };
  344. /**
  345. * Attempt a reconnection.
  346. *
  347. * @api private
  348. */
  349. Manager.prototype.reconnect = function(){
  350. if (this.reconnecting || this.skipReconnect) return this;
  351. var self = this;
  352. this.attempts++;
  353. if (this.attempts > this._reconnectionAttempts) {
  354. debug('reconnect failed');
  355. this.emitAll('reconnect_failed');
  356. this.reconnecting = false;
  357. } else {
  358. var delay = this.attempts * this.reconnectionDelay();
  359. delay = Math.min(delay, this.reconnectionDelayMax());
  360. debug('will wait %dms before reconnect attempt', delay);
  361. this.reconnecting = true;
  362. var timer = setTimeout(function(){
  363. if (self.skipReconnect) return;
  364. debug('attempting reconnect');
  365. self.emitAll('reconnect_attempt', self.attempts);
  366. self.emitAll('reconnecting', self.attempts);
  367. // check again for the case socket closed in above events
  368. if (self.skipReconnect) return;
  369. self.open(function(err){
  370. if (err) {
  371. debug('reconnect attempt error');
  372. self.reconnecting = false;
  373. self.reconnect();
  374. self.emitAll('reconnect_error', err.data);
  375. } else {
  376. debug('reconnect success');
  377. self.onreconnect();
  378. }
  379. });
  380. }, delay);
  381. this.subs.push({
  382. destroy: function(){
  383. clearTimeout(timer);
  384. }
  385. });
  386. }
  387. };
  388. /**
  389. * Called upon successful reconnect.
  390. *
  391. * @api private
  392. */
  393. Manager.prototype.onreconnect = function(){
  394. var attempt = this.attempts;
  395. this.attempts = 0;
  396. this.reconnecting = false;
  397. this.emitAll('reconnect', attempt);
  398. };