From 9118b0a8e5ff8ef6e48f824c486d10b30bfc7360 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 26 Aug 2025 12:15:07 -0400 Subject: [PATCH 1/3] feat: use vlq instead of base64 for encoding/decoding --- .changeset/famous-words-invite.md | 5 +++++ package.json | 5 ++++- pnpm-lock.yaml | 33 +++++++++++++++++++------------ src/parse.js | 5 ++--- src/stringify.js | 7 ++----- test/test.js | 8 ++++---- 6 files changed, 37 insertions(+), 26 deletions(-) create mode 100644 .changeset/famous-words-invite.md diff --git a/.changeset/famous-words-invite.md b/.changeset/famous-words-invite.md new file mode 100644 index 0000000..e4b532e --- /dev/null +++ b/.changeset/famous-words-invite.md @@ -0,0 +1,5 @@ +--- +'devalue': minor +--- + +feat: use vlq instead of base64 for encoding/decoding diff --git a/package.json b/package.json index 5b49dcc..e9266b1 100644 --- a/package.json +++ b/package.json @@ -34,5 +34,8 @@ }, "license": "MIT", "type": "module", - "packageManager": "pnpm@8.15.9" + "packageManager": "pnpm@8.15.9", + "dependencies": { + "vlq": "^2.0.4" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b88a77..88057b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +dependencies: + vlq: + specifier: ^2.0.4 + version: 2.0.4 + devDependencies: '@changesets/cli': specifier: ^2.29.6 @@ -255,6 +260,13 @@ packages: '@jridgewell/sourcemap-codec': 1.5.5 dev: true + /@js-temporal/polyfill@0.5.1: + resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} + engines: {node: '>=12'} + dependencies: + jsbi: 4.3.2 + dev: true + /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: @@ -283,15 +295,6 @@ packages: run-parallel: 1.2.0 dev: true - /@js-temporal/polyfill@0.5.1: - resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} - engines: {node: '>=12'} - dependencies: - jsbi: 4.3.2 - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} /@nodelib/fs.stat@2.0.5: resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} @@ -567,16 +570,16 @@ packages: esprima: 4.0.1 dev: true + /jsbi@4.3.2: + resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} + dev: true + /jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} optionalDependencies: graceful-fs: 4.2.11 dev: true - /jsbi@4.3.2: - resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} - dev: true - /kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -864,6 +867,10 @@ packages: sade: 1.8.1 dev: true + /vlq@2.0.4: + resolution: {integrity: sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA==} + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} diff --git a/src/parse.js b/src/parse.js index f5d5b09..4f526bd 100644 --- a/src/parse.js +++ b/src/parse.js @@ -1,4 +1,5 @@ import { decode64 } from './base64.js'; +import * as vlq from 'vlq'; import { HOLE, NAN, @@ -125,9 +126,7 @@ export function unflatten(parsed, revivers) { } case 'ArrayBuffer': { - const base64 = value[1]; - const arraybuffer = decode64(base64); - hydrated[index] = arraybuffer; + hydrated[index] = new Uint8Array(vlq.decode(value[1])).buffer; break; } diff --git a/src/stringify.js b/src/stringify.js index 4cb6fa5..383e603 100644 --- a/src/stringify.js +++ b/src/stringify.js @@ -1,3 +1,4 @@ +import * as vlq from 'vlq'; import { DevalueError, enumerable_symbols, @@ -177,11 +178,7 @@ export function stringify(value, reducers) { } case 'ArrayBuffer': { - /** @type {ArrayBuffer} */ - const arraybuffer = thing; - const base64 = encode64(arraybuffer); - - str = `["ArrayBuffer","${base64}"]`; + str = `["ArrayBuffer","${vlq.encode(new Uint8Array(thing))}"]`; break; } diff --git a/test/test.js b/test/test.js index dcd4d0f..2902ff0 100644 --- a/test/test.js +++ b/test/test.js @@ -172,13 +172,13 @@ const fixtures = { name: 'Uint8Array', value: new Uint8Array([1, 2, 3]), js: 'new Uint8Array([1,2,3])', - json: '[["Uint8Array",1],["ArrayBuffer","AQID"]]' + json: '[["Uint8Array",1],["ArrayBuffer","CEG"]]' }, { name: 'ArrayBuffer', value: new Uint8Array([1, 2, 3]).buffer, js: 'new Uint8Array([1,2,3]).buffer', - json: '[["ArrayBuffer","AQID"]]' + json: '[["ArrayBuffer","CEG"]]' }, { name: 'URL', @@ -198,7 +198,7 @@ const fixtures = { name: 'Sliced typed array', value: new Uint16Array([10, 20, 30, 40]).subarray(1, 3), js: 'new Uint16Array([10,20,30,40]).subarray(1,3)', - json: '[["Uint16Array",1,1,3],["ArrayBuffer","CgAUAB4AKAA="]]' + json: '[["Uint16Array",1,1,3],["ArrayBuffer","UAoBA8BAwCA"]]' }, { name: 'Temporal.Duration', @@ -478,7 +478,7 @@ const fixtures = { return [uint8, uint16]; })(), js: '(function(a){return [new Uint8Array([a]),new Uint16Array([a])]}(new Uint8Array([0,1,2,3,4,5,6,7,8,9]).buffer))', - json: '[[1,3],["Uint8Array",2],["ArrayBuffer","AAECAwQFBgcICQ=="],["Uint16Array",2]]', + json: '[[1,3],["Uint8Array",2],["ArrayBuffer","ACEGIKMOQS"],["Uint16Array",2]]', validate: ([uint8, uint16]) => { return uint8.buffer === uint16.buffer; } From 305ce00fdf39143ede1248e31ea5fe7162b1a724 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 26 Aug 2025 12:26:23 -0400 Subject: [PATCH 2/3] remove unused code --- src/base64.js | 110 ----------------------------------------------- src/parse.js | 1 - src/stringify.js | 1 - 3 files changed, 112 deletions(-) delete mode 100644 src/base64.js diff --git a/src/base64.js b/src/base64.js deleted file mode 100644 index 23bd6b6..0000000 --- a/src/base64.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Base64 Encodes an arraybuffer - * @param {ArrayBuffer} arraybuffer - * @returns {string} - */ -export function encode64(arraybuffer) { - const dv = new DataView(arraybuffer); - let binaryString = ""; - - for (let i = 0; i < arraybuffer.byteLength; i++) { - binaryString += String.fromCharCode(dv.getUint8(i)); - } - - return binaryToAscii(binaryString); -} - -/** - * Decodes a base64 string into an arraybuffer - * @param {string} string - * @returns {ArrayBuffer} - */ -export function decode64(string) { - const binaryString = asciiToBinary(string); - const arraybuffer = new ArrayBuffer(binaryString.length); - const dv = new DataView(arraybuffer); - - for (let i = 0; i < arraybuffer.byteLength; i++) { - dv.setUint8(i, binaryString.charCodeAt(i)); - } - - return arraybuffer; -} - -const KEY_STRING = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -/** - * Substitute for atob since it's deprecated in node. - * Does not do any input validation. - * - * @see https://github.com/jsdom/abab/blob/master/lib/atob.js - * - * @param {string} data - * @returns {string} - */ -function asciiToBinary(data) { - if (data.length % 4 === 0) { - data = data.replace(/==?$/, ""); - } - - let output = ""; - let buffer = 0; - let accumulatedBits = 0; - - for (let i = 0; i < data.length; i++) { - buffer <<= 6; - buffer |= KEY_STRING.indexOf(data[i]); - accumulatedBits += 6; - if (accumulatedBits === 24) { - output += String.fromCharCode((buffer & 0xff0000) >> 16); - output += String.fromCharCode((buffer & 0xff00) >> 8); - output += String.fromCharCode(buffer & 0xff); - buffer = accumulatedBits = 0; - } - } - if (accumulatedBits === 12) { - buffer >>= 4; - output += String.fromCharCode(buffer); - } else if (accumulatedBits === 18) { - buffer >>= 2; - output += String.fromCharCode((buffer & 0xff00) >> 8); - output += String.fromCharCode(buffer & 0xff); - } - return output; -} - -/** - * Substitute for btoa since it's deprecated in node. - * Does not do any input validation. - * - * @see https://github.com/jsdom/abab/blob/master/lib/btoa.js - * - * @param {string} str - * @returns {string} - */ -function binaryToAscii(str) { - let out = ""; - for (let i = 0; i < str.length; i += 3) { - /** @type {[number, number, number, number]} */ - const groupsOfSix = [undefined, undefined, undefined, undefined]; - groupsOfSix[0] = str.charCodeAt(i) >> 2; - groupsOfSix[1] = (str.charCodeAt(i) & 0x03) << 4; - if (str.length > i + 1) { - groupsOfSix[1] |= str.charCodeAt(i + 1) >> 4; - groupsOfSix[2] = (str.charCodeAt(i + 1) & 0x0f) << 2; - } - if (str.length > i + 2) { - groupsOfSix[2] |= str.charCodeAt(i + 2) >> 6; - groupsOfSix[3] = str.charCodeAt(i + 2) & 0x3f; - } - for (let j = 0; j < groupsOfSix.length; j++) { - if (typeof groupsOfSix[j] === "undefined") { - out += "="; - } else { - out += KEY_STRING[groupsOfSix[j]]; - } - } - } - return out; -} diff --git a/src/parse.js b/src/parse.js index 4f526bd..2196d9d 100644 --- a/src/parse.js +++ b/src/parse.js @@ -1,4 +1,3 @@ -import { decode64 } from './base64.js'; import * as vlq from 'vlq'; import { HOLE, diff --git a/src/stringify.js b/src/stringify.js index 383e603..d9030bc 100644 --- a/src/stringify.js +++ b/src/stringify.js @@ -16,7 +16,6 @@ import { POSITIVE_INFINITY, UNDEFINED } from './constants.js'; -import { encode64 } from './base64.js'; /** * Turn a value into a JSON string that can be parsed with `devalue.parse` From 3066e6e066b94467795f2e9932bfb8a76c664aa0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 26 Aug 2025 12:56:06 -0400 Subject: [PATCH 3/3] update tsconfig --- tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 27f54d8..542e6ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,9 @@ "noImplicitThis": true, "noEmitOnError": true, "lib": ["es6", "es2020", "dom"], - "target": "es2020" + "target": "es2020", + "module": "nodenext", + "moduleResolution": "nodenext" }, "module": "ES6", "include": ["index.js", "src/*.js"],