@mastra/core
Version:
1 lines • 5.72 kB
Source Map (JSON)
{"version":3,"file":"safe-stringify.cjs","names":[],"sources":["../../src/utils/safe-stringify.ts"],"sourcesContent":["/**\n * Safely JSON-stringifies a value, replacing circular references with \"[Circular]\".\n * Uses a stack-based approach so shared (non-circular) references are preserved.\n */\nexport function safeStringify(value: unknown, space?: string | number): string {\n const stack: unknown[] = [];\n const result: string | undefined = JSON.stringify(\n value,\n function (this: unknown, _key: string, val: unknown) {\n if (typeof val === 'bigint') return val.toString();\n if (val !== null && typeof val === 'object') {\n while (stack.length > 0 && stack[stack.length - 1] !== this) {\n stack.pop();\n }\n if (stack.includes(val)) return '[Circular]';\n stack.push(val);\n }\n return val;\n },\n space,\n );\n // JSON.stringify returns undefined for unsupported top-level values (undefined, functions, symbols).\n return result ?? 'null';\n}\n\n/**\n * Maximum number of nodes the `isBoundedSerializable` probe lets `JSON.stringify`\n * visit for a single value.\n *\n * `JSON.stringify` expands shared (non-circular) references once per path, so an\n * acyclic graph with layered sharing (`{ a: n, b: n }` nested `d` deep) holds\n * `d + 1` objects but expands to `2^d` visited nodes — enough to block the event\n * loop for minutes. The budget makes the probe bail in bounded time; a value\n * that exceeds it is treated as \"not directly serializable\".\n */\nconst SERIALIZATION_NODE_BUDGET = 1_000_000;\n\n/**\n * Serialize a value to JSON while visiting no more than\n * `SERIALIZATION_NODE_BUDGET` nodes. Returns the JSON string, or `undefined`\n * when the value cannot be represented as JSON: it threw (a cycle or BigInt),\n * exhausted the budget (a shared-reference graph `JSON.stringify` would expand\n * exponentially), or produced no output (top-level `undefined`, a function, a\n * symbol, or an object whose `toJSON()` returns `undefined`).\n *\n * The value is read exactly once, so a caller that needs both a serializability\n * check and the serialized result can use this rather than probing and then\n * re-serializing — closing a TOCTOU gap where a stateful getter/`toJSON()`\n * returns a different (e.g. much larger) value the second time.\n */\nexport function boundedStringify(value: unknown): string | undefined {\n let budget = SERIALIZATION_NODE_BUDGET;\n try {\n return JSON.stringify(value, (_key, val) => {\n if (--budget < 0) {\n throw new RangeError('boundedStringify: value exceeds the serialization node budget');\n }\n return val;\n });\n } catch {\n return undefined;\n }\n}\n\n/**\n * Whether `value` can be serialized to JSON within `SERIALIZATION_NODE_BUDGET`\n * nodes. `false` for cycles, BigInt, over-budget shared-reference graphs, and\n * values that produce no JSON output (top-level undefined/function/symbol, or an\n * object whose `toJSON()` returns undefined).\n */\nexport function isBoundedSerializable(value: unknown): boolean {\n return boundedStringify(value) !== undefined;\n}\n\n/**\n * Cycle- and shared-reference-safe stringify: every object is serialized at most\n * once, and any repeat — a true cycle OR a shared/diamond reference — becomes\n * \"[Circular]\". Unlike `safeStringify`, this cannot expand a shared-reference\n * graph exponentially, so it is a bounded fallback for values that overflow the\n * `isBoundedSerializable` probe.\n */\nfunction collapseStringify(value: unknown): string {\n const seen = new WeakSet<object>();\n const result: string | undefined = JSON.stringify(value, function (_key: string, val: unknown) {\n if (typeof val === 'bigint') return val.toString();\n if (val !== null && typeof val === 'object') {\n if (seen.has(val)) return '[Circular]';\n seen.add(val);\n }\n return val;\n });\n return result ?? 'null';\n}\n\n/**\n * Returns a JSON-serializable copy of a value.\n *\n * If the value already serializes within the node budget it is returned\n * unchanged (no cloning overhead). Otherwise — a cycle, a BigInt, or a\n * shared-reference graph too large for the probe — it is rebuilt through\n * `collapseStringify`, which dedupes repeated references to `[Circular]` and so\n * completes in bounded time instead of hanging on the exponential expansion.\n */\nexport function ensureSerializable(value: unknown): unknown {\n if (value === null || typeof value !== 'object') return value;\n if (isBoundedSerializable(value)) return value;\n return JSON.parse(collapseStringify(value));\n}\n"],"mappings":";;;;;;AAIA,SAAgB,cAAc,OAAgB,OAAiC;CAC7E,MAAM,QAAmB,CAAC;CAiB1B,OAhBmC,KAAK,UACtC,OACA,SAAyB,MAAc,KAAc;EACnD,IAAI,OAAO,QAAQ,UAAU,OAAO,IAAI,SAAS;EACjD,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU;GAC3C,OAAO,MAAM,SAAS,KAAK,MAAM,MAAM,SAAS,OAAO,MACrD,MAAM,IAAI;GAEZ,IAAI,MAAM,SAAS,GAAG,GAAG,OAAO;GAChC,MAAM,KAAK,GAAG;EAChB;EACA,OAAO;CACT,GACA,KAGU,KAAK;AACnB;;;;;;;;;;;AAYA,MAAM,4BAA4B;;;;;;;;;;;;;;AAelC,SAAgB,iBAAiB,OAAoC;CACnE,IAAI,SAAS;CACb,IAAI;EACF,OAAO,KAAK,UAAU,QAAQ,MAAM,QAAQ;GAC1C,IAAI,EAAE,SAAS,GACb,MAAM,IAAI,WAAW,+DAA+D;GAEtF,OAAO;EACT,CAAC;CACH,QAAQ;EACN;CACF;AACF;;;;;;;AAQA,SAAgB,sBAAsB,OAAyB;CAC7D,OAAO,iBAAiB,KAAK,MAAM,KAAA;AACrC;;;;;;;;AASA,SAAS,kBAAkB,OAAwB;CACjD,MAAM,uBAAO,IAAI,QAAgB;CASjC,OARmC,KAAK,UAAU,OAAO,SAAU,MAAc,KAAc;EAC7F,IAAI,OAAO,QAAQ,UAAU,OAAO,IAAI,SAAS;EACjD,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU;GAC3C,IAAI,KAAK,IAAI,GAAG,GAAG,OAAO;GAC1B,KAAK,IAAI,GAAG;EACd;EACA,OAAO;CACT,CACY,KAAK;AACnB;;;;;;;;;;AAWA,SAAgB,mBAAmB,OAAyB;CAC1D,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,IAAI,sBAAsB,KAAK,GAAG,OAAO;CACzC,OAAO,KAAK,MAAM,kBAAkB,KAAK,CAAC;AAC5C"}