Эх сурвалжийг харах

feat: MsgPack/Cbor serialization

Jonas Gloning 1 жил өмнө
parent
commit
fcffbf243c

+ 18 - 0
e2e/datachannel/Int32Array.js

@@ -0,0 +1,18 @@
+import { int32_arrays } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, typed_array] of int32_arrays.entries()) {
+		expect(received[i]).to.be.an.instanceof(Int32Array);
+		expect(received[i]).to.deep.equal(typed_array);
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const typed_array of int32_arrays) {
+		dataConnection.send(typed_array);
+	}
+};

+ 18 - 0
e2e/datachannel/Int32Array_as_Uint8Array.js

@@ -0,0 +1,18 @@
+import { int32_arrays } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, typed_array] of int32_arrays.entries()) {
+		expect(received[i]).to.be.an.instanceof(Uint8Array);
+		expect(received[i]).to.deep.equal(new Uint8Array(typed_array.buffer));
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const typed_array of int32_arrays) {
+		dataConnection.send(typed_array);
+	}
+};

+ 18 - 0
e2e/datachannel/Uint8Array.js

@@ -0,0 +1,18 @@
+import { uint8_arrays } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, typed_array] of uint8_arrays.entries()) {
+		expect(received[i]).to.be.an.instanceof(Uint8Array);
+		expect(received[i]).to.deep.equal(typed_array);
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const typed_array of uint8_arrays) {
+		dataConnection.send(typed_array);
+	}
+};

+ 18 - 0
e2e/datachannel/arraybuffers_as_uint8array.js

@@ -0,0 +1,18 @@
+import { array_buffers } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	for (const [i, array_buffer] of array_buffers.entries()) {
+		expect(received[i]).to.be.an.instanceof(Uint8Array);
+		expect(received[i]).to.deep.equal(new Uint8Array(array_buffer));
+	}
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const array_buffer of array_buffers) {
+		dataConnection.send(array_buffer);
+	}
+};

+ 15 - 0
e2e/datachannel/dates.js

@@ -0,0 +1,15 @@
+import { dates } from "../data.js";
+import { expect } from "https://esm.sh/v126/chai@4.3.7/X-dHMvZXhwZWN0/es2021/chai.bundle.mjs";
+
+/** @param {unknown[]} received */
+export const check = (received) => {
+	expect(received).to.deep.equal(dates);
+};
+/**
+ * @param {import("../peerjs").DataConnection} dataConnection
+ */
+export const send = (dataConnection) => {
+	for (const date of dates) {
+		dataConnection.send(date);
+	}
+};

+ 14 - 3
e2e/datachannel/serialization.js

@@ -7,9 +7,20 @@ const params = new URLSearchParams(document.location.search);
 const testfile = params.get("testfile");
 const serialization = params.get("serialization");
 
-const serializers = {};
+(async () => {
+	let serializers = {};
+	try {
+		const { Cbor } = await import("/dist/serializer.cbor.mjs");
+		const { MsgPack } = await import("/dist/serializer.msgpack.mjs");
+		serializers = {
+			Cbor,
+			MsgPack,
+		};
+	} catch (e) {
+		console.log(e);
+	}
 
-import(`./${testfile}.js`).then(({ check, send }) => {
+	const { check, send } = await import(`./${testfile}.js`);
 	document.getElementsByTagName("title")[0].innerText =
 		window.location.hash.substring(1);
 
@@ -80,4 +91,4 @@ import(`./${testfile}.js`).then(({ check, send }) => {
 		messages.textContent = "Sent!";
 	});
 	window["connect-btn"].disabled = false;
-});
+})();

+ 35 - 0
e2e/datachannel/serialization_cbor.spec.ts

