defuss-dson
Version:
Typed serialization/deserialization for JavaScript datatypes (a superset of JSON).
239 lines (231 loc) • 7.32 kB
JavaScript
'use strict';
const VOID = -1;
const PRIMITIVE = 0;
const ARRAY = 1;
const OBJECT = 2;
const DATE = 3;
const REGEXP = 4;
const MAP = 5;
const SET = 6;
const ERROR = 7;
const BIGINT = 8;
const env = typeof self === "object" ? self : globalThis;
const deserializer = (input, result) => {
const getValue = (out, index) => {
input.set(index, out);
return out;
};
const decoder = (index) => {
if (input.has(index)) return input.get(index);
const [type, value] = result[index];
switch (type) {
case PRIMITIVE:
case VOID:
return getValue(value, index);
case ARRAY: {
const arr = getValue([], index);
for (const index2 of value) arr.push(decoder(index2));
return arr;
}
case OBJECT: {
const object = getValue({}, index);
for (const [key, index2] of value) object[decoder(key)] = decoder(index2);
return object;
}
case DATE:
return getValue(new Date(value), index);
case REGEXP: {
const { source, flags } = value;
return getValue(new RegExp(source, flags), index);
}
case MAP: {
const map = getValue(/* @__PURE__ */ new Map(), index);
for (const [key, index2] of value) map.set(decoder(key), decoder(index2));
return map;
}
case SET: {
const set = getValue(/* @__PURE__ */ new Set(), index);
for (const index2 of value) set.add(decoder(index2));
return set;
}
case ERROR: {
const { name, message } = value;
try {
return getValue(new env[name](message), index);
} catch (e) {
const error = new Error(message);
error.name = name;
return getValue(error, index);
}
}
case BIGINT:
return getValue(BigInt(value), index);
case "BigInt":
return getValue(Object(BigInt(value)), index);
case "ArrayBuffer":
return getValue(new Uint8Array(value).buffer, value);
case "DataView": {
const { buffer } = new Uint8Array(value);
return getValue(new DataView(buffer), value);
}
}
return getValue(new env[type](value), index);
};
return decoder;
};
const deserialize = (serialized) => deserializer(/* @__PURE__ */ new Map(), serialized)(0);
const EMPTY = "";
const { toString: objectToString } = {};
const { keys: objectKeys } = Object;
const getType = (value) => {
const type = typeof value;
if (type !== "object" || !value) return [PRIMITIVE, type];
const asString = objectToString.call(value).slice(8, -1);
switch (asString) {
case "Array":
return [ARRAY, EMPTY];
case "Object":
return [OBJECT, EMPTY];
case "Date":
return [DATE, EMPTY];
case "RegExp":
return [REGEXP, EMPTY];
case "Map":
return [MAP, EMPTY];
case "Set":
return [SET, EMPTY];
case "DataView":
return [ARRAY, asString];
}
if (asString.includes("Array")) return [ARRAY, asString];
if (asString.includes("Error")) return [ERROR, asString];
return [OBJECT, asString];
};
const isIgnoredType = ([TYPE, type]) => TYPE === PRIMITIVE && (type === "function" || type === "symbol");
const serializer = (strict, json, input, result) => {
const getIndex = (out, value) => {
const index = result.push(out) - 1;
input.set(value, index);
return index;
};
const encoder = (value) => {
if (input.has(value)) return input.get(value);
let [TYPE, type] = getType(value);
switch (TYPE) {
case PRIMITIVE: {
let entry = value;
switch (type) {
case "bigint":
TYPE = BIGINT;
entry = value.toString();
break;
case "function":
case "symbol":
if (strict) throw new TypeError(`unable to serialize ${type}`);
entry = null;
break;
/* c8 ignore stop */
case "undefined":
return getIndex([VOID, void 0], value);
}
return getIndex([TYPE, entry], value);
}
case ARRAY: {
if (type) {
let spread = value;
if (type === "DataView") {
spread = new Uint8Array(value.buffer);
} else if (type === "ArrayBuffer") {
spread = new Uint8Array(value);
}
return getIndex([type, [...spread]], value);
}
const arr = [];
const index = getIndex([TYPE, arr], value);
for (const entry of value) arr.push(encoder(entry));
return index;
}
case OBJECT: {
if (type) {
switch (type) {
case "BigInt":
return getIndex([type, value.toString()], value);
case "Boolean":
case "Number":
case "String":
return getIndex([type, value.valueOf()], value);
}
}
if (json && "toJSON" in value) return encoder(value.toJSON());
const entries = [];
const index = getIndex([TYPE, entries], value);
for (const key of objectKeys(value)) {
if (strict || !isIgnoredType(getType(value[key])))
entries.push([encoder(key), encoder(value[key])]);
}
return index;
}
case DATE:
return getIndex([TYPE, value.toISOString()], value);
case REGEXP: {
const { source, flags } = value;
return getIndex([TYPE, { source, flags }], value);
}
case MAP: {
const entries = [];
const index = getIndex([TYPE, entries], value);
for (const [key, entry] of value) {
if (strict || !(isIgnoredType(getType(key)) || isIgnoredType(getType(entry))))
entries.push([encoder(key), encoder(entry)]);
}
return index;
}
case SET: {
const entries = [];
const index = getIndex([TYPE, entries], value);
for (const entry of value) {
if (strict || !isIgnoredType(getType(entry)))
entries.push(encoder(entry));
}
return index;
}
case ERROR: {
const { name, message: message2 } = value;
return getIndex([TYPE, { name, message: message2 }], value);
}
}
const { message } = value;
return getIndex([TYPE, { name: type, message }], value);
};
return encoder;
};
const serialize = (value, options = {}) => {
const { json, lossy } = options;
const result = [];
serializer(!(json || lossy), !!json, /* @__PURE__ */ new Map(), result)(value);
return result;
};
const options = { json: true, lossy: true };
const parse = (str) => deserialize(JSON.parse(str));
const stringify = (any) => JSON.stringify(serialize(any, options));
function isEqual(objectA, objectB) {
const serializedA = stringify(objectA);
const normalizedA = parse(serializedA);
const serializedB = stringify(objectB);
const normalizedB = parse(serializedB);
return stringify(normalizedA) === stringify(normalizedB);
}
const createClone = (value, options) => {
return typeof structuredClone === "function" ? (
/* c8 ignore next */
structuredClone(value)
) : deserialize(serialize(value, options));
};
const clone = (value) => createClone(value, { json: false, lossy: false });
const DSON = {
parse,
stringify,
isEqual,
clone
};
exports.DSON = DSON;