@mastra/core
Version:
97 lines (96 loc) • 4.01 kB
JavaScript
//#region src/utils/safe-stringify.ts
/**
* Safely JSON-stringifies a value, replacing circular references with "[Circular]".
* Uses a stack-based approach so shared (non-circular) references are preserved.
*/
function safeStringify(value, space) {
const stack = [];
return JSON.stringify(value, function(_key, val) {
if (typeof val === "bigint") return val.toString();
if (val !== null && typeof val === "object") {
while (stack.length > 0 && stack[stack.length - 1] !== this) stack.pop();
if (stack.includes(val)) return "[Circular]";
stack.push(val);
}
return val;
}, space) ?? "null";
}
/**
* Maximum number of nodes the `isBoundedSerializable` probe lets `JSON.stringify`
* visit for a single value.
*
* `JSON.stringify` expands shared (non-circular) references once per path, so an
* acyclic graph with layered sharing (`{ a: n, b: n }` nested `d` deep) holds
* `d + 1` objects but expands to `2^d` visited nodes — enough to block the event
* loop for minutes. The budget makes the probe bail in bounded time; a value
* that exceeds it is treated as "not directly serializable".
*/
const SERIALIZATION_NODE_BUDGET = 1e6;
/**
* Serialize a value to JSON while visiting no more than
* `SERIALIZATION_NODE_BUDGET` nodes. Returns the JSON string, or `undefined`
* when the value cannot be represented as JSON: it threw (a cycle or BigInt),
* exhausted the budget (a shared-reference graph `JSON.stringify` would expand
* exponentially), or produced no output (top-level `undefined`, a function, a
* symbol, or an object whose `toJSON()` returns `undefined`).
*
* The value is read exactly once, so a caller that needs both a serializability
* check and the serialized result can use this rather than probing and then
* re-serializing — closing a TOCTOU gap where a stateful getter/`toJSON()`
* returns a different (e.g. much larger) value the second time.
*/
function boundedStringify(value) {
let budget = SERIALIZATION_NODE_BUDGET;
try {
return JSON.stringify(value, (_key, val) => {
if (--budget < 0) throw new RangeError("boundedStringify: value exceeds the serialization node budget");
return val;
});
} catch {
return;
}
}
/**
* Whether `value` can be serialized to JSON within `SERIALIZATION_NODE_BUDGET`
* nodes. `false` for cycles, BigInt, over-budget shared-reference graphs, and
* values that produce no JSON output (top-level undefined/function/symbol, or an
* object whose `toJSON()` returns undefined).
*/
function isBoundedSerializable(value) {
return boundedStringify(value) !== void 0;
}
/**
* Cycle- and shared-reference-safe stringify: every object is serialized at most
* once, and any repeat — a true cycle OR a shared/diamond reference — becomes
* "[Circular]". Unlike `safeStringify`, this cannot expand a shared-reference
* graph exponentially, so it is a bounded fallback for values that overflow the
* `isBoundedSerializable` probe.
*/
function collapseStringify(value) {
const seen = /* @__PURE__ */ new WeakSet();
return JSON.stringify(value, function(_key, val) {
if (typeof val === "bigint") return val.toString();
if (val !== null && typeof val === "object") {
if (seen.has(val)) return "[Circular]";
seen.add(val);
}
return val;
}) ?? "null";
}
/**
* Returns a JSON-serializable copy of a value.
*
* If the value already serializes within the node budget it is returned
* unchanged (no cloning overhead). Otherwise — a cycle, a BigInt, or a
* shared-reference graph too large for the probe — it is rebuilt through
* `collapseStringify`, which dedupes repeated references to `[Circular]` and so
* completes in bounded time instead of hanging on the exponential expansion.
*/
function ensureSerializable(value) {
if (value === null || typeof value !== "object") return value;
if (isBoundedSerializable(value)) return value;
return JSON.parse(collapseStringify(value));
}
//#endregion
export { boundedStringify, ensureSerializable, isBoundedSerializable, safeStringify };
//# sourceMappingURL=safe-stringify.js.map