@@ -0,0 +1,35 @@
+import P from "./serialization.page.js";
+import { serializationTest } from "./serializationTest.js";
+import { browser, $, $$, expect } from "@wdio/globals";
+
+describe("DataChannel:CBOR", function () {
+	beforeAll(async function () {
+		await P.init();
+	});
+	beforeEach(async function () {
+		if (
+			// @ts-ignore
+			browser.capabilities.browserName === "firefox" &&
+			// @ts-ignore
+			parseInt(browser.capabilities.browserVersion) < 102
+		) {
+			pending("Firefox 102+ required for Streams");
+		}
+	});
+	it("should transfer numbers", serializationTest("./numbers", "Cbor"));
+	it("should transfer strings", serializationTest("./strings", "Cbor"));
+	it("should transfer long string", serializationTest("./long_string", "Cbor"));
+	it("should transfer objects", serializationTest("./objects", "Cbor"));
+	it("should transfer arrays", serializationTest("./arrays", "Cbor"));
+	it("should transfer dates", serializationTest("./dates", "Cbor"));
+	it(
+		"should transfer ArrayBuffers as Uint8Arrays",
+		serializationTest("./arraybuffers_as_uint8array", "Cbor"),
+	);
+	it(
+		"should transfer TypedArrayView",
+		serializationTest("./typed_array_view", "Cbor"),
+	);
+	it("should transfer Uint8Arrays", serializationTest("./Uint8Array", "Cbor"));
+	it("should transfer Int32Arrays", serializationTest("./Int32Array", "Cbor"));
+});

+ 44 - 0
e2e/datachannel/serialization_msgpack.spec.ts

@@ -0,0 +1,44 @@
+import P from "./serialization.page.js";
+import { serializationTest } from "./serializationTest.js";
+import { browser } from "@wdio/globals";
+
+describe("DataChannel:MsgPack", function () {
+	beforeAll(async function () {
+		await P.init();
+	});
+	beforeEach(async function () {
+		if (
+			// @ts-ignore
+			browser.capabilities.browserName === "firefox" &&
+			// @ts-ignore
+			parseInt(browser.capabilities.browserVersion) < 102
+		) {
+			pending("Firefox 102+ required for Streams");
+		}
+	});
+	it("should transfer numbers", serializationTest("./numbers", "MsgPack"));
+	it("should transfer strings", serializationTest("./strings", "MsgPack"));
+	it(
+		"should transfer long string",
+		serializationTest("./long_string", "MsgPack"),
+	);
+	it("should transfer objects", serializationTest("./objects", "MsgPack"));
+	it("should transfer arrays", serializationTest("./arrays", "MsgPack"));
+	it(
+		"should transfer Dates as strings",
+		serializationTest("./dates", "MsgPack"),
+	);
+	// it("should transfer ArrayBuffers", serializationTest("./arraybuffers", "MsgPack"));
+	it(
+		"should transfer TypedArrayView",
+		serializationTest("./typed_array_view", "MsgPack"),
+	);
+	it(
+		"should transfer Uint8Arrays",
+		serializationTest("./Uint8Array", "MsgPack"),
+	);
+	it(
+		"should transfer Int32Arrays as Uint8Arrays",
+		serializationTest("./Int32Array_as_Uint8Array", "MsgPack"),
+	);
+});

+ 1 - 1
lib/api.ts

@@ -1,6 +1,6 @@
 import { util } from "./util";
 import logger from "./logger";
