Skip to content

Commit bbf86c2

Browse files
committed
Add support for ArrayBuffers and TypedArrays
1 parent 50af63e commit bbf86c2

File tree

7 files changed

+238
-0
lines changed

7 files changed

+238
-0
lines changed

playrgound.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { stringify } from "./src/stringify.js"
2+
import { parse} from "./src/parse.js"
3+
4+
5+
const thing = new Uint8Array(3);
6+
thing[0] = 1;
7+
thing[1] = 2;
8+
thing[2] = 3;
9+
10+
const otherThing = new Float32Array(10);
11+
otherThing[0] = -Infinity;
12+
13+
14+
const a = {
15+
foo: thing,
16+
bar: otherThing,
17+
buff: otherThing.buffer
18+
}
19+
20+
21+
const stringified = stringify(a);
22+
const parsed = parse(stringified);
23+
console.log(a, stringified, parsed);

src/base64.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Base64 Encodes an arraybuffer
3+
* @param {ArrayBuffer} arraybuffer
4+
* @returns {string}
5+
*/
6+
export function encode64(arraybuffer) {
7+
const dv = new DataView(arraybuffer);
8+
let binaryString = "";
9+
10+
for (let i = 0; i < arraybuffer.byteLength; i++) {
11+
binaryString += String.fromCharCode(dv.getUint8(i));
12+
}
13+
14+
return binaryToAscii(binaryString);
15+
}
16+
17+
/**
18+
* Decodes a base64 string into an arraybuffer
19+
* @param {string} string
20+
* @returns {ArrayBuffer}
21+
*/
22+
export function decode64(string) {
23+
const binaryString = asciiToBinary(string);
24+
const arraybuffer = new ArrayBuffer(binaryString.length);
25+
const dv = new DataView(arraybuffer);
26+
27+
for (let i = 0; i < arraybuffer.byteLength; i++) {
28+
dv.setUint8(i, binaryString.charCodeAt(i));
29+
}
30+
31+
return arraybuffer;
32+
}
33+
34+
const KEY_STRING =
35+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
36+
37+
/**
38+
* Substitute for atob since it's deprecated in node.
39+
* Does not do any input validation.
40+
*
41+
* @see https://github.com/jsdom/abab/blob/master/lib/atob.js
42+
*
43+
* @param {string} data
44+
* @returns {string}
45+
*/
46+
function asciiToBinary(data) {
47+
if (data.length % 4 === 0) {
48+
data = data.replace(/==?$/, "");
49+
}
50+
51+
let output = "";
52+
let buffer = 0;
53+
let accumulatedBits = 0;
54+
55+
for (let i = 0; i < data.length; i++) {
56+
buffer <<= 6;
57+
buffer |= KEY_STRING.indexOf(data[i]);
58+
accumulatedBits += 6;
59+
if (accumulatedBits === 24) {
60+
output += String.fromCharCode((buffer & 0xff0000) >> 16);
61+
output += String.fromCharCode((buffer & 0xff00) >> 8);
62+
output += String.fromCharCode(buffer & 0xff);
63+
buffer = accumulatedBits = 0;
64+
}
65+
}
66+
if (accumulatedBits === 12) {
67+
buffer >>= 4;
68+
output += String.fromCharCode(buffer);
69+
} else if (accumulatedBits === 18) {
70+
buffer >>= 2;
71+
output += String.fromCharCode((buffer & 0xff00) >> 8);
72+
output += String.fromCharCode(buffer & 0xff);
73+
}
74+
return output;
75+
}
76+
77+
/**
78+
* Substitute for btoa since it's deprecated in node.
79+
* Does not do any input validation.
80+
*
81+
* @see https://github.com/jsdom/abab/blob/master/lib/btoa.js
82+
*
83+
* @param {string} str
84+
* @returns {string}
85+
*/
86+
function binaryToAscii(str) {
87+
let out = "";
88+
for (let i = 0; i < str.length; i += 3) {
89+
/** @type {[number, number, number, number]} */
90+
const groupsOfSix = [undefined, undefined, undefined, undefined];
91+
groupsOfSix[0] = str.charCodeAt(i) >> 2;
92+
groupsOfSix[1] = (str.charCodeAt(i) & 0x03) << 4;
93+
if (str.length > i + 1) {
94+
groupsOfSix[1] |= str.charCodeAt(i + 1) >> 4;
95+
groupsOfSix[2] = (str.charCodeAt(i + 1) & 0x0f) << 2;
96+
}
97+
if (str.length > i + 2) {
98+
groupsOfSix[2] |= str.charCodeAt(i + 2) >> 6;
99+
groupsOfSix[3] = str.charCodeAt(i + 2) & 0x3f;
100+
}
101+
for (let j = 0; j < groupsOfSix.length; j++) {
102+
if (typeof groupsOfSix[j] === "undefined") {
103+
out += "=";
104+
} else {
105+
out += KEY_STRING[groupsOfSix[j]];
106+
}
107+
}
108+
}
109+
return out;
110+
}

