205 lines
4.9 KiB
JavaScript
205 lines
4.9 KiB
JavaScript
import { decode64 } from './base64.js';
|
|
import {
|
|
HOLE,
|
|
NAN,
|
|
NEGATIVE_INFINITY,
|
|
NEGATIVE_ZERO,
|
|
POSITIVE_INFINITY,
|
|
UNDEFINED
|
|
} from './constants.js';
|
|
|
|
/**
|
|
* Revive a value serialized with `devalue.stringify`
|
|
* @param {string} serialized
|
|
* @param {Record<string, (value: any) => any>} [revivers]
|
|
*/
|
|
export function parse(serialized, revivers) {
|
|
return unflatten(JSON.parse(serialized), revivers);
|
|
}
|
|
|
|
/**
|
|
* Revive a value flattened with `devalue.stringify`
|
|
* @param {number | any[]} parsed
|
|
* @param {Record<string, (value: any) => any>} [revivers]
|
|
*/
|
|
export function unflatten(parsed, revivers) {
|
|
if (typeof parsed === 'number') return hydrate(parsed, true);
|
|
|
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
throw new Error('Invalid input');
|
|
}
|
|
|
|
const values = /** @type {any[]} */ (parsed);
|
|
|
|
const hydrated = Array(values.length);
|
|
|
|
/**
|
|
* @param {number} index
|
|
* @returns {any}
|
|
*/
|
|
function hydrate(index, standalone = false) {
|
|
if (index === UNDEFINED) return undefined;
|
|
if (index === NAN) return NaN;
|
|
if (index === POSITIVE_INFINITY) return Infinity;
|
|
if (index === NEGATIVE_INFINITY) return -Infinity;
|
|
if (index === NEGATIVE_ZERO) return -0;
|
|
|
|
if (standalone || typeof index !== 'number') {
|
|
throw new Error(`Invalid input`);
|
|
}
|
|
|
|
if (index in hydrated) return hydrated[index];
|
|
|
|
const value = values[index];
|
|
|
|
if (!value || typeof value !== 'object') {
|
|
hydrated[index] = value;
|
|
} else if (Array.isArray(value)) {
|
|
if (typeof value[0] === 'string') {
|
|
const type = value[0];
|
|
|
|
const reviver =
|
|
revivers && Object.hasOwn(revivers, type)
|
|
? revivers[type]
|
|
: undefined;
|
|
|
|
if (reviver) {
|
|
let i = value[1];
|
|
if (typeof i !== 'number') {
|
|
// if it's not a number, it was serialized by a builtin reviver
|
|
// so we need to munge it into the format expected by a custom reviver
|
|
i = values.push(value[1]) - 1;
|
|
}
|
|
return (hydrated[index] = reviver(hydrate(i)));
|
|
}
|
|
|
|
switch (type) {
|
|
case 'Date':
|
|
hydrated[index] = new Date(value[1]);
|
|
break;
|
|
|
|
case 'Set':
|
|
const set = new Set();
|
|
hydrated[index] = set;
|
|
for (let i = 1; i < value.length; i += 1) {
|
|
set.add(hydrate(value[i]));
|
|
}
|
|
break;
|
|
|
|
case 'Map':
|
|
const map = new Map();
|
|
hydrated[index] = map;
|
|
for (let i = 1; i < value.length; i += 2) {
|
|
map.set(hydrate(value[i]), hydrate(value[i + 1]));
|
|
}
|
|
break;
|
|
|
|
case 'RegExp':
|
|
hydrated[index] = new RegExp(value[1], value[2]);
|
|
break;
|
|
|
|
case 'Object':
|
|
hydrated[index] = Object(value[1]);
|
|
break;
|
|
|
|
case 'BigInt':
|
|
hydrated[index] = BigInt(value[1]);
|
|
break;
|
|
|
|
case 'null':
|
|
const obj = Object.create(null);
|
|
hydrated[index] = obj;
|
|
for (let i = 1; i < value.length; i += 2) {
|
|
obj[value[i]] = hydrate(value[i + 1]);
|
|
}
|
|
break;
|
|
|
|
case 'Int8Array':
|
|
case 'Uint8Array':
|
|
case 'Uint8ClampedArray':
|
|
case 'Int16Array':
|
|
case 'Uint16Array':
|
|
case 'Int32Array':
|
|
case 'Uint32Array':
|
|
case 'Float32Array':
|
|
case 'Float64Array':
|
|
case 'BigInt64Array':
|
|
case 'BigUint64Array': {
|
|
const TypedArrayConstructor = globalThis[type];
|
|
const typedArray = new TypedArrayConstructor(hydrate(value[1]));
|
|
|
|
hydrated[index] =
|
|
value[2] !== undefined
|
|
? typedArray.subarray(value[2], value[3])
|
|
: typedArray;
|
|
|
|
break;
|
|
}
|
|
|
|
case 'ArrayBuffer': {
|
|
const base64 = value[1];
|
|
const arraybuffer = decode64(base64);
|
|
hydrated[index] = arraybuffer;
|
|
break;
|
|
}
|
|
|
|
case 'Temporal.Duration':
|
|
case 'Temporal.Instant':
|
|
case 'Temporal.PlainDate':
|
|
case 'Temporal.PlainTime':
|
|
case 'Temporal.PlainDateTime':
|
|
case 'Temporal.PlainMonthDay':
|
|
case 'Temporal.PlainYearMonth':
|
|
case 'Temporal.ZonedDateTime': {
|
|
const temporalName = type.slice(9);
|
|
// @ts-expect-error TS doesn't know about Temporal yet
|
|
hydrated[index] = Temporal[temporalName].from(value[1]);
|
|
break;
|
|
}
|
|
|
|
case 'URL': {
|
|
const url = new URL(value[1]);
|
|
hydrated[index] = url;
|
|
break;
|
|
}
|
|
|
|
case 'URLSearchParams': {
|
|
const url = new URLSearchParams(value[1]);
|
|
hydrated[index] = url;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw new Error(`Unknown type ${type}`);
|
|
}
|
|
} else {
|
|
const array = new Array(value.length);
|
|
hydrated[index] = array;
|
|
|
|
for (let i = 0; i < value.length; i += 1) {
|
|
const n = value[i];
|
|
if (n === HOLE) continue;
|
|
|
|
array[i] = hydrate(n);
|
|
}
|
|
}
|
|
} else {
|
|
/** @type {Record<string, any>} */
|
|
const object = {};
|
|
hydrated[index] = object;
|
|
|
|
for (const key in value) {
|
|
if (key === '__proto__') {
|
|
throw new Error('Cannot parse an object with a `__proto__` property');
|
|
}
|
|
|
|
const n = value[key];
|
|
object[key] = hydrate(n);
|
|
}
|
|
}
|
|
|
|
return hydrated[index];
|
|
}
|
|
|
|
return hydrate(0);
|
|
}
|