Compare commits

...

2 Commits

6 changed files with 123 additions and 4 deletions

View File

@@ -1,5 +1,13 @@
# Changelog
## 2025-09-12 - 5.2.0 - feat(smartjson)
Implement stableOneWayStringify: deterministic, cycle-safe JSON for hashing/comparisons; update docs and tests
- Implement stableOneWayStringify in ts/index.ts: produces deterministic JSON, encodes buffers, replaces circular references with "__cycle__" and marks unserializable values as "__unserializable__".
- Update README: add documentation and examples for stableOneWayStringify, clarify JSONL wording, and update performance/migration notes.
- Add unit test to verify stableOneWayStringify handles circular references without throwing.
- Add .claude/settings.local.json (local settings/config) as part of the change set.
## 2025-09-12 - 5.1.0 - feat(smartjson)
Add JSONL stringify and ordering comparator; fix empty buffer handling; refactor Smartjson folding/enfolding

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartjson",
"version": "5.1.0",
"version": "5.2.0",
"private": false,
"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.",
"main": "dist_ts/index.js",

View File

@@ -21,9 +21,10 @@ pnpm add @push.rocks/smartjson
**Type-Safe JSON Operations** - Full TypeScript support with proper typing
🎯 **Class Instance Serialization** - Fold and unfold class instances to/from JSON
🔐 **Buffer & Binary Support** - Seamless handling of Buffers and Typed Arrays
📊 **JSON Lines Support** - Parse and compare JSONL data streams
📊 **JSON Lines Support** - Parse, stringify and compare JSONL data streams
🎨 **Pretty Printing** - Beautiful formatted JSON output
**Stable Stringification** - Consistent key ordering for reliable comparisons
♻️ **Circular Reference Handling** - Safe one-way stringify for objects with cycles
🔍 **Deep Equality Checks** - Compare complex objects and JSON structures
🌐 **Base64 Encoding** - Built-in base64 JSON encoding/decoding
@@ -67,6 +68,40 @@ const prettyJson = smartjson.stringifyPretty({
});
```
### Safe One-Way Stringification (New in v5.1.0)
Handle circular references and unserializable values safely for hashing and comparisons:
```typescript
// Create objects with circular references
const objA = { name: 'A' };
const objB = { name: 'B', ref: objA };
objA['ref'] = objB; // Circular reference!
// Normal stringify would throw, but stableOneWayStringify handles it
const safeJson = smartjson.stableOneWayStringify(objA);
// Circular references are replaced with "__cycle__"
// Perfect for object hashing
const hash1 = crypto.createHash('sha256')
.update(smartjson.stableOneWayStringify(complexObject))
.digest('hex');
// Handles unserializable values gracefully
const objWithGetter = {
normal: 'value',
get throwing() { throw new Error('Cannot serialize!'); }
};
const safe = smartjson.stableOneWayStringify(objWithGetter);
// Throwing getters are replaced with "__unserializable__"
// Custom key ordering for consistent hashes
const ordered = smartjson.stableOneWayStringify(
{ z: 1, a: 2 },
['a', 'z'] // Specify key order
);
```
### Base64 JSON Encoding
Encode JSON data as base64 for safe transmission:
@@ -311,6 +346,7 @@ class AppConfig extends Smartjson {
- `parse(jsonString: string): any` - Parse JSON with automatic buffer handling
- `stringify(obj: any, simpleOrderArray?: string[], options?: Options): string` - Convert to JSON with stable ordering
- `stableOneWayStringify(obj: any, simpleOrderArray?: string[], options?: Options): string` - Safe stringify with circular reference handling (one-way, for hashing/comparison)
- `stringifyPretty(obj: any): string` - Pretty print JSON with 2-space indentation
- `stringifyBase64(obj: any): string` - Encode JSON as base64
- `parseBase64(base64String: string): any` - Decode base64 JSON
@@ -336,7 +372,8 @@ class AppConfig extends Smartjson {
2. **Enable pretty printing** only for debugging (it's slower)
3. **Cache base64 encodings** when repeatedly sending the same data
4. **Use JSON Lines** for streaming large datasets
5. **Avoid circular references** in objects being serialized
5. **Use stableOneWayStringify** for objects with circular references (for hashing/caching)
6. **Prefer regular stringify** for round-trip serialization without cycles
## Migration Guide

View File

@@ -104,4 +104,15 @@ tap.test('should respect simpleOrderArray comparator', async () => {
expect(ordered.indexOf('"a"')).toBeLessThan(ordered.indexOf('"c"'));
});
tap.test('stableOneWayStringify should handle circular references without throwing', async () => {
const a: any = { name: 'x' };
a.self = a;
const b: any = { name: 'x' };
b.self = b;
const s1 = smartjson.stableOneWayStringify(a);
const s2 = smartjson.stableOneWayStringify(b);
expect(typeof s1).toEqual('string');
expect(s1).toEqual(s2);
});
tap.start();

View File

@@ -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.'
}

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