mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
objectUtils: estimate JSON size, deep clone with string limit, find largestStringPaths
This commit is contained in:
@@ -40,3 +40,173 @@ export function stripUndefined<T extends object>(obj: T | null): T | null {
|
||||
if (!obj) return null;
|
||||
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined)) as T;
|
||||
}
|
||||
|
||||
|
||||
// === Size Estimation ===
|
||||
|
||||
/**
|
||||
* Estimates JSON serialized size without actually stringifying.
|
||||
*
|
||||
* Avoids memory allocation spike on large objects. Useful for progress tracking,
|
||||
* batching decisions, or debug output sizing.
|
||||
*
|
||||
* Note: This is an ESTIMATE. It doesn't account for:
|
||||
* - UTF-8 multi-byte characters (assumes 1 byte per char)
|
||||
* - JSON escape sequences (\n, \t, unicode escapes)
|
||||
* - Floating point precision differences
|
||||
*
|
||||
* Returns 0 for the cyclic portion if circular references are detected.
|
||||
*/
|
||||
export function objectEstimateJsonSize(value: unknown, debugCaller: string): number {
|
||||
const seen = new WeakSet<object>();
|
||||
|
||||
function estimate(val: unknown): number {
|
||||
if (val === null) return 4; // "null"
|
||||
if (val === undefined) return 0; // omitted in JSON
|
||||
|
||||
switch (typeof val) {
|
||||
case 'string':
|
||||
return val.length + 2; // quotes
|
||||
case 'number':
|
||||
return String(val).length;
|
||||
case 'boolean':
|
||||
return val ? 4 : 5; // "true" or "false"
|
||||
case 'object': {
|
||||
// Cycle detection
|
||||
if (seen.has(val as object)) {
|
||||
console.warn(`[estimateJsonSize (${debugCaller})] Circular reference detected, returning 0 for this branch`);
|
||||
return 0;
|
||||
}
|
||||
seen.add(val as object);
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
let size = 2; // []
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
size += estimate(val[i]);
|
||||
if (i < val.length - 1) size += 1; // comma
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
// Plain object
|
||||
let size = 2; // {}
|
||||
const keys = Object.keys(val);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
size += key.length + 3; // "key":
|
||||
size += estimate((val as Record<string, unknown>)[key]);
|
||||
if (i < keys.length - 1) size += 1; // comma
|
||||
}
|
||||
return size;
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return estimate(value);
|
||||
}
|
||||
|
||||
|
||||
// === Object Traversal ===
|
||||
|
||||
/**
|
||||
* Deep clones an object while truncating strings that exceed maxBytes.
|
||||
*
|
||||
* Useful for debug logging of large objects (e.g., requests with base64 images).
|
||||
* Truncates strings in the middle, preserving start and end with a byte count.
|
||||
*
|
||||
* @returns Deep clone with truncated strings, or "[Circular]" for cyclic refs
|
||||
*/
|
||||
export function objectDeepCloneWithStringLimit(value: unknown, debugCaller: string, maxBytes: number = 2048): unknown {
|
||||
const seen = new WeakSet<object>();
|
||||
|
||||
function clone(val: unknown): unknown {
|
||||
// Handle primitives
|
||||
if (val === null || val === undefined) return val;
|
||||
|
||||
// Handle strings - truncate if too long
|
||||
if (typeof val === 'string') {
|
||||
if (val.length <= maxBytes) return val;
|
||||
const ellipsis = `...[${(val.length - maxBytes).toLocaleString()} bytes]...`;
|
||||
const half = Math.floor((maxBytes - ellipsis.length) / 2);
|
||||
return val.slice(0, half) + ellipsis + val.slice(-half);
|
||||
}
|
||||
|
||||
// Handle other primitives
|
||||
if (typeof val !== 'object') return val;
|
||||
|
||||
// Cycle detection
|
||||
if (seen.has(val)) return '[Circular]';
|
||||
seen.add(val);
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(val)) {
|
||||
return val.map(item => clone(item));
|
||||
}
|
||||
|
||||
// Handle objects
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const key in val) {
|
||||
if (Object.prototype.hasOwnProperty.call(val, key)) {
|
||||
result[key] = clone((val as Record<string, unknown>)[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return clone(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the largest string values in an object tree
|
||||
*
|
||||
* Recursively traverses an object to find the top N largest string values,
|
||||
* returning their paths, lengths, and preview snippets.
|
||||
*
|
||||
* @returns Array of {path, length, preview} sorted by length (descending)
|
||||
*/
|
||||
export function objectFindLargestStringPaths(obj: unknown, debugCaller: string, topN: number = 5, maxDepth: number = 20): Array<{ path: string; length: number; preview: string }> {
|
||||
const results: Array<{ path: string; length: number; preview: string }> = [];
|
||||
const seen = new WeakSet<object>();
|
||||
|
||||
function traverse(current: unknown, path: string, depth: number) {
|
||||
// prevent infinite recursion
|
||||
if (depth > maxDepth) return;
|
||||
|
||||
// handle strings
|
||||
if (typeof current === 'string') {
|
||||
results.push({
|
||||
path,
|
||||
length: current.length,
|
||||
preview: current.substring(0, 100) + (current.length > 100 ? '...' : ''),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// handle non-objects
|
||||
if (current === null || typeof current !== 'object') return;
|
||||
|
||||
// cycle detection
|
||||
if (seen.has(current)) {
|
||||
console.warn(`[findLargestStringPaths (${debugCaller}] Circular reference at path: ${path}`);
|
||||
return;
|
||||
}
|
||||
seen.add(current);
|
||||
|
||||
// handle arrays
|
||||
if (Array.isArray(current))
|
||||
return current.forEach((item, index) => traverse(item, `${path}[${index}]`, depth + 1));
|
||||
|
||||
// handle objects
|
||||
for (const [key, value] of Object.entries(current))
|
||||
traverse(value, path ? `${path}.${key}` : key, depth + 1);
|
||||
}
|
||||
|
||||
traverse(obj, '', 0);
|
||||
|
||||
// sort by length descending and return top N
|
||||
return results
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.slice(0, topN);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user