polling-xhr.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /* global attachEvent */
  2. import XMLHttpRequest from "./xmlhttprequest.js";
  3. import globalThis from "../globalThis.js";
  4. import { installTimerFunctions, pick } from "../util.js";
  5. import { Emitter } from "@socket.io/component-emitter";
  6. import { Polling } from "./polling.js";
  7. /**
  8. * Empty function
  9. */
  10. function empty() { }
  11. const hasXHR2 = (function () {
  12. const xhr = new XMLHttpRequest({
  13. xdomain: false
  14. });
  15. return null != xhr.responseType;
  16. })();
  17. export class XHR extends Polling {
  18. /**
  19. * XHR Polling constructor.
  20. *
  21. * @param {Object} opts
  22. * @api public
  23. */
  24. constructor(opts) {
  25. super(opts);
  26. if (typeof location !== "undefined") {
  27. const isSSL = "https:" === location.protocol;
  28. let port = location.port;
  29. // some user agents have empty `location.port`
  30. if (!port) {
  31. port = isSSL ? "443" : "80";
  32. }
  33. this.xd =
  34. (typeof location !== "undefined" &&
  35. opts.hostname !== location.hostname) ||
  36. port !== opts.port;
  37. this.xs = opts.secure !== isSSL;
  38. }
  39. /**
  40. * XHR supports binary
  41. */
  42. const forceBase64 = opts && opts.forceBase64;
  43. this.supportsBinary = hasXHR2 && !forceBase64;
  44. }
  45. /**
  46. * Creates a request.
  47. *
  48. * @param {String} method
  49. * @api private
  50. */
  51. request(opts = {}) {
  52. Object.assign(opts, { xd: this.xd, xs: this.xs }, this.opts);
  53. return new Request(this.uri(), opts);
  54. }
  55. /**
  56. * Sends data.
  57. *
  58. * @param {String} data to send.
  59. * @param {Function} called upon flush.
  60. * @api private
  61. */
  62. doWrite(data, fn) {
  63. const req = this.request({
  64. method: "POST",
  65. data: data
  66. });
  67. req.on("success", fn);
  68. req.on("error", err => {
  69. this.onError("xhr post error", err);
  70. });
  71. }
  72. /**
  73. * Starts a poll cycle.
  74. *
  75. * @api private
  76. */
  77. doPoll() {
  78. const req = this.request();
  79. req.on("data", this.onData.bind(this));
  80. req.on("error", err => {
  81. this.onError("xhr poll error", err);
  82. });
  83. this.pollXhr = req;
  84. }
  85. }
  86. export class Request extends Emitter {
  87. /**
  88. * Request constructor
  89. *
  90. * @param {Object} options
  91. * @api public
  92. */
  93. constructor(uri, opts) {
  94. super();
  95. installTimerFunctions(this, opts);
  96. this.opts = opts;
  97. this.method = opts.method || "GET";
  98. this.uri = uri;
  99. this.async = false !== opts.async;
  100. this.data = undefined !== opts.data ? opts.data : null;
  101. this.create();
  102. }
  103. /**
  104. * Creates the XHR object and sends the request.
  105. *
  106. * @api private
  107. */
  108. create() {
  109. const opts = pick(this.opts, "agent", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized", "autoUnref");
  110. opts.xdomain = !!this.opts.xd;
  111. opts.xscheme = !!this.opts.xs;
  112. const xhr = (this.xhr = new XMLHttpRequest(opts));
  113. try {
  114. xhr.open(this.method, this.uri, this.async);
  115. try {
  116. if (this.opts.extraHeaders) {
  117. xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
  118. for (let i in this.opts.extraHeaders) {
  119. if (this.opts.extraHeaders.hasOwnProperty(i)) {
  120. xhr.setRequestHeader(i, this.opts.extraHeaders[i]);
  121. }
  122. }
  123. }
  124. }
  125. catch (e) { }
  126. if ("POST" === this.method) {
  127. try {
  128. xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
  129. }
  130. catch (e) { }
  131. }
  132. try {
  133. xhr.setRequestHeader("Accept", "*/*");
  134. }
  135. catch (e) { }
  136. // ie6 check
  137. if ("withCredentials" in xhr) {
  138. xhr.withCredentials = this.opts.withCredentials;
  139. }
  140. if (this.opts.requestTimeout) {
  141. xhr.timeout = this.opts.requestTimeout;
  142. }
  143. xhr.onreadystatechange = () => {
  144. if (4 !== xhr.readyState)
  145. return;
  146. if (200 === xhr.status || 1223 === xhr.status) {
  147. this.onLoad();
  148. }
  149. else {
  150. // make sure the `error` event handler that's user-set
  151. // does not throw in the same tick and gets caught here
  152. this.setTimeoutFn(() => {
  153. this.onError(typeof xhr.status === "number" ? xhr.status : 0);
  154. }, 0);
  155. }
  156. };
  157. xhr.send(this.data);
  158. }
  159. catch (e) {
  160. // Need to defer since .create() is called directly from the constructor
  161. // and thus the 'error' event can only be only bound *after* this exception
  162. // occurs. Therefore, also, we cannot throw here at all.
  163. this.setTimeoutFn(() => {
  164. this.onError(e);
  165. }, 0);
  166. return;
  167. }
  168. if (typeof document !== "undefined") {
  169. this.index = Request.requestsCount++;
  170. Request.requests[this.index] = this;
  171. }
  172. }
  173. /**
  174. * Called upon successful response.
  175. *
  176. * @api private
  177. */
  178. onSuccess() {
  179. this.emit("success");
  180. this.cleanup();
  181. }
  182. /**
  183. * Called if we have data.
  184. *
  185. * @api private
  186. */
  187. onData(data) {
  188. this.emit("data", data);
  189. this.onSuccess();
  190. }
  191. /**
  192. * Called upon error.
  193. *
  194. * @api private
  195. */
  196. onError(err) {
  197. this.emit("error", err);
  198. this.cleanup(true);
  199. }
  200. /**
  201. * Cleans up house.
  202. *
  203. * @api private
  204. */
  205. cleanup(fromError) {
  206. if ("undefined" === typeof this.xhr || null === this.xhr) {
  207. return;
  208. }
  209. this.xhr.onreadystatechange = empty;
  210. if (fromError) {
  211. try {
  212. this.xhr.abort();
  213. }
  214. catch (e) { }
  215. }
  216. if (typeof document !== "undefined") {
  217. delete Request.requests[this.index];
  218. }
  219. this.xhr = null;
  220. }
  221. /**
  222. * Called upon load.
  223. *
  224. * @api private
  225. */
  226. onLoad() {
  227. const data = this.xhr.responseText;
  228. if (data !== null) {
  229. this.onData(data);
  230. }
  231. }
  232. /**
  233. * Aborts the request.
  234. *
  235. * @api public
  236. */
  237. abort() {
  238. this.cleanup();
  239. }
  240. }
  241. Request.requestsCount = 0;
  242. Request.requests = {};
  243. /**
  244. * Aborts pending requests when unloading the window. This is needed to prevent
  245. * memory leaks (e.g. when using IE) and to ensure that no spurious error is
  246. * emitted.
  247. */
  248. if (typeof document !== "undefined") {
  249. // @ts-ignore
  250. if (typeof attachEvent === "function") {
  251. // @ts-ignore
  252. attachEvent("onunload", unloadHandler);
  253. }
  254. else if (typeof addEventListener === "function") {
  255. const terminationEvent = "onpagehide" in globalThis ? "pagehide" : "unload";
  256. addEventListener(terminationEvent, unloadHandler, false);
  257. }
  258. }
  259. function unloadHandler() {
  260. for (let i in Request.requests) {
  261. if (Request.requests.hasOwnProperty(i)) {
  262. Request.requests[i].abort();
  263. }
  264. }
  265. }