Skip to content

Commit 0b5e985

Browse files
refactor: prepend a header to each WebTransport chunk
This commit updates the format of the header added in [1], in order to match the format used for a WebSocket frame ([2]). Two advantages: - small payloads only need 1 byte instead of 4 - payloads larger than 2^31 bytes are supported [1]: 6142324 [2]: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#decoding_payload_length
1 parent aea321c commit 0b5e985

File tree

4 files changed

+126
-62
lines changed

4 files changed

+126
-62
lines changed

lib/index.ts

+71-18
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,27 @@ const decodePayload = (
4646
return packets;
4747
};
4848

49-
const HEADER_LENGTH = 4;
50-
5149
export function createPacketEncoderStream() {
5250
return new TransformStream({
5351
transform(packet: Packet, controller) {
5452
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);
53+
const payloadLength = encodedPacket.length;
54+
let header;
55+
// inspired by the WebSocket format: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#decoding_payload_length
56+
if (payloadLength < 126) {
57+
header = new Uint8Array(1);
58+
new DataView(header.buffer).setUint8(0, payloadLength);
59+
} else if (payloadLength < 65536) {
60+
header = new Uint8Array(3);
61+
const view = new DataView(header.buffer);
62+
view.setUint8(0, 126);
63+
view.setUint16(1, payloadLength);
64+
} else {
65+
header = new Uint8Array(9);
66+
const view = new DataView(header.buffer);
67+
view.setUint8(0, 127);
68+
view.setBigUint64(1, BigInt(payloadLength));
69+
}
5870
// first bit indicates whether the payload is plain text (0) or binary (1)
5971
if (packet.data && typeof packet.data !== "string") {
6072
header[0] |= 0x80;
@@ -91,6 +103,13 @@ function concatChunks(chunks: Uint8Array[], size: number) {
91103
return buffer;
92104
}
93105

106+
const enum State {
107+
READ_HEADER,
108+
READ_EXTENDED_LENGTH_16,
109+
READ_EXTENDED_LENGTH_64,
110+
READ_PAYLOAD
111+
}
112+
94113
export function createPacketDecoderStream(
95114
maxPayload: number,
96115
binaryType: BinaryType
@@ -99,44 +118,78 @@ export function createPacketDecoderStream(
99118
TEXT_DECODER = new TextDecoder();
100119
}
101120
const chunks: Uint8Array[] = [];
102-
let expectedSize = -1;
121+
let state = State.READ_HEADER;
122+
let expectedLength = -1;
103123
let isBinary = false;
104124

105125
return new TransformStream({
106126
transform(chunk: Uint8Array, controller) {
107127
chunks.push(chunk);
108128
while (true) {
109-
const expectHeader = expectedSize === -1;
110-
if (expectHeader) {
111-
if (totalLength(chunks) < HEADER_LENGTH) {
129+
if (state === State.READ_HEADER) {
130+
if (totalLength(chunks) < 1) {
131+
break;
132+
}
133+
const header = concatChunks(chunks, 1);
134+
isBinary = (header[0] & 0x80) === 0x80;
135+
expectedLength = header[0] & 0x7f;
136+
if (expectedLength < 126) {
137+
state = State.READ_PAYLOAD;
138+
} else if (expectedLength === 126) {
139+
state = State.READ_EXTENDED_LENGTH_16;
140+
} else {
141+
state = State.READ_EXTENDED_LENGTH_64;
142+
}
143+
} else if (state === State.READ_EXTENDED_LENGTH_16) {
144+
if (totalLength(chunks) < 2) {
145+
break;
146+
}
147+
const headerArray = concatChunks(chunks, 2);
148+
expectedLength = new DataView(
149+
headerArray.buffer,
150+
headerArray.byteOffset,
151+
headerArray.length
152+
).getUint16(0);
153+
state = State.READ_PAYLOAD;
154+
} else if (state === State.READ_EXTENDED_LENGTH_64) {
155+
if (totalLength(chunks) < 8) {
112156
break;
113157
}
114-
const headerArray = concatChunks(chunks, HEADER_LENGTH);
115-
const header = new DataView(
158+
const headerArray = concatChunks(chunks, 8);
159+
160+
const view = new DataView(
116161
headerArray.buffer,
117162
headerArray.byteOffset,
118163
headerArray.length
119-
).getUint32(0);
164+
);
120165

121-
isBinary = header >> 31 === -1;
122-
expectedSize = header & 0x7fffffff;
166+
const n = view.getUint32(0);
123167

124-
if (expectedSize === 0 || expectedSize > maxPayload) {
168+
if (n > Math.pow(2, 53 - 32) - 1) {
169+
// the maximum safe integer in JavaScript is 2^53 - 1
125170
controller.enqueue(ERROR_PACKET);
126171
break;
127172
}
173+
174+
expectedLength = n * Math.pow(2, 32) + view.getUint32(4);
175+
state = State.READ_PAYLOAD;
128176
} else {
129-
if (totalLength(chunks) < expectedSize) {
177+
if (totalLength(chunks) < expectedLength) {
130178
break;
131179
}
132-
const data = concatChunks(chunks, expectedSize);
180+
const data = concatChunks(chunks, expectedLength);
133181
controller.enqueue(
134182
decodePacket(
135183
isBinary ? data : TEXT_DECODER.decode(data),
136184
binaryType
137185
)
138186
);
139-
expectedSize = -1;
187+
state = State.READ_HEADER;
188+
}
189+
190+
if (expectedLength === 0 || expectedLength > maxPayload) {
191+
controller.enqueue(ERROR_PACKET);
192+
break;
140193
}
141194
}
142195
}

test/browser.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ describe("engine.io-parser (browser only)", () => {
128128
});
129129

130130
const header = await reader.read();
131-
expect(header.value).to.eql(Uint8Array.of(128, 0, 0, 3));
131+
expect(header.value).to.eql(Uint8Array.of(131));
132132

133133
const payload = await reader.read();
134134
expect(payload.value).to.eql(Uint8Array.of(1, 2, 3));
@@ -142,7 +142,7 @@ describe("engine.io-parser (browser only)", () => {
142142
const writer = stream.writable.getWriter();
143143
const reader = stream.readable.getReader();
144144

145-
writer.write(Uint8Array.of(128, 0, 0, 3, 1, 2, 3));
145+
writer.write(Uint8Array.of(131, 1, 2, 3));
146146

147147
const { value } = await reader.read();
148148

test/index.ts

+51-40
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe("engine.io-parser", () => {
7979
});
8080

8181
const header = await reader.read();
82-
expect(header.value).to.eql(Uint8Array.of(0, 0, 0, 5));
82+
expect(header.value).to.eql(Uint8Array.of(5));
8383

8484
const payload = await reader.read();
8585
expect(payload.value).to.eql(Uint8Array.of(52, 49, 226, 130, 172));
@@ -99,7 +99,7 @@ describe("engine.io-parser", () => {
9999
});
100100

101101
const header = await reader.read();
102-
expect(header.value).to.eql(Uint8Array.of(128, 0, 0, 3));
102+
expect(header.value).to.eql(Uint8Array.of(131));
103103

104104
const payload = await reader.read();
105105
expect(payload.value === data).to.be(true);
@@ -117,7 +117,7 @@ describe("engine.io-parser", () => {
117117
});
118118

119119
const header = await reader.read();
120-
expect(header.value).to.eql(Uint8Array.of(128, 0, 0, 3));
120+
expect(header.value).to.eql(Uint8Array.of(131));
121121

122122
const payload = await reader.read();
123123
expect(payload.value).to.eql(Uint8Array.of(1, 2, 3));
@@ -135,11 +135,53 @@ describe("engine.io-parser", () => {
135135
});
136136

137137
const header = await reader.read();
138-
expect(header.value).to.eql(Uint8Array.of(128, 0, 0, 6));
138+
expect(header.value).to.eql(Uint8Array.of(134));
139139

140140
const payload = await reader.read();
141141
expect(payload.value).to.eql(Uint8Array.of(1, 0, 2, 0, 1, 1));
142142
});
143+
144+
it("should encode a binary packet (Uint8Array - medium)", async () => {
145+
const stream = createPacketEncoderStream();
146+
147+
const writer = stream.writable.getWriter();
148+
const reader = stream.readable.getReader();
149+
150+
const data = new Uint8Array(12345);
151+
152+
writer.write({
153+
type: "message",
154+
data
155+
});
156+
157+
const header = await reader.read();
158+
expect(header.value).to.eql(Uint8Array.of(254, 48, 57));
159+
160+
const payload = await reader.read();
161+
expect(payload.value === data).to.be(true);
162+
});
163+
164+
it("should encode a binary packet (Uint8Array - big)", async () => {
165+
const stream = createPacketEncoderStream();
166+
167+
const writer = stream.writable.getWriter();
168+
const reader = stream.readable.getReader();
169+
170+
const data = new Uint8Array(123456789);
171+
172+
writer.write({
173+
type: "message",
174+
data
175+
});
176+
177+
const header = await reader.read();
178+
expect(header.value).to.eql(
179+
Uint8Array.of(255, 0, 0, 0, 0, 7, 91, 205, 21)
180+
);
181+
182+
const payload = await reader.read();
183+
expect(payload.value === data).to.be(true);
184+
});
143185
});
144186

145187
describe("createPacketDecoderStream", () => {
@@ -149,7 +191,7 @@ describe("engine.io-parser", () => {
149191
const writer = stream.writable.getWriter();
150192
const reader = stream.readable.getReader();
151193

152-
writer.write(Uint8Array.of(0, 0, 0, 5));
194+
writer.write(Uint8Array.of(5));
153195
writer.write(Uint8Array.of(52, 49, 226, 130, 172));
154196

155197
const packet = await reader.read();
@@ -165,25 +207,16 @@ describe("engine.io-parser", () => {
165207
const writer = stream.writable.getWriter();
166208
const reader = stream.readable.getReader();
167209

168-
writer.write(Uint8Array.of(0));
169-
writer.write(Uint8Array.of(0));
170-
writer.write(Uint8Array.of(0));
171210
writer.write(Uint8Array.of(5));
172211
writer.write(Uint8Array.of(52));
173212
writer.write(Uint8Array.of(49));
174213
writer.write(Uint8Array.of(226));
175214
writer.write(Uint8Array.of(130));
176215
writer.write(Uint8Array.of(172));
177216

178-
writer.write(Uint8Array.of(0));
179-
writer.write(Uint8Array.of(0));
180-
writer.write(Uint8Array.of(0));
181217
writer.write(Uint8Array.of(1));
182218
writer.write(Uint8Array.of(50));
183219

184-
writer.write(Uint8Array.of(0));
185-
writer.write(Uint8Array.of(0));
186-
writer.write(Uint8Array.of(0));
187220
writer.write(Uint8Array.of(1));
188221
writer.write(Uint8Array.of(51));
189222

@@ -203,29 +236,7 @@ describe("engine.io-parser", () => {
203236
const writer = stream.writable.getWriter();
204237
const reader = stream.readable.getReader();
205238

206-
writer.write(
207-
Uint8Array.of(
208-
0,
209-
0,
210-
0,
211-
5,
212-
52,
213-
49,
214-
226,
215-
130,
216-
172,
217-
0,
218-
0,
219-
0,
220-
1,
221-
50,
222-
0,
223-
0,
224-
0,
225-
1,
226-
51
227-
)
228-
);
239+
writer.write(Uint8Array.of(5, 52, 49, 226, 130, 172, 1, 50, 1, 51));
229240

230241
const { value } = await reader.read();
231242
expect(value).to.eql({ type: "message", data: "1€" });
@@ -243,7 +254,7 @@ describe("engine.io-parser", () => {
243254
const writer = stream.writable.getWriter();
244255
const reader = stream.readable.getReader();
245256

246-
writer.write(Uint8Array.of(128, 0, 0, 3, 1, 2, 3));
257+
writer.write(Uint8Array.of(131, 1, 2, 3));
247258

248259
const { value } = await reader.read();
249260

@@ -258,7 +269,7 @@ describe("engine.io-parser", () => {
258269
const writer = stream.writable.getWriter();
259270
const reader = stream.readable.getReader();
260271

261-
writer.write(Uint8Array.of(0, 0, 1, 0));
272+
writer.write(Uint8Array.of(11));
262273

263274
const packet = await reader.read();
264275
expect(packet.value).to.eql({ type: "error", data: "parser error" });
@@ -270,7 +281,7 @@ describe("engine.io-parser", () => {
270281
const writer = stream.writable.getWriter();
271282
const reader = stream.readable.getReader();
272283

273-
writer.write(Uint8Array.of(0, 0, 0, 0));
284+
writer.write(Uint8Array.of(0));
274285

275286
const packet = await reader.read();
276287
expect(packet.value).to.eql({ type: "error", data: "parser error" });

test/node.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ describe("engine.io-parser (node.js only)", () => {
123123
});
124124

125125
const header = await reader.read();
126-
expect(header.value).to.eql(Uint8Array.of(128, 0, 0, 3));
126+
expect(header.value).to.eql(Uint8Array.of(131));
127127

128128
const payload = await reader.read();
129129
expect(payload.value).to.eql(Uint8Array.of(1, 2, 3));
@@ -137,7 +137,7 @@ describe("engine.io-parser (node.js only)", () => {
137137
const writer = stream.writable.getWriter();
138138
const reader = stream.readable.getReader();
139139

140-
writer.write(Uint8Array.of(128, 0, 0, 3, 1, 2, 3));
140+
writer.write(Uint8Array.of(131, 1, 2, 3));
141141

142142
const { value } = await reader.read();
143143

0 commit comments

Comments
 (0)