Skip to content

Commit b11763b

Browse files
committedApr 23, 2024··
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: - socketio/socket.io#4980 - #716
1 parent 218c344 commit b11763b

13 files changed

+499
-333
lines changed
 

‎.github/workflows/ci.yml

+4
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@ jobs:
3030

3131
- name: Run tests
3232
run: npm test
33+
34+
- name: Run tests with fetch()
35+
run: npm run test:node-fetch
36+
if: ${{ matrix.node-version == '20' }} # fetch() was added in Node.js v18.0.0 (without experimental flag)

‎lib/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ export { transports } from "./transports/index.js";
88
export { installTimerFunctions } from "./util.js";
99
export { parse } from "./contrib/parseuri.js";
1010
export { nextTick } from "./transports/websocket-constructor.js";
11+
12+
export { Fetch } from "./transports/polling-fetch.js";
13+
export { XHR } from "./transports/polling-xhr.js";

‎lib/transports/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Polling } from "./polling.js";
1+
import { XHR } from "./polling-xhr.js";
22
import { WS } from "./websocket.js";
33
import { WT } from "./webtransport.js";
44

55
export const transports = {
66
websocket: WS,
77
webtransport: WT,
8-
polling: Polling,
8+
polling: XHR,
99
};

‎lib/transports/polling-fetch.ts

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Polling } from "./polling.js";
2+
import { CookieJar, createCookieJar } from "./xmlhttprequest.js";
3+
4+
/**
5+
* HTTP long-polling based on `fetch()`
6+
*
7+
* @see https://developer.mozilla.org/en-US/docs/Web/API/fetch
8+
*/
9+
export class Fetch extends Polling {
10+
private readonly cookieJar?: CookieJar;
11+
12+
constructor(opts) {
13+
super(opts);
14+
15+
if (this.opts.withCredentials) {
16+
this.cookieJar = createCookieJar();
17+
}
18+
}
19+
20+
override doPoll() {
21+
this._fetch()
22+
.then((res) => {
23+
if (!res.ok) {
24+
return this.onError("fetch read error", res.status, res);
25+
}
26+
27+
res.text().then((data) => this.onData(data));
28+
})
29+
.catch((err) => {
30+
this.onError("fetch read error", err);
31+
});
32+
}
33+
34+
override doWrite(data: string, callback: () => void) {
35+
this._fetch(data)
36+
.then((res) => {
37+
if (!res.ok) {
38+
return this.onError("fetch write error", res.status, res);
39+
}
40+
41+
callback();
42+
})
43+
.catch((err) => {
44+
this.onError("fetch write error", err);
45+
});
46+
}
47+
48+
private _fetch(data?: string) {
49+
const isPost = data !== undefined;
50+
const headers = new Headers(this.opts.extraHeaders);
51+
52+
if (isPost) {
53+
headers.set("content-type", "text/plain;charset=UTF-8");
54+
}
55+
56+
this.cookieJar?.appendCookies(headers);
57+
58+
return fetch(this.uri(), {
59+
method: isPost ? "POST" : "GET",
60+
body: isPost ? data : null,
61+
headers,
62+
credentials: this.opts.withCredentials ? "include" : "omit",
63+
}).then((res) => {
64+
if (this.cookieJar) {
65+
// @ts-ignore getSetCookie() was added in Node.js v19.7.0
66+
this.cookieJar.parseCookies(res.headers.getSetCookie());
67+
}
68+
69+
return res;
70+
});
71+
}
72+
}

‎lib/transports/polling-xhr.ts