-import { PeerJSOption } from "./optionInterfaces";
+import type { PeerJSOption } from "./optionInterfaces";
 import { version } from "../package.json";
 
 export class API {

+ 3 - 3
lib/baseconnection.ts

@@ -1,7 +1,7 @@
 import { EventEmitter, ValidEventTypes } from "eventemitter3";
-import { Peer } from "./peer";
-import { ServerMessage } from "./servermessage";
-import { ConnectionType } from "./enums";
+import type { Peer } from "./peer";
+import type { ServerMessage } from "./servermessage";
+import type { ConnectionType } from "./enums";
 
 export type BaseConnectionEvents = {
 	/**

+ 7 - 6
lib/dataconnection/BufferedConnection/BinaryJSConnection.ts → lib/dataconnection/BufferedConnection/BinaryPack.ts

@@ -1,11 +1,12 @@
-import { concatArrayBuffers, util } from "../../util";
+import { BinaryPackChunker, concatArrayBuffers } from "./binaryPackChunker";
 import logger from "../../logger";
-import { Peer } from "../../peer";
+import type { Peer } from "../../peer";
 import { BufferedConnection } from "./BufferedConnection";
 import { SerializationType } from "../../enums";
-import { Packable, pack, unpack } from "peerjs-js-binarypack";
+import { type Packable, pack, unpack } from "peerjs-js-binarypack";
 
-export class BinaryJSConnection extends BufferedConnection {
+export class BinaryPack extends BufferedConnection {
+	private readonly chunker = new BinaryPackChunker();
 	readonly serialization = SerializationType.Binary;
 
 	private _chunkedData: {
@@ -80,7 +81,7 @@ export class BinaryJSConnection extends BufferedConnection {
 	): void | Promise<void> {
 		const blob = pack(data);
 
-		if (!chunked && blob.byteLength > util.chunkedMTU) {
+		if (!chunked && blob.byteLength > this.chunker.chunkedMTU) {
 			this._sendChunks(blob);
 			return;
 		}
@@ -89,7 +90,7 @@ export class BinaryJSConnection extends BufferedConnection {
 	}
 
 	private _sendChunks(blob: ArrayBuffer) {
-		const blobs = util.chunk(blob);
+		const blobs = this.chunker.chunk(blob);
 		logger.log(`DC#${this.connectionId} Try to send ${blobs.length} chunks...`);
 
 		for (let blob of blobs) {

+ 1 - 1
lib/dataconnection/BufferedConnection/JsonConnection.ts → lib/dataconnection/BufferedConnection/Json.ts

@@ -1,7 +1,7 @@
 import { BufferedConnection } from "./BufferedConnection";
 import { SerializationType } from "../../enums";
 import { util } from "../../util";
-export class JsonConnection extends BufferedConnection {
+export class Json extends BufferedConnection {
 	readonly serialization = SerializationType.JSON;
 	private readonly encoder = new TextEncoder();
 	private readonly decoder = new TextDecoder();

+ 1 - 1
lib/dataconnection/BufferedConnection/RawConnection.ts → lib/dataconnection/BufferedConnection/Raw.ts

@@ -1,7 +1,7 @@
 import { BufferedConnection } from "./BufferedConnection";
 import { SerializationType } from "../../enums";
 
-export class RawConnection extends BufferedConnection {
+export class Raw extends BufferedConnection {
 	readonly serialization = SerializationType.None;
 
 	protected _handleDataMessage({ data }) {

+ 53 - 0
lib/dataconnection/BufferedConnection/binaryPackChunker.ts

@@ -0,0 +1,53 @@
+export class BinaryPackChunker {
+	readonly chunkedMTU = 16300; // The original 60000 bytes setting does not work when sending data from Firefox to Chrome, which is "cut off" after 16384 bytes and delivered individually.
+
+	// Binary stuff
+
+	private _dataCount: number = 1;
+
+	chunk = (
+		blob: ArrayBuffer,
+	): { __peerData: number; n: number; total: number; data: Uint8Array }[] => {
+		const chunks = [];
+		const size = blob.byteLength;
+		const total = Math.ceil(size / this.chunkedMTU);
+
+		let index = 0;
+		let start = 0;
+
+		while (start < size) {
+			const end = Math.min(size, start + this.chunkedMTU);
+			const b = blob.slice(start, end);
+
+			const chunk = {
+				__peerData: this._dataCount,
+				n: index,
+				data: b,
+				total,
+			};
+
+			chunks.push(chunk);
+
+			start = end;
+			index++;
+		}
+
+		this._dataCount++;
+
+		return chunks;
+	};
+}
+
+export function concatArrayBuffers(bufs: Uint8Array[]) {
+	let size = 0;
+	for (const buf of bufs) {
+		size += buf.byteLength;
+	}
+	const result = new Uint8Array(size);
+	let offset = 0;
+	for (const buf of bufs) {
+		result.set(buf, offset);
+		offset += buf.byteLength;
+	}
+	return result;
+}

+ 4 - 5
lib/dataconnection/DataConnection.ts

@@ -1,11 +1,11 @@
-import { util } from "../util";
 import logger from "../logger";
 import { Negotiator } from "../negotiator";
 import { ConnectionType, ServerMessageType } from "../enums";
-import { Peer } from "../peer";
+import type { Peer } from "../peer";
 import { BaseConnection } from "../baseconnection";
-import { ServerMessage } from "../servermessage";
+import type { ServerMessage } from "../servermessage";
 import type { DataConnection as IDataConnection } from "./DataConnection";
+import { randomToken } from "../utils/randomToken";
 
 type DataConnectionEvents = {
 	/**
@@ -41,8 +41,7 @@ export abstract class DataConnection
 		super(peerId, provider, options);
 
 		this.connectionId =
-			this.options.connectionId ||
-			DataConnection.ID_PREFIX + util.randomToken();
+			this.options.connectionId || DataConnection.ID_PREFIX + randomToken();
 
 		this.label = this.options.label || this.connectionId;
 		this.reliable = !!this.options.reliable;

+ 75 - 0
lib/dataconnection/StreamConnection/Cbor.ts

@@ -0,0 +1,75 @@
+import type { Peer } from "../../peer.js";
+import { Encoder, Decoder } from "cbor-x";
+import { StreamConnection } from "./StreamConnection.js";
+
+const NullValue = Symbol.for(null);
+
+function concatUint8Array(buffer1: Uint8Array, buffer2: Uint8Array) {
+	const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
+	tmp.set(buffer1, 0);
+	tmp.set(buffer2, buffer1.byteLength);
+	return new Uint8Array(tmp.buffer);
+}
+
+const iterateOver = async function* (stream: ReadableStream) {
+	const reader = stream.getReader();
+	try {
+		while (true) {
+			const { done, value } = await reader.read();
+			if (done) return;
+			yield value;
+		}
+	} finally {
+		reader.releaseLock();
+	}
+};
+
+export class Cbor extends StreamConnection {
+	readonly serialization = "Cbor";
+	private _encoder = new Encoder();
+	private _decoder = new Decoder();
+	private _inc;
+	private _decoderStream = new TransformStream<ArrayBuffer, unknown>({
+		transform: (abchunk, controller) => {
+			let chunk = new Uint8Array(abchunk);
+			if (this._inc) {
+				chunk = concatUint8Array(this._inc, chunk);
+				this._inc = null;
+			}
+			let values;
+			try {
+				values = this._decoder.decodeMultiple(chunk);
+			} catch (error) {
+				if (error.incomplete) {
+					this._inc = chunk.subarray(error.lastPosition);
+					values = error.values;
+				} else throw error;
+			} finally {
+				for (let value of values || []) {
+					if (value === null) value = NullValue;
+					controller.enqueue(value);
+				}
+			}
+		},
+	});
+
+	constructor(peerId: string, provider: Peer, options: any) {
+		super(peerId, provider, { ...options, reliable: true });
+
+		this._rawReadStream.pipeTo(this._decoderStream.writable);
+
+		(async () => {
+			for await (const msg of iterateOver(this._decoderStream.readable)) {
+				if (msg.__peerData?.type === "close") {
+					this.close();
+					return;
+				}
+				this.emit("data", msg);
+			}
+		})();
+	}
+
+	protected override _send(data) {
+		return this.writer.write(this._encoder.encode(data));
+	}
+}

+ 27 - 0
lib/dataconnection/StreamConnection/MsgPack.ts

@@ -0,0 +1,27 @@
+import { decodeMultiStream, Encoder } from "@msgpack/msgpack";
+import { StreamConnection } from "./StreamConnection.js";
+import type { Peer } from "../../peer.js";
+
+export class MsgPack extends StreamConnection {
+	readonly serialization = "MsgPack";
+	private _encoder = new Encoder();
+
+	constructor(peerId: string, provider: Peer, options: any) {
+		super(peerId, provider, options);
+
+		(async () => {
+			for await (const msg of decodeMultiStream(this._rawReadStream)) {
+				// @ts-ignore
+				if (msg.__peerData?.type === "close") {
+					this.close();
+					return;
+				}
+				this.emit("data", msg);
+			}
+		})();
+	}
+
+	protected override _send(data) {
+		return this.writer.write(this._encoder.encode(data));
+	}
+}

+ 67 - 0
lib/dataconnection/StreamConnection/StreamConnection.ts

@@ -0,0 +1,67 @@
+import logger from "../../logger.js";
+import type { Peer } from "../../peer.js";
+import { DataConnection } from "../DataConnection.js";
+
+export abstract class StreamConnection extends DataConnection {
+	private _CHUNK_SIZE = 1024 * 8 * 4;
+	private _splitStream = new TransformStream<Uint8Array>({
+		transform: (chunk, controller) => {
+			for (let split = 0; split < chunk.length; split += this._CHUNK_SIZE) {
+				controller.enqueue(chunk.subarray(split, split + this._CHUNK_SIZE));
+			}
+		},
+	});
+	private _rawSendStream = new WritableStream<ArrayBuffer>({
+		write: async (chunk, controller) => {
+			const openEvent = new Promise((resolve) =>
+				this.dataChannel.addEventListener("bufferedamountlow", resolve, {
+					once: true,
+				}),
+			);
+
+			// if we can send the chunk now, send it
+			// if not, we wait until at least half of the sending buffer is free again
+			await (this.dataChannel.bufferedAmount <=
+				DataConnection.MAX_BUFFERED_AMOUNT - chunk.byteLength || openEvent);
+
+			// TODO: what can go wrong here?
+			try {
+				this.dataChannel.send(chunk);
+			} catch (e) {
+				logger.error(`DC#:${this.connectionId} Error when sending:`, e);
+				controller.error(e);
+				this.close();
+			}
+		},
+	});
+	protected writer = this._splitStream.writable.getWriter();
+
+	protected _rawReadStream = new ReadableStream<ArrayBuffer>({
+		start: (controller) => {
+			this.once("open", () => {
+				this.dataChannel.addEventListener("message", (e) => {
+					// if(e.data?.__peerData?.type === "close")
+					// {
+					// 	controller.close()
+					// 	this.close()
+					// 	return
+					// }
+					controller.enqueue(e.data);
+				});
+			});
+		},
+	});
+
+	protected constructor(peerId: string, provider: Peer, options: any) {
+		super(peerId, provider, { ...options, reliable: true });
+
+		this._splitStream.readable.pipeTo(this._rawSendStream);
+	}
+
+	public override _initializeDataChannel(dc) {
+		super._initializeDataChannel(dc);
+		this.dataChannel.binaryType = "arraybuffer";
+		this.dataChannel.bufferedAmountLowThreshold =
+			DataConnection.MAX_BUFFERED_AMOUNT / 2;
+	}
+}

+ 5 - 0
lib/exports.ts

@@ -20,5 +20,10 @@ export type {
 	ServerMessageType,
 } from "./enums";
 
+export { BufferedConnection } from "./dataconnection/BufferedConnection/BufferedConnection";
+export { StreamConnection } from "./dataconnection/StreamConnection/StreamConnection";
+export { Cbor } from "./dataconnection/StreamConnection/Cbor";
+export { MsgPack } from "./dataconnection/StreamConnection/MsgPack";
+
 export { Peer };
 export default Peer;

+ 2 - 2
lib/mediaconnection.ts

@@ -2,9 +2,9 @@ import { util } from "./util";
 import logger from "./logger";
 import { Negotiator } from "./negotiator";
 import { ConnectionType, ServerMessageType } from "./enums";
-import { Peer } from "./peer";
+import type { Peer } from "./peer";
 import { BaseConnection } from "./baseconnection";
-import { ServerMessage } from "./servermessage";
+import type { ServerMessage } from "./servermessage";
 import type { AnswerOption } from "./optionInterfaces";
 
 export type MediaConnectionEvents = {

+ 5 - 8
lib/negotiator.ts

@@ -1,10 +1,9 @@
-import { util } from "./util";
 import logger from "./logger";
-import { MediaConnection } from "./mediaconnection";
-import { DataConnection } from "./dataconnection/DataConnection";
+import type { MediaConnection } from "./mediaconnection";
+import type { DataConnection } from "./dataconnection/DataConnection";
 import { ConnectionType, PeerErrorType, ServerMessageType } from "./enums";
-import { BaseConnection, BaseConnectionEvents } from "./baseconnection";
-import { ValidEventTypes } from "eventemitter3";
+import type { BaseConnection, BaseConnectionEvents } from "./baseconnection";
+import type { ValidEventTypes } from "eventemitter3";
 
 /**
  * Manages all negotiations between Peers.
@@ -112,7 +111,7 @@ export class Negotiator<
 					);
 					break;
 				case "completed":
-					peerConnection.onicecandidate = util.noop;
+					peerConnection.onicecandidate = () => {};
 					break;
 			}
 
@@ -220,7 +219,6 @@ export class Negotiator<
 					type: this.connection.type,
 					connectionId: this.connection.connectionId,
 					metadata: this.connection.metadata,
-					browser: util.browser,
 				};
 
 				if (this.connection.type === ConnectionType.Data) {
@@ -286,7 +284,6 @@ export class Negotiator<
 						sdp: answer,
 						type: this.connection.type,
 						connectionId: this.connection.connectionId,
-						browser: util.browser,
 					},
 					dst: this.connection.peer,
 				});

+ 10 - 10
lib/peer.ts

@@ -3,23 +3,23 @@ import { util } from "./util";
 import logger, { LogLevel } from "./logger";
 import { Socket } from "./socket";
 import { MediaConnection } from "./mediaconnection";
-import { DataConnection } from "./dataconnection/DataConnection";
+import type { DataConnection } from "./dataconnection/DataConnection";
 import {
 	ConnectionType,
 	PeerErrorType,
 	SocketEventType,
 	ServerMessageType,
 } from "./enums";
-import { ServerMessage } from "./servermessage";
+import type { ServerMessage } from "./servermessage";
 import { API } from "./api";
 import type {
 	PeerConnectOption,
 	PeerJSOption,
 	CallOption,
 } from "./optionInterfaces";
-import { BinaryJSConnection } from "./dataconnection/BufferedConnection/BinaryJSConnection";
-import { RawConnection } from "./dataconnection/BufferedConnection/RawConnection";
-import { JsonConnection } from "./dataconnection/BufferedConnection/JsonConnection";
+import { BinaryPack } from "./dataconnection/BufferedConnection/BinaryPack";
+import { Raw } from "./dataconnection/BufferedConnection/Raw";
+import { Json } from "./dataconnection/BufferedConnection/Json";
 
 class PeerOptions implements PeerJSOption {
 	/**
@@ -127,12 +127,12 @@ export class Peer extends EventEmitter<PeerEvents> {
 	private static readonly DEFAULT_KEY = "peerjs";
 
 	private readonly _serializers: SerializerMapping = {
-		raw: RawConnection,
-		json: JsonConnection,
-		binary: BinaryJSConnection,
-		"binary-utf8": BinaryJSConnection,
+		raw: Raw,
+		json: Json,
+		binary: BinaryPack,
+		"binary-utf8": BinaryPack,
 
-		default: BinaryJSConnection,
+		default: BinaryPack,
 	};
 	private readonly _options: PeerOptions;
 	private readonly _api: API;

+ 1 - 1
lib/servermessage.ts

@@ -1,4 +1,4 @@
-import { ServerMessageType } from "./enums";
+import type { ServerMessageType } from "./enums";
 
 export class ServerMessage {
 	type: ServerMessageType;

+ 9 - 63
lib/util.ts

@@ -1,5 +1,8 @@
+import { BinaryPackChunker } from "./dataconnection/BufferedConnection/binaryPackChunker";
 import * as BinaryPack from "peerjs-js-binarypack";
 import { Supports } from "./supports";
+import { validateId } from "./utils/validateId";
+import { randomToken } from "./utils/randomToken";
 
 export interface UtilSupportsObj {
 	/**
@@ -47,7 +50,7 @@ const DEFAULT_CONFIG = {
 	sdpSemantics: "unified-plan",
 };
 
-export class Util {
+export class Util extends BinaryPackChunker {
 	noop(): void {}
 
 	readonly CLOUD_HOST = "0.peerjs.com";
@@ -55,7 +58,6 @@ export class Util {
 
 	// Browsers that need chunking:
 	readonly chunkedBrowsers = { Chrome: 1, chrome: 1 };
-	readonly chunkedMTU = 16300; // The original 60000 bytes setting does not work when sending data from Firefox to Chrome, which is "cut off" after 16384 bytes and delivered individually.
 
 	// Returns browser-agnostic default config
 	readonly defaultConfig = DEFAULT_CONFIG;
@@ -63,6 +65,9 @@ export class Util {
 	readonly browser = Supports.getBrowser();
 	readonly browserVersion = Supports.getVersion();
 
+	pack = BinaryPack.pack;
+	unpack = BinaryPack.unpack;
+
 	/**
 	 * A hash of WebRTC features mapped to booleans that correspond to whether the feature is supported by the current browser.
 	 *
@@ -118,49 +123,8 @@ export class Util {
 	})();
 
 	// Ensure alphanumeric ids
-	validateId(id: string): boolean {
-		// Allow empty ids
-		return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.test(id);
-	}
-
-	pack = BinaryPack.pack;
-	unpack = BinaryPack.unpack;
-
-	// Binary stuff
-
-	private _dataCount: number = 1;
-
-	chunk(
-		blob: ArrayBuffer,
-	): { __peerData: number; n: number; total: number; data: Uint8Array }[] {
-		const chunks = [];
-		const size = blob.byteLength;
-		const total = Math.ceil(size / util.chunkedMTU);
-
-		let index = 0;
-		let start = 0;
-
-		while (start < size) {
-			const end = Math.min(size, start + util.chunkedMTU);
-			const b = blob.slice(start, end);
-
-			const chunk = {
-				__peerData: this._dataCount,
-				n: index,
-				data: b,
-				total,
-			};
-
-			chunks.push(chunk);
-
-			start = end;
-			index++;
-		}
-
-		this._dataCount++;
-
-		return chunks;
-	}
+	validateId = validateId;
+	randomToken = randomToken;
 
 	blobToArrayBuffer(
 		blob: Blob,
@@ -188,11 +152,6 @@ export class Util {
 
 		return byteArray.buffer;
 	}
-
-	randomToken(): string {
-		return Math.random().toString(36).slice(2);
-	}
-
 	isSecure(): boolean {
 		return location.protocol === "https:";
 	}
@@ -208,16 +167,3 @@ export class Util {
  * :::
  */
 export const util = new Util();
-export function concatArrayBuffers(bufs: Uint8Array[]) {
-	let size = 0;
-	for (const buf of bufs) {
-		size += buf.byteLength;
-	}
-	const result = new Uint8Array(size);
-	let offset = 0;
-	for (const buf of bufs) {
-		result.set(buf, offset);
-		offset += buf.byteLength;
-	}
-	return result;
-}

+ 1 - 0
lib/utils/randomToken.ts

@@ -0,0 +1 @@
+export const randomToken = () => Math.random().toString(36).slice(2);

+ 4 - 0
lib/utils/validateId.ts

@@ -0,0 +1,4 @@
+export const validateId = (id: string): boolean => {
+	// Allow empty ids
+	return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.test(id);
+};

+ 122 - 0
package-lock.json

@@ -9,6 +9,8 @@
 			"version": "1.4.7",
 			"license": "MIT",
 			"dependencies": {
+				"@msgpack/msgpack": "^2.8.0",
+				"cbor-x": "^1.5.3",
 				"eventemitter3": "^4.0.7",
 				"peerjs-js-binarypack": "^2.0.0",
 				"webrtc-adapter": "^8.0.0"
@@ -654,6 +656,78 @@
 			"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
 			"dev": true
 		},
+		"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz",
+			"integrity": "sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"darwin"
+			]
+		},
+		"node_modules/@cbor-extract/cbor-extract-darwin-x64": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.1.1.tgz",
+			"integrity": "sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"darwin"
+			]
+		},
+		"node_modules/@cbor-extract/cbor-extract-linux-arm": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.1.1.tgz",
+			"integrity": "sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ==",
+			"cpu": [
+				"arm"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@cbor-extract/cbor-extract-linux-arm64": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.1.1.tgz",
+			"integrity": "sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@cbor-extract/cbor-extract-linux-x64": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.1.1.tgz",
+			"integrity": "sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			]
+		},
+		"node_modules/@cbor-extract/cbor-extract-win32-x64": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz",
+			"integrity": "sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"win32"
+			]
+		},
 		"node_modules/@colors/colors": {
 			"version": "1.5.0",
 			"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -1544,6 +1618,14 @@
 				"node": ">=12.0.0"
 			}
 		},
