@dbl0null/slow-json-stringify
Version:
The slow.. well actually fastest JSON stringifier in the galaxy.
279 lines (215 loc) • 7.62 kB
JavaScript
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);
};
};
exports.attr = attr;
exports.escape = escape;
exports.sjs = sjs;
//# sourceMappingURL=sjs.js.map