import { toByte } from "../../file" /** * 记录一个对象,根据设置处理对象,防止过大或过深 * 去除循环引用 * 返回一个新对象,这个新对象可以立即被 JSON.stringify * * ## 转换规则 * - `循环引用` → `"[Circular]"` * - `超过深度` → `"[Max Depth]"` * - `超过键数` → 截断并添加 `"... N more keys"` * - `超过字节` → `"[Truncated: bytes limit exceeded]"` * * ## 类型转换 * - `Date` → ISO 字符串 * - `RegExp` → 字符串 * - `Error` → `{ name, message, stack }` * - `Map` → 普通对象 * - `Set` → 数组 * - `Function` → `"[Function]"` * - `Symbol` → `"Symbol(...)"` * - `BigInt` → 字符串 * - `Infinity / NaN` → `null` * - `undefined` → `null` * * @param obj 要记录的对象 * @param options 配置选项 * @param options.maxDepth 最大深度,默认无限制 * @param options.maxKeys 最大键数量,默认无限制 * @param options.maxBytes 最大字节数,支持数字或字符串(如 "1KB", "1MB") * @returns 可以被 JSON.stringify 的新对象 */ export function objectRecord( obj: T, options: { /** 最大深度 */ maxDepth?: number /** 最大键数量 */ maxKeys?: number /** 对象最大字节数 */ maxBytes?: number | string } = {} ): T { const { maxDepth = Infinity, maxKeys = Infinity, maxBytes } = options const maxBytesNum = maxBytes !== undefined ? toByte(maxBytes) : Infinity // 循环引用检测 const seen = new WeakSet() // 用于估算字节数 let currentBytes = 0 let bytesExceeded = false function processValue(value: unknown, depth: number): unknown { // 如果已经超过字节限制,直接返回截断标记 if (bytesExceeded) { return "[Truncated: bytes limit exceeded]" } // 处理 null if (value === null) { addBytes(4) // "null" return null } // 处理 undefined - JSON 不支持,转为 null if (value === undefined) { addBytes(4) return null } const type = typeof value // 处理基本类型 if (type === "string") { const str = value as string addBytes(str.length + 2) // 加上引号 return str } if (type === "number") { if (!Number.isFinite(value as number)) { addBytes(4) // "null" return null // Infinity, -Infinity, NaN 转为 null } addBytes(String(value).length) return value } if (type === "boolean") { addBytes((value as boolean) ? 4 : 5) // "true" or "false" return value } if (type === "bigint") { const str = String(value) addBytes(str.length + 2) return str // BigInt 转为字符串 } if (type === "symbol") { const str = String(value) addBytes(str.length + 2) return str // Symbol 转为字符串 } if (type === "function") { const str = "[Function]" addBytes(str.length + 2) return str // 函数转为字符串 } // 处理对象类型 if (type === "object") { const obj = value as object // 检测循环引用 if (seen.has(obj)) { const str = "[Circular]" addBytes(str.length + 2) return str } // 检查深度限制 if (depth >= maxDepth) { const str = "[Max Depth]" addBytes(str.length + 2) return str } seen.add(obj) // 处理数组 if (Array.isArray(obj)) { addBytes(2) // [] const result: unknown[] = [] const len = Math.min(obj.length, maxKeys) for (let i = 0; i < len; i++) { if (bytesExceeded) break result.push(processValue(obj[i], depth + 1)) if (i > 0) addBytes(1) // 逗号 } if (obj.length > maxKeys) { result.push(`[... ${obj.length - maxKeys} more items]`) } seen.delete(obj) return result } // 处理 Date if (obj instanceof Date) { const str = obj.toISOString() addBytes(str.length + 2) return str } // 处理 RegExp if (obj instanceof RegExp) { const str = obj.toString() addBytes(str.length + 2) return str } // 处理 Error if (obj instanceof Error) { const errorObj: Record = { name: obj.name, message: obj.message, } if (obj.stack) { errorObj.stack = obj.stack } return processValue(errorObj, depth) } // 处理 Map if (obj instanceof Map) { const mapObj: Record = {} let count = 0 for (const [k, v] of obj) { if (count >= maxKeys) break const key = typeof k === "string" ? k : String(k) mapObj[key] = v count++ } return processValue(mapObj, depth) } // 处理 Set if (obj instanceof Set) { return processValue(Array.from(obj), depth) } // 处理普通对象 addBytes(2) // {} const result: Record = {} const keys = Object.keys(obj) const len = Math.min(keys.length, maxKeys) for (let i = 0; i < len; i++) { if (bytesExceeded) break const key = keys[i] addBytes(key.length + 3) // "key": if (i > 0) addBytes(1) // 逗号 result[key] = processValue((obj as Record)[key], depth + 1) } if (keys.length > maxKeys) { result["..."] = `${keys.length - maxKeys} more keys` } seen.delete(obj) return result } // 其他类型转为字符串 const str = String(value) addBytes(str.length + 2) return str } function addBytes(bytes: number) { currentBytes += bytes if (currentBytes > maxBytesNum) { bytesExceeded = true } } return processValue(obj, 0) as T }