export interface PreviewOptions { maxDepth?: number; maxArrayItems?: number; maxObjectKeys?: number; maxStringLength?: number; } const DEFAULT_PREVIEW_OPTIONS: Required = { maxDepth: 4, maxArrayItems: 6, maxObjectKeys: 32, maxStringLength: 1000, }; function optionsWithDefaults(options: PreviewOptions = {}): Required { return { maxDepth: options.maxDepth ?? DEFAULT_PREVIEW_OPTIONS.maxDepth, maxArrayItems: options.maxArrayItems ?? DEFAULT_PREVIEW_OPTIONS.maxArrayItems, maxObjectKeys: options.maxObjectKeys ?? DEFAULT_PREVIEW_OPTIONS.maxObjectKeys, maxStringLength: options.maxStringLength ?? DEFAULT_PREVIEW_OPTIONS.maxStringLength, }; } export function jsonByteLength(value: unknown): number { try { return Buffer.byteLength(JSON.stringify(value), "utf8"); } catch { return Buffer.byteLength(String(value), "utf8"); } } export function previewJson(value: unknown, options: PreviewOptions = {}, depth = 0): unknown { const limits = optionsWithDefaults(options); if (value === null || typeof value === "number" || typeof value === "boolean") return value; if (typeof value === "string") { if (value.length <= limits.maxStringLength) return value; return { _previewType: "string", length: value.length, text: value.slice(0, limits.maxStringLength), truncated: true, }; } if (typeof value !== "object") return String(value); if (depth >= limits.maxDepth) { return { _previewType: Array.isArray(value) ? "array" : "object", truncated: true, ...(Array.isArray(value) ? { length: value.length } : { keys: Object.keys(value as Record).length }), }; } if (Array.isArray(value)) { return { _previewType: "array", length: value.length, items: value.slice(0, limits.maxArrayItems).map((item) => previewJson(item, limits, depth + 1)), truncatedItems: Math.max(0, value.length - limits.maxArrayItems), }; } const record = value as Record; const entries = Object.entries(record); const preview: Record = {}; for (const [key, item] of entries.slice(0, limits.maxObjectKeys)) { preview[key] = previewJson(item, limits, depth + 1); } if (entries.length > limits.maxObjectKeys) { preview._preview = { truncatedKeys: entries.length - limits.maxObjectKeys, totalKeys: entries.length, }; } return preview; } export function boundedJsonDetail(value: unknown, maxBytes: number, options: PreviewOptions = {}): unknown { const originalBytes = jsonByteLength(value); if (originalBytes <= maxBytes) return value; return { _truncated: true, originalBytes, maxBytes, preview: previewJson(value, options), }; }