From b11763beecfe4622867b4dec9d1db77460733ffb Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Tue, 23 Apr 2024 11:03:42 +0200 Subject: [PATCH] feat: add HTTP long-polling implementation based on fetch() Usage: ```js import { Socket, transports, Fetch } from "engine.io-client"; transports.polling = Fetch; const socket = new Socket("https://example.com"); ``` Note: tree-shaking unused transports is not currently supported and will be added later. Related: - https://github.com/socketio/socket.io/issues/4980 - https://github.com/socketio/engine.io-client/issues/716 --- .github/workflows/ci.yml | 4 + lib/index.ts | 3 + lib/transports/index.ts | 4 +- lib/transports/polling-fetch.ts | 72 +++++++ lib/transports/polling-xhr.ts | 327 +++++++++++++++++++++++++++++++ lib/transports/polling.ts | 322 +----------------------------- lib/transports/xmlhttprequest.ts | 13 +- package-lock.json | 31 +++ package.json | 2 + test/connection.js | 4 +- test/socket.js | 36 ++-- test/support/env.js | 8 + test/transport.js | 6 +- 13 files changed, 499 insertions(+), 333 deletions(-) create mode 100644 lib/transports/polling-fetch.ts create mode 100644 lib/transports/polling-xhr.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32398fe20..c31008bc8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,3 +30,7 @@ jobs: - name: Run tests run: npm test + + - name: Run tests with fetch() + run: npm run test:node-fetch + if: ${{ matrix.node-version == '20' }} # fetch() was added in Node.js v18.0.0 (without experimental flag) diff --git a/lib/index.ts b/lib/index.ts index 6a44065b8..2688b8bd0 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -8,3 +8,6 @@ export { transports } from "./transports/index.js"; export { installTimerFunctions } from "./util.js"; export { parse } from "./contrib/parseuri.js"; export { nextTick } from "./transports/websocket-constructor.js"; + +export { Fetch } from "./transports/polling-fetch.js"; +export { XHR } from "./transports/polling-xhr.js"; diff --git a/lib/transports/index.ts b/lib/transports/index.ts index bb2460f20..c4b49f881 100755 --- a/lib/transports/index.ts +++ b/lib/transports/index.ts @@ -1,9 +1,9 @@ -import { Polling } from "./polling.js"; +import { XHR } from "./polling-xhr.js"; import { WS } from "./websocket.js"; import { WT } from "./webtransport.js"; export const transports = { websocket: WS, webtransport: WT, - polling: Polling, + polling: XHR, }; diff --git a/lib/transports/polling-fetch.ts b/lib/transports/polling-fetch.ts new file mode 100644 index 000000000..b9d1c2f0e --- /dev/null +++ b/lib/transports/polling-fetch.ts @@ -0,0 +1,72 @@ +import { Polling } from "./polling.js"; +import { CookieJar, createCookieJar } from "./xmlhttprequest.js"; + +/** + * HTTP long-polling based on `fetch()` + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ +export class Fetch extends Polling { + private readonly cookieJar?: CookieJar; + + constructor(opts) { + super(opts); + + if (this.opts.withCredentials) { + this.cookieJar = createCookieJar(); + } + } + + override doPoll() { + this._fetch() + .then((res) => { + if (!res.ok) { + return this.onError("fetch read error", res.status, res); + } + + res.text().then((data) => this.onData(data)); + }) + .catch((err) => { + this.onError("fetch read error", err); + }); + } + + override doWrite(data: string, callback: () => void) { + this._fetch(data) + .then((res) => { + if (!res.ok) { + return this.onError("fetch write error", res.status, res); + } + + callback(); + }) + .catch((err) => { + this.onError("fetch write error", err); + }); + } + + private _fetch(data?: string) { + const isPost = data !== undefined; + const headers = new Headers(this.opts.extraHeaders); + + if (isPost) { + headers.set("content-type", "text/plain;charset=UTF-8"); + } + + this.cookieJar?.appendCookies(headers); + + return fetch(this.uri(), { + method: isPost ? "POST" : "GET", + body: isPost ? data : null, + headers, + credentials: this.opts.withCredentials ? "include" : "omit", + }).then((res) => { + if (this.cookieJar) { + // @ts-ignore getSetCookie() was added in Node.js v19.7.0 + this.cookieJar.parseCookies(res.headers.getSetCookie()); + } + + return res; + }); + } +} diff --git a/lib/transports/polling-xhr.ts b/lib/transports/polling-xhr.ts new file mode 100644 index 000000000..467309c74 --- /dev/null +++ b/lib/transports/polling-xhr.ts @@ -0,0 +1,327 @@ +import { Polling } from "./polling.js"; +import { + CookieJar, + createCookieJar, + XHR as XMLHttpRequest, +} from "./xmlhttprequest.js"; +import { Emitter } from "@socket.io/component-emitter"; +import type { SocketOptions } from "../socket.js"; +import { installTimerFunctions, pick } from "../util.js"; +import { globalThisShim as globalThis } from "../globalThis.js"; +import type { RawData } from "engine.io-parser"; +import debugModule from "debug"; // debug() + +const debug = debugModule("engine.io-client:polling"); // debug() + +function empty() {} + +const hasXHR2 = (function () { + const xhr = new XMLHttpRequest({ + xdomain: false, + }); + return null != xhr.responseType; +})(); + +/** + * HTTP long-polling based on `XMLHttpRequest` + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest + */ +export class XHR extends Polling { + private readonly xd: boolean; + + private pollXhr: any; + private cookieJar?: CookieJar; + + /** + * XHR Polling constructor. + * + * @param {Object} opts + * @package + */ + constructor(opts) { + super(opts); + + 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(); + } + } + + /** + * 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 + */ + override 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 + */ + override doPoll() { + debug("xhr poll"); + 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; + } +} + +interface RequestReservedEvents { + success: () => void; + data: (data: RawData) => void; + error: (err: number | Error, context: unknown) => void; // context should be typed as XMLHttpRequest, but this type is not available on non-browser platforms +} + +export class Request extends Emitter<{}, {}, RequestReservedEvents> { + private readonly opts: { xd; cookieJar: CookieJar } & SocketOptions; + private readonly method: string; + private readonly uri: string; + private readonly data: string | ArrayBuffer; + + private xhr: any; + private setTimeoutFn: typeof setTimeout; + private index: number; + + static requestsCount = 0; + static requests = {}; + + /** + * 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 + */ + private create() { + 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 { + debug("xhr open %s: %s", this.method, this.uri); + 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) {} + + this.opts.cookieJar?.addCookies(xhr); + + // ie6 check + if ("withCredentials" in xhr) { + xhr.withCredentials = this.opts.withCredentials; + } + + if (this.opts.requestTimeout) { + xhr.timeout = this.opts.requestTimeout; + } + + xhr.onreadystatechange = () => { + if (xhr.readyState === 3) { + this.opts.cookieJar?.parseCookies( + xhr.getResponseHeader("set-cookie") + ); + } + + 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); + } + }; + + debug("xhr data %s", this.data); + 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 + */ + private onError(err: number | Error) { + this.emitReserved("error", err, this.xhr); + this.cleanup(true); + } + + /** + * Cleans up house. + * + * @private + */ + 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 + */ + private onLoad() { + const data = this.xhr.responseText; + if (data !== null) { + this.emitReserved("data", data); + this.emitReserved("success"); + this.cleanup(); + } + } + + /** + * Aborts the request. + * + * @package + */ + public abort() { + this.cleanup(); + } +} + +/** + * 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(); + } + } +} diff --git a/lib/transports/polling.ts b/lib/transports/polling.ts index 525499d2c..0750c733b 100644 --- a/lib/transports/polling.ts +++ b/lib/transports/polling.ts @@ -1,69 +1,12 @@ import { Transport } from "../transport.js"; -import debugModule from "debug"; // debug() import { yeast } from "../contrib/yeast.js"; -import { encode } from "../contrib/parseqs.js"; -import { encodePayload, decodePayload, RawData } from "engine.io-parser"; -import { - CookieJar, - createCookieJar, - XHR as XMLHttpRequest, -} from "./xmlhttprequest.js"; -import { Emitter } from "@socket.io/component-emitter"; -import { SocketOptions } from "../socket.js"; -import { installTimerFunctions, pick } from "../util.js"; -import { globalThisShim as globalThis } from "../globalThis.js"; +import { encodePayload, decodePayload } from "engine.io-parser"; +import debugModule from "debug"; // debug() const debug = debugModule("engine.io-client:polling"); // debug() -function empty() {} - -const hasXHR2 = (function () { - const xhr = new XMLHttpRequest({ - xdomain: false, - }); - return null != xhr.responseType; -})(); - -export class Polling extends Transport { - private readonly xd: boolean; - +export abstract class Polling extends Transport { private polling: boolean = false; - private pollXhr: any; - private cookieJar?: CookieJar; - - /** - * XHR Polling constructor. - * - * @param {Object} opts - * @package - */ - constructor(opts) { - super(opts); - - 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(); - } - } override get name() { return "polling"; @@ -215,7 +158,7 @@ export class Polling extends Transport { * * @private */ - private uri() { + protected uri() { const schema = this.opts.secure ? "https" : "http"; const query: { b64?: number; sid?: string } = this.query || {}; @@ -231,259 +174,6 @@ export class Polling extends Transport { 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 - */ - 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 - */ - private doPoll() { - debug("xhr poll"); - 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; - } -} - -interface RequestReservedEvents { - success: () => void; - data: (data: RawData) => void; - error: (err: number | Error, context: unknown) => void; // context should be typed as XMLHttpRequest, but this type is not available on non-browser platforms -} - -export class Request extends Emitter<{}, {}, RequestReservedEvents> { - private readonly opts: { xd; cookieJar: CookieJar } & SocketOptions; - private readonly method: string; - private readonly uri: string; - private readonly data: string | ArrayBuffer; - - private xhr: any; - private setTimeoutFn: typeof setTimeout; - private index: number; - - static requestsCount = 0; - static requests = {}; - - /** - * 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 - */ - private create() { - 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 { - debug("xhr open %s: %s", this.method, this.uri); - 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) {} - - this.opts.cookieJar?.addCookies(xhr); - - // ie6 check - if ("withCredentials" in xhr) { - xhr.withCredentials = this.opts.withCredentials; - } - - if (this.opts.requestTimeout) { - xhr.timeout = this.opts.requestTimeout; - } - - xhr.onreadystatechange = () => { - if (xhr.readyState === 3) { - this.opts.cookieJar?.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); - } - }; - - debug("xhr data %s", this.data); - 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 - */ - private onError(err: number | Error) { - this.emitReserved("error", err, this.xhr); - this.cleanup(true); - } - - /** - * Cleans up house. - * - * @private - */ - 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 - */ - private onLoad() { - const data = this.xhr.responseText; - if (data !== null) { - this.emitReserved("data", data); - this.emitReserved("success"); - this.cleanup(); - } - } - - /** - * Aborts the request. - * - * @package - */ - public abort() { - this.cleanup(); - } -} - -/** - * 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(); - } - } + abstract doPoll(); + abstract doWrite(data: string, callback: () => void); } diff --git a/lib/transports/xmlhttprequest.ts b/lib/transports/xmlhttprequest.ts index 5c2b3ce14..6d52c45a3 100644 --- a/lib/transports/xmlhttprequest.ts +++ b/lib/transports/xmlhttprequest.ts @@ -70,8 +70,7 @@ export function parse(setCookieString: string): Cookie { export class CookieJar { private cookies = new Map(); - public parseCookies(xhr: any) { - const values = xhr.getResponseHeader("set-cookie"); + public parseCookies(values: string[]) { if (!values) { return; } @@ -99,4 +98,14 @@ export class CookieJar { xhr.setRequestHeader("cookie", cookies.join("; ")); } } + + public appendCookies(headers: Headers) { + this.cookies.forEach((cookie, name) => { + if (cookie.expires?.getTime() < Date.now()) { + this.cookies.delete(name); + } else { + headers.append("cookie", `${name}=${cookie.value}`); + } + }); + } } diff --git a/package-lock.json b/package-lock.json index c15ff2e1f..2d75f74c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@rollup/plugin-commonjs": "^21.0.0", "@rollup/plugin-node-resolve": "^13.0.5", "@sinonjs/fake-timers": "^7.1.2", + "@types/debug": "^4.1.12", "@types/mocha": "^9.0.0", "@types/node": "^16.10.1", "@types/sinonjs__fake-timers": "^6.0.3", @@ -1535,6 +1536,15 @@ "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", "dev": true }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -1553,6 +1563,12 @@ "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", "dev": true }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, "node_modules/@types/node": { "version": "16.18.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.35.tgz", @@ -15508,6 +15524,15 @@ "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", "dev": true }, + "@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -15526,6 +15551,12 @@ "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", "dev": true }, + "@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, "@types/node": { "version": "16.18.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.35.tgz", diff --git a/package.json b/package.json index f800ea910..fd02316f3 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@rollup/plugin-commonjs": "^21.0.0", "@rollup/plugin-node-resolve": "^13.0.5", "@sinonjs/fake-timers": "^7.1.2", + "@types/debug": "^4.1.12", "@types/mocha": "^9.0.0", "@types/node": "^16.10.1", "@types/sinonjs__fake-timers": "^6.0.3", @@ -94,6 +95,7 @@ "compile": "rimraf ./build && tsc && tsc -p tsconfig.esm.json && ./postcompile.sh", "test": "npm run format:check && npm run compile && if test \"$BROWSERS\" = \"1\" ; then npm run test:browser; else npm run test:node; fi", "test:node": "mocha --bail --require test/support/hooks.js test/index.js test/webtransport.mjs", + "test:node-fetch": "USE_FETCH=1 npm run test:node", "test:browser": "zuul test/index.js", "build": "rollup -c support/rollup.config.umd.js && rollup -c support/rollup.config.esm.js", "format:check": "prettier --check 'lib/**/*.ts' 'test/**/*.js' 'test/webtransport.mjs' 'support/**/*.js'", diff --git a/test/connection.js b/test/connection.js index 49dd1257c..231393127 100644 --- a/test/connection.js +++ b/test/connection.js @@ -199,7 +199,9 @@ describe("connection", function () { if (env.browser && typeof addEventListener === "function") { it("should close the socket when receiving a beforeunload event", (done) => { - const socket = new Socket(); + const socket = new Socket({ + closeOnBeforeunload: true, + }); const createEvent = (name) => { if (typeof Event === "function") { diff --git a/test/socket.js b/test/socket.js index 94c11bbca..5bc60f14a 100644 --- a/test/socket.js +++ b/test/socket.js @@ -1,6 +1,12 @@ const expect = require("expect.js"); const { Socket } = require("../"); -const { isIE11, isAndroid, isEdge, isIPad } = require("./support/env"); +const { + isIE11, + isAndroid, + isEdge, + isIPad, + useFetch, +} = require("./support/env"); const FakeTimers = require("@sinonjs/fake-timers"); const { repeat } = require("./util"); @@ -92,11 +98,15 @@ describe("Socket", function () { socket.on("error", (err) => { expect(err).to.be.an(Error); expect(err.type).to.eql("TransportError"); - expect(err.message).to.eql("xhr post error"); expect(err.description).to.eql(413); - // err.context is a XMLHttpRequest object - expect(err.context.readyState).to.eql(4); - expect(err.context.responseText).to.eql(""); + if (useFetch) { + expect(err.message).to.eql("fetch write error"); + } else { + expect(err.message).to.eql("xhr post error"); + // err.context is a XMLHttpRequest object + expect(err.context.readyState).to.eql(4); + expect(err.context.responseText).to.eql(""); + } }); socket.on("close", (reason, details) => { @@ -137,13 +147,17 @@ describe("Socket", function () { socket.on("error", (err) => { expect(err).to.be.an(Error); expect(err.type).to.eql("TransportError"); - expect(err.message).to.eql("xhr poll error"); expect(err.description).to.eql(400); - // err.context is a XMLHttpRequest object - expect(err.context.readyState).to.eql(4); - expect(err.context.responseText).to.eql( - '{"code":1,"message":"Session ID unknown"}' - ); + if (useFetch) { + expect(err.message).to.eql("fetch read error"); + } else { + expect(err.message).to.eql("xhr poll error"); + // err.context is a XMLHttpRequest object + expect(err.context.readyState).to.eql(4); + expect(err.context.responseText).to.eql( + '{"code":1,"message":"Session ID unknown"}' + ); + } }); socket.on("close", (reason, details) => { diff --git a/test/support/env.js b/test/support/env.js index 2f05f07fe..c9ee4ea56 100644 --- a/test/support/env.js +++ b/test/support/env.js @@ -27,3 +27,11 @@ if (typeof location === "undefined") { port: 3000, }; } + +exports.useFetch = !exports.browser && process.env.USE_FETCH !== undefined; + +if (exports.useFetch) { + console.warn("testing with fetch() instead of XMLHttpRequest"); + const { transports, Fetch } = require("../.."); + transports.polling = Fetch; +} diff --git a/test/transport.js b/test/transport.js index 178655690..614c39537 100644 --- a/test/transport.js +++ b/test/transport.js @@ -222,7 +222,11 @@ describe("Transport", () => { }); polling.doOpen(); }); - it("should accept an `agent` option for XMLHttpRequest", (done) => { + it("should accept an `agent` option for XMLHttpRequest", function (done) { + if (env.useFetch) { + return this.skip(); + } + const polling = new eio.transports.polling({ path: "/engine.io", hostname: "localhost",