+327
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import { Polling } from "./polling.js";
2+
import {
3+
CookieJar,
4+
createCookieJar,
5+
XHR as XMLHttpRequest,
6+
} from "./xmlhttprequest.js";
7+
import { Emitter } from "@socket.io/component-emitter";
8+
import type { SocketOptions } from "../socket.js";
9+
import { installTimerFunctions, pick } from "../util.js";
10+
import { globalThisShim as globalThis } from "../globalThis.js";
11+
import type { RawData } from "engine.io-parser";
12+
import debugModule from "debug"; // debug()
13+
14+
const debug = debugModule("engine.io-client:polling"); // debug()
15+
16+
function empty() {}
17+
18+
const hasXHR2 = (function () {
19+
const xhr = new XMLHttpRequest({
20+
xdomain: false,
21+
});
22+
return null != xhr.responseType;
23+
})();
24+
25+
/**
26+
* HTTP long-polling based on `XMLHttpRequest`
27+
*
28+
* @see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
29+
*/
30+
export class XHR extends Polling {
31+
private readonly xd: boolean;
32+
33+
private pollXhr: any;
34+
private cookieJar?: CookieJar;
35+
36+
/**
37+
* XHR Polling constructor.
38+
*
39+
* @param {Object} opts
40+
* @package
41+
*/
42+
constructor(opts) {
43+
super(opts);
44+
45+
if (typeof location !== "undefined") {
46+
const isSSL = "https:" === location.protocol;
47+
let port = location.port;
48+
49+
// some user agents have empty `location.port`
50+
if (!port) {
51+
port = isSSL ? "443" : "80";
52+
}
53+
54+
this.xd =
55+
(typeof location !== "undefined" &&
56+
opts.hostname !== location.hostname) ||
57+
port !== opts.port;
58+
}
59+
/**
60+
* XHR supports binary
61+
*/
62+
const forceBase64 = opts && opts.forceBase64;
63+
this.supportsBinary = hasXHR2 && !forceBase64;
64+
65+
if (this.opts.withCredentials) {
66+
this.cookieJar = createCookieJar();
67+
}
68+
}
69+
70+
/**
71+
* Creates a request.
72+
*
73+
* @param {String} method
74+
* @private
75+
*/
76+
request(opts = {}) {
77+
Object.assign(opts, { xd: this.xd, cookieJar: this.cookieJar }, this.opts);
78+
return new Request(this.uri(), opts);
79+
}
80+
81+
/**
82+
* Sends data.
83+
*
84+
* @param {String} data to send.
85+
* @param {Function} called upon flush.
86+
* @private
87+
*/
88+
override doWrite(data, fn) {
89+
const req = this.request({
90+
method: "POST",
91+
data: data,
92+
});
93+
req.on("success", fn);
94+
req.on("error", (xhrStatus, context) => {
95+
this.onError("xhr post error", xhrStatus, context);
96+
});
97+
}
98+
99+
/**
100+
* Starts a poll cycle.
101+
*
102+
* @private
103+
*/
104+
override doPoll() {
105+
debug("xhr poll");
106+
const req = this.request();
107+
req.on("data", this.onData.bind(this));
108+
req.on("error", (xhrStatus, context) => {
109+
this.onError("xhr poll error", xhrStatus, context);
110+
});
111+
this.pollXhr = req;
112+
}
113+
}
114+
115+
interface RequestReservedEvents {
116+
success: () => void;
117+
data: (data: RawData) => void;
118+
error: (err: number | Error, context: unknown) => void; // context should be typed as XMLHttpRequest, but this type is not available on non-browser platforms
119+
}
120+
121+
export class Request extends Emitter<{}, {}, RequestReservedEvents> {
122+
private readonly opts: { xd; cookieJar: CookieJar } & SocketOptions;
123+
private readonly method: string;
124+
private readonly uri: string;
125+
private readonly data: string | ArrayBuffer;
126+
127+
private xhr: any;
128+
private setTimeoutFn: typeof setTimeout;
129+
private index: number;
130+
131+
static requestsCount = 0;
132+
static requests = {};
133+
134+
/**
135+
* Request constructor
136+
*
137+
* @param {Object} options
138+
* @package
139+
*/
140+
constructor(uri, opts) {
141+
super();
142+
installTimerFunctions(this, opts);
143+
this.opts = opts;
144+
145+
this.method = opts.method || "GET";
146+
this.uri = uri;
147+
this.data = undefined !== opts.data ? opts.data : null;
148+
149+
this.create();
150+
}
151+
152+
/**
153+
* Creates the XHR object and sends the request.
154+
*
155+
* @private
156+
*/
157+
private create() {
158+
const opts = pick(
159+
this.opts,
160+
"agent",
161+
"pfx",
162+
"key",
163+
"passphrase",
164+
"cert",
165+
"ca",
166+
"ciphers",
167+
"rejectUnauthorized",
168+
"autoUnref"
169+
);
170+
opts.xdomain = !!this.opts.xd;
171+
172+
const xhr = (this.xhr = new XMLHttpRequest(opts));
173+
174+
try {
175+
debug("xhr open %s: %s", this.method, this.uri);
176+
xhr.open(this.method, this.uri, true);
177+
try {
178+
if (this.opts.extraHeaders) {
179+
xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
180+
for (let i in this.opts.extraHeaders) {
181+
if (this.opts.extraHeaders.hasOwnProperty(i)) {
182+
xhr.setRequestHeader(i, this.opts.extraHeaders[i]);
183+
}
184+
}
185+
}
186+
} catch (e) {}
187+
188+
if ("POST" === this.method) {
189+
try {
190+
xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
191+
} catch (e) {}
192+
}
193+
194+
try {
195+
xhr.setRequestHeader("Accept", "*/*");
196+
} catch (e) {}
197+
198+
this.opts.cookieJar?.addCookies(xhr);
199+
200+
// ie6 check
201+
if ("withCredentials" in xhr) {
202+
xhr.withCredentials = this.opts.withCredentials;
203+
}
204+
205+
if (this.opts.requestTimeout) {
206+
xhr.timeout = this.opts.requestTimeout;
207+
}
208+
209+
xhr.onreadystatechange = () => {
210+
if (xhr.readyState === 3) {
211+
this.opts.cookieJar?.parseCookies(
212+
xhr.getResponseHeader("set-cookie")
213+
);
214+
}
215+
216+
if (4 !== xhr.readyState) return;
217+
if (200 === xhr.status || 1223 === xhr.status) {
218+
this.onLoad();
219+
} else {
220+
// make sure the `error` event handler that's user-set
221+
// does not throw in the same tick and gets caught here
222+
this.setTimeoutFn(() => {
223+
this.onError(typeof xhr.status === "number" ? xhr.status : 0);
224+
}, 0);
225+
}
226+
};
227+
228+
debug("xhr data %s", this.data);
229+
xhr.send(this.data);
230+
} catch (e) {
231+
// Need to defer since .create() is called directly from the constructor
232+
// and thus the 'error' event can only be only bound *after* this exception
233+
// occurs. Therefore, also, we cannot throw here at all.
234+
this.setTimeoutFn(() => {
235+
this.onError(e);
236+
}, 0);
237+
return;
238+
}
239+
240+
if (typeof document !== "undefined") {
241+
this.index = Request.requestsCount++;
242+
Request.requests[this.index] = this;
243+
}
244+
}
245+
246+
/**
247+
* Called upon error.
248+
*
249+
* @private
250+
*/
251+
private onError(err: number | Error) {
252+
this.emitReserved("error", err, this.xhr);
253+
this.cleanup(true);
254+
}
255+
256+
/**
257+
* Cleans up house.
258+
*
259+
* @private
260+
*/
261+
private cleanup(fromError?) {
262+
if ("undefined" === typeof this.xhr || null === this.xhr) {
263+
return;
264+
}
265+
this.xhr.onreadystatechange = empty;
266+
267+
if (fromError) {
268+
try {
269+
this.xhr.abort();
270+
} catch (e) {}
271+
}
272+
273+
if (typeof document !== "undefined") {
274+
delete Request.requests[this.index];
275+
}
276+
277+
this.xhr = null;
278+
}
279+
280+
/**
281+
* Called upon load.
282+
*
283+
* @private
284+
*/
285+
private onLoad() {
286+
const data = this.xhr.responseText;
287+
if (data !== null) {
288+
this.emitReserved("data", data);
289+
this.emitReserved("success");
290+
this.cleanup();
291+
}
292+
}
293+
294+
/**
295+
* Aborts the request.
296+
*
297+
* @package
298+
*/
299+
public abort() {
300+
this.cleanup();
301+
}
302+
}
303+
304+
/**
305+
* Aborts pending requests when unloading the window. This is needed to prevent
306+
* memory leaks (e.g. when using IE) and to ensure that no spurious error is
307+
* emitted.
308+
*/
309+
310+
if (typeof document !== "undefined") {
311+
// @ts-ignore
312+
if (typeof attachEvent === "function") {
313+
// @ts-ignore
314+
attachEvent("onunload", unloadHandler);
315+
} else if (typeof addEventListener === "function") {
316+
const terminationEvent = "onpagehide" in globalThis ? "pagehide" : "unload";
317+
addEventListener(terminationEvent, unloadHandler, false);
318+
}
319+
}
320+
321+
function unloadHandler() {
322+
for (let i in Request.requests) {
323+
if (Request.requests.hasOwnProperty(i)) {
324+
Request.requests[i].abort();
325+
}
326+
}
327+
}

