@webbuf/webbuf
Version:
Rust/WASM optimized buffers for the web, node.js, deno, and bun.
286 lines (285 loc) • 9.51 kB
JavaScript
import { encode_base64, decode_base64, decode_base64_strip_whitespace, encode_hex, decode_hex, } from "./rs-webbuf-inline-base64/webbuf.js";
function verifyOffset(offset, ext, length) {
if (offset % 1 !== 0 || offset < 0) {
throw new Error("offset is not uint");
}
if (offset + ext > length) {
throw new Error("Trying to access beyond buffer length");
}
}
export class WebBuf extends Uint8Array {
static concat(list) {
const size = list.reduce((acc, buf) => acc + buf.length, 0);
const result = new WebBuf(size);
let offset = 0;
for (const buf of list) {
result.set(buf, offset);
offset += buf.length;
}
return result;
}
static alloc(size, fill = 0) {
const buf = new WebBuf(size);
if (fill !== 0) {
buf.fill(fill);
}
return buf;
}
fill(value, start = 0, end = this.length) {
for (let i = start; i < end; i++) {
this[i] = value;
}
return this;
}
// Override slice method to return WebBuf instead of Uint8Array
slice(start, end) {
const slicedArray = super.slice(start, end); // Create a slice using Uint8Array's slice method
return new WebBuf(slicedArray.buffer, slicedArray.byteOffset, slicedArray.byteLength); // Return a WebBuf instead
}
subarray(start, end) {
const subArray = super.subarray(start, end);
return new WebBuf(subArray.buffer, subArray.byteOffset, subArray.byteLength);
}
/**
* Reverse the buffer in place
* @returns webbuf
*/
reverse() {
super.reverse();
return this;
}
clone() {
return new WebBuf(this);
}
toReverse() {
const cloned = new WebBuf(this);
cloned.reverse();
return cloned;
}
copy(target, targetStart = 0, sourceStart = 0, sourceEnd = this.length) {
if (sourceStart >= sourceEnd) {
return 0;
}
if (targetStart >= target.length) {
throw new Error("targetStart out of bounds");
}
if (sourceEnd > this.length) {
throw new Error("sourceEnd out of bounds");
}
if (targetStart + sourceEnd - sourceStart > target.length) {
throw new Error("source is too large");
}
target.set(this.subarray(sourceStart, sourceEnd), targetStart);
return sourceEnd - sourceStart;
}
/**
* Return a WebBuf that is a view of the same data as the input Uint8Array
*
* @param buffer
* @returns WebBuf
*/
static view(buffer) {
return new WebBuf(buffer.buffer, buffer.byteOffset, buffer.byteLength);
}
/**
* Create a new WebBuf from a Uint8Array (copy)
* @param buffer
* @returns webbuf
*/
static fromUint8Array(buffer) {
return new WebBuf(buffer);
}
static fromArray(array) {
return new WebBuf(array);
}
static fromUtf8(str) {
const encoder = new TextEncoder();
return new WebBuf(encoder.encode(str));
}
static fromString(str, encoding = "utf8") {
if (encoding === "hex") {
return WebBuf.fromHex(str);
}
if (encoding === "base64") {
return WebBuf.fromBase64(str);
}
if (encoding === "utf8") {
return WebBuf.fromUtf8(str);
}
return WebBuf.fromUtf8(str);
}
// we use wasm for big data, because small data is faster in js
// experiments show wasm is always faster
static FROM_BASE64_ALGO_THRESHOLD = 10; // str len
// experiments show wasm is always faster
static TO_BASE64_ALGO_THRESHOLD = 10; // buf len
// experimentally derived for optimal performance
static FROM_HEX_ALGO_THRESHOLD = 1_000; // str len
// experiments show wasm is always faster
static TO_HEX_ALGO_THRESHOLD = 10; // buf len
static fromHexPureJs(hex) {
const result = new WebBuf(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
result[i / 2] = Number.parseInt(hex.slice(i, i + 2), 16);
}
return result;
}
static fromHexWasm(hex) {
const uint8array = decode_hex(hex);
return new WebBuf(uint8array.buffer, uint8array.byteOffset, uint8array.byteLength);
}
static fromHex(hex) {
if (hex.length % 2 !== 0) {
throw new Error("Invalid hex string");
}
if (hex.length < WebBuf.FROM_HEX_ALGO_THRESHOLD) {
return WebBuf.fromHexPureJs(hex);
}
return WebBuf.fromHexWasm(hex);
}
toHexPureJs() {
return Array.from(this)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
toHexWasm() {
return encode_hex(this);
}
toHex() {
// disabled: experiments show this is not faster, even for small buffers
// if (this.length < WebBuf.TO_HEX_ALGO_THRESHOLD) {
// return this.toHexPureJs();
// }
return this.toHexWasm();
}
static fromBase64PureJs(b64, stripWhitespace = false) {
const uint8array = new Uint8Array(atob(stripWhitespace ? b64.replace(/\s+/g, "") : b64)
.split("")
.map((c) => c.charCodeAt(0)));
return new WebBuf(uint8array.buffer, uint8array.byteOffset, uint8array.byteLength);
}
static fromBase64Wasm(b64, stripWhitespace = false) {
const uint8array = stripWhitespace
? decode_base64_strip_whitespace(b64)
: decode_base64(b64);
return new WebBuf(uint8array.buffer, uint8array.byteOffset, uint8array.byteLength);
}
/**
* Convert a base64 string to a Uint8Array. Tolerant of whitespace, but
* throws if the string has invalid characters.
*
* @param b64
* @returns Uint8Array
* @throws {Error} if the input string is not valid base64
*/
static fromBase64(b64, stripWhitespace = false) {
// disabled: experiments show this is not faster, even for small buffers
// if (b64.length < WebBuf.FROM_BASE64_ALGO_THRESHOLD) {
// return WebBuf.fromBase64PureJs(b64, stripWhitespace);
// }
return WebBuf.fromBase64Wasm(b64, stripWhitespace);
}
toBase64PureJs() {
return btoa(String.fromCharCode(...new Uint8Array(this)));
}
toBase64Wasm() {
return encode_base64(this);
}
toBase64() {
// disabled: experiments show this is not faster, even for small buffers
// if (this.length < WebBuf.TO_BASE64_ALGO_THRESHOLD) {
// return this.toBase64PureJs();
// }
return this.toBase64Wasm();
}
/**
* Override Uint8Array.from to return a WebBuf
*
* @param source An array-like or iterable object to convert to WebBuf
* @param mapFn Optional map function to call on every element of the array
* @param thisArg Optional value to use as `this` when executing `mapFn`
* @returns WebBuf
*/
static from(source, mapFn,
// biome-ignore lint:
thisArg) {
if (typeof mapFn === "string") {
if (typeof source !== "string") {
throw new TypeError("Invalid mapFn");
}
if (mapFn === "hex") {
return WebBuf.fromHex(source);
}
if (mapFn === "base64") {
return WebBuf.fromBase64(source);
}
if (mapFn === "utf8") {
return WebBuf.fromUtf8(source);
}
throw new TypeError("Invalid mapFn");
}
if (typeof source === "string") {
return WebBuf.fromUtf8(source);
}
if (source instanceof Uint8Array) {
return WebBuf.view(source);
}
const sourceArray = Array.from(source);
// biome-ignore lint:
const uint8Array = super.from(sourceArray, mapFn, thisArg);
return new WebBuf(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
}
toUtf8() {
const decoder = new TextDecoder();
return decoder.decode(this);
}
toString(encoding) {
if (encoding === "hex") {
return this.toHex();
}
if (encoding === "base64") {
return this.toBase64();
}
if (encoding === "utf8") {
const decoder = new TextDecoder();
return decoder.decode(this);
}
return this.toUtf8();
}
inspect() {
return `<WebBuf ${this.toHex().slice(0, 40) + (this.length > 40 ? "..." : "")}>`;
}
toArray() {
return Array.from(this);
}
compare(other) {
const len = Math.min(this.length, other.length);
for (let i = 0; i < len; i++) {
if (this[i] !== other[i]) {
return this[i] < other[i] ? -1 : 1;
}
}
if (this.length < other.length) {
return -1;
}
if (this.length > other.length) {
return 1;
}
return 0;
}
static compare(buf1, buf2) {
return buf1.compare(buf2);
}
equals(other) {
return this.compare(other) === 0;
}
write(buf, offset = 0) {
verifyOffset(offset, buf.length, this.length);
this.set(buf, offset);
return buf.length;
}
read(offset, ext) {
verifyOffset(offset, ext, this.length);
return this.subarray(offset, offset + ext);
}
}