Skip to content

Commit bed70a4

Browse files
feat: implement WebTransport-related encoding/decoding
1 parent 0fc3694 commit bed70a4

File tree

8 files changed

+351
-43
lines changed

8 files changed

+351
-43
lines changed

lib/decodePacket.browser.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { decode } from "./contrib/base64-arraybuffer.js";
99

1010
const withNativeArrayBuffer = typeof ArrayBuffer === "function";
1111

12-
const decodePacket = (
12+
export const decodePacket = (
1313
encodedPacket: RawData,
1414
binaryType?: BinaryType
1515
): Packet => {
@@ -52,11 +52,21 @@ const decodeBase64Packet = (data, binaryType) => {
5252
const mapBinary = (data, binaryType) => {
5353
switch (binaryType) {
5454
case "blob":
55-
return data instanceof ArrayBuffer ? new Blob([data]) : data;
55+
if (data instanceof Blob) {
56+
// from WebSocket + binaryType "blob"
57+
return data;
58+
} else {
59+
// from HTTP long-polling or WebTransport
60+
return new Blob([data]);
61+
}
5662
case "arraybuffer":
5763
default:
58-
return data; // assuming the data is already an ArrayBuffer
64+
if (data instanceof ArrayBuffer) {
65+
// from HTTP long-polling (base64) or WebSocket + binaryType "arraybuffer"
66+
return data;
67+
} else {
68+
// from WebTransport (Uint8Array)
69+
return data.buffer;
70+
}
5971
}
6072
};
61-
62-
export default decodePacket;

lib/decodePacket.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
RawData
77
} from "./commons.js";
88

9-
const decodePacket = (
9+
export const decodePacket = (
1010
encodedPacket: RawData,
1111
binaryType?: BinaryType
1212
): Packet => {
@@ -38,23 +38,29 @@ const decodePacket = (
3838
};
3939

4040
const mapBinary = (data: RawData, binaryType?: BinaryType) => {
41-
const isBuffer = Buffer.isBuffer(data);
4241
switch (binaryType) {
4342
case "arraybuffer":
44-
return isBuffer ? toArrayBuffer(data) : data;
43+
if (data instanceof ArrayBuffer) {
44+
// from WebSocket & binaryType "arraybuffer"
45+
return data;
46+
} else if (Buffer.isBuffer(data)) {
47+
// from HTTP long-polling
48+
return data.buffer.slice(
49+
data.byteOffset,
50+
data.byteOffset + data.byteLength
51+
);
52+
} else {
53+
// from WebTransport (Uint8Array)
54+
return data.buffer;
55+
}
4556
case "nodebuffer":
4657
default:
47-
return data; // assuming the data is already a Buffer
48-
}
49-
};
50-
51-
const toArrayBuffer = (buffer: Buffer): ArrayBuffer => {
52-
const arrayBuffer = new ArrayBuffer(buffer.length);
53-
const view = new Uint8Array(arrayBuffer);
54-
for (let i = 0; i < buffer.length; i++) {
55-
view[i] = buffer[i];
58+
if (Buffer.isBuffer(data)) {
59+
// from HTTP long-polling or WebSocket & binaryType "nodebuffer" (default)
60+
return data;
61+
} else {
62+
// from WebTransport (Uint8Array)
63+
return Buffer.from(data);
64+
}
5665
}
57-
return arrayBuffer;
5866
};
59-
60-
export default decodePacket;

lib/encodePacket.browser.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,39 @@ const encodeBlobAsBase64 = (
5050
return fileReader.readAsDataURL(data);
5151
};
5252

53-
export default encodePacket;
53+
function toArray(data: BufferSource) {
54+
if (data instanceof Uint8Array) {
55+
return data;
56+
} else if (data instanceof ArrayBuffer) {
57+
return new Uint8Array(data);
58+
} else {
59+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
60+
}
61+
}
62+
63+
let TEXT_ENCODER;
64+
65+
export function encodePacketToBinary(
66+
packet: Packet,
67+
callback: (encodedPacket: RawData) => void
68+
) {
69+
if (withNativeBlob && packet.data instanceof Blob) {
70+
return packet.data
71+
.arrayBuffer()
72+
.then(toArray)
73+
.then(callback);
74+
} else if (
75+
withNativeArrayBuffer &&
76+
(packet.data instanceof ArrayBuffer || isView(packet.data))
77+
) {
78+
return callback(toArray(packet.data));
79+
}
80+
encodePacket(packet, false, encoded => {
81+
if (!TEXT_ENCODER) {
82+
TEXT_ENCODER = new TextEncoder();
83+
}
84+
callback(TEXT_ENCODER.encode(encoded));
85+
});
86+
}
87+
88+
export { encodePacket };

lib/encodePacket.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import { PACKET_TYPES, Packet, RawData } from "./commons.js";
22

3-
const encodePacket = (
3+
export const encodePacket = (
44
{ type, data }: Packet,
55
supportsBinary: boolean,
66
callback: (encodedPacket: RawData) => void
77
) => {
88
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
9-
const buffer = toBuffer(data);
10-
return callback(encodeBuffer(buffer, supportsBinary));
9+
return callback(
10+
supportsBinary ? data : "b" + toBuffer(data, true).toString("base64")
11+
);
1112
}
1213
// plain string
1314
return callback(PACKET_TYPES[type] + (data || ""));
1415
};
1516

16-
const toBuffer = data => {
17-
if (Buffer.isBuffer(data)) {
17+
const toBuffer = (data: BufferSource, forceBufferConversion: boolean) => {
18+
if (
19+
Buffer.isBuffer(data) ||
20+
(data instanceof Uint8Array && !forceBufferConversion)
21+
) {
1822
return data;
1923
} else if (data instanceof ArrayBuffer) {
2024
return Buffer.from(data);
@@ -23,9 +27,20 @@ const toBuffer = data => {
2327
}
2428
};
2529

26-
// only 'message' packets can contain binary, so the type prefix is not needed
27-
const encodeBuffer = (data: Buffer, supportsBinary: boolean): RawData => {
28-
return supportsBinary ? data : "b" + data.toString("base64");
29-
};
30+
let TEXT_ENCODER;
3031

31-
export default encodePacket;
32+
export function encodePacketToBinary(
33+
packet: Packet,
34+
callback: (encodedPacket: RawData) => void
35+
) {
36+
if (packet.data instanceof ArrayBuffer || ArrayBuffer.isView(packet.data)) {
37+
return callback(toBuffer(packet.data, false));
38+
}
39+
encodePacket(packet, true, encoded => {
40+
if (!TEXT_ENCODER) {
41+
// lazily created for compatibility with Node.js 10
42+
TEXT_ENCODER = new TextEncoder();
43+
}
44+
callback(TEXT_ENCODER.encode(encoded));
45+
});
46+
}

lib/index.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import encodePacket from "./encodePacket.js";
2-
import decodePacket from "./decodePacket.js";
1+
import { encodePacket, encodePacketToBinary } from "./encodePacket.js";
2+
import { decodePacket } from "./decodePacket.js";
33
import { Packet, PacketType, RawData, BinaryType } from "./commons.js";
44

55
const SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
@@ -40,9 +40,30 @@ const decodePayload = (
4040
return packets;
4141
};
4242

43+
let TEXT_DECODER;
44+
45+
export function decodePacketFromBinary(
46+
data: Uint8Array,
47+
isBinary: boolean,
48+
binaryType: BinaryType
49+
) {
50+
if (!TEXT_DECODER) {
51+
// lazily created for compatibility with old browser platforms
52+
TEXT_DECODER = new TextDecoder();
53+
}
54+
// 48 === "0".charCodeAt(0) (OPEN packet type)
55+
// 54 === "6".charCodeAt(0) (NOOP packet type)
56+
const isPlainBinary = isBinary || data[0] < 48 || data[0] > 54;
57+
return decodePacket(
58+
isPlainBinary ? data : TEXT_DECODER.decode(data),
59+
binaryType
60+
);
61+
}
62+
4363
export const protocol = 4;
4464
export {
4565
encodePacket,
66+
encodePacketToBinary,
4667
encodePayload,
4768
decodePacket,
4869
decodePayload,

test/browser.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {
22
decodePacket,
3+
decodePacketFromBinary,
34
decodePayload,
45
encodePacket,
6+
encodePacketToBinary,
57
encodePayload,
68
Packet
79
} from "..";
@@ -111,4 +113,113 @@ describe("engine.io-parser (browser only)", () => {
111113
});
112114
}
113115
});
116+
117+
describe("single packet (to/from Uint8Array)", function() {
118+
if (!withNativeArrayBuffer) {
119+
// @ts-ignore
120+
return this.skip();
121+
}
122+
123+
it("should encode a plaintext packet", done => {
124+
const packet: Packet = {
125+
type: "message",
126+
data: "1€"
127+
};
128+
encodePacketToBinary(packet, encodedPacket => {
129+
expect(encodedPacket).to.be.an(Uint8Array);
130+
expect(encodedPacket).to.eql(Uint8Array.from([52, 49, 226, 130, 172]));
131+
132+
const decoded = decodePacketFromBinary(
133+
encodedPacket,
134+
false,
135+
"arraybuffer"
136+
);
137+
expect(decoded).to.eql(packet);
138+
done();
139+
});
140+
});
141+
142+
it("should encode a binary packet (Uint8Array)", done => {
143+
const packet: Packet = {
144+
type: "message",
145+
data: Uint8Array.from([1, 2, 3])
146+
};
147+
encodePacketToBinary(packet, encodedPacket => {
148+
expect(encodedPacket === packet.data).to.be(true);
149+
done();
150+
});
151+
});
152+
153+
it("should encode a binary packet (Blob)", done => {
154+
const packet: Packet = {
155+
type: "message",
156+
data: new Blob([Uint8Array.from([1, 2, 3])])
157+
};
158+
encodePacketToBinary(packet, encodedPacket => {
159+
expect(encodedPacket).to.be.an(Uint8Array);
160+
expect(encodedPacket).to.eql(Uint8Array.from([1, 2, 3]));
161+
done();
162+
});
163+
});
164+
165+
it("should encode a binary packet (ArrayBuffer)", done => {
166+
const packet: Packet = {
167+
type: "message",
168+
data: Uint8Array.from([1, 2, 3]).buffer
169+
};
170+
encodePacketToBinary(packet, encodedPacket => {
171+
expect(encodedPacket).to.be.an(Uint8Array);
172+
expect(encodedPacket).to.eql(Uint8Array.from([1, 2, 3]));
173+
done();
174+
});
175+
});
176+
177+
it("should encode a binary packet (Uint16Array)", done => {
178+
const packet: Packet = {
179+
type: "message",
180+
data: Uint16Array.from([1, 2, 257])
181+
};
182+
encodePacketToBinary(packet, encodedPacket => {
183+
expect(encodedPacket).to.be.an(Uint8Array);
184+
expect(encodedPacket).to.eql(Uint8Array.from([1, 0, 2, 0, 1, 1]));
185+
done();
186+
});
187+
});
188+
189+
it("should decode a binary packet (Blob)", () => {
190+
const decoded = decodePacketFromBinary(
191+
Uint8Array.from([1, 2, 3]),
192+
false,
193+
"blob"
194+
);
195+
196+
expect(decoded.type).to.eql("message");
197+
expect(decoded.data).to.be.a(Blob);
198+
});
199+
200+
it("should decode a binary packet (ArrayBuffer)", () => {
201+
const decoded = decodePacketFromBinary(
202+
Uint8Array.from([1, 2, 3]),
203+
false,
204+
"arraybuffer"
205+
);
206+
207+
expect(decoded.type).to.eql("message");
208+
expect(decoded.data).to.be.an(ArrayBuffer);
209+
expect(areArraysEqual(decoded.data, Uint8Array.from([1, 2, 3])));
210+
});
211+
212+
it("should decode a binary packet (with binary header)", () => {
213+
// 52 === "4".charCodeAt(0)
214+
const decoded = decodePacketFromBinary(
215+
Uint8Array.from([52]),
216+
true,
217+
"arraybuffer"
218+
);
219+
220+
expect(decoded.type).to.eql("message");
221+
expect(decoded.data).to.be.an(ArrayBuffer);
222+
expect(areArraysEqual(decoded.data, Uint8Array.from([52])));
223+
});
224+
});
114225
});

test/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
Packet
77
} from "..";
88
import * as expect from "expect.js";
9-
import "./node";
9+
import "./node"; // replaced by "./browser" for the tests in the browser (see "browser" field in the package.json file)
1010

1111
describe("engine.io-parser", () => {
1212
describe("single packet", () => {

0 commit comments

Comments
 (0)