Skip to content

Commit 6142324

Browse files
feat: prepend a header to each WebTransport chunk
WebTransport is a stream-based protocol, so chunking boundaries are not always preserved. That's why we will now prepend a 4-bytes header to each chunk: - first bit indicates whether the payload is plain text (0) or binary (1) - next 31 bits indicate the length of the payload See also: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#format
1 parent 2d0b755 commit 6142324

File tree

4 files changed

+365
-205
lines changed

4 files changed

+365
-205
lines changed

lib/index.ts

+95-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { encodePacket, encodePacketToBinary } from "./encodePacket.js";
22
import { decodePacket } from "./decodePacket.js";
3-
import { Packet, PacketType, RawData, BinaryType } from "./commons.js";
3+
import {
4+
Packet,
5+
PacketType,
6+
RawData,
7+
BinaryType,
8+
ERROR_PACKET
9+
} from "./commons.js";
410

511
const SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
612

@@ -40,30 +46,106 @@ const decodePayload = (
4046
return packets;
4147
};
4248

49+
const HEADER_LENGTH = 4;
50+
51+
export function createPacketEncoderStream() {
52+
return new TransformStream({
53+
transform(packet: Packet, controller) {
54+
encodePacketToBinary(packet, encodedPacket => {
55+
const header = new Uint8Array(HEADER_LENGTH);
56+
// last 31 bits indicate the length of the payload
57+
new DataView(header.buffer).setUint32(0, encodedPacket.length);
58+
// first bit indicates whether the payload is plain text (0) or binary (1)
59+
if (packet.data && typeof packet.data !== "string") {
60+
header[0] |= 0x80;
61+
}
62+
controller.enqueue(header);
63+
controller.enqueue(encodedPacket);
64+
});
65+
}
66+
});
67+
}
68+
4369
let TEXT_DECODER;
4470

45-
export function decodePacketFromBinary(
46-
data: Uint8Array,
47-
isBinary: boolean,
71+
function totalLength(chunks: Uint8Array[]) {
72+
return chunks.reduce((acc, chunk) => acc + chunk.length, 0);
73+
}
74+
75+
function concatChunks(chunks: Uint8Array[], size: number) {
76+
if (chunks[0].length === size) {
77+
return chunks.shift();
78+
}
79+
const buffer = new Uint8Array(size);
80+
let j = 0;
81+
for (let i = 0; i < size; i++) {
82+
buffer[i] = chunks[0][j++];
83+
if (j === chunks[0].length) {
84+
chunks.shift();
85+
j = 0;
86+
}
87+
}
88+
if (chunks.length && j < chunks[0].length) {
89+
chunks[0] = chunks[0].slice(j);
90+
}
91+
return buffer;
92+
}
93+
94+
export function createPacketDecoderStream(
95+
maxPayload: number,
4896
binaryType: BinaryType
4997
) {
5098
if (!TEXT_DECODER) {
51-
// lazily created for compatibility with old browser platforms
5299
TEXT_DECODER = new TextDecoder();
53100
}
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-
);
101+
const chunks: Uint8Array[] = [];
102+
let expectedSize = -1;
103+
let isBinary = false;
104+
105+
return new TransformStream({
106+
transform(chunk: Uint8Array, controller) {
107+
chunks.push(chunk);
108+
while (true) {
109+
const expectHeader = expectedSize === -1;
110+
if (expectHeader) {
111+
if (totalLength(chunks) < HEADER_LENGTH) {
112+
break;
113+
}
114+
const headerArray = concatChunks(chunks, HEADER_LENGTH);
115+
const header = new DataView(
116+
headerArray.buffer,
117+
headerArray.byteOffset,
118+
headerArray.length
119+
).getUint32(0);
120+
121+
isBinary = header >> 31 === -1;
122+
expectedSize = header & 0x7fffffff;
123+
124+
if (expectedSize === 0 || expectedSize > maxPayload) {
125+
controller.enqueue(ERROR_PACKET);
126+
break;
127+
}
128+
} else {
129+
if (totalLength(chunks) < expectedSize) {
130+
break;
131+
}
132+
const data = concatChunks(chunks, expectedSize);
133+
controller.enqueue(
134+
decodePacket(
135+
isBinary ? data : TEXT_DECODER.decode(data),
136+
binaryType
137+
)
138+
);
139+
expectedSize = -1;
140+
}
141+
}
142+
}
143+
});
61144
}
62145

63146
export const protocol = 4;
64147
export {
65148
encodePacket,
66-
encodePacketToBinary,
67149
encodePayload,
68150
decodePacket,
69151
decodePayload,

test/browser.ts

+27-98
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
22
decodePacket,
3-
decodePacketFromBinary,
43
decodePayload,
54
encodePacket,
6-
encodePacketToBinary,
75
encodePayload,
6+
createPacketEncoderStream,
7+
createPacketDecoderStream,
88
Packet
99
} from "..";
1010
import * as expect from "expect.js";
@@ -114,112 +114,41 @@ describe("engine.io-parser (browser only)", () => {
114114
}
115115
});
116116

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-
});
117+
if (typeof TextEncoder === "function") {
118+
describe("createPacketEncoderStream", () => {
119+
it("should encode a binary packet (Blob)", async () => {
120+
const stream = createPacketEncoderStream();
141121

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-
});
122+
const writer = stream.writable.getWriter();
123+
const reader = stream.readable.getReader();
152124

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-
});
125+
writer.write({
126+
type: "message",
127+
data: new Blob([Uint8Array.from([1, 2, 3])])
128+
});
164129

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-
});
130+
const header = await reader.read();
131+
expect(header.value).to.eql(Uint8Array.of(128, 0, 0, 3));
176132

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();
133+
const payload = await reader.read();
134+
expect(payload.value).to.eql(Uint8Array.of(1, 2, 3));
186135
});
187136
});
188137

189-
it("should decode a binary packet (Blob)", () => {
190-
const decoded = decodePacketFromBinary(
191-
Uint8Array.from([1, 2, 3]),
192-
false,
193-
"blob"
194-
);
138+
describe("createPacketDecoderStream", () => {
139+
it("should decode a binary packet (Blob)", async () => {
140+
const stream = createPacketDecoderStream(1e6, "blob");
195141

196-
expect(decoded.type).to.eql("message");
197-
expect(decoded.data).to.be.a(Blob);
198-
});
142+
const writer = stream.writable.getWriter();
143+
const reader = stream.readable.getReader();
199144

200-
it("should decode a binary packet (ArrayBuffer)", () => {
201-
const decoded = decodePacketFromBinary(
202-
Uint8Array.from([1, 2, 3]),
203-
false,
204-
"arraybuffer"
205-
);
145+
writer.write(Uint8Array.of(128, 0, 0, 3, 1, 2, 3));
206146

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-
});
147+
const { value } = await reader.read();
211148

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])));
149+
expect(value.type).to.eql("message");
150+
expect(value.data).to.be.a(Blob);
151+
});
223152
});
224-
});
153+
}
225154
});

0 commit comments

Comments
 (0)