‎lib/transports/polling.ts

+6-316
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,12 @@
11
import { Transport } from "../transport.js";
2-
import debugModule from "debug"; // debug()
32
import { yeast } from "../contrib/yeast.js";
4-
import { encode } from "../contrib/parseqs.js";
5-
import { encodePayload, decodePayload, RawData } from "engine.io-parser";
6-
import {
7-
CookieJar,
8-
createCookieJar,
9-
XHR as XMLHttpRequest,
10-
} from "./xmlhttprequest.js";
11-
import { Emitter } from "@socket.io/component-emitter";
12-
import { SocketOptions } from "../socket.js";
13-
import { installTimerFunctions, pick } from "../util.js";
14-
import { globalThisShim as globalThis } from "../globalThis.js";
3+
import { encodePayload, decodePayload } from "engine.io-parser";
4+
import debugModule from "debug"; // debug()
155

166
const debug = debugModule("engine.io-client:polling"); // debug()
177

18-
function empty() {}
19-
20-
const hasXHR2 = (function () {
21-
const xhr = new XMLHttpRequest({
22-
xdomain: false,
23-
});
24-
return null != xhr.responseType;
25-
})();
26-
27-
export class Polling extends Transport {
28-
private readonly xd: boolean;
29-
8+
export abstract class Polling extends Transport {
309
private polling: boolean = false;
31-
private pollXhr: any;
32-
private cookieJar?: CookieJar;
33-
34-
/**
35-
* XHR Polling constructor.
36-
*
37-
* @param {Object} opts
38-
* @package
39-
*/
40-
constructor(opts) {
41-
super(opts);
42-
43-
if (typeof location !== "undefined") {
44-
const isSSL = "https:" === location.protocol;
45-
let port = location.port;
46-
47-
// some user agents have empty `location.port`
48-
if (!port) {
49-
port = isSSL ? "443" : "80";
50-
}
51-
52-
this.xd =
53-
(typeof location !== "undefined" &&
54-
opts.hostname !== location.hostname) ||
55-
port !== opts.port;
56-
}
57-
/**
58-
* XHR supports binary
59-
*/
60-
const forceBase64 = opts && opts.forceBase64;
61-
this.supportsBinary = hasXHR2 && !forceBase64;
62-
63-
if (this.opts.withCredentials) {
64-
this.cookieJar = createCookieJar();
65-
}
66-
}
6710

6811
override get name() {
6912
return "polling";
@@ -215,7 +158,7 @@ export class Polling extends Transport {
215158
*
216159
* @private
217160
*/
218-
private uri() {
161+
protected uri() {
219162
const schema = this.opts.secure ? "https" : "http";
220163
const query: { b64?: number; sid?: string } = this.query || {};
221164

@@ -231,259 +174,6 @@ export class Polling extends Transport {
231174
return this.createUri(schema, query);
232175
}
233176

234-
/**
235-
* Creates a request.
236-
*
237-
* @param {String} method
238-
* @private
239-
*/
240-
request(opts = {}) {
241-
Object.assign(opts, { xd: this.xd, cookieJar: this.cookieJar }, this.opts);
242-
return new Request(this.uri(), opts);
243-
}
244-
245-
/**
246-
* Sends data.
247-
*
248-
* @param {String} data to send.
249-
* @param {Function} called upon flush.
250-
* @private
251-
*/
252-
private doWrite(data, fn) {
253-
const req = this.request({
254-
method: "POST",
255-
data: data,
256-
});
257-
req.on("success", fn);
258-
req.on("error", (xhrStatus, context) => {
259-
this.onError("xhr post error", xhrStatus, context);
260-
});
261-
}
262-
263-
/**
264-
* Starts a poll cycle.
265-
*
266-
* @private
267-
*/
268-
private doPoll() {
269-
debug("xhr poll");
270-
const req = this.request();
271-
req.on("data", this.onData.bind(this));
272-
req.on("error", (xhrStatus, context) => {
273-
this.onError("xhr poll error", xhrStatus, context);
274-
});
275-
this.pollXhr = req;
276-
}
277-
}
278-
279-
interface RequestReservedEvents {
280-
success: () => void;
281-
data: (data: RawData) => void;
282-
error: (err: number | Error, context: unknown) => void; // context should be typed as XMLHttpRequest, but this type is not available on non-browser platforms
283-
}
284-
285-
export class Request extends Emitter<{}, {}, RequestReservedEvents> {
286-
private readonly opts: { xd; cookieJar: CookieJar } & SocketOptions;
287-
private readonly method: string;
288-
private readonly uri: string;
289-
private readonly data: string | ArrayBuffer;
290-
291-
private xhr: any;
292-
private setTimeoutFn: typeof setTimeout;
293-
private index: number;
294-
295-
static requestsCount = 0;
296-
static requests = {};
297-
298-
/**
299-
* Request constructor
300-
*
301-
* @param {Object} options
302-
* @package
303-
*/
304-
constructor(uri, opts) {
305-
super();
306-
installTimerFunctions(this, opts);
307-
this.opts = opts;
308-
309-
this.method = opts.method || "GET";
310-
this.uri = uri;
311-
this.data = undefined !== opts.data ? opts.data : null;
312-
313-
this.create();
314-
}
315-
316-
/**
317-
* Creates the XHR object and sends the request.
318-
*
319-
* @private
320-
*/
321-
private create() {
322-
const opts = pick(
323-
this.opts,
324-
"agent",
325-
"pfx",
326-
"key",
327-
"passphrase",
328-
"cert",
329-
"ca",
330-
"ciphers",
331-
"rejectUnauthorized",
332-
"autoUnref"
333-
);
334-
opts.xdomain = !!this.opts.xd;
335-
336-
const xhr = (this.xhr = new XMLHttpRequest(opts));
337-
338-
try {
339-
debug("xhr open %s: %s", this.method, this.uri);
340-
xhr.open(this.method, this.uri, true);
341-
try {
342-
if (this.opts.extraHeaders) {
343-
xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
344-
for (let i in this.opts.extraHeaders) {
345-
if (this.opts.extraHeaders.hasOwnProperty(i)) {
346-
xhr.setRequestHeader(i, this.opts.extraHeaders[i]);
347-
}
348-
}
349-
}
350-
} catch (e) {}
351-
352-
if ("POST" === this.method) {
353-
try {
354-
xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
355-
} catch (e) {}
356-
}
357-
358-
try {
359-
xhr.setRequestHeader("Accept", "*/*");
360-
} catch (e) {}
361-
362-
this.opts.cookieJar?.addCookies(xhr);
363-
364-
// ie6 check
365-
if ("withCredentials" in xhr) {
366-
xhr.withCredentials = this.opts.withCredentials;
367-
}
368-
369-
if (this.opts.requestTimeout) {
370-
xhr.timeout = this.opts.requestTimeout;
371-
}
372-
373-
xhr.onreadystatechange = () => {
374-
if (xhr.readyState === 3) {
375-
this.opts.cookieJar?.parseCookies(xhr);
376-
}
377-
378-
if (4 !== xhr.readyState) return;
379-
if (200 === xhr.status || 1223 === xhr.status) {
380-
this.onLoad();
381-
} else {
382-
// make sure the `error` event handler that's user-set
383-
// does not throw in the same tick and gets caught here
384-
this.setTimeoutFn(() => {
385-
this.onError(typeof xhr.status === "number" ? xhr.status : 0);
386-
}, 0);
387-
}
388-
};
389-
390-
debug("xhr data %s", this.data);
391-
xhr.send(this.data);
392-
} catch (e) {
393-
// Need to defer since .create() is called directly from the constructor
394-
// and thus the 'error' event can only be only bound *after* this exception
395-
// occurs. Therefore, also, we cannot throw here at all.
396-
this.setTimeoutFn(() => {
397-
this.onError(e);
398-
}, 0);
399-
return;
400-
}
401-
402-
if (typeof document !== "undefined") {
403-
this.index = Request.requestsCount++;
404-
Request.requests[this.index] = this;
405-
}
406-
}
407-
408-
/**
409-
* Called upon error.
410-
*
411-
* @private
412-
*/
413-
private onError(err: number | Error) {
414-
this.emitReserved("error", err, this.xhr);
415-
this.cleanup(true);
416-
}
417-
418-
/**
419-
* Cleans up house.
420-
*
421-
* @private
422-
*/
423-
private cleanup(fromError?) {
424-
if ("undefined" === typeof this.xhr || null === this.xhr) {
425-
return;
426-
}
427-
this.xhr.onreadystatechange = empty;
428-
429-
if (fromError) {
430-
try {
431-
this.xhr.abort();
432-
} catch (e) {}
433-
}
434-
435-
if (typeof document !== "undefined") {
436-
delete Request.requests[this.index];
437-
}
438-
439-
this.xhr = null;
440-
}
441-
442-
/**
443-
* Called upon load.
444-
*
445-
* @private
446-
*/
447-
private onLoad() {
448-
const data = this.xhr.responseText;
449-
if (data !== null) {
450-
this.emitReserved("data", data);
451-
this.emitReserved("success");
452-
this.cleanup();
453-
}
454-
}
455-
456-
/**
457-
* Aborts the request.
458-
*
459-
* @package
460-
*/
461-
public abort() {
462-
this.cleanup();
463-
}
464-
}
465-
466-
/**
467-
* Aborts pending requests when unloading the window. This is needed to prevent
468-
* memory leaks (e.g. when using IE) and to ensure that no spurious error is
469-
* emitted.
470-
*/
471-
472-
if (typeof document !== "undefined") {
473-
// @ts-ignore
474-
if (typeof attachEvent === "function") {
475-
// @ts-ignore
476-
attachEvent("onunload", unloadHandler);
477-
} else if (typeof addEventListener === "function") {
478-
const terminationEvent = "onpagehide" in globalThis ? "pagehide" : "unload";
479-
addEventListener(terminationEvent, unloadHandler, false);
480-
}
481-
}
482-
483-
function unloadHandler() {
484-
for (let i in Request.requests) {
485-
if (Request.requests.hasOwnProperty(i)) {
486-
Request.requests[i].abort();
487-
}
488-
}
177+
abstract doPoll();
178+
abstract doWrite(data: string, callback: () => void);
489179
}

‎lib/transports/xmlhttprequest.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ export function parse(setCookieString: string): Cookie {
7070
export class CookieJar {
7171
private cookies = new Map<string, Cookie>();
7272

73-
public parseCookies(xhr: any) {
74-
const values = xhr.getResponseHeader("set-cookie");
73+
public parseCookies(values: string[]) {
7574
if (!values) {
7675
return;
7776
}
@@ -99,4 +98,14 @@ export class CookieJar {
9998
xhr.setRequestHeader("cookie", cookies.join("; "));
10099
}
101100
}
101+
102+
public appendCookies(headers: Headers) {
103+
this.cookies.forEach((cookie, name) => {
104+
if (cookie.expires?.getTime() < Date.now()) {
105+
this.cookies.delete(name);
106+
} else {
107+
headers.append("cookie", `${name}=${cookie.value}`);
108+
}
109+
});
110+
}
102111
}

‎package-lock.json

+31
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"@rollup/plugin-commonjs": "^21.0.0",
6969
"@rollup/plugin-node-resolve": "^13.0.5",
7070
"@sinonjs/fake-timers": "^7.1.2",
71+
"@types/debug": "^4.1.12",
7172
"@types/mocha": "^9.0.0",
7273
"@types/node": "^16.10.1",
7374
"@types/sinonjs__fake-timers": "^6.0.3",
@@ -94,6 +95,7 @@
9495
"compile": "rimraf ./build && tsc && tsc -p tsconfig.esm.json && ./postcompile.sh",
9596
"test": "npm run format:check && npm run compile && if test \"$BROWSERS\" = \"1\" ; then npm run test:browser; else npm run test:node; fi",
9697
"test:node": "mocha --bail --require test/support/hooks.js test/index.js test/webtransport.mjs",
98+
"test:node-fetch": "USE_FETCH=1 npm run test:node",
9799
"test:browser": "zuul test/index.js",
98100
"build": "rollup -c support/rollup.config.umd.js && rollup -c support/rollup.config.esm.js",
99101
"format:check": "prettier --check 'lib/**/*.ts' 'test/**/*.js' 'test/webtransport.mjs' 'support/**/*.js'",

‎test/connection.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,9 @@ describe("connection", function () {
199199

200200
if (env.browser && typeof addEventListener === "function") {
201201
it("should close the socket when receiving a beforeunload event", (done) => {
202-
const socket = new Socket();
202+
const socket = new Socket({
203+
closeOnBeforeunload: true,
204+
});
203205

204206
const createEvent = (name) => {
205207
if (typeof Event === "function") {

‎test/socket.js

+25-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
const expect = require("expect.js");
22
const { Socket } = require("../");
3-
const { isIE11, isAndroid, isEdge, isIPad } = require("./support/env");
3+
const {
4+
isIE11,
5+
isAndroid,
6+
isEdge,
7+
isIPad,
8+
useFetch,
9+
} = require("./support/env");
410
const FakeTimers = require("@sinonjs/fake-timers");
511
const { repeat } = require("./util");
612

@@ -92,11 +98,15 @@ describe("Socket", function () {
9298
socket.on("error", (err) => {
9399
expect(err).to.be.an(Error);
94100
expect(err.type).to.eql("TransportError");
95-
expect(err.message).to.eql("xhr post error");
96101
expect(err.description).to.eql(413);
97-
// err.context is a XMLHttpRequest object
98-
expect(err.context.readyState).to.eql(4);
99-
expect(err.context.responseText).to.eql("");
102+
if (useFetch) {
103+
expect(err.message).to.eql("fetch write error");
104+
} else {
105+
expect(err.message).to.eql("xhr post error");
106+
// err.context is a XMLHttpRequest object
107+
expect(err.context.readyState).to.eql(4);
108+
expect(err.context.responseText).to.eql("");
109+
}
100110
});
101111

102112
socket.on("close", (reason, details) => {
@@ -137,13 +147,17 @@ describe("Socket", function () {
137147
socket.on("error", (err) => {
138148
expect(err).to.be.an(Error);
139149
expect(err.type).to.eql("TransportError");
140-
expect(err.message).to.eql("xhr poll error");
141150
expect(err.description).to.eql(400);
142-
// err.context is a XMLHttpRequest object
143-
expect(err.context.readyState).to.eql(4);
144-
expect(err.context.responseText).to.eql(
145-
'{"code":1,"message":"Session ID unknown"}'
146-
);
151+
if (useFetch) {
152+
expect(err.message).to.eql("fetch read error");
153+
} else {
154+
expect(err.message).to.eql("xhr poll error");
155+
// err.context is a XMLHttpRequest object
156+
expect(err.context.readyState).to.eql(4);
157+
expect(err.context.responseText).to.eql(
158+
'{"code":1,"message":"Session ID unknown"}'
159+
);
160+
}
147161
});
148162

149163
socket.on("close", (reason, details) => {

‎test/support/env.js

+8
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,11 @@ if (typeof location === "undefined") {
2727
port: 3000,
2828
};
2929
}
30+
31+
exports.useFetch = !exports.browser && process.env.USE_FETCH !== undefined;
32+
33+
if (exports.useFetch) {
34+
console.warn("testing with fetch() instead of XMLHttpRequest");
35+
const { transports, Fetch } = require("../..");
36+
transports.polling = Fetch;
37+
}

‎test/transport.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,11 @@ describe("Transport", () => {
222222
});
223223
polling.doOpen();
224224
});
225-
it("should accept an `agent` option for XMLHttpRequest", (done) => {
225+
it("should accept an `agent` option for XMLHttpRequest", function (done) {
226+
if (env.useFetch) {
227+
return this.skip();
228+
}
229+
226230
const polling = new eio.transports.polling({
227231
path: "/engine.io",
228232
hostname: "localhost",

0 commit comments

Comments
 (0)
Please sign in to comment.