socket.js 21 KB

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