Skip to content

Commit 7195c0f

Browse files
feat: add support for WebTransport
Reference: https://developer.mozilla.org/en-US/docs/Web/API/WebTransport Related: - #703 - socketio/socket.io#3769
1 parent db3de84 commit 7195c0f

File tree

10 files changed

+2644
-443
lines changed

10 files changed

+2644
-443
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212

1313
strategy:
1414
matrix:
15-
node-version: [14, 16]
15+
node-version: [16]
1616

1717
steps:
1818
- name: Checkout repository

lib/transport.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Emitter } from "@socket.io/component-emitter";
44
import { installTimerFunctions } from "./util.js";
55
import debugModule from "debug"; // debug()
66
import { SocketOptions } from "./socket.js";
7+
import { encode } from "./contrib/parseqs.js";
78

89
const debug = debugModule("engine.io-client:transport"); // debug()
910

@@ -171,6 +172,39 @@ export abstract class Transport extends Emitter<
171172
*/
172173
public pause(onPause: () => void) {}
173174

175+
protected uri(schema: string, query: Record<string, unknown> = {}) {
176+
return (
177+
schema +
178+
"://" +
179+
this._hostname() +
180+
this._port() +
181+
this.opts.path +
182+
this._query(query)
183+
);
184+
}
185+
186+
private _hostname() {
187+
const hostname = this.opts.hostname;
188+
return hostname.indexOf(":") === -1 ? hostname : "[" + hostname + "]";
189+
}
190+
191+
private _port() {
192+
if (
193+
this.opts.port &&
194+
((this.opts.secure && this.opts.port !== "443") ||
195+
(!this.opts.secure && this.opts.port !== "80"))
196+
) {
197+
return ":" + this.opts.port;
198+
} else {
199+
return "";
200+
}
201+
}
202+
203+
private _query(query: Record<string, unknown>) {
204+
const encodedQuery = encode(query);
205+
return encodedQuery.length ? "?" + encodedQuery : "";
206+
}
207+
174208
protected abstract doOpen();
175209
protected abstract doClose();
176210
protected abstract write(packets: Packet[]);

lib/transports/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Polling } from "./polling.js";
22
import { WS } from "./websocket.js";
3+
import { WT } from "./webtransport.js";
34

45
export const transports = {
56
websocket: WS,
7+
webtransport: WT,
68
polling: Polling,
79
};

lib/transports/webtransport.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Transport } from "../transport.js";
2+
import { nextTick } from "./websocket-constructor.js";
3+
import {
4+
encodePacketToBinary,
5+
decodePacketFromBinary,
6+
Packet,
7+
} from "engine.io-parser";
8+
import debugModule from "debug"; // debug()
9+
10+
const debug = debugModule("engine.io-client:webtransport"); // debug()
11+
12+
function shouldIncludeBinaryHeader(packet, encoded) {
13+
// 48 === "0".charCodeAt(0) (OPEN packet type)
14+
// 54 === "6".charCodeAt(0) (NOOP packet type)
15+
return (
16+
packet.type === "message" &&
17+
typeof packet.data !== "string" &&
18+
encoded[0] >= 48 &&
19+
encoded[0] <= 54
20+
);
21+
}
22+
23+
export class WT extends Transport {
24+
private transport: any;
25+
private writer: any;
26+
27+
get name() {
28+
return "webtransport";
29+
}
30+
31+
protected doOpen() {
32+
// @ts-ignore
33+
if (typeof WebTransport !== "function") {
34+
return;
35+
}
36+
// @ts-ignore
37+
this.transport = new WebTransport(
38+
this.uri("https"),
39+
this.opts.transportOptions[this.name]
40+
);
41+
42+
this.transport.closed.then(() => this.onClose());
43+
44+
// note: we could have used async/await, but that would require some additional polyfills
45+
this.transport.ready.then(() => {
46+
this.transport.createBidirectionalStream().then((stream) => {
47+
const reader = stream.readable.getReader();
48+
this.writer = stream.writable.getWriter();
49+
50+
let binaryFlag;
51+
52+
const read = () => {
53+
reader.read().then(({ done, value }) => {
54+
if (done) {
55+
debug("session is closed");
56+
return;
57+
}
58+
debug("received chunk: %o", value);
59+
if (!binaryFlag && value.byteLength === 1 && value[0] === 54) {
60+
binaryFlag = true;
61+
} else {
62+
// TODO expose binarytype
63+
this.onPacket(
64+
decodePacketFromBinary(value, binaryFlag, "arraybuffer")
65+
);
66+
binaryFlag = false;
67+
}
68+
read();
69+
});
70+
};
71+
72+
read();
73+
74+
const handshake = this.query.sid ? `0{"sid":"${this.query.sid}"}` : "0";
75+
this.writer
76+
.write(new TextEncoder().encode(handshake))
77+
.then(() => this.onOpen());
78+
});
79+
});
80+
}
81+
82+
protected write(packets: Packet[]) {
83+
this.writable = false;
84+
85+
for (let i = 0; i < packets.length; i++) {
86+
const packet = packets[i];
87+
const lastPacket = i === packets.length - 1;
88+
89+
encodePacketToBinary(packet, (data) => {
90+
if (shouldIncludeBinaryHeader(packet, data)) {
91+
debug("writing binary header");
92+
this.writer.write(Uint8Array.of(54));
93+
}
94+
debug("writing chunk: %o", data);
95+
this.writer.write(data).then(() => {
96+
if (lastPacket) {
97+
nextTick(() => {
98+
this.writable = true;
99+
this.emitReserved("drain");
100+
}, this.setTimeoutFn);
101+
}
102+
});
103+
});
104+
}
105+
}
106+
107+
protected doClose() {
108+
this.transport?.close();
109+
}
110+
}

0 commit comments

Comments
 (0)