162 lines
4.0 KiB
JavaScript
162 lines
4.0 KiB
JavaScript
import {
|
|
VOID, PRIMITIVE,
|
|
ARRAY, OBJECT,
|
|
DATE, REGEXP, MAP, SET,
|
|
ERROR, BIGINT
|
|
} from './types.js';
|
|
|
|
const EMPTY = '';
|
|
|
|
const {toString} = {};
|
|
const {keys} = Object;
|
|
|
|
const typeOf = value => {
|
|
const type = typeof value;
|
|
if (type !== 'object' || !value)
|
|
return [PRIMITIVE, type];
|
|
|
|
const asString = toString.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];
|
|
}
|
|
|
|
if (asString.includes('Array'))
|
|
return [ARRAY, asString];
|
|
|
|
if (asString.includes('Error'))
|
|
return [ERROR, asString];
|
|
|
|
return [OBJECT, asString];
|
|
};
|
|
|
|
const shouldSkip = ([TYPE, type]) => (
|
|
TYPE === PRIMITIVE &&
|
|
(type === 'function' || type === 'symbol')
|
|
);
|
|
|
|
const serializer = (strict, json, $, _) => {
|
|
|
|
const as = (out, value) => {
|
|
const index = _.push(out) - 1;
|
|
$.set(value, index);
|
|
return index;
|
|
};
|
|
|
|
const pair = value => {
|
|
if ($.has(value))
|
|
return $.get(value);
|
|
|
|
let [TYPE, type] = typeOf(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;
|
|
case 'undefined':
|
|
return as([VOID], value);
|
|
}
|
|
return as([TYPE, entry], value);
|
|
}
|
|
case ARRAY: {
|
|
if (type)
|
|
return as([type, [...value]], value);
|
|
|
|
const arr = [];
|
|
const index = as([TYPE, arr], value);
|
|
for (const entry of value)
|
|
arr.push(pair(entry));
|
|
return index;
|
|
}
|
|
case OBJECT: {
|
|
if (type) {
|
|
switch (type) {
|
|
case 'BigInt':
|
|
return as([type, value.toString()], value);
|
|
case 'Boolean':
|
|
case 'Number':
|
|
case 'String':
|
|
return as([type, value.valueOf()], value);
|
|
}
|
|
}
|
|
|
|
if (json && ('toJSON' in value))
|
|
return pair(value.toJSON());
|
|
|
|
const entries = [];
|
|
const index = as([TYPE, entries], value);
|
|
for (const key of keys(value)) {
|
|
if (strict || !shouldSkip(typeOf(value[key])))
|
|
entries.push([pair(key), pair(value[key])]);
|
|
}
|
|
return index;
|
|
}
|
|
case DATE:
|
|
return as([TYPE, value.toISOString()], value);
|
|
case REGEXP: {
|
|
const {source, flags} = value;
|
|
return as([TYPE, {source, flags}], value);
|
|
}
|
|
case MAP: {
|
|
const entries = [];
|
|
const index = as([TYPE, entries], value);
|
|
for (const [key, entry] of value) {
|
|
if (strict || !(shouldSkip(typeOf(key)) || shouldSkip(typeOf(entry))))
|
|
entries.push([pair(key), pair(entry)]);
|
|
}
|
|
return index;
|
|
}
|
|
case SET: {
|
|
const entries = [];
|
|
const index = as([TYPE, entries], value);
|
|
for (const entry of value) {
|
|
if (strict || !shouldSkip(typeOf(entry)))
|
|
entries.push(pair(entry));
|
|
}
|
|
return index;
|
|
}
|
|
}
|
|
|
|
const {message} = value;
|
|
return as([TYPE, {name: type, message}], value);
|
|
};
|
|
|
|
return pair;
|
|
};
|
|
|
|
/**
|
|
* @typedef {Array<string,any>} Record a type representation
|
|
*/
|
|
|
|
/**
|
|
* Returns an array of serialized Records.
|
|
* @param {any} value a serializable value.
|
|
* @param {{json?: boolean, lossy?: boolean}?} options an object with a `lossy` or `json` property that,
|
|
* if `true`, will not throw errors on incompatible types, and behave more
|
|
* like JSON stringify would behave. Symbol and Function will be discarded.
|
|
* @returns {Record[]}
|
|
*/
|
|
export const serialize = (value, {json, lossy} = {}) => {
|
|
const _ = [];
|
|
return serializer(!(json || lossy), !!json, new Map, _)(value), _;
|
|
};
|