p2p-media-loader-core
Version:
P2P Media Loader core functionality
197 lines (167 loc) • 5.76 kB
text/typescript
import { joinChunks } from "../../utils/utils.js";
// restricted up to 16 item types (4 bits to type definition)
export const enum SerializedItem {
Min = -1,
Int,
SimilarIntArray,
String,
Max,
}
function abs(num: bigint): bigint {
return num < 0 ? -num : num;
}
function getRequiredBytesForInt(num: bigint): number {
const binaryString = num.toString(2);
const necessaryBits = num < 0 ? binaryString.length : binaryString.length + 1;
return Math.ceil(necessaryBits / 8);
}
function intToBytes(num: bigint): Uint8Array {
const isNegative = num < 0;
const bytesAmountNumber = getRequiredBytesForInt(num);
const bytes = new Uint8Array(bytesAmountNumber);
const bytesAmount = BigInt(bytesAmountNumber);
num = abs(num);
for (let i = 0; i < bytesAmountNumber; i++) {
const shift = 8n * (bytesAmount - 1n - BigInt(i));
const byte = (num >> shift) & 0xffn;
bytes[i] = Number(byte);
}
if (isNegative) bytes[0] = bytes[0] | 0b10000000;
return bytes;
}
function bytesToInt(bytes: Uint8Array): bigint {
const byteLength = BigInt(bytes.length);
const getNumberPart = (byte: number, i: number): bigint => {
const shift = 8n * (byteLength - 1n - BigInt(i));
return BigInt(byte) << shift;
};
// ignore first bit of first byte as it is sign bit
let number = getNumberPart(bytes[0] & 0b01111111, 0);
for (let i = 1; i < byteLength; i++) {
number = getNumberPart(bytes[i], i) | number;
}
if ((bytes[0] & 0b10000000) >> 7 !== 0) number = -number;
return number;
}
export function serializeInt(num: bigint): Uint8Array {
const numBytes = intToBytes(num);
const numberMetadata = (SerializedItem.Int << 4) | numBytes.length;
return new Uint8Array([numberMetadata, ...numBytes]);
}
export function deserializeInt(bytes: Uint8Array) {
const metadata = bytes[0];
const code: SerializedItem = metadata >> 4;
if (code !== SerializedItem.Int) {
throw new Error(
"Trying to deserialize integer with invalid serialized item code",
);
}
const numberBytesLength = metadata & 0b1111;
const start = 1;
const end = start + numberBytesLength;
return {
number: bytesToInt(bytes.slice(start, end)),
byteLength: numberBytesLength + 1,
};
}
export function serializeSimilarIntArray(numbers: bigint[]) {
const commonPartNumbersMap = new Map<bigint, ResizableUint8Array>();
for (const number of numbers) {
const common = number & ~0xffn;
const diffByte = number & 0xffn;
const bytes = commonPartNumbersMap.get(common) ?? new ResizableUint8Array();
if (!bytes.length) commonPartNumbersMap.set(common, bytes);
bytes.push(Number(diffByte));
}
const result = new ResizableUint8Array();
result.push([SerializedItem.SimilarIntArray << 4, commonPartNumbersMap.size]);
for (const [commonPart, binaryArray] of commonPartNumbersMap) {
const { length } = binaryArray.getBytesChunks();
const commonPartWithLength = commonPart | (BigInt(length) & 0xffn);
binaryArray.unshift(serializeInt(commonPartWithLength));
result.push(binaryArray.getBuffer());
}
return result.getBuffer();
}
export function deserializeSimilarIntArray(bytes: Uint8Array) {
const [codeByte, commonPartArraysAmount] = bytes;
const code: SerializedItem = codeByte >> 4;
if (code !== SerializedItem.SimilarIntArray) {
throw new Error(
"Trying to deserialize similar int array with invalid serialized item code",
);
}
let offset = 2;
const originalIntArr: bigint[] = [];
for (let i = 0; i < commonPartArraysAmount; i++) {
const { number: commonPartWithLength, byteLength } = deserializeInt(
bytes.slice(offset),
);
offset += byteLength;
const arrayLength = commonPartWithLength & 0xffn;
const commonPart = commonPartWithLength & ~0xffn;
for (let j = 0; j < arrayLength; j++) {
const diffPart = BigInt(bytes[offset]);
originalIntArr.push(commonPart | diffPart);
offset++;
}
}
return { numbers: originalIntArr, byteLength: offset };
}
export function serializeString(string: string) {
const { length } = string;
const bytes = new ResizableUint8Array();
bytes.push([
(SerializedItem.String << 4) | ((length >> 8) & 0x0f),
length & 0xff,
]);
bytes.push(new TextEncoder().encode(string));
return bytes.getBuffer();
}
export function deserializeString(bytes: Uint8Array) {
const [codeByte, lengthByte] = bytes;
const code: SerializedItem = codeByte >> 4;
if (code !== SerializedItem.String) {
throw new Error(
"Trying to deserialize bytes (sting) with invalid serialized item code.",
);
}
const length = ((codeByte & 0x0f) << 8) | lengthByte;
const stringBytes = bytes.slice(2, length + 2);
const string = new TextDecoder("utf8").decode(stringBytes);
return { string, byteLength: length + 2 };
}
export class ResizableUint8Array {
private bytes: Uint8Array[] = [];
private _length = 0;
push(bytes: Uint8Array | number | number[]) {
this.addBytes(bytes, "end");
}
unshift(bytes: Uint8Array | number | number[]) {
this.addBytes(bytes, "start");
}
private addBytes(
bytes: Uint8Array | number | number[],
position: "start" | "end",
) {
let bytesToAdd: Uint8Array;
if (bytes instanceof Uint8Array) {
bytesToAdd = bytes;
} else if (Array.isArray(bytes)) {
bytesToAdd = new Uint8Array(bytes);
} else {
bytesToAdd = new Uint8Array([bytes]);
}
this._length += bytesToAdd.length;
this.bytes[position === "start" ? "unshift" : "push"](bytesToAdd);
}
getBytesChunks(): readonly Uint8Array[] {
return this.bytes;
}
getBuffer(): Uint8Array {
return joinChunks(this.bytes, this._length);
}
get length() {
return this._length;
}
}