diff --git a/benchmark.mjs b/benchmark.mjs new file mode 100644 index 0000000..9a01ac4 --- /dev/null +++ b/benchmark.mjs @@ -0,0 +1,49 @@ +/* eslint no-undef: "off" */ + +import {Buffer} from 'node:buffer'; +import {randomBytes} from 'node:crypto'; +import benchmark from 'benchmark'; +import { + base64ToString, + concatUint8Arrays, + hexToUint8Array, + isUint8Array, + stringToBase64, + stringToUint8Array, + uint8ArrayToBase64, + uint8ArrayToHex, + uint8ArrayToString, +} from './index.js'; + +const oneMb = 1024 * 1024; +const largeUint8Array = new Uint8Array(randomBytes(oneMb).buffer); +const textFromUint8Array = uint8ArrayToString(largeUint8Array); +const base64FromUint8Array = Buffer.from(textFromUint8Array).toString('base64'); +const hexFromUint8Array = uint8ArrayToHex(largeUint8Array); + +const suite = new benchmark.Suite(); + +suite.add('isUint8Array', () => isUint8Array(largeUint8Array)); + +suite.add('concatUint8Arrays with 2 arrays', () => concatUint8Arrays([largeUint8Array, largeUint8Array])); + +suite.add('concatUint8Arrays with 3 arrays', () => concatUint8Arrays([largeUint8Array, largeUint8Array])); + +suite.add('concatUint8Arrays with 4 arrays', () => concatUint8Arrays([largeUint8Array, largeUint8Array, largeUint8Array, largeUint8Array])); + +suite.add('uint8ArrayToString', () => uint8ArrayToString(largeUint8Array)); + +suite.add('stringToUint8Array', () => stringToUint8Array(textFromUint8Array)); + +suite.add('uint8ArrayToBase64', () => uint8ArrayToBase64(largeUint8Array)); + +suite.add('stringToBase64', () => stringToBase64(textFromUint8Array)); + +suite.add('base64ToString', () => base64ToString(base64FromUint8Array)); + +suite.add('uint8ArrayToHex', () => uint8ArrayToHex(largeUint8Array)); + +suite.add('hexToUint8Array', () => hexToUint8Array(hexFromUint8Array)); + +suite.on('cycle', event => console.log(event.target.toString())); +suite.run({async: false}); diff --git a/index.js b/index.js index bbe02f5..562945a 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,16 @@ const objectToString = Object.prototype.toString; +const uint8ArrayStringified = '[object Uint8Array]'; export function isUint8Array(value) { - return value && objectToString.call(value) === '[object Uint8Array]'; + if (!value) { + return false; + } + + if (value.constructor === Uint8Array) { + return true; + } + + return objectToString.call(value) === uint8ArrayStringified; } export function assertUint8Array(value) { @@ -92,9 +101,11 @@ export function compareUint8Arrays(a, b) { return 0; } +const cachedDecoder = new globalThis.TextDecoder(); + export function uint8ArrayToString(array) { assertUint8Array(array); - return (new globalThis.TextDecoder()).decode(array); + return cachedDecoder.decode(array); } function assertString(value) { @@ -103,9 +114,11 @@ function assertString(value) { } } +const cachedEncoder = new globalThis.TextEncoder(); + export function stringToUint8Array(string) { assertString(string); - return (new globalThis.TextEncoder()).encode(string); + return cachedEncoder.encode(string); } function base64ToBase64Url(base64) { @@ -116,11 +129,25 @@ function base64UrlToBase64(base64url) { return base64url.replaceAll('-', '+').replaceAll('_', '/'); } +// Reference: https://phuoc.ng/collection/this-vs-that/concat-vs-push/ +const MAX_BLOCK_SIZE = 65_535; + export function uint8ArrayToBase64(array, {urlSafe = false} = {}) { assertUint8Array(array); + let base64; + + if (array.length < MAX_BLOCK_SIZE) { // Required as `btoa` and `atob` don't properly support Unicode: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem - const base64 = globalThis.btoa(String.fromCodePoint(...array)); + base64 = globalThis.btoa(String.fromCodePoint.apply(this, array)); + } else { + base64 = ''; + for (const value of array) { + base64 += String.fromCodePoint(value); + } + + base64 = globalThis.btoa(base64); + } return urlSafe ? base64ToBase64Url(base64) : base64; } diff --git a/package.json b/package.json index dcef9ec..58c945d 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "devDependencies": { "ava": "^5.3.1", "typescript": "^5.2.2", - "xo": "^0.56.0" + "xo": "^0.56.0", + "benchmark": "2.1.4" } } diff --git a/test.js b/test.js index 92fd192..afa914a 100644 --- a/test.js +++ b/test.js @@ -126,6 +126,12 @@ test('uint8ArrayToBase64 and base64ToUint8Array', t => { t.deepEqual(base64ToUint8Array(base64), fixture); }); +test('should handle uint8ArrayToBase64 with 200k items', t => { + const fixture = stringToUint8Array('H'.repeat(200_000)); + const base64 = uint8ArrayToBase64(fixture); + t.deepEqual(base64ToUint8Array(base64), fixture); +}); + test('uint8ArrayToBase64 and base64ToUint8Array #2', t => { const fixture = stringToUint8Array('a Ā 𐀀 文 🦄'); t.deepEqual(base64ToUint8Array(uint8ArrayToBase64(base64ToUint8Array(uint8ArrayToBase64(fixture)))), fixture);