polling.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import { Transport } from "../transport.js";
  2. import { yeast } from "../contrib/yeast.js";
  3. import { encodePayload, decodePayload } from "engine.io-parser";
  4. import { createCookieJar, XHR as XMLHttpRequest, } from "./xmlhttprequest.js";
  5. import { Emitter } from "@socket.io/component-emitter";
  6. import { installTimerFunctions, pick } from "../util.js";
  7. import { globalThisShim as globalThis } from "../globalThis.js";
  8. function empty() { }
  9. const hasXHR2 = (function () {
  10. const xhr = new XMLHttpRequest({
  11. xdomain: false,
  12. });
  13. return null != xhr.responseType;
  14. })();
  15. export class Polling extends Transport {
  16. /**
  17. * XHR Polling constructor.
  18. *
  19. * @param {Object} opts
  20. * @package
  21. */
  22. constructor(opts) {
  23. super(opts);
  24. this.polling = false;
  25. if (typeof location !== "undefined") {
  26. const isSSL = "https:" === location.protocol;
  27. let port = location.port;
  28. // some user agents have empty `location.port`
  29. if (!port) {
  30. port = isSSL ? "443" : "80";
  31. }
  32. this.xd =
  33. (typeof location !== "undefined" &&
  34. opts.hostname !== location.hostname) ||
  35. port !== opts.port;
  36. }
  37. /**
  38. * XHR supports binary
  39. */
  40. const forceBase64 = opts && opts.forceBase64;
  41. this.supportsBinary = hasXHR2 && !forceBase64;
  42. if (this.opts.withCredentials) {
  43. this.cookieJar = createCookieJar();
  44. }
  45. }
  46. get name() {
  47. return "polling";
  48. }
  49. /**
  50. * Opens the socket (triggers polling). We write a PING message to determine
  51. * when the transport is open.
  52. *
  53. * @protected
  54. */
  55. doOpen() {
  56. this.poll();
  57. }
  58. /**
  59. * Pauses polling.
  60. *
  61. * @param {Function} onPause - callback upon buffers are flushed and transport is paused
  62. * @package
  63. */
  64. pause(onPause) {
  65. this.readyState = "pausing";
  66. const pause = () => {
  67. this.readyState = "paused";
  68. onPause();
  69. };
  70. if (this.polling || !this.writable) {
  71. let total = 0;
  72. if (this.polling) {
  73. total++;
  74. this.once("pollComplete", function () {
  75. --total || pause();
  76. });
  77. }
  78. if (!this.writable) {
  79. total++;
  80. this.once("drain", function () {
  81. --total || pause();
  82. });
  83. }
  84. }
  85. else {
  86. pause();
  87. }
  88. }
  89. /**
  90. * Starts polling cycle.
  91. *
  92. * @private
  93. */
  94. poll() {
  95. this.polling = true;
  96. this.doPoll();
  97. this.emitReserved("poll");
  98. }
  99. /**
  100. * Overloads onData to detect payloads.
  101. *
  102. * @protected
  103. */
  104. onData(data) {
  105. const callback = (packet) => {
  106. // if its the first message we consider the transport open
  107. if ("opening" === this.readyState && packet.type === "open") {
  108. this.onOpen();
  109. }
  110. // if its a close packet, we close the ongoing requests
  111. if ("close" === packet.type) {
  112. this.onClose({ description: "transport closed by the server" });
  113. return false;
  114. }
  115. // otherwise bypass onData and handle the message
  116. this.onPacket(packet);
  117. };
  118. // decode payload
  119. decodePayload(data, this.socket.binaryType).forEach(callback);
  120. // if an event did not trigger closing
  121. if ("closed" !== this.readyState) {
  122. // if we got data we're not polling
  123. this.polling = false;
  124. this.emitReserved("pollComplete");
  125. if ("open" === this.readyState) {
  126. this.poll();
  127. }
  128. else {
  129. }
  130. }
  131. }
  132. /**
  133. * For polling, send a close packet.
  134. *
  135. * @protected
  136. */
  137. doClose() {
  138. const close = () => {
  139. this.write([{ type: "close" }]);
  140. };
  141. if ("open" === this.readyState) {
  142. close();
  143. }
  144. else {
  145. // in case we're trying to close while
  146. // handshaking is in progress (GH-164)
  147. this.once("open", close);
  148. }
  149. }
  150. /**
  151. * Writes a packets payload.
  152. *
  153. * @param {Array} packets - data packets
  154. * @protected
  155. */
  156. write(packets) {
  157. this.writable = false;
  158. encodePayload(packets, (data) => {
  159. this.doWrite(data, () => {
  160. this.writable = true;
  161. this.emitReserved("drain");
  162. });
  163. });
  164. }
  165. /**
  166. * Generates uri for connection.
  167. *
  168. * @private
  169. */
  170. uri() {
  171. const schema = this.opts.secure ? "https" : "http";
  172. const query = this.query || {};
  173. // cache busting is forced
  174. if (false !== this.opts.timestampRequests) {
  175. query[this.opts.timestampParam] = yeast();
  176. }
  177. if (!this.supportsBinary && !query.sid) {
  178. query.b64 = 1;
  179. }
  180. return this.createUri(schema, query);
  181. }
  182. /**
  183. * Creates a request.
  184. *
  185. * @param {String} method
  186. * @private
  187. */
  188. request(opts = {}) {
  189. Object.assign(opts, { xd: this.xd, cookieJar: this.cookieJar }, this.opts);
  190. return new Request(this.uri(), opts);
  191. }
  192. /**
  193. * Sends data.
  194. *
  195. * @param {String} data to send.
  196. * @param {Function} called upon flush.
  197. * @private
  198. */
  199. doWrite(data, fn) {
  200. const req = this.request({
  201. method: "POST",
  202. data: data,
  203. });
  204. req.on("success", fn);
  205. req.on("error", (xhrStatus, context) => {
  206. this.onError("xhr post error", xhrStatus, context);
  207. });
  208. }
  209. /**
  210. * Starts a poll cycle.
  211. *
  212. * @private
  213. */
  214. doPoll() {
  215. const req = this.request();
  216. req.on("data", this.onData.bind(this));
  217. req.on("error", (xhrStatus, context) => {
  218. this.onError("xhr poll error", xhrStatus, context);
  219. });
  220. this.pollXhr = req;
  221. }
  222. }
  223. export class Request extends Emitter {
  224. /**
  225. * Request constructor
  226. *
  227. * @param {Object} options
  228. * @package
  229. */
  230. constructor(uri, opts) {
  231. super();
  232. installTimerFunctions(this, opts);
  233. this.opts = opts;
  234. this.method = opts.method || "GET";
  235. this.uri = uri;
  236. this.data = undefined !== opts.data ? opts.data : null;
  237. this.create();
  238. }
  239. /**
  240. * Creates the XHR object and sends the request.
  241. *
  242. * @private
  243. */
  244. create() {
  245. var _a;
  246. const opts = pick(this.opts, "agent", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized", "autoUnref");
  247. opts.xdomain = !!this.opts.xd;
  248. const xhr = (this.xhr = new XMLHttpRequest(opts));
  249. try {
  250. xhr.open(this.method, this.uri, true);
  251. try {
  252. if (this.opts.extraHeaders) {
  253. xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
  254. for (let i in this.opts.extraHeaders) {
  255. if (this.opts.extraHeaders.hasOwnProperty(i)) {
  256. xhr.setRequestHeader(i, this.opts.extraHeaders[i]);
  257. }
  258. }
  259. }
  260. }
  261. catch (e) { }
  262. if ("POST" === this.method) {
  263. try {
  264. xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
  265. }
  266. catch (e) { }
  267. }
  268. try {
  269. xhr.setRequestHeader("Accept", "*/*");
  270. }
  271. catch (e) { }
  272. (_a = this.opts.cookieJar) === null || _a === void 0 ? void 0 : _a.addCookies(xhr);
  273. // ie6 check
  274. if ("withCredentials" in xhr) {
  275. xhr.withCredentials = this.opts.withCredentials;
  276. }
  277. if (this.opts.requestTimeout) {
  278. xhr.timeout = this.opts.requestTimeout;
  279. }
  280. xhr.onreadystatechange = () => {
  281. var _a;
  282. if (xhr.readyState === 3) {
  283. (_a = this.opts.cookieJar) === null || _a === void 0 ? void 0 : _a.parseCookies(xhr);
  284. }
  285. if (4 !== xhr.readyState)
  286. return;
  287. if (200 === xhr.status || 1223 === xhr.status) {
  288. this.onLoad();
  289. }
  290. else {
  291. // make sure the `error` event handler that's user-set
  292. // does not throw in the same tick and gets caught here
  293. this.setTimeoutFn(() => {
  294. this.onError(typeof xhr.status === "number" ? xhr.status : 0);
  295. }, 0);
  296. }
  297. };
  298. xhr.send(this.data);
  299. }
  300. catch (e) {
  301. // Need to defer since .create() is called directly from the constructor
  302. // and thus the 'error' event can only be only bound *after* this exception
  303. // occurs. Therefore, also, we cannot throw here at all.
  304. this.setTimeoutFn(() => {
  305. this.onError(e);
  306. }, 0);
  307. return;
  308. }
  309. if (typeof document !== "undefined") {
  310. this.index = Request.requestsCount++;
  311. Request.requests[this.index] = this;
  312. }
  313. }
  314. /**
  315. * Called upon error.
  316. *
  317. * @private
  318. */
  319. onError(err) {
  320. this.emitReserved("error", err, this.xhr);
  321. this.cleanup(true);
  322. }
  323. /**
  324. * Cleans up house.
  325. *
  326. * @private
  327. */
  328. cleanup(fromError) {
  329. if ("undefined" === typeof this.xhr || null === this.xhr) {
  330. return;
  331. }
  332. this.xhr.onreadystatechange = empty;
  333. if (fromError) {
  334. try {
  335. this.xhr.abort();
  336. }
  337. catch (e) { }
  338. }
  339. if (typeof document !== "undefined") {
  340. delete Request.requests[this.index];
  341. }
  342. this.xhr = null;
  343. }
  344. /**
  345. * Called upon load.
  346. *
  347. * @private
  348. */
  349. onLoad() {
  350. const data = this.xhr.responseText;
  351. if (data !== null) {
  352. this.emitReserved("data", data);
  353. this.emitReserved("success");
  354. this.cleanup();
  355. }
  356. }
  357. /**
  358. * Aborts the request.
  359. *
  360. * @package
  361. */
  362. abort() {
  363. this.cleanup();
  364. }
  365. }
  366. Request.requestsCount = 0;
  367. Request.requests = {};
  368. /**
  369. * Aborts pending requests when unloading the window. This is needed to prevent
  370. * memory leaks (e.g. when using IE) and to ensure that no spurious error is
  371. * emitted.
  372. */
  373. if (typeof document !== "undefined") {
  374. // @ts-ignore
  375. if (typeof attachEvent === "function") {
  376. // @ts-ignore
  377. attachEvent("onunload", unloadHandler);
  378. }
  379. else if (typeof addEventListener === "function") {
  380. const terminationEvent = "onpagehide" in globalThis ? "pagehide" : "unload";
  381. addEventListener(terminationEvent, unloadHandler, false);
  382. }
  383. }
  384. function unloadHandler() {
  385. for (let i in Request.requests) {
  386. if (Request.requests.hasOwnProperty(i)) {
  387. Request.requests[i].abort();
  388. }
  389. }
  390. }