src/parse.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { decode64 } from './base64.js';
12
import {
23
HOLE,
34
NAN,
@@ -101,6 +102,32 @@ export function unflatten(parsed, revivers) {
101102
}
102103
break;
103104

105+
case "Int8Array":
106+
case "Uint8Array":
107+
case "Uint8ClampedArray":
108+
case "Int16Array":
109+
case "Uint16Array":
110+
case "Int32Array":
111+
case "Uint32Array":
112+
case "Float32Array":
113+
case "Float64Array":
114+
case "BigInt64Array":
115+
case "BigUint64Array": {
116+
const TypedArrayConstructor = globalThis[type];
117+
const base64 = value[1];
118+
const arraybuffer = decode64(base64);
119+
const typedArray = new TypedArrayConstructor(arraybuffer);
120+
hydrated[index] = typedArray;
121+
break;
122+
}
123+
124+
case "ArrayBuffer": {
125+
const base64 = value[1];
126+
const arraybuffer = decode64(base64);
127+
hydrated[index] = arraybuffer;
128+
break;
129+
}
130+
104131
default:
105132
throw new Error(`Unknown type ${type}`);
106133
}

src/stringify.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
POSITIVE_INFINITY,
1414
UNDEFINED
1515
} from './constants.js';
16+
import { encode64 } from './base64.js';
1617

1718
/**
1819
* Turn a value into a JSON string that can be parsed with `devalue.parse`
@@ -133,6 +134,33 @@ export function stringify(value, reducers) {
133134
str += ']';
134135
break;
135136

137+
case "Int8Array":
138+
case "Uint8Array":
139+
case "Uint8ClampedArray":
140+
case "Int16Array":
141+
case "Uint16Array":
142+
case "Int32Array":
143+
case "Uint32Array":
144+
case "Float32Array":
145+
case "Float64Array":
146+
case "BigInt64Array":
147+
case "BigUint64Array": {
148+
/** @type {import("./types.js").TypedArray} */
149+
const typedArray = thing;
150+
const base64 = encode64(typedArray.buffer);
151+
str = '["' + type + '","' + base64 + '"]';
152+
break;
153+
}
154+
155+
case "ArrayBuffer": {
156+
/** @type {ArrayBuffer} */
157+
const arraybuffer = thing;
158+
const base64 = encode64(arraybuffer);
159+
160+
str = `["ArrayBuffer","${base64}"]`;
161+
break;
162+
}
163+
136164
default:
137165
if (!is_plain_object(thing)) {
138166
throw new DevalueError(

src/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;

src/uneval.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,22 @@ export function uneval(value, replacer) {
8080
keys.pop();
8181
}
8282
break;
83+
84+
case "Int8Array":
85+
case "Uint8Array":
86+
case "Uint8ClampedArray":
87+
case "Int16Array":
88+
case "Uint16Array":
89+
case "Int32Array":
90+
case "Uint32Array":
91+
case "Float32Array":
92+
case "Float64Array":
93+
case "BigInt64Array":
94+
case "BigUint64Array":
95+
return;
96+
97+
case "ArrayBuffer":
98+
return;
8399

84100
default:
85101
if (!is_plain_object(thing)) {
@@ -159,6 +175,27 @@ export function uneval(value, replacer) {
159175
case 'Set':
160176
case 'Map':
161177
return `new ${type}([${Array.from(thing).map(stringify).join(',')}])`;
178+
179+
case "Int8Array":
180+
case "Uint8Array":
181+
case "Uint8ClampedArray":
182+
case "Int16Array":
183+
case "Uint16Array":
184+
case "Int32Array":
185+
case "Uint32Array":
186+
case "Float32Array":
187+
case "Float64Array":
188+
case "BigInt64Array":
189+
case "BigUint64Array": {
190+
/** @type {import("./types.js").TypedArray} */
191+
const typedArray = thing;
192+
return `new ${type}([${typedArray.toString()}])`;
193+
}
194+
195+
case "ArrayBuffer": {
196+
const ui8 = new Uint8Array(thing);
197+
return `new Uint8Array([${ui8.toString()}]).buffer`;
198+
}
162199

163200
default:
164201
const obj = `{${Object.keys(thing)

test/test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ const fixtures = {
148148
value: BigInt('1'),
149149
js: '1n',
150150
json: '[["BigInt","1"]]'
151+
},
152+
{
153+
name: 'Uint8Array',
154+
value: new Uint8Array([1, 2, 3]),
155+
js: 'new Uint8Array([1,2,3])',
156+
json: '[["Uint8Array","AQID"]]'
157+
},
158+
{
159+
name: "ArrayBuffer",
160+
value: new Uint8Array([1, 2, 3]).buffer,
161+
js: 'new Uint8Array([1,2,3]).buffer',
162+
json: '[["ArrayBuffer","AQID"]]'
151163
}
152164
],
153165

0 commit comments

Comments
 (0)