feat(smartjson): Implement stableOneWayStringify: deterministic, cycle-safe JSON for hashing/comparisons; update docs and tests

This commit is contained in:
2025-09-12 20:43:03 +00:00
parent 374a8e411a
commit bd67581b75
5 changed files with 122 additions and 3 deletions

View File

@@ -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