import { Transport } from "../transport.js"; import { yeast } from "../contrib/yeast.js"; import { encodePayload, decodePayload } from "engine.io-parser"; import { createCookieJar, XHR as XMLHttpRequest, } from "./xmlhttprequest.js"; import { Emitter } from "@socket.io/component-emitter"; import { installTimerFunctions, pick } from "../util.js"; import { globalThisShim as globalThis } from "../globalThis.js"; function empty() { } const hasXHR2 = (function () { const xhr = new XMLHttpRequest({ xdomain: false, }); return null != xhr.responseType; })(); export class Polling extends Transport { /** * XHR Polling constructor. * * @param {Object} opts * @package */ constructor(opts) { super(opts); this.polling = false; if (typeof location !== "undefined") { const isSSL = "https:" === location.protocol; let port = location.port; // some user agents have empty `location.port` if (!port) { port = isSSL ? "443" : "80"; } this.xd = (typeof location !== "undefined" && opts.hostname !== location.hostname) || port !== opts.port; } /** * XHR supports binary */ const forceBase64 = opts && opts.forceBase64; this.supportsBinary = hasXHR2 && !forceBase64; if (this.opts.withCredentials) { this.cookieJar = createCookieJar(); } } get name() { return "polling"; } /** * Opens the socket (triggers polling). We write a PING message to determine * when the transport is open. * * @protected */ doOpen() { this.poll(); } /** * Pauses polling. * * @param {Function} onPause - callback upon buffers are flushed and transport is paused * @package */ pause(onPause) { this.readyState = "pausing"; const pause = () => { this.readyState = "paused"; onPause(); }; if (this.polling || !this.writable) { let total = 0; if (this.polling) { total++; this.once("pollComplete", function () { --total || pause(); }); } if (!this.writable) { total++; this.once("drain", function () { --total || pause(); }); } } else { pause(); } } /** * Starts polling cycle. * * @private */ poll() { this.polling = true; this.doPoll(); this.emitReserved("poll"); } /** * Overloads onData to detect payloads. * * @protected */ onData(data) { const callback = (packet) => { // if its the first message we consider the transport open if ("opening" === this.readyState && packet.type === "open") { this.onOpen(); } // if its a close packet, we close the ongoing requests if ("close" === packet.type) { this.onClose({ description: "transport closed by the server" }); return false; } // otherwise bypass onData and handle the message this.onPacket(packet); }; // decode payload decodePayload(data, this.socket.binaryType).forEach(callback); // if an event did not trigger closing if ("closed" !== this.readyState) { // if we got data we're not polling this.polling = false; this.emitReserved("pollComplete"); if ("open" === this.readyState) { this.poll(); } else { } } } /** * For polling, send a close packet. * * @protected */ doClose() { const close = () => { this.write([{ type: "close" }]); }; if ("open" === this.readyState) { close(); } else { // in case we're trying to close while // handshaking is in progress (GH-164) this.once("open", close); } } /** * Writes a packets payload. * * @param {Array} packets - data packets * @protected */ write(packets) { this.writable = false; encodePayload(packets, (data) => { this.doWrite(data, () => { this.writable = true; this.emitReserved("drain"); }); }); } /** * Generates uri for connection. * * @private */ uri() { const schema = this.opts.secure ? "https" : "http"; const query = this.query || {}; // cache busting is forced if (false !== this.opts.timestampRequests) { query[this.opts.timestampParam] = yeast(); } if (!this.supportsBinary && !query.sid) { query.b64 = 1; } return this.createUri(schema, query); } /** * Creates a request. * * @param {String} method * @private */ request(opts = {}) { Object.assign(opts, { xd: this.xd, cookieJar: this.cookieJar }, this.opts); return new Request(this.uri(), opts); } /** * Sends data. * * @param {String} data to send. * @param {Function} called upon flush. * @private */ doWrite(data, fn) { const req = this.request({ method: "POST", data: data, }); req.on("success", fn); req.on("error", (xhrStatus, context) => { this.onError("xhr post error", xhrStatus, context); }); } /** * Starts a poll cycle. * * @private */ doPoll() { const req = this.request(); req.on("data", this.onData.bind(this)); req.on("error", (xhrStatus, context) => { this.onError("xhr poll error", xhrStatus, context); }); this.pollXhr = req; } } export class Request extends Emitter { /** * Request constructor * * @param {Object} options * @package */ constructor(uri, opts) { super(); installTimerFunctions(this, opts); this.opts = opts; this.method = opts.method || "GET"; this.uri = uri; this.data = undefined !== opts.data ? opts.data : null; this.create(); } /** * Creates the XHR object and sends the request. * * @private */ create() { var _a; const opts = pick(this.opts, "agent", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized", "autoUnref"); opts.xdomain = !!this.opts.xd; const xhr = (this.xhr = new XMLHttpRequest(opts)); try { xhr.open(this.method, this.uri, true); try { if (this.opts.extraHeaders) { xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true); for (let i in this.opts.extraHeaders) { if (this.opts.extraHeaders.hasOwnProperty(i)) { xhr.setRequestHeader(i, this.opts.extraHeaders[i]); } } } } catch (e) { } if ("POST" === this.method) { try { xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8"); } catch (e) { } } try { xhr.setRequestHeader("Accept", "*/*"); } catch (e) { } (_a = this.opts.cookieJar) === null || _a === void 0 ? void 0 : _a.addCookies(xhr); // ie6 check if ("withCredentials" in xhr) { xhr.withCredentials = this.opts.withCredentials; } if (this.opts.requestTimeout) { xhr.timeout = this.opts.requestTimeout; } xhr.onreadystatechange = () => { var _a; if (xhr.readyState === 3) { (_a = this.opts.cookieJar) === null || _a === void 0 ? void 0 : _a.parseCookies(xhr); } if (4 !== xhr.readyState) return; if (200 === xhr.status || 1223 === xhr.status) { this.onLoad(); } else { // make sure the `error` event handler that's user-set // does not throw in the same tick and gets caught here this.setTimeoutFn(() => { this.onError(typeof xhr.status === "number" ? xhr.status : 0); }, 0); } }; xhr.send(this.data); } catch (e) { // Need to defer since .create() is called directly from the constructor // and thus the 'error' event can only be only bound *after* this exception // occurs. Therefore, also, we cannot throw here at all. this.setTimeoutFn(() => { this.onError(e); }, 0); return; } if (typeof document !== "undefined") { this.index = Request.requestsCount++; Request.requests[this.index] = this; } } /** * Called upon error. * * @private */ onError(err) { this.emitReserved("error", err, this.xhr); this.cleanup(true); } /** * Cleans up house. * * @private */ cleanup(fromError) { if ("undefined" === typeof this.xhr || null === this.xhr) { return; } this.xhr.onreadystatechange = empty; if (fromError) { try { this.xhr.abort(); } catch (e) { } } if (typeof document !== "undefined") { delete Request.requests[this.index]; } this.xhr = null; } /** * Called upon load. * * @private */ onLoad() { const data = this.xhr.responseText; if (data !== null) { this.emitReserved("data", data); this.emitReserved("success"); this.cleanup(); } } /** * Aborts the request. * * @package */ abort() { this.cleanup(); } } Request.requestsCount = 0; Request.requests = {}; /** * Aborts pending requests when unloading the window. This is needed to prevent * memory leaks (e.g. when using IE) and to ensure that no spurious error is * emitted. */ if (typeof document !== "undefined") { // @ts-ignore if (typeof attachEvent === "function") { // @ts-ignore attachEvent("onunload", unloadHandler); } else if (typeof addEventListener === "function") { const terminationEvent = "onpagehide" in globalThis ? "pagehide" : "unload"; addEventListener(terminationEvent, unloadHandler, false); } } function unloadHandler() { for (let i in Request.requests) { if (Request.requests.hasOwnProperty(i)) { Request.requests[i].abort(); } } }