Skip to content

Commit

Permalink
feat: implement WebTransport-related encoding/decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
darrachequesne committed Jun 11, 2023
1 parent 0fc3694 commit bed70a4
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 43 deletions.
20 changes: 15 additions & 5 deletions lib/decodePacket.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { decode } from "./contrib/base64-arraybuffer.js";

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

const decodePacket = (
export const decodePacket = (
encodedPacket: RawData,
binaryType?: BinaryType
): Packet => {
Expand Down Expand Up @@ -52,11 +52,21 @@ const decodeBase64Packet = (data, binaryType) => {
const mapBinary = (data, binaryType) => {
switch (binaryType) {
case "blob":
return data instanceof ArrayBuffer ? new Blob([data]) : data;
if (data instanceof Blob) {
// from WebSocket + binaryType "blob"
return data;
} else {
// from HTTP long-polling or WebTransport
return new Blob([data]);
}
case "arraybuffer":
default:
return data; // assuming the data is already an ArrayBuffer
if (data instanceof ArrayBuffer) {
// from HTTP long-polling (base64) or WebSocket + binaryType "arraybuffer"
return data;
} else {
// from WebTransport (Uint8Array)
return data.buffer;
}
}
};

export default decodePacket;
36 changes: 21 additions & 15 deletions lib/decodePacket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
RawData
} from "./commons.js";

const decodePacket = (
export const decodePacket = (
encodedPacket: RawData,
binaryType?: BinaryType
): Packet => {
Expand Down Expand Up @@ -38,23 +38,29 @@ const decodePacket = (
};

const mapBinary = (data: RawData, binaryType?: BinaryType) => {
const isBuffer = Buffer.isBuffer(data);
switch (binaryType) {
case "arraybuffer":
return isBuffer ? toArrayBuffer(data) : data;
if (data instanceof ArrayBuffer) {
// from WebSocket & binaryType "arraybuffer"
return data;
} else if (Buffer.isBuffer(data)) {
// from HTTP long-polling
return data.buffer.slice(
data.byteOffset,
data.byteOffset + data.byteLength
);
} else {
// from WebTransport (Uint8Array)
return data.buffer;
}
case "nodebuffer":
default:
return data; // assuming the data is already a Buffer
}
};

const toArrayBuffer = (buffer: Buffer): ArrayBuffer => {
const arrayBuffer = new ArrayBuffer(buffer.length);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; i++) {
view[i] = buffer[i];
if (Buffer.isBuffer(data)) {
// from HTTP long-polling or WebSocket & binaryType "nodebuffer" (default)
return data;
} else {
// from WebTransport (Uint8Array)
return Buffer.from(data);
}
}
return arrayBuffer;
};

export default decodePacket;
37 changes: 36 additions & 1 deletion lib/encodePacket.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,39 @@ const encodeBlobAsBase64 = (
return fileReader.readAsDataURL(data);
};

export default encodePacket;
function toArray(data: BufferSource) {
if (data instanceof Uint8Array) {
return data;
} else if (data instanceof ArrayBuffer) {
return new Uint8Array(data);
} else {
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
}
}

let TEXT_ENCODER;

export function encodePacketToBinary(
packet: Packet,
callback: (encodedPacket: RawData) => void
) {
if (withNativeBlob && packet.data instanceof Blob) {
return packet.data
.arrayBuffer()
.then(toArray)
.then(callback);
} else if (
withNativeArrayBuffer &&
(packet.data instanceof ArrayBuffer || isView(packet.data))
) {
return callback(toArray(packet.data));
}
encodePacket(packet, false, encoded => {
if (!TEXT_ENCODER) {
TEXT_ENCODER = new TextEncoder();
}
callback(TEXT_ENCODER.encode(encoded));
});
}

export { encodePacket };
35 changes: 25 additions & 10 deletions lib/encodePacket.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { PACKET_TYPES, Packet, RawData } from "./commons.js";

const encodePacket = (
export const encodePacket = (
{ type, data }: Packet,
supportsBinary: boolean,
callback: (encodedPacket: RawData) => void
) => {
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const buffer = toBuffer(data);
return callback(encodeBuffer(buffer, supportsBinary));
return callback(
supportsBinary ? data : "b" + toBuffer(data, true).toString("base64")
);
}
// plain string
return callback(PACKET_TYPES[type] + (data || ""));
};

const toBuffer = data => {
if (Buffer.isBuffer(data)) {
const toBuffer = (data: BufferSource, forceBufferConversion: boolean) => {
if (
Buffer.isBuffer(data) ||
(data instanceof Uint8Array && !forceBufferConversion)
) {
return data;
} else if (data instanceof ArrayBuffer) {
return Buffer.from(data);
Expand All @@ -23,9 +27,20 @@ const toBuffer = data => {
}
};

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

export default encodePacket;
export function encodePacketToBinary(
packet: Packet,
callback: (encodedPacket: RawData) => void
) {
if (packet.data instanceof ArrayBuffer || ArrayBuffer.isView(packet.data)) {
return callback(toBuffer(packet.data, false));
}
encodePacket(packet, true, encoded => {
if (!TEXT_ENCODER) {
// lazily created for compatibility with Node.js 10
TEXT_ENCODER = new TextEncoder();
}
callback(TEXT_ENCODER.encode(encoded));
});
}
25 changes: 23 additions & 2 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import encodePacket from "./encodePacket.js";
import decodePacket from "./decodePacket.js";
import { encodePacket, encodePacketToBinary } from "./encodePacket.js";
import { decodePacket } from "./decodePacket.js";
import { Packet, PacketType, RawData, BinaryType } from "./commons.js";

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

let TEXT_DECODER;

export function decodePacketFromBinary(
data: Uint8Array,
isBinary: boolean,
binaryType: BinaryType
) {
if (!TEXT_DECODER) {
// lazily created for compatibility with old browser platforms
TEXT_DECODER = new TextDecoder();
}
// 48 === "0".charCodeAt(0) (OPEN packet type)
// 54 === "6".charCodeAt(0) (NOOP packet type)
const isPlainBinary = isBinary || data[0] < 48 || data[0] > 54;
return decodePacket(
isPlainBinary ? data : TEXT_DECODER.decode(data),
binaryType
);
}

export const protocol = 4;
export {
encodePacket,
encodePacketToBinary,
encodePayload,
decodePacket,
decodePayload,
Expand Down
111 changes: 111 additions & 0 deletions test/browser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
decodePacket,
decodePacketFromBinary,
decodePayload,
encodePacket,
encodePacketToBinary,
encodePayload,
Packet
} from "..";
Expand Down Expand Up @@ -111,4 +113,113 @@ describe("engine.io-parser (browser only)", () => {
});
}
});

describe("single packet (to/from Uint8Array)", function() {
if (!withNativeArrayBuffer) {
// @ts-ignore
return this.skip();
}

it("should encode a plaintext packet", done => {
const packet: Packet = {
type: "message",
data: "1€"
};
encodePacketToBinary(packet, encodedPacket => {
expect(encodedPacket).to.be.an(Uint8Array);
expect(encodedPacket).to.eql(Uint8Array.from([52, 49, 226, 130, 172]));

const decoded = decodePacketFromBinary(
encodedPacket,
false,
"arraybuffer"
);
expect(decoded).to.eql(packet);
done();
});
});

it("should encode a binary packet (Uint8Array)", done => {
const packet: Packet = {
type: "message",
data: Uint8Array.from([1, 2, 3])
};
encodePacketToBinary(packet, encodedPacket => {
expect(encodedPacket === packet.data).to.be(true);
done();
});
});

it("should encode a binary packet (Blob)", done => {
const packet: Packet = {
type: "message",
data: new Blob([Uint8Array.from([1, 2, 3])])
};
encodePacketToBinary(packet, encodedPacket => {
expect(encodedPacket).to.be.an(Uint8Array);
expect(encodedPacket).to.eql(Uint8Array.from([1, 2, 3]));
done();
});
});

it("should encode a binary packet (ArrayBuffer)", done => {
const packet: Packet = {
type: "message",
data: Uint8Array.from([1, 2, 3]).buffer
};
encodePacketToBinary(packet, encodedPacket => {
expect(encodedPacket).to.be.an(Uint8Array);
expect(encodedPacket).to.eql(Uint8Array.from([1, 2, 3]));
done();
});
});

it("should encode a binary packet (Uint16Array)", done => {
const packet: Packet = {
type: "message",
data: Uint16Array.from([1, 2, 257])
};
encodePacketToBinary(packet, encodedPacket => {
expect(encodedPacket).to.be.an(Uint8Array);
expect(encodedPacket).to.eql(Uint8Array.from([1, 0, 2, 0, 1, 1]));
done();
});
});

it("should decode a binary packet (Blob)", () => {
const decoded = decodePacketFromBinary(
Uint8Array.from([1, 2, 3]),
false,
"blob"
);

expect(decoded.type).to.eql("message");
expect(decoded.data).to.be.a(Blob);
});

it("should decode a binary packet (ArrayBuffer)", () => {
const decoded = decodePacketFromBinary(
Uint8Array.from([1, 2, 3]),
false,
"arraybuffer"
);

expect(decoded.type).to.eql("message");
expect(decoded.data).to.be.an(ArrayBuffer);
expect(areArraysEqual(decoded.data, Uint8Array.from([1, 2, 3])));
});

it("should decode a binary packet (with binary header)", () => {
// 52 === "4".charCodeAt(0)
const decoded = decodePacketFromBinary(
Uint8Array.from([52]),
true,
"arraybuffer"
);

expect(decoded.type).to.eql("message");
expect(decoded.data).to.be.an(ArrayBuffer);
expect(areArraysEqual(decoded.data, Uint8Array.from([52])));
});
});
});
2 changes: 1 addition & 1 deletion test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Packet
} from "..";
import * as expect from "expect.js";
import "./node";
import "./node"; // replaced by "./browser" for the tests in the browser (see "browser" field in the package.json file)

describe("engine.io-parser", () => {
describe("single packet", () => {
Expand Down

0 comments on commit bed70a4

Please sign in to comment.