mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-09 23:18:02 -05:00
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
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
72
lib/transports/polling-fetch.ts
Normal file
72
lib/transports/polling-fetch.ts
Normal file
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
327
lib/transports/polling-xhr.ts
Normal file
327
lib/transports/polling-xhr.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -70,8 +70,7 @@ export function parse(setCookieString: string): Cookie {
|
||||
export class CookieJar {
|
||||
private cookies = new Map<string, Cookie>();
|
||||
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
31
package-lock.json
generated
31
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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'",
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user