UNPKG

@dbl0null/slow-json-stringify

Version:

The slow.. well actually fastest JSON stringifier in the galaxy.

277 lines (213 loc) 7.59 kB
const _replaceString = type => type.indexOf('string') !== -1 ? '"__par__"' : '__par__'; // 3 possibilities after arbitrary property: // - ", => non-last string property // - , => non-last non-string property // - " => last string property const _matchStartRe = /^(",|,|")/; const _chunkRegex = /"\w+__sjs"/g; /** * @param {string} str - prepared string already validated. * @param {array} queue - queue containing the property name to match * (used for building dynamic regex) needed for the preparation of * chunks used in different scenarios. */ const _makeChunks = (str, queue) => { const chunks = str // Matching prepared properties and replacing with target with or without // double quotes. // => Avoiding unnecessary concatenation of doublequotes during serialization. .replace(_chunkRegex, _replaceString).split('__par__'); const result = []; let _i; const length = chunks.length; for (let i = 0; i < length; ++i) { var _queue$i; const chunk = chunks[i]; // Using dynamic regex to ensure that only the correct property // at the end of the string it's actually selected. // => e.g. ,"a":{"a": => ,"a":{ const matchProp = `("${(_queue$i = queue[i]) == null ? void 0 : _queue$i.name}":("?))$`; // Check if current chunk is the last one inside a nested property const isLast = (_i = i + 1) === length || (_i = chunks[_i].indexOf('}')) && (_i === 0 || _i === 1); // If the chunk is the last one the `isUndef` case should match // the preceding comma too. const matchPropRe = new RegExp(isLast ? `(,?)${matchProp}` : matchProp); const withoutInitial = chunk.replace(_matchStartRe, ''); result.push({ // notify that the chunk preceding the current one has not // its corresponding property undefined. // => This is a V8 optimization as it's way faster writing // the value of a property than writing the entire property. flag: false, pure: chunk, // Without initial part prevUndef: withoutInitial, // Without property chars isUndef: chunk.replace(matchPropRe, ''), // Only remaining chars (can be zero chars) bothUndef: withoutInitial.replace(matchPropRe, '') }); } return result; }; /** * `_find` is a super fast deep property finder. * It dynamically build the function needed to reach the desired * property. * * e.g. * obj = {a: {b: {c: 1}}} * _find(['a','b','c']) => (obj) => (((obj || {}).a || {}).b || {}).c * * @param {array} path - path to reach object property. */ const __find = path => { let str = 'obj'; for (let i = 0; i < path.length; ++i) { str += `?.['${path[i]}']`; } return eval(`(obj => ${str})`); }; function _arraySerializer(serializer, array) { // Stringifying more complex array using the provided sjs schema let acc = ''; const len = array.length - 1; for (let i = 0; i < len; ++i) { acc += `${serializer(array[i])},`; } // Prevent slice for removing unnecessary comma. acc += serializer(array[len]); return `[${acc}]`; } /** * `_makeArraySerializer` is simply a wrapper of another `sjs` schema * used for the serialization of arrais. */ const _makeArraySerializer = serializer => { if (typeof serializer === 'function') return _arraySerializer.bind(undefined, serializer); return JSON.stringify; }; const fnUser = value => value; /** * @param type number|string|boolean|array|null * @param serializer * @returns */ const attr = (type, serializer) => { const usedSerializer = serializer || fnUser; return { isSJS: true, type, serializer: type === 'array' ? _makeArraySerializer(serializer) : usedSerializer }; }; // Little utility for escaping convenience. // => if no regex is provided, a default one will be used. const _defaultRegex = /[\t\n\r"\\]/g; const _escapeCallback = char => '\\' + char; const escape = (regex = _defaultRegex) => str => str.replace(regex, _escapeCallback); /** * @param {object} originalSchema * @param {array} queue * @param {string|object} obj * @param {array} acc */ function _prepareQueue(originalSchema, queue, obj, acc = []) { // this looks weird for objects, but is actually exactly what we want: object.toString() === '[object Object]'. We only want actual strings. if (obj.toString().indexOf('__sjs') !== -1) { const find = __find(acc); const { serializer } = find(originalSchema); queue.push({ serializer, find, name: acc[acc.length - 1] }); return; } // Recursively going deeper. // NOTE: While going deeper, the current prop is pushed into the accumulator // to keep track of the position inside of the object. const keys = Object.keys(obj); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; _prepareQueue(originalSchema, queue, obj[key], [...acc, key]); } } /** * @param {object} preparedSchema - schema already validated * with modified prop values to avoid clashes. * @param {object} originalSchema - User provided schema * => contains array stringification serializers that are lost during preparation. */ const _makeQueue = (preparedSchema, originalSchema) => { const queue = []; _prepareQueue(originalSchema, queue, preparedSchema); return queue; }; const _stringifyCallback = (_, value) => { if (!value.isSJS) return value; return `${value.type}__sjs`; }; /** * `_prepare` - aims to normalize the schema provided by the user. * It will convert the schema in both a parseable string and an object * useable for making the chunks needed for the serialization part. * @param {object} schema - user provided schema */ const _prepare = schema => { const _preparedString = JSON.stringify(schema, _stringifyCallback); const _preparedSchema = JSON.parse(_preparedString); return { _preparedString, _preparedSchema }; }; /** * `select` function takes all the possible chunks from the * current index and set the more appropriate one in relation * to the current `value` and the `flag` state. * * => This approach avoids the use of Regex during serialization. * * @param {any} chunks - value to serialize. */ const _select = chunks => (value, index) => { const chunk = chunks[index]; if (value !== undefined) { if (chunk.flag) { return chunk.prevUndef + value; } return chunk.pure + value; } // If the current value is undefined set a flag on the next // chunk stating that the previous prop is undefined. chunks[index + 1].flag = true; if (chunk.flag) { return chunk.bothUndef; } return chunk.isUndef; }; // the stringification. const sjs = schema => { const { _preparedString, _preparedSchema } = _prepare(schema); // Providing preparedSchema for univocal correspondence between created queue and chunks. // Provided original schema to keep track of the original properties that gets destroied // during schema preparation => e.g. array stringification method. const queue = _makeQueue(_preparedSchema, schema); const chunks = _makeChunks(_preparedString, queue); const selectChunk = _select(chunks); // Exposed function return obj => { let temp = ''; for (let i = 0; i < queue.length; ++i) { const { serializer, find } = queue[i]; const raw = find(obj); temp += selectChunk(serializer(raw), i); } const { flag, pure, prevUndef } = chunks[chunks.length - 1]; return temp + (flag ? prevUndef : pure); }; }; export { attr, escape, sjs }; //# sourceMappingURL=sjs.modern.js.map