feat(smartjson): Implement stableOneWayStringify: deterministic, cycle-safe JSON for hashing/comparisons; update docs and tests
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartjson',
|
||||
version: '5.1.0',
|
||||
version: '5.2.0',
|
||||
description: 'A library for handling typed JSON data, providing functionalities for parsing, stringifying, and working with JSON objects, including support for encoding and decoding buffers.'
|
||||
}
|
||||
|
63
ts/index.ts
63
ts/index.ts
@@ -26,6 +26,69 @@ export const stringifyJsonL = (items: any[]): string => {
|
||||
return items.map((item) => stringify(item)).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* stableOneWayStringify
|
||||
* - Produces a stable, deterministic JSON string
|
||||
* - Handles circular references without throwing (replaces cycles)
|
||||
* - Safe for hashing/comparisons ("one-way"; not intended for round-trips)
|
||||
*/
|
||||
export const stableOneWayStringify = (
|
||||
objArg: any,
|
||||
simpleOrderArray?: string[],
|
||||
optionsArg: plugins.IStableJsonTypes['Options'] = {}
|
||||
): string => {
|
||||
// Prepare object without throwing on circular references, and encode buffers
|
||||
const visited = new WeakSet<object>();
|
||||
const sanitize = (val: any): any => {
|
||||
// primitives
|
||||
if (val === null || typeof val !== 'object') {
|
||||
return val;
|
||||
}
|
||||
// Encode buffers/typed arrays via existing replacer
|
||||
const replaced = (bufferhandling.replacer as any)('', val);
|
||||
if (replaced && replaced.type === 'EncodedBuffer' && typeof replaced.data === 'string') {
|
||||
return replaced;
|
||||
}
|
||||
// Handle circular references
|
||||
if (visited.has(val)) {
|
||||
return '__cycle__';
|
||||
}
|
||||
visited.add(val);
|
||||
// Arrays
|
||||
if (Array.isArray(val)) {
|
||||
return val.map((item) => sanitize(item));
|
||||
}
|
||||
// Plain objects and class instances: copy enumerable own props
|
||||
const out: Record<string, any> = {};
|
||||
for (const key of Object.keys(val)) {
|
||||
try {
|
||||
out[key] = sanitize((val as any)[key]);
|
||||
} catch (e) {
|
||||
// In case of getters throwing or non-serializable, mark
|
||||
out[key] = '__unserializable__';
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const obj = sanitize(objArg);
|
||||
const options: plugins.IStableJsonTypes['Options'] = {
|
||||
...optionsArg,
|
||||
cycles: true,
|
||||
};
|
||||
if (simpleOrderArray && !options.cmp) {
|
||||
const order = new Map<string, number>();
|
||||
simpleOrderArray.forEach((key, idx) => order.set(key, idx));
|
||||
options.cmp = (a, b) => {
|
||||
const aIdx = order.has(a.key) ? (order.get(a.key) as number) : Number.POSITIVE_INFINITY;
|
||||
const bIdx = order.has(b.key) ? (order.get(b.key) as number) : Number.POSITIVE_INFINITY;
|
||||
if (aIdx !== bIdx) return aIdx - bIdx;
|
||||
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
||||
};
|
||||
}
|
||||
return plugins.stableJson(obj, options);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param objArg
|
||||
|
Reference in New Issue
Block a user