+		"node_modules/@msgpack/msgpack": {
+			"version": "2.8.0",
+			"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
+			"integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==",
+			"engines": {
+				"node": ">= 10"
+			}
+		},
 		"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
 			"version": "3.0.2",
 			"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz",
@@ -5760,6 +5842,46 @@
 				"cdl": "bin/cdl.js"
 			}
 		},
+		"node_modules/cbor-extract": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.1.1.tgz",
+			"integrity": "sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA==",
+			"hasInstallScript": true,
+			"optional": true,
+			"dependencies": {
+				"node-gyp-build-optional-packages": "5.0.3"
+			},
+			"bin": {
+				"download-cbor-prebuilds": "bin/download-prebuilds.js"
+			},
+			"optionalDependencies": {
+				"@cbor-extract/cbor-extract-darwin-arm64": "2.1.1",
+				"@cbor-extract/cbor-extract-darwin-x64": "2.1.1",
+				"@cbor-extract/cbor-extract-linux-arm": "2.1.1",
+				"@cbor-extract/cbor-extract-linux-arm64": "2.1.1",
+				"@cbor-extract/cbor-extract-linux-x64": "2.1.1",
+				"@cbor-extract/cbor-extract-win32-x64": "2.1.1"
+			}
+		},
+		"node_modules/cbor-extract/node_modules/node-gyp-build-optional-packages": {
+			"version": "5.0.3",
+			"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz",
+			"integrity": "sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==",
+			"optional": true,
+			"bin": {
+				"node-gyp-build-optional-packages": "bin.js",
+				"node-gyp-build-optional-packages-optional": "optional.js",
+				"node-gyp-build-optional-packages-test": "build-test.js"
+			}
+		},
+		"node_modules/cbor-x": {
+			"version": "1.5.3",
+			"resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.5.3.tgz",
+			"integrity": "sha512-adrN0S67C7jY2hgqeGcw+Uj6iEGLQa5D/p6/9YNl5AaVIYJaJz/bARfWsP8UikBZWbhS27LN0DJK4531vo9ODw==",
+			"optionalDependencies": {
+				"cbor-extract": "^2.1.1"
+			}
+		},
 		"node_modules/chainsaw": {
 			"version": "0.1.0",
 			"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",

+ 30 - 0
package.json

@@ -107,6 +107,8 @@
 	"module": "dist/bundler.mjs",
 	"browser-minified": "dist/peerjs.min.js",
 	"browser-unminified": "dist/peerjs.js",
+	"browser-minified-cbor": "dist/serializer.cbor.mjs",
+	"browser-minified-msgpack": "dist/serializer.msgpack.mjs",
 	"types": "dist/types.d.ts",
 	"engines": {
 		"node": ">= 14"
@@ -147,6 +149,28 @@
 				"browsers": "chrome >= 83, edge >= 83, firefox >= 80, safari >= 15"
 			},
 			"source": "lib/global.ts"
+		},
+		"browser-minified-cbor": {
+			"context": "node",
+			"outputFormat": "esmodule",
+			"includeNodeModules": true,
+			"isLibrary": true,
+			"optimize": true,
+			"engines": {
+				"browsers": "chrome >= 83, edge >= 83, firefox >= 102, safari >= 15"
+			},
+			"source": "lib/dataconnection/StreamConnection/Cbor.ts"
+		},
+		"browser-minified-msgpack": {
+			"context": "browser",
+			"outputFormat": "esmodule",
+			"includeNodeModules": true,
+			"isLibrary": true,
+			"optimize": true,
+			"engines": {
+				"browsers": "chrome >= 83, edge >= 83, firefox >= 102, safari >= 15"
+			},
+			"source": "lib/dataconnection/StreamConnection/MsgPack.ts"
 		}
 	},
 	"scripts": {
@@ -193,8 +217,14 @@
 		"wdio-geckodriver-service": "^5.0.1"
 	},
 	"dependencies": {
+		"@msgpack/msgpack": "^2.8.0",
+		"cbor-x": "^1.5.3",
 		"eventemitter3": "^4.0.7",
 		"peerjs-js-binarypack": "^2.0.0",
 		"webrtc-adapter": "^8.0.0"
+	},
+	"alias": {
+		"process": false,
+		"buffer": false
 	}
 }