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} 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), _; };