"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.$ZodCyclicError = void 0; exports.isRecursiveSchema = isRecursiveSchema; exports.memoizer = memoizer; exports.isBackEdge = isBackEdge; class $ZodCyclicError extends Error { constructor() { super(`Cannot parse a reference cycle that closes through a transform`); this.name = "ZodCyclicError"; } } exports.$ZodCyclicError = $ZodCyclicError; /** Keyed off the context object every schema in one parse call already shares. */ const STATE = "~memo"; const NO_ISSUES = []; // Receivers prefix paths in place, so the cache and every hand-out need their own copies. function cloneIssues(issues) { return issues.map((iss) => (iss.path ? { ...iss, path: iss.path.slice() } : { ...iss })); } const recursive = /*@__PURE__*/ new WeakMap(); /** Whether this schema's subtree contains a cycle, so one parse can re-enter it. */ function isRecursive(inst, stack) { const cached = recursive.get(inst); if (cached !== undefined) return cached; // Relative to the walk in progress, so not cached. if (stack.has(inst)) return true; stack.add(inst); let result = false; const check = (child) => { if (!result && child?._zod && isRecursive(child, stack)) result = true; }; const def = inst._zod.def; const kind = def.type; switch (kind) { case "object": { // `Reflect.ownKeys` rather than `Object.keys`, so a cycle through a declared symbol key is still seen for (const key of Reflect.ownKeys(def.shape)) check(def.shape[key]); check(def.catchall); break; } case "array": check(def.element); break; case "tuple": for (const el of def.items) check(el); check(def.rest); break; case "record": case "map": check(def.keyType); check(def.valueType); break; case "set": check(def.valueType); break; case "union": for (const el of def.options) check(el); break; case "intersection": check(def.left); check(def.right); break; case "optional": case "nullable": case "default": case "prefault": case "catch": case "readonly": case "nonoptional": case "promise": case "success": check(def.innerType); break; case "pipe": check(def.in); check(def.out); break; case "function": check(def.input); check(def.output); break; // reading `_zod.innerType` resolves the getter once and caches it case "lazy": check(inst._zod.innerType); break; // a leaf by choice: `parts` are regex fragments, not data positions case "template_literal": // leaves case "string": case "number": case "int": case "boolean": case "bigint": case "symbol": case "undefined": case "null": case "void": case "never": case "any": case "unknown": case "date": case "nan": case "enum": case "literal": case "file": case "transform": case "custom": break; default: { // a new built-in kind becomes a compile error here kind; // a user-defined kind can still hold children, and only its author knows where, so fall back to scanning the def — skipping accessors, since reading one can run user code for (const key in def) { const desc = Object.getOwnPropertyDescriptor(def, key); if (!desc || desc.get) continue; const value = desc.value; if (!value || typeof value !== "object") continue; if (value._zod) check(value); else if (Array.isArray(value)) for (const el of value) check(el); } } } stack.delete(inst); recursive.set(inst, result); return result; } /** * Whether one parse can re-enter this schema, i.e. its subtree contains a cycle. * Exported for `z.compile`, which refuses to compile such a schema: cycle * breaking is driven from here off state keyed on the parse context, and a * generated fast path has no context to key on. */ function isRecursiveSchema(inst) { return isRecursive(inst, new Set()); } function bucketFor(state, inst) { let bucket = state.buckets.get(inst); if (!bucket) { bucket = new Map(); state.buckets.set(inst, bucket); } return bucket; } // Set immediately before delegating to core and cleared immediately after, so `alloc` registers only for a visit this module is driving. let handoff; // Allocated but unfinished entries. `alloc` and the matching pop both happen in the synchronous part of a parse, so they nest even when children are async, and one stack serves every schema. const open = []; const memo = { alloc(_inst, payload, empty) { const bucket = handoff; if (!bucket) return empty; handoff = undefined; const entry = { value: empty, issues: null }; bucket.set(payload.value, entry); open.push(entry); return empty; }, guard(inst) { var _a; (_a = inst._zod).deferred ?? (_a.deferred = []); inst._zod.deferred.push(() => { const base = inst._zod.parse; const wrapped = (payload, ctx) => { // The value is a placeholder a back-edge is still waiting on, so the cycle closes through this transform. Its output can't exist in time to bind. if (ctx.direction !== "backward" && isBackEdge(ctx, payload.value)) throw new $ZodCyclicError(); return base(payload, ctx); }; inst._zod.parse = wrapped; if (inst._zod.run === base) inst._zod.run = wrapped; }); }, attach(inst) { var _a; let isRecursiveInst; // `bucket` memoized for one parse; a recursive schema is re-entered many times and its bucket never changes let lastCtx; let lastBucket; // Wraps `parse` in a deferred so it sees the container's final parse. Core's own deferred copies `parse` into `run` when there are no checks, and it ran first, so `run` is patched to match; with checks, `run` reads `parse` dynamically. (_a = inst._zod).deferred ?? (_a.deferred = []); inst._zod.deferred.push(() => { const base = inst._zod.parse; const wrapped = (payload, ctx) => { if (isRecursiveInst === undefined) { isRecursiveInst = isRecursive(inst, new Set()); if (!isRecursiveInst) { // Nothing here can ever fire, so take it back out. inst._zod.parse = base; if (inst._zod.run === wrapped) inst._zod.run = base; return base(payload, ctx); } } const input = payload.value; if (input === null || typeof input !== "object") return base(payload, ctx); let state = ctx[STATE]; if (!state) { state = { buckets: new Map(), backEdges: undefined }; ctx[STATE] = state; } let bucket; if (lastCtx === ctx) { bucket = lastBucket; } else { bucket = bucketFor(state, inst); lastCtx = ctx; lastBucket = bucket; } const hit = bucket.get(input); if (hit) { payload.value = hit.value; if (hit.issues) { if (hit.issues.length) payload.issues.push(...cloneIssues(hit.issues)); } else { // Still being parsed: its own checks cover it, so skip them here. payload.memo = true; state.backEdges ?? (state.backEdges = new Set()); state.backEdges.add(hit.value); } return payload; } handoff = bucket; const depth = open.length; const result = base(payload, ctx); handoff = undefined; // A container that rejected its input outright allocated nothing. const entry = open.length > depth ? open.pop() : undefined; // Both paths written out so the sync one allocates no closure. It runs once per node, and capturing here cost more than everything else combined. if (result instanceof Promise) { return result.then((r) => { if (entry) entry.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES; return r; }); } if (entry) entry.issues = result.issues.length ? cloneIssues(result.issues) : NO_ISSUES; return result; }; inst._zod.parse = wrapped; if (inst._zod.run === base) inst._zod.run = wrapped; }); }, }; /** The memoizer that gives containers cycle support. `zod` installs it by default; `zod/mini` opts in with `config({ memoizer: memoizer() })`. */ function memoizer() { return memo; } /** Whether this value is a node a back-edge resolved to before it finished. */ function isBackEdge(ctx, value) { const backEdges = ctx[STATE]?.backEdges; return backEdges !== undefined && value !== null && typeof value === "object" && backEdges.has(value); }