socket.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.Socket = void 0;
  7. const index_js_1 = require("./transports/index.js");
  8. const util_js_1 = require("./util.js");
  9. const parseqs_1 = __importDefault(require("parseqs"));
  10. const parseuri_1 = __importDefault(require("parseuri"));
  11. const debug_1 = __importDefault(require("debug")); // debug()
  12. const component_emitter_1 = require("@socket.io/component-emitter");
  13. const engine_io_parser_1 = require("engine.io-parser");
  14. const debug = (0, debug_1.default)("engine.io-client:socket"); // debug()
  15. class Socket extends component_emitter_1.Emitter {
  16. /**
  17. * Socket constructor.
  18. *
  19. * @param {String|Object} uri or options
  20. * @param {Object} opts - options
  21. * @api public
  22. */
  23. constructor(uri, opts = {}) {
  24. super();
  25. if (uri && "object" === typeof uri) {
  26. opts = uri;
  27. uri = null;
  28. }
  29. if (uri) {
  30. uri = (0, parseuri_1.default)(uri);
  31. opts.hostname = uri.host;
  32. opts.secure = uri.protocol === "https" || uri.protocol === "wss";
  33. opts.port = uri.port;
  34. if (uri.query)
  35. opts.query = uri.query;
  36. }
  37. else if (opts.host) {
  38. opts.hostname = (0, parseuri_1.default)(opts.host).host;
  39. }
  40. (0, util_js_1.installTimerFunctions)(this, opts);
  41. this.secure =
  42. null != opts.secure
  43. ? opts.secure
  44. : typeof location !== "undefined" && "https:" === location.protocol;
  45. if (opts.hostname && !opts.port) {
  46. // if no port is specified manually, use the protocol default
  47. opts.port = this.secure ? "443" : "80";
  48. }
  49. this.hostname =
  50. opts.hostname ||
  51. (typeof location !== "undefined" ? location.hostname : "localhost");
  52. this.port =
  53. opts.port ||
  54. (typeof location !== "undefined" && location.port
  55. ? location.port
  56. : this.secure
  57. ? "443"
  58. : "80");
  59. this.transports = opts.transports || ["polling", "websocket"];
  60. this.readyState = "";
  61. this.writeBuffer = [];
  62. this.prevBufferLen = 0;
  63. this.opts = Object.assign({
  64. path: "/engine.io",
  65. agent: false,
  66. withCredentials: false,
  67. upgrade: true,
  68. timestampParam: "t",
  69. rememberUpgrade: false,
  70. rejectUnauthorized: true,
  71. perMessageDeflate: {
  72. threshold: 1024
  73. },
  74. transportOptions: {},
  75. closeOnBeforeunload: true
  76. }, opts);
  77. this.opts.path = this.opts.path.replace(/\/$/, "") + "/";
  78. if (typeof this.opts.query === "string") {
  79. this.opts.query = parseqs_1.default.decode(this.opts.query);
  80. }
  81. // set on handshake
  82. this.id = null;
  83. this.upgrades = null;
  84. this.pingInterval = null;
  85. this.pingTimeout = null;
  86. // set on heartbeat
  87. this.pingTimeoutTimer = null;
  88. if (typeof addEventListener === "function") {
  89. if (this.opts.closeOnBeforeunload) {
  90. // Firefox closes the connection when the "beforeunload" event is emitted but not Chrome. This event listener
  91. // ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is
  92. // closed/reloaded)
  93. addEventListener("beforeunload", () => {
  94. if (this.transport) {
  95. // silently close the transport
  96. this.transport.removeAllListeners();
  97. this.transport.close();
  98. }
  99. }, false);
  100. }
  101. if (this.hostname !== "localhost") {
  102. this.offlineEventListener = () => {
  103. this.onClose("transport close");
  104. };
  105. addEventListener("offline", this.offlineEventListener, false);
  106. }
  107. }
  108. this.open();
  109. }
  110. /**
  111. * Creates transport of the given type.
  112. *
  113. * @param {String} transport name
  114. * @return {Transport}
  115. * @api private
  116. */
  117. createTransport(name) {
  118. debug('creating transport "%s"', name);
  119. const query = clone(this.opts.query);
  120. // append engine.io protocol identifier
  121. query.EIO = engine_io_parser_1.protocol;
  122. // transport name
  123. query.transport = name;
  124. // session id if we already have one
  125. if (this.id)
  126. query.sid = this.id;
  127. const opts = Object.assign({}, this.opts.transportOptions[name], this.opts, {
  128. query,
  129. socket: this,
  130. hostname: this.hostname,
  131. secure: this.secure,
  132. port: this.port
  133. });
  134. debug("options: %j", opts);
  135. return new index_js_1.transports[name](opts);
  136. }
  137. /**
  138. * Initializes transport to use and starts probe.
  139. *
  140. * @api private
  141. */
  142. open() {
  143. let transport;
  144. if (this.opts.rememberUpgrade &&
  145. Socket.priorWebsocketSuccess &&
  146. this.transports.indexOf("websocket") !== -1) {
  147. transport = "websocket";
  148. }
  149. else if (0 === this.transports.length) {
  150. // Emit error on next tick so it can be listened to
  151. this.setTimeoutFn(() => {
  152. this.emitReserved("error", "No transports available");
  153. }, 0);
  154. return;
  155. }
  156. else {
  157. transport = this.transports[0];
  158. }
  159. this.readyState = "opening";
  160. // Retry with the next transport if the transport is disabled (jsonp: false)
  161. try {
  162. transport = this.createTransport(transport);
  163. }
  164. catch (e) {
  165. debug("error while creating transport: %s", e);
  166. this.transports.shift();
  167. this.open();
  168. return;
  169. }
  170. transport.open();
  171. this.setTransport(transport);
  172. }
  173. /**
  174. * Sets the current transport. Disables the existing one (if any).
  175. *
  176. * @api private
  177. */
  178. setTransport(transport) {
  179. debug("setting transport %s", transport.name);
  180. if (this.transport) {
  181. debug("clearing existing transport %s", this.transport.name);
  182. this.transport.removeAllListeners();
  183. }
  184. // set up transport
  185. this.transport = transport;
  186. // set up transport listeners
  187. transport
  188. .on("drain", this.onDrain.bind(this))
  189. .on("packet", this.onPacket.bind(this))
  190. .on("error", this.onError.bind(this))
  191. .on("close", () => {
  192. this.onClose("transport close");
  193. });
  194. }
  195. /**
  196. * Probes a transport.
  197. *
  198. * @param {String} transport name
  199. * @api private
  200. */
  201. probe(name) {
  202. debug('probing transport "%s"', name);
  203. let transport = this.createTransport(name);
  204. let failed = false;
  205. Socket.priorWebsocketSuccess = false;
  206. const onTransportOpen = () => {
  207. if (failed)
  208. return;
  209. debug('probe transport "%s" opened', name);
  210. transport.send([{ type: "ping", data: "probe" }]);
  211. transport.once("packet", msg => {
  212. if (failed)
  213. return;
  214. if ("pong" === msg.type && "probe" === msg.data) {
  215. debug('probe transport "%s" pong', name);
  216. this.upgrading = true;
  217. this.emitReserved("upgrading", transport);
  218. if (!transport)
  219. return;
  220. Socket.priorWebsocketSuccess = "websocket" === transport.name;
  221. debug('pausing current transport "%s"', this.transport.name);
  222. this.transport.pause(() => {
  223. if (failed)
  224. return;
  225. if ("closed" === this.readyState)
  226. return;
  227. debug("changing transport and sending upgrade packet");
  228. cleanup();
  229. this.setTransport(transport);
  230. transport.send([{ type: "upgrade" }]);
  231. this.emitReserved("upgrade", transport);
  232. transport = null;
  233. this.upgrading = false;
  234. this.flush();
  235. });
  236. }
  237. else {
  238. debug('probe transport "%s" failed', name);
  239. const err = new Error("probe error");
  240. // @ts-ignore
  241. err.transport = transport.name;
  242. this.emitReserved("upgradeError", err);
  243. }
  244. });
  245. };
  246. function freezeTransport() {
  247. if (failed)
  248. return;
  249. // Any callback called by transport should be ignored since now
  250. failed = true;
  251. cleanup();
  252. transport.close();
  253. transport = null;
  254. }
  255. // Handle any error that happens while probing
  256. const onerror = err => {
  257. const error = new Error("probe error: " + err);
  258. // @ts-ignore
  259. error.transport = transport.name;
  260. freezeTransport();
  261. debug('probe transport "%s" failed because of error: %s', name, err);
  262. this.emitReserved("upgradeError", error);
  263. };
  264. function onTransportClose() {
  265. onerror("transport closed");
  266. }
  267. // When the socket is closed while we're probing
  268. function onclose() {
  269. onerror("socket closed");
  270. }
  271. // When the socket is upgraded while we're probing
  272. function onupgrade(to) {
  273. if (transport && to.name !== transport.name) {
  274. debug('"%s" works - aborting "%s"', to.name, transport.name);
  275. freezeTransport();
  276. }
  277. }
  278. // Remove all listeners on the transport and on self
  279. const cleanup = () => {
  280. transport.removeListener("open", onTransportOpen);
  281. transport.removeListener("error", onerror);
  282. transport.removeListener("close", onTransportClose);
  283. this.off("close", onclose);
  284. this.off("upgrading", onupgrade);
  285. };
  286. transport.once("open", onTransportOpen);
  287. transport.once("error", onerror);
  288. transport.once("close", onTransportClose);
  289. this.once("close", onclose);
  290. this.once("upgrading", onupgrade);
  291. transport.open();
  292. }
  293. /**
  294. * Called when connection is deemed open.
  295. *
  296. * @api private
  297. */
  298. onOpen() {
  299. debug("socket open");
  300. this.readyState = "open";
  301. Socket.priorWebsocketSuccess = "websocket" === this.transport.name;
  302. this.emitReserved("open");
  303. this.flush();
  304. // we check for `readyState` in case an `open`
  305. // listener already closed the socket
  306. if ("open" === this.readyState &&
  307. this.opts.upgrade &&
  308. this.transport.pause) {
  309. debug("starting upgrade probes");
  310. let i = 0;
  311. const l = this.upgrades.length;
  312. for (; i < l; i++) {
  313. this.probe(this.upgrades[i]);
  314. }
  315. }
  316. }
  317. /**
  318. * Handles a packet.
  319. *
  320. * @api private
  321. */
  322. onPacket(packet) {
  323. if ("opening" === this.readyState ||
  324. "open" === this.readyState ||
  325. "closing" === this.readyState) {
  326. debug('socket receive: type "%s", data "%s"', packet.type, packet.data);
  327. this.emitReserved("packet", packet);
  328. // Socket is live - any packet counts
  329. this.emitReserved("heartbeat");
  330. switch (packet.type) {
  331. case "open":
  332. this.onHandshake(JSON.parse(packet.data));
  333. break;
  334. case "ping":
  335. this.resetPingTimeout();
  336. this.sendPacket("pong");
  337. this.emitReserved("ping");
  338. this.emitReserved("pong");
  339. break;
  340. case "error":
  341. const err = new Error("server error");
  342. // @ts-ignore
  343. err.code = packet.data;
  344. this.onError(err);
  345. break;
  346. case "message":
  347. this.emitReserved("data", packet.data);
  348. this.emitReserved("message", packet.data);
  349. break;
  350. }
  351. }
  352. else {
  353. debug('packet received with socket readyState "%s"', this.readyState);
  354. }
  355. }
  356. /**
  357. * Called upon handshake completion.
  358. *
  359. * @param {Object} data - handshake obj
  360. * @api private
  361. */
  362. onHandshake(data) {
  363. this.emitReserved("handshake", data);
  364. this.id = data.sid;
  365. this.transport.query.sid = data.sid;
  366. this.upgrades = this.filterUpgrades(data.upgrades);
  367. this.pingInterval = data.pingInterval;
  368. this.pingTimeout = data.pingTimeout;
  369. this.onOpen();
  370. // In case open handler closes socket
  371. if ("closed" === this.readyState)
  372. return;
  373. this.resetPingTimeout();
  374. }
  375. /**
  376. * Sets and resets ping timeout timer based on server pings.
  377. *
  378. * @api private
  379. */
  380. resetPingTimeout() {
  381. this.clearTimeoutFn(this.pingTimeoutTimer);
  382. this.pingTimeoutTimer = this.setTimeoutFn(() => {
  383. this.onClose("ping timeout");
  384. }, this.pingInterval + this.pingTimeout);
  385. if (this.opts.autoUnref) {
  386. this.pingTimeoutTimer.unref();
  387. }
  388. }
  389. /**
  390. * Called on `drain` event
  391. *
  392. * @api private
  393. */
  394. onDrain() {
  395. this.writeBuffer.splice(0, this.prevBufferLen);
  396. // setting prevBufferLen = 0 is very important
  397. // for example, when upgrading, upgrade packet is sent over,
  398. // and a nonzero prevBufferLen could cause problems on `drain`
  399. this.prevBufferLen = 0;
  400. if (0 === this.writeBuffer.length) {
  401. this.emitReserved("drain");
  402. }
  403. else {
  404. this.flush();
  405. }
  406. }
  407. /**
  408. * Flush write buffers.
  409. *
  410. * @api private
  411. */
  412. flush() {
  413. if ("closed" !== this.readyState &&
  414. this.transport.writable &&
  415. !this.upgrading &&
  416. this.writeBuffer.length) {
  417. debug("flushing %d packets in socket", this.writeBuffer.length);
  418. this.transport.send(this.writeBuffer);
  419. // keep track of current length of writeBuffer
  420. // splice writeBuffer and callbackBuffer on `drain`
  421. this.prevBufferLen = this.writeBuffer.length;
  422. this.emitReserved("flush");
  423. }
  424. }
  425. /**
  426. * Sends a message.
  427. *
  428. * @param {String} message.
  429. * @param {Function} callback function.
  430. * @param {Object} options.
  431. * @return {Socket} for chaining.
  432. * @api public
  433. */
  434. write(msg, options, fn) {
  435. this.sendPacket("message", msg, options, fn);
  436. return this;
  437. }
  438. send(msg, options, fn) {
  439. this.sendPacket("message", msg, options, fn);
  440. return this;
  441. }
  442. /**
  443. * Sends a packet.
  444. *
  445. * @param {String} packet type.
  446. * @param {String} data.
  447. * @param {Object} options.
  448. * @param {Function} callback function.
  449. * @api private
  450. */
  451. sendPacket(type, data, options, fn) {
  452. if ("function" === typeof data) {
  453. fn = data;
  454. data = undefined;
  455. }
  456. if ("function" === typeof options) {
  457. fn = options;
  458. options = null;
  459. }
  460. if ("closing" === this.readyState || "closed" === this.readyState) {
  461. return;
  462. }
  463. options = options || {};
  464. options.compress = false !== options.compress;
  465. const packet = {
  466. type: type,
  467. data: data,
  468. options: options
  469. };
  470. this.emitReserved("packetCreate", packet);
  471. this.writeBuffer.push(packet);
  472. if (fn)
  473. this.once("flush", fn);
  474. this.flush();
  475. }
  476. /**
  477. * Closes the connection.
  478. *
  479. * @api public
  480. */
  481. close() {
  482. const close = () => {
  483. this.onClose("forced close");
  484. debug("socket closing - telling transport to close");
  485. this.transport.close();
  486. };
  487. const cleanupAndClose = () => {
  488. this.off("upgrade", cleanupAndClose);
  489. this.off("upgradeError", cleanupAndClose);
  490. close();
  491. };
  492. const waitForUpgrade = () => {
  493. // wait for upgrade to finish since we can't send packets while pausing a transport
  494. this.once("upgrade", cleanupAndClose);
  495. this.once("upgradeError", cleanupAndClose);
  496. };
  497. if ("opening" === this.readyState || "open" === this.readyState) {
  498. this.readyState = "closing";
  499. if (this.writeBuffer.length) {
  500. this.once("drain", () => {
  501. if (this.upgrading) {
  502. waitForUpgrade();
  503. }
  504. else {
  505. close();
  506. }
  507. });
  508. }
  509. else if (this.upgrading) {
  510. waitForUpgrade();
  511. }
  512. else {
  513. close();
  514. }
  515. }
  516. return this;
  517. }
  518. /**
  519. * Called upon transport error
  520. *
  521. * @api private
  522. */
  523. onError(err) {
  524. debug("socket error %j", err);
  525. Socket.priorWebsocketSuccess = false;
  526. this.emitReserved("error", err);
  527. this.onClose("transport error", err);
  528. }
  529. /**
  530. * Called upon transport close.
  531. *
  532. * @api private
  533. */
  534. onClose(reason, desc) {
  535. if ("opening" === this.readyState ||
  536. "open" === this.readyState ||
  537. "closing" === this.readyState) {
  538. debug('socket close with reason: "%s"', reason);
  539. // clear timers
  540. this.clearTimeoutFn(this.pingTimeoutTimer);
  541. // stop event from firing again for transport
  542. this.transport.removeAllListeners("close");
  543. // ensure transport won't stay open
  544. this.transport.close();
  545. // ignore further transport communication
  546. this.transport.removeAllListeners();
  547. if (typeof removeEventListener === "function") {
  548. removeEventListener("offline", this.offlineEventListener, false);
  549. }
  550. // set ready state
  551. this.readyState = "closed";
  552. // clear session id
  553. this.id = null;
  554. // emit close event
  555. this.emitReserved("close", reason, desc);
  556. // clean buffers after, so users can still
  557. // grab the buffers on `close` event
  558. this.writeBuffer = [];
  559. this.prevBufferLen = 0;
  560. }
  561. }
  562. /**
  563. * Filters upgrades, returning only those matching client transports.
  564. *
  565. * @param {Array} server upgrades
  566. * @api private
  567. *
  568. */
  569. filterUpgrades(upgrades) {
  570. const filteredUpgrades = [];
  571. let i = 0;
  572. const j = upgrades.length;
  573. for (; i < j; i++) {
  574. if (~this.transports.indexOf(upgrades[i]))
  575. filteredUpgrades.push(upgrades[i]);
  576. }
  577. return filteredUpgrades;
  578. }
  579. }
  580. exports.Socket = Socket;
  581. Socket.protocol = engine_io_parser_1.protocol;
  582. function clone(obj) {
  583. const o = {};
  584. for (let i in obj) {
  585. if (obj.hasOwnProperty(i)) {
  586. o[i] = obj[i];
  587. }
  588. }
  589. return o;